diff --git a/.github/labeler.yml b/.github/labeler.yml index 65310f6f15..f9e16ccc81 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,41 +1,42 @@ -"C:x/auth": +--- +C:x/auth: - x/auth/**/* -"C:x/authz": +C:x/authz: - x/authz/**/* -"C:x/bank": +C:x/bank: - x/bank/**/* -"C:x/capability": +C:x/capability: - x/capability/**/* -"C:x/crisis": +C:x/crisis: - x/crisis/**/* -"C:x/distribution": +C:x/distribution: - x/distribution/**/* -"C:x/evidence": +C:x/evidence: - x/evidence/**/* -"C:x/feegrant": +C:x/feegrant: - x/feegrant/**/* -"C:x/genutil": +C:x/genutil: - x/genutil/**/* -"C:x/gov": +C:x/gov: - x/gov/**/* -"C:x/mint": +C:x/mint: - x/mint/**/* -"C:x/params": +C:x/params: - x/params/**/* -"C:Simulations": +C:Simulations: - x/simulation/**/* - x/*/simulation/**/* -"C:x/slashing": +C:x/slashing: - x/slashing/**/* -"C:x/staking": +C:x/staking: - x/staking/**/* -"C:x/upgrade": +C:x/upgrade: - x/upgrade/**/* -"C:Cosmovisor": +C:Cosmovisor: - cosmovisor/**/* -"C:Rosetta": +C:Rosetta: - contrib/rosetta/**/* -"C:Keys": +C:Keys: - client/keys/**/* "Type: Build": - Makefile @@ -47,7 +48,7 @@ - buf.yaml - .mergify.yml - .golangci.yml -"C:CLI": +C:CLI: - client/**/* - x/*/client/**/* "Type: ADR": diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2f40ea5d36..8a69eb38d4 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -6,6 +6,11 @@ jobs: labeler: runs-on: ubuntu-latest steps: - - uses: actions/labeler@main + - name: Checkout + uses: actions/checkout@v4 with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" \ No newline at end of file + repo-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Label PR + uses: actions/labeler@v5 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/client/keys/export.go b/client/keys/export.go index 13491b5e83..5e2d66aaec 100644 --- a/client/keys/export.go +++ b/client/keys/export.go @@ -12,8 +12,9 @@ import ( ) const ( - flagUnarmoredHex = "unarmored-hex" - flagUnsafe = "unsafe" + flagUnarmoredHex = "unarmored-hex" + flagUnarmoredKeyAlgo = "unarmored-key-algo" + flagUnsafe = "unsafe" ) // ExportKeyCommand exports private keys from the key store. diff --git a/client/keys/import.go b/client/keys/import.go index a9d5c185ac..3537781d75 100644 --- a/client/keys/import.go +++ b/client/keys/import.go @@ -2,7 +2,13 @@ package keys import ( "bufio" + "encoding/hex" + "fmt" "os" + "strings" + + "github.com/cosmos/cosmos-sdk/crypto/hd" + "github.com/cosmos/cosmos-sdk/crypto/keyring" "github.com/spf13/cobra" @@ -38,3 +44,142 @@ func ImportKeyCommand() *cobra.Command { }, } } + +// ImportUnarmoredKeyCommand imports private keys from a keyfile. +func ImportUnarmoredKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "import-unarmored [keyfile]", + Short: "Imports unarmored private key into the local keybase", + Long: `Imports hex encoded raw unarmored private key into the local keybase + +[keyfile] - Path to the file containing unarmored hex encoded private key. + => *IF* this non-mandatory 2nd positional argument has been + *PROVIDED*, then private key will be read from that file. + + => *ELSE* If this positional argument has been *OMITTED*, then + user will be prompted on terminal to provide the private key + at SECURE PROMPT = passed in characters of the key hex value + will *not* be displayed on the terminal. + + File format: The only condition for the file format is, that + the unarmored key must be on the first line (the file can also + contain further lines, though they are ignored). + + The 1st line must contain only hex encoded unarmored raw value, + serialised *exactly* as it is expected by given cryptographic + algorithm specified by the '--unarmored-key-algo ' flag + (see the description of that flag). + Hex key value can be preceded & followed by any number of any + whitespace characters, they will be ignored. + +Key value: +As mentioned above, key is expected to be hex encoded. Hex encoding can be +lowercase, uppercase or mixed case, it does not matter, and it can (but +does NOT need to) contain the '0x' or just 'x' prefix at the beginning of +the hex encoded value. + +Output: +The command will print key info after the import, the same way as the +'keys add ...' command does. +This is quite useful, since user will immediately see the address (and pub +key value) derived from the imported private key.`, + Args: cobra.RangeArgs(1, 2), + RunE: func(cmd *cobra.Command, args []string) error { + clientCtx, err := client.GetClientQueryContext(cmd) + if err != nil { + return err + } + + buf := bufio.NewReader(clientCtx.Input) + var privKeyHex string + if len(args) == 1 { + privKeyHex, err = input.GetPassword("Enter hex encoded private key:", buf) + if err != nil { + return err + } + } else { + filename := args[1] + f, err := os.OpenFile(filename, os.O_RDONLY, os.ModePerm) + if err != nil { + return fmt.Errorf("open file \"%s\" error: %w", filename, err) + } + defer f.Close() + + sc := bufio.NewScanner(f) + if sc.Scan() { + firstLine := sc.Text() + privKeyHex = strings.TrimSpace(firstLine) + } else { + return fmt.Errorf("unable to read 1st line from the \"%s\" file", filename) + } + + if err := sc.Err(); err != nil { + return fmt.Errorf("error while scanning the \"%s\" file: %w", filename, err) + } + } + + algo, _ := cmd.Flags().GetString(flagUnarmoredKeyAlgo) + + privKeyHexLC := strings.ToLower(privKeyHex) + acceptedHexValPrefixes := []string{"0x", "x"} + for _, prefix := range acceptedHexValPrefixes { + if strings.HasPrefix(privKeyHexLC, prefix) { + privKeyHexLC = privKeyHexLC[len(prefix):] + break + } + } + + privKeyRaw, err := hex.DecodeString(privKeyHexLC) + if err != nil { + return fmt.Errorf("failed to decode provided hex value of private key: %w", err) + } + + info, err := clientCtx.Keyring.ImportUnarmoredPrivKey(args[0], privKeyRaw, algo) + if err != nil { + return fmt.Errorf("importing unarmored private key: %w", err) + } + + if err := printCreateUnarmored(cmd, info, clientCtx.OutputFormat); err != nil { + return fmt.Errorf("printing private key info: %w", err) + } + + return nil + }, + } + + cmd.Flags().String(flagUnarmoredKeyAlgo, string(hd.Secp256k1Type), fmt.Sprintf( + `Defines cryptographic scheme algorithm of the provided unarmored private key. +At the moment *ONLY* the "%s" and "%s" algorithms are supported. +Expected serialisation format of the raw unarmored key value: +* for "%s": 32 bytes raw private key (hex encoded) +* for "%s": 32 bytes raw public key immediatelly followed by 32 bytes + private key = 64 bytes alltogether (hex encoded) +`, hd.Secp256k1Type, hd.Ed25519Type, hd.Secp256k1Type, hd.Ed25519Type)) + + return cmd +} + +func printCreateUnarmored(cmd *cobra.Command, info keyring.Info, outputFormat string) error { + switch outputFormat { + case OutputFormatText: + cmd.PrintErrln() + printKeyInfo(cmd.OutOrStdout(), info, keyring.MkAccKeyOutput, outputFormat) + case OutputFormatJSON: + out, err := keyring.MkAccKeyOutput(info) + if err != nil { + return err + } + + jsonString, err := KeysCdc.MarshalJSON(out) + if err != nil { + return err + } + + cmd.Println(string(jsonString)) + + default: + return fmt.Errorf("invalid output format %s", outputFormat) + } + + return nil +} diff --git a/client/keys/root.go b/client/keys/root.go index 938a0d53a1..e5c97b8b87 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -42,6 +42,7 @@ The pass backend requires GnuPG: https://gnupg.org/ AddKeyCommand(), ExportKeyCommand(), ImportKeyCommand(), + ImportUnarmoredKeyCommand(), ListKeysCmd(), ShowKeysCmd(), DeleteKeyCommand(), diff --git a/crypto/keyring/keyring.go b/crypto/keyring/keyring.go index d41152978a..b91bad6a6d 100644 --- a/crypto/keyring/keyring.go +++ b/crypto/keyring/keyring.go @@ -4,12 +4,15 @@ import ( "bufio" "encoding/hex" "fmt" + "github.com/cosmos/cosmos-sdk/crypto/keys/ed25519" "io" "os" "path/filepath" "sort" "strings" + "github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1" + "github.com/99designs/keyring" bip39 "github.com/cosmos/go-bip39" "github.com/pkg/errors" @@ -111,6 +114,9 @@ type Importer interface { // ImportPrivKey imports ASCII armored passphrase-encrypted private keys. ImportPrivKey(uid, armor, passphrase string) error + // ImportUnarmoredPrivKey imports UNARMORED private key. + ImportUnarmoredPrivKey(uid string, unarmoredPrivKeyRaw []byte, algo string) (Info, error) + // ImportPubKey imports ASCII armored public keys. ImportPubKey(uid string, armor string) error } @@ -304,6 +310,38 @@ func (ks keystore) ImportPrivKey(uid, armor, passphrase string) error { return nil } +func (ks keystore) ImportUnarmoredPrivKey(uid string, unarmoredPrivKeyRaw []byte, algo string) (Info, error) { + if _, err := ks.Key(uid); err == nil { + return nil, fmt.Errorf("cannot overwrite key: %s", uid) + } + + var privKey types.PrivKey + switch hd.PubKeyType(algo) { + case hd.Secp256k1Type: + privKey = &secp256k1.PrivKey{Key: unarmoredPrivKeyRaw} + case hd.Ed25519Type: + privKey = &ed25519.PrivKey{Key: unarmoredPrivKeyRaw} + case hd.Sr25519Type: + fallthrough + case hd.MultiType: + fallthrough + default: + return nil, fmt.Errorf("only the \"%s\" and \"%s\" algos are supported at the moment", hd.Secp256k1Type, hd.Ed25519Type) + } + + // privKey, err := legacy.PrivKeyFromBytes(privKeyRaw) + // if err != nil { + // return errors.Wrap(err, "failed to create private key from provided hex value") + // } + + info, err := ks.writeLocalKey(uid, privKey, hd.PubKeyType(algo)) + if err != nil { + return nil, err + } + + return info, nil +} + func (ks keystore) ImportPubKey(uid string, armor string) error { if _, err := ks.Key(uid); err == nil { return fmt.Errorf("cannot overwrite key: %s", uid)