diff --git a/client/keys/export.go b/client/keys/export.go index 3f75ee2e52..ac2b6b4768 100644 --- a/client/keys/export.go +++ b/client/keys/export.go @@ -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 ", 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 { @@ -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 } diff --git a/client/keys/export_test.go b/client/keys/export_test.go index 4276db1c12..b01bbf8230 100644 --- a/client/keys/export_test.go +++ b/client/keys/export_test.go @@ -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()) } diff --git a/crypto/keyring/keyring.go b/crypto/keyring/keyring.go index d3220b5b43..f26a06b938 100644 --- a/crypto/keyring/keyring.go +++ b/crypto/keyring/keyring.go @@ -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. @@ -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) @@ -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) } diff --git a/crypto/keyring/keyring_test.go b/crypto/keyring/keyring_test.go index 540ea57bf3..bcd7eddf39 100644 --- a/crypto/keyring/keyring_test.go +++ b/crypto/keyring/keyring_test.go @@ -1,6 +1,7 @@ package keyring import ( + "encoding/hex" "fmt" "strings" "testing" @@ -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)