From 85ebf5f72ea21f926b9f371b5dae6af65292c02d Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Thu, 30 May 2019 16:44:28 +0100 Subject: [PATCH] Implement private keys export/import symmetric functionalities (#4436) Add Keybase's ExportPrivKey()/ImportPrivKey() API calls to export/import ASCII-armored private keys. Relevant keys subcommands are provided as well. Closes: #2020 --- .pending/features/sdk/2020-New-keys-export | 2 + client/keys/export.go | 45 ++++++++++++++++++++++ client/keys/export_test.go | 34 ++++++++++++++++ client/keys/import.go | 39 +++++++++++++++++++ client/keys/import_test.go | 43 +++++++++++++++++++++ client/keys/root.go | 2 + client/keys/root_test.go | 2 +- crypto/keys/keybase.go | 29 ++++++++++++++ crypto/keys/lazy_keybase.go | 22 +++++++++++ crypto/keys/lazy_keybase_test.go | 29 ++++++++++++++ crypto/keys/types.go | 2 + 11 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 .pending/features/sdk/2020-New-keys-export create mode 100644 client/keys/export.go create mode 100644 client/keys/export_test.go create mode 100644 client/keys/import.go create mode 100644 client/keys/import_test.go diff --git a/.pending/features/sdk/2020-New-keys-export b/.pending/features/sdk/2020-New-keys-export new file mode 100644 index 000000000..03bc34259 --- /dev/null +++ b/.pending/features/sdk/2020-New-keys-export @@ -0,0 +1,2 @@ +#2020 New keys export/import command line utilities to export/import private keys in ASCII format +that rely on Keybase's new underlying ExportPrivKey()/ImportPrivKey() API calls. diff --git a/client/keys/export.go b/client/keys/export.go new file mode 100644 index 000000000..fac21e0da --- /dev/null +++ b/client/keys/export.go @@ -0,0 +1,45 @@ +package keys + +import ( + "fmt" + + "github.com/spf13/cobra" + + "github.com/cosmos/cosmos-sdk/client/input" +) + +func exportKeyCommand() *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), + RunE: runExportCmd, + } + return cmd +} + +func runExportCmd(_ *cobra.Command, args []string) error { + kb, err := NewKeyBaseFromHomeFlag() + if err != nil { + return err + } + + buf := input.BufferStdin() + decryptPassword, err := input.GetPassword("Enter passphrase to decrypt your key:", buf) + if err != nil { + return err + } + encryptPassword, err := input.GetPassword("Enter passphrase to encrypt the exported key:", buf) + if err != nil { + return err + } + + armored, err := kb.ExportPrivKey(args[0], decryptPassword, encryptPassword) + if err != nil { + return err + } + + fmt.Println(armored) + return nil +} diff --git a/client/keys/export_test.go b/client/keys/export_test.go new file mode 100644 index 000000000..18a6cf0da --- /dev/null +++ b/client/keys/export_test.go @@ -0,0 +1,34 @@ +package keys + +import ( + "bufio" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/cosmos/cosmos-sdk/tests" +) + +func Test_runExportCmd(t *testing.T) { + exportKeyCommand := exportKeyCommand() + + // Now add a temporary keybase + kbHome, cleanUp := tests.NewTestCaseDir(t) + defer cleanUp() + viper.Set(flags.FlagHome, kbHome) + + // create a key + kb, err := NewKeyBaseFromHomeFlag() + assert.NoError(t, err) + _, err = kb.CreateAccount("keyname1", tests.TestMnemonic, "", "123456789", 0, 0) + assert.NoError(t, err) + + // Now enter password + cleanUp1 := input.OverrideStdin(bufio.NewReader(strings.NewReader("123456789\n123456789\n"))) + defer cleanUp1() + assert.NoError(t, runExportCmd(exportKeyCommand, []string{"keyname1"})) +} diff --git a/client/keys/import.go b/client/keys/import.go new file mode 100644 index 000000000..433c703a6 --- /dev/null +++ b/client/keys/import.go @@ -0,0 +1,39 @@ +package keys + +import ( + "io/ioutil" + + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/spf13/cobra" +) + +func importKeyCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "import ", + Short: "Import private keys into the local keybase", + Long: "Import a ASCII armored private key into the local keybase.", + Args: cobra.ExactArgs(2), + RunE: runImportCmd, + } + return cmd +} + +func runImportCmd(_ *cobra.Command, args []string) error { + kb, err := NewKeyBaseFromHomeFlag() + if err != nil { + return err + } + + bz, err := ioutil.ReadFile(args[1]) + if err != nil { + return err + } + + buf := input.BufferStdin() + passphrase, err := input.GetPassword("Enter passphrase to decrypt your key:", buf) + if err != nil { + return err + } + + return kb.ImportPrivKey(args[0], string(bz), passphrase) +} diff --git a/client/keys/import_test.go b/client/keys/import_test.go new file mode 100644 index 000000000..c608728ce --- /dev/null +++ b/client/keys/import_test.go @@ -0,0 +1,43 @@ +package keys + +import ( + "bufio" + "io/ioutil" + "path/filepath" + "strings" + "testing" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/cosmos/cosmos-sdk/tests" +) + +func Test_runImportCmd(t *testing.T) { + importKeyCommand := importKeyCommand() + + // Now add a temporary keybase + kbHome, cleanUp := tests.NewTestCaseDir(t) + defer cleanUp() + viper.Set(flags.FlagHome, kbHome) + + keyfile := filepath.Join(kbHome, "key.asc") + armoredKey := `-----BEGIN TENDERMINT PRIVATE KEY----- +salt: A790BB721D1C094260EA84F5E5B72289 +kdf: bcrypt + +HbP+c6JmeJy9JXe2rbbF1QtCX1gLqGcDQPBXiCtFvP7/8wTZtVOPj8vREzhZ9ElO +3P7YnrzPQThG0Q+ZnRSbl9MAS8uFAM4mqm5r/Ys= +=f3l4 +-----END TENDERMINT PRIVATE KEY----- +` + require.NoError(t, ioutil.WriteFile(keyfile, []byte(armoredKey), 0644)) + + // Now enter password + cleanUp1 := input.OverrideStdin(bufio.NewReader(strings.NewReader("123456789\n"))) + defer cleanUp1() + assert.NoError(t, runImportCmd(importKeyCommand, []string{"keyname1", keyfile})) +} diff --git a/client/keys/root.go b/client/keys/root.go index 026c12a03..bd22f3194 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -21,6 +21,8 @@ func Commands() *cobra.Command { cmd.AddCommand( mnemonicKeyCommand(), addKeyCommand(), + exportKeyCommand(), + importKeyCommand(), listKeysCmd(), showKeysCmd(), flags.LineBreak, diff --git a/client/keys/root_test.go b/client/keys/root_test.go index 07c1460ac..6a81d0c12 100644 --- a/client/keys/root_test.go +++ b/client/keys/root_test.go @@ -11,5 +11,5 @@ func TestCommands(t *testing.T) { assert.NotNil(t, rootCommands) // Commands are registered - assert.Equal(t, 8, len(rootCommands.Commands())) + assert.Equal(t, 10, len(rootCommands.Commands())) } diff --git a/crypto/keys/keybase.go b/crypto/keys/keybase.go index 8d25aa64d..3282c8cc3 100644 --- a/crypto/keys/keybase.go +++ b/crypto/keys/keybase.go @@ -339,6 +339,35 @@ func (kb dbKeybase) ExportPubKey(name string) (armor string, err error) { return mintkey.ArmorPubKeyBytes(info.GetPubKey().Bytes()), nil } +// 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. +func (kb dbKeybase) ExportPrivKey(name string, decryptPassphrase string, + encryptPassphrase string) (armor string, err error) { + priv, err := kb.ExportPrivateKeyObject(name, decryptPassphrase) + if err != nil { + return "", err + } + + return mintkey.EncryptArmorPrivKey(priv, encryptPassphrase), nil +} + +// ImportPrivKey imports a private key in ASCII armor format. +// It returns an error if a key with the same name exists or a wrong encryption passphrase is +// supplied. +func (kb dbKeybase) ImportPrivKey(name string, armor string, passphrase string) error { + if _, err := kb.Get(name); err == nil { + return errors.New("Cannot overwrite key " + name) + } + + privKey, err := mintkey.UnarmorDecryptPrivKey(armor, passphrase) + if err != nil { + return errors.Wrap(err, "couldn't import private key") + } + + kb.writeLocalKey(name, privKey, passphrase) + return nil +} + func (kb dbKeybase) Import(name string, armor string) (err error) { bz := kb.db.Get(infoKey(name)) if len(bz) > 0 { diff --git a/crypto/keys/lazy_keybase.go b/crypto/keys/lazy_keybase.go index 6922bd152..d1e855fe6 100644 --- a/crypto/keys/lazy_keybase.go +++ b/crypto/keys/lazy_keybase.go @@ -156,6 +156,16 @@ func (lkb lazyKeybase) Import(name string, armor string) (err error) { return newDbKeybase(db).Import(name, armor) } +func (lkb lazyKeybase) ImportPrivKey(name string, armor string, passphrase string) error { + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return err + } + defer db.Close() + + return newDbKeybase(db).ImportPrivKey(name, armor, passphrase) +} + func (lkb lazyKeybase) ImportPubKey(name string, armor string) (err error) { db, err := sdk.NewLevelDB(lkb.name, lkb.dir) if err != nil { @@ -196,4 +206,16 @@ func (lkb lazyKeybase) ExportPrivateKeyObject(name string, passphrase string) (c return newDbKeybase(db).ExportPrivateKeyObject(name, passphrase) } +func (lkb lazyKeybase) ExportPrivKey(name string, decryptPassphrase string, + encryptPassphrase string) (armor string, err error) { + + db, err := sdk.NewLevelDB(lkb.name, lkb.dir) + if err != nil { + return "", err + } + defer db.Close() + + return newDbKeybase(db).ExportPrivKey(name, decryptPassphrase, encryptPassphrase) +} + func (lkb lazyKeybase) CloseDB() {} diff --git a/crypto/keys/lazy_keybase_test.go b/crypto/keys/lazy_keybase_test.go index 8ad9f1c64..b97ffc40d 100644 --- a/crypto/keys/lazy_keybase_test.go +++ b/crypto/keys/lazy_keybase_test.go @@ -209,6 +209,35 @@ func TestLazyExportImport(t *testing.T) { require.Equal(t, john, john2) } +func TestLazyExportImportPrivKey(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + kb := New("keybasename", dir) + + info, _, err := kb.CreateMnemonic("john", English, "secretcpw", Secp256k1) + require.NoError(t, err) + require.Equal(t, info.GetName(), "john") + priv1, err := kb.Get("john") + require.NoError(t, err) + + // decrypt local private key, and produce encrypted ASCII armored output + armored, err := kb.ExportPrivKey("john", "secretcpw", "new_secretcpw") + require.NoError(t, err) + + // delete exported key + require.NoError(t, kb.Delete("john", "", true)) + _, err = kb.Get("john") + require.Error(t, err) + + // import armored key + require.NoError(t, kb.ImportPrivKey("john", armored, "new_secretcpw")) + + // ensure old and new keys match + priv2, err := kb.Get("john") + require.NoError(t, err) + require.True(t, priv1.GetPubKey().Equals(priv2.GetPubKey())) +} + func TestLazyExportImportPubKey(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() diff --git a/crypto/keys/types.go b/crypto/keys/types.go index 5389f9368..89e59da61 100644 --- a/crypto/keys/types.go +++ b/crypto/keys/types.go @@ -46,9 +46,11 @@ type Keybase interface { // The following operations will *only* work on locally-stored keys Update(name, oldpass string, getNewpass func() (string, error)) error Import(name string, armor string) (err error) + ImportPrivKey(name, armor, passphrase string) error ImportPubKey(name string, armor string) (err error) Export(name string) (armor string, err error) ExportPubKey(name string) (armor string, err error) + ExportPrivKey(name, decryptPassphrase, encryptPassphrase string) (armor string, err error) // ExportPrivateKeyObject *only* works on locally-stored keys. Temporary method until we redo the exporting API ExportPrivateKeyObject(name string, passphrase string) (crypto.PrivKey, error)