Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support for importing unarmored key (secp256k1 algo only atm) #176

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 21 additions & 20 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -47,7 +48,7 @@
- buf.yaml
- .mergify.yml
- .golangci.yml
"C:CLI":
C:CLI:
- client/**/*
- x/*/client/**/*
"Type: ADR":
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}"
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Label PR
uses: actions/labeler@v5
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
5 changes: 3 additions & 2 deletions client/keys/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
145 changes: 145 additions & 0 deletions client/keys/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@

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"

Expand Down Expand Up @@ -38,3 +44,142 @@
},
}
}

// ImportUnarmoredKeyCommand imports private keys from a keyfile.
func ImportUnarmoredKeyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "import-unarmored <name> [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 <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

Check failure on line 155 in client/keys/import.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`immediatelly` is a misspelling of `immediately` (misspell)
private key = 64 bytes alltogether (hex encoded)

Check failure on line 156 in client/keys/import.go

View workflow job for this annotation

GitHub Actions / golangci-lint

`alltogether` is a misspelling of `altogether` (misspell)
`, 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
}
1 change: 1 addition & 0 deletions client/keys/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ The pass backend requires GnuPG: https://gnupg.org/
AddKeyCommand(),
ExportKeyCommand(),
ImportKeyCommand(),
ImportUnarmoredKeyCommand(),
ListKeysCmd(),
ShowKeysCmd(),
DeleteKeyCommand(),
Expand Down
38 changes: 38 additions & 0 deletions crypto/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@
"bufio"
"encoding/hex"
"fmt"
"github.com/cosmos/cosmos-sdk/crypto/keys/ed25519"

Check failure on line 7 in crypto/keyring/keyring.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not `gofumpt`-ed (gofumpt)
"io"
"os"
"path/filepath"
"sort"
"strings"

Check failure on line 12 in crypto/keyring/keyring.go

View workflow job for this annotation

GitHub Actions / golangci-lint

File is not `gofumpt`-ed (gofumpt)

"github.com/cosmos/cosmos-sdk/crypto/keys/secp256k1"

"github.com/99designs/keyring"
bip39 "github.com/cosmos/go-bip39"
"github.com/pkg/errors"
Expand Down Expand Up @@ -111,6 +114,9 @@
// 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
}
Expand Down Expand Up @@ -304,6 +310,38 @@
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)
Expand Down
Loading