Skip to content

Commit

Permalink
client/keys: support export of unarmored private key (cosmos#8043)
Browse files Browse the repository at this point in the history
The --unarmored-hex and --unsafe flags are added to
the keys export command. Users must use both to
export private keys material. The output would be in
hexadecimal format and unarmored.

See cosmos#8042 for scope and motivations.

introduce new UnsafeKeyring interface.

Unsafe operations are supported by UnsafeKeyring types.
By doing so, we try to make the client developer aware
of the risks.

Co-authored-by: Sunny Aggarwal <[email protected]>
  • Loading branch information
Alessio Treglia and sunnya97 authored Nov 30, 2020
1 parent 6476b09 commit 513daba
Show file tree
Hide file tree
Showing 4 changed files with 133 additions and 6 deletions.
52 changes: 49 additions & 3 deletions client/keys/export.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,45 @@ package keys

import (
"bufio"
"fmt"

"github.com/spf13/cobra"

"github.com/cosmos/cosmos-sdk/client"
"github.com/cosmos/cosmos-sdk/client/input"
"github.com/cosmos/cosmos-sdk/crypto/keyring"
)

const (
flagUnarmoredHex = "unarmored-hex"
flagUnsafe = "unsafe"
)

// ExportKeyCommand exports private keys from the key store.
func ExportKeyCommand() *cobra.Command {
return &cobra.Command{
cmd := &cobra.Command{
Use: "export <name>",
Short: "Export private keys",
Long: `Export a private key from the local keybase in ASCII-armored encrypted format.`,
Args: cobra.ExactArgs(1),
Long: `Export a private key from the local keyring in ASCII-armored encrypted format.
When both the --unarmored-hex and --unsafe flags are selected, cryptographic
private key material is exported in an INSECURE fashion that is designed to
allow users to import their keys in hot wallets. This feature is for advanced
users only that are confident about how to handle private keys work and are
FULLY AWARE OF THE RISKS. If you are unsure, you may want to do some research
and export your keys in ASCII-armored encrypted format.`,
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
buf := bufio.NewReader(cmd.InOrStdin())
clientCtx := client.GetClientContextFromCmd(cmd)
unarmored, _ := cmd.Flags().GetBool(flagUnarmoredHex)
unsafe, _ := cmd.Flags().GetBool(flagUnsafe)

if unarmored && unsafe {
return exportUnsafeUnarmored(cmd, args[0], buf, clientCtx.Keyring)
} else if unarmored || unsafe {
return fmt.Errorf("the flags %s and %s must be used together", flagUnsafe, flagUnarmoredHex)
}

encryptPassword, err := input.GetPassword("Enter passphrase to encrypt the exported key:", buf)
if err != nil {
Expand All @@ -31,7 +53,31 @@ func ExportKeyCommand() *cobra.Command {
}

cmd.Println(armored)

return nil
},
}

cmd.Flags().Bool(flagUnarmoredHex, false, "Export unarmored hex privkey. Requires --unsafe.")
cmd.Flags().Bool(flagUnsafe, false, "Enable unsafe operations. This flag must be switched on along with all unsafe operation-specific options.")

return cmd
}

func exportUnsafeUnarmored(cmd *cobra.Command, uid string, buf *bufio.Reader, kr keyring.Keyring) error {
// confirm deletion, unless -y is passed
if yes, err := input.GetConfirmation("WARNING: The private key will be exported as an unarmored hexadecimal string. USE AT YOUR OWN RISK. Continue?", buf, cmd.ErrOrStderr()); err != nil {
return err
} else if !yes {
return nil
}

hexPrivKey, err := keyring.NewUnsafe(kr).UnsafeExportPrivKeyHex(uid)
if err != nil {
return err
}

cmd.Println(hexPrivKey)

return nil
}
25 changes: 22 additions & 3 deletions client/keys/export_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,34 @@ func Test_runExportCmd(t *testing.T) {
require.NoError(t, err)

// Now enter password
mockIn.Reset("123456789\n123456789\n")
cmd.SetArgs([]string{
args := []string{
"keyname1",
fmt.Sprintf("--%s=%s", flags.FlagHome, kbHome),
fmt.Sprintf("--%s=%s", flags.FlagKeyringBackend, keyring.BackendTest),
})
}

mockIn.Reset("123456789\n123456789\n")
cmd.SetArgs(args)

clientCtx := client.Context{}.WithKeyring(kb)
ctx := context.WithValue(context.Background(), client.ClientContextKey, &clientCtx)

require.NoError(t, cmd.ExecuteContext(ctx))

argsUnsafeOnly := append(args, "--unsafe")
cmd.SetArgs(argsUnsafeOnly)
require.Error(t, cmd.ExecuteContext(ctx))

argsUnarmoredHexOnly := append(args, "--unarmored-hex")
cmd.SetArgs(argsUnarmoredHexOnly)
require.Error(t, cmd.ExecuteContext(ctx))

argsUnsafeUnarmoredHex := append(args, "--unsafe", "--unarmored-hex")
cmd.SetArgs(argsUnsafeUnarmoredHex)
require.Error(t, cmd.ExecuteContext(ctx))

mockIn, mockOut := testutil.ApplyMockIO(cmd)
mockIn.Reset("y\n")
require.NoError(t, cmd.ExecuteContext(ctx))
require.Equal(t, "2485e33678db4175dc0ecef2d6e1fc493d4a0d7f7ce83324b6ed70afe77f3485\n", mockOut.String())
}
38 changes: 38 additions & 0 deletions crypto/keyring/keyring.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,13 @@ type Keyring interface {
Exporter
}

// UnsafeKeyring exposes unsafe operations such as unsafe unarmored export in
// addition to those that are made available by the Keyring interface.
type UnsafeKeyring interface {
Keyring
UnsafeExporter
}

// Signer is implemented by key stores that want to provide signing capabilities.
type Signer interface {
// Sign sign byte messages with a user key.
Expand All @@ -110,12 +117,20 @@ type Exporter interface {
// Export public key
ExportPubKeyArmor(uid string) (string, error)
ExportPubKeyArmorByAddress(address sdk.Address) (string, error)

// ExportPrivKey returns a private key in ASCII armored format.
// It returns an error if the key does not exist or a wrong encryption passphrase is supplied.
ExportPrivKeyArmor(uid, encryptPassphrase string) (armor string, err error)
ExportPrivKeyArmorByAddress(address sdk.Address, encryptPassphrase string) (armor string, err error)
}

// UnsafeExporter is implemented by key stores that support unsafe export
// of private keys' material.
type UnsafeExporter interface {
// UnsafeExportPrivKeyHex returns a private key in unarmored hex format
UnsafeExportPrivKeyHex(uid string) (string, error)
}

// Option overrides keyring configuration options.
type Option func(options *Options)

Expand Down Expand Up @@ -774,6 +789,29 @@ func (ks keystore) writeMultisigKey(name string, pub types.PubKey) (Info, error)
return info, nil
}

type unsafeKeystore struct {
keystore
}

// NewUnsafe returns a new keyring that provides support for unsafe operations.
func NewUnsafe(kr Keyring) UnsafeKeyring {
// The type assertion is against the only keystore
// implementation that is currently provided.
ks := kr.(keystore)

return unsafeKeystore{ks}
}

// UnsafeExportPrivKeyHex exports private keys in unarmored hexadecimal format.
func (ks unsafeKeystore) UnsafeExportPrivKeyHex(uid string) (privkey string, err error) {
priv, err := ks.ExportPrivateKeyObject(uid)
if err != nil {
return "", err
}

return hex.EncodeToString(priv.Bytes()), nil
}

func addrHexKeyAsString(address sdk.Address) string {
return fmt.Sprintf("%s.%s", hex.EncodeToString(address.Bytes()), addressSuffix)
}
24 changes: 24 additions & 0 deletions crypto/keyring/keyring_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package keyring

import (
"encoding/hex"
"fmt"
"strings"
"testing"
Expand Down Expand Up @@ -1092,6 +1093,29 @@ func TestAltKeyring_ImportExportPubKey_ByAddress(t *testing.T) {
require.EqualError(t, err, fmt.Sprintf("cannot overwrite key: %s", newUID))
}

func TestAltKeyring_UnsafeExportPrivKeyHex(t *testing.T) {
keyring, err := New(t.Name(), BackendTest, t.TempDir(), nil)
require.NoError(t, err)

uid := theID

_, _, err = keyring.NewMnemonic(uid, English, sdk.FullFundraiserPath, hd.Secp256k1)
require.NoError(t, err)

unsafeKeyring := NewUnsafe(keyring)
privKey, err := unsafeKeyring.UnsafeExportPrivKeyHex(uid)

require.NoError(t, err)
require.Equal(t, 64, len(privKey))

_, err = hex.DecodeString(privKey)
require.NoError(t, err)

// test error on non existing key
_, err = unsafeKeyring.UnsafeExportPrivKeyHex("non-existing")
require.Error(t, err)
}

func TestAltKeyring_ConstructorSupportedAlgos(t *testing.T) {
keyring, err := New(t.Name(), BackendTest, t.TempDir(), nil)
require.NoError(t, err)
Expand Down

0 comments on commit 513daba

Please sign in to comment.