diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e0016324dba..1ec01950f72e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -77,6 +77,9 @@ the following [issue](https://github.com/keybase/go-keychain/issues/47) with the you encounter this issue, you must upgrade your xcode command line tools to version >= `10.2`. You can upgrade via: `sudo rm -rf /Library/Developer/CommandLineTools; xcode-select --install`. Verify the correct version via: `pkgutil --pkg-info=com.apple.pkg.CLTools_Executables`. +* (keys) [\#5097](https://github.com/cosmos/cosmos-sdk/pull/5097) New `keys migrate` command to assist users migrate their keys +to the new keyring. + ### Improvements diff --git a/client/keys/migrate.go b/client/keys/migrate.go new file mode 100644 index 000000000000..ae9c6e6b7804 --- /dev/null +++ b/client/keys/migrate.go @@ -0,0 +1,91 @@ +package keys + +import ( + "bufio" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/client/input" + "github.com/cosmos/cosmos-sdk/crypto/keys" + "github.com/cosmos/cosmos-sdk/types" + + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// migratePassphrase is used as a no-op migration key passphrase as a passphrase +// is not needed for importing into the Keyring keystore. +const migratePassphrase = "NOOP_PASSPHRASE" + +func migrateCommand() *cobra.Command { + cmd := &cobra.Command{ + Use: "migrate", + Short: "Migrate key information from the lagacy key database to the OS secret store, or encrypted file store as a fall-back and save it", + Long: `Migrate keys from the legacy on-disk secret store to the OS keyring. +The command asks for every passphrase. If the passphrase is incorrect, it skips the respective key. +`, + Args: cobra.ExactArgs(0), + RunE: runMigrateCmd, + } + + cmd.Flags().Bool(flags.FlagDryRun, false, "Do everything which is supposed to be done, but don't write any changes to the keyring.") + return cmd +} + +func runMigrateCmd(cmd *cobra.Command, args []string) error { + // instantiate legacy keybase + rootDir := viper.GetString(flags.FlagHome) + legacykb, err := NewKeyBaseFromDir(rootDir) + if err != nil { + return err + } + + // fetch list of keys from legacy keybase + oldKeys, err := legacykb.List() + if err != nil { + return err + } + + // instantiate keyring + var keyring keys.Keybase + buf := bufio.NewReader(cmd.InOrStdin()) + if viper.GetBool(flags.FlagDryRun) { + keyring = keys.NewTestKeyring(types.GetConfig().GetKeyringServiceName(), rootDir) + } else { + keyring = keys.NewKeyring(types.GetConfig().GetKeyringServiceName(), rootDir, buf) + } + + for _, key := range oldKeys { + legKeyInfo, err := legacykb.Export(key.GetName()) + if err != nil { + return err + } + + keyName := key.GetName() + keyType := key.GetType() + cmd.PrintErrf("Migrating %s (%s) ...\n", key.GetName(), keyType) + if keyType != keys.TypeLocal { + if err := keyring.Import(keyName, legKeyInfo); err != nil { + return err + } + continue + } + + password, err := input.GetPassword("Enter passphrase to decrypt your key:", buf) + if err != nil { + return err + } + + // NOTE: A passphrase is not actually needed here as when the key information + // is imported into the Keyring keystore it only needs the password (see: writeLocalKey). + armoredPriv, err := legacykb.ExportPrivKey(keyName, password, migratePassphrase) + if err != nil { + return err + } + + if err := keyring.ImportPrivKey(keyName, armoredPriv, migratePassphrase); err != nil { + return err + } + } + + return err +} diff --git a/client/keys/migrate_test.go b/client/keys/migrate_test.go new file mode 100644 index 000000000000..61a55ef90804 --- /dev/null +++ b/client/keys/migrate_test.go @@ -0,0 +1,36 @@ +package keys + +import ( + "testing" + + "github.com/cosmos/cosmos-sdk/client/flags" + "github.com/cosmos/cosmos-sdk/tests" + + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/libs/cli" +) + +func Test_runMigrateCmd(t *testing.T) { + cmd := addKeyCommand() + assert.NotNil(t, cmd) + mockIn, _, _ := tests.ApplyMockIO(cmd) + + kbHome, kbCleanUp := tests.NewTestCaseDir(t) + assert.NotNil(t, kbHome) + defer kbCleanUp() + viper.Set(flags.FlagHome, kbHome) + + viper.Set(cli.OutputFlag, OutputFormatText) + + mockIn.Reset("test1234\ntest1234\n") + err := runAddCmd(cmd, []string{"keyname1"}) + assert.NoError(t, err) + + viper.Set(flags.FlagDryRun, true) + cmd = migrateCommand() + mockIn, _, _ = tests.ApplyMockIO(cmd) + mockIn.Reset("test1234\n") + assert.NoError(t, runMigrateCmd(cmd, []string{})) +} diff --git a/client/keys/root.go b/client/keys/root.go index bd22f3194322..b95740a2c036 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -29,6 +29,7 @@ func Commands() *cobra.Command { deleteKeyCommand(), updateKeyCommand(), parseKeyStringCommand(), + migrateCommand(), ) return cmd } diff --git a/client/keys/root_test.go b/client/keys/root_test.go index 6a81d0c12aaf..6794d725e6f0 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, 10, len(rootCommands.Commands())) + assert.Equal(t, 11, len(rootCommands.Commands())) } diff --git a/crypto/keys/keybase_keyring.go b/crypto/keys/keybase_keyring.go index b89671b0b934..1336a77b965e 100644 --- a/crypto/keys/keybase_keyring.go +++ b/crypto/keys/keybase_keyring.go @@ -310,10 +310,11 @@ func (kb keyringKeybase) ImportPrivKey(name, armor, passphrase string) error { privKey, err := mintkey.UnarmorDecryptPrivKey(armor, passphrase) if err != nil { - return errors.Wrap(err, "failed to import private key") + return errors.Wrap(err, "failed to decrypt private key") } - kb.writeLocalKey(name, privKey, passphrase) + // NOTE: The keyring keystore has no need for a passphrase. + kb.writeLocalKey(name, privKey, "") return nil } @@ -406,7 +407,7 @@ func (kb keyringKeybase) Update(name, oldpass string, getNewpass func() (string, // CloseDB releases the lock and closes the storage backend. func (kb keyringKeybase) CloseDB() {} -func (kb keyringKeybase) writeLocalKey(name string, priv tmcrypto.PrivKey, passphrase string) Info { +func (kb keyringKeybase) writeLocalKey(name string, priv tmcrypto.PrivKey, _ string) Info { // encrypt private key using keyring pub := priv.PubKey() info := newLocalInfo(name, pub, string(priv.Bytes())) diff --git a/crypto/keys/lazy_keybase_keyring.go b/crypto/keys/lazy_keybase_keyring.go index 311ed29517d6..2c869f351325 100644 --- a/crypto/keys/lazy_keybase_keyring.go +++ b/crypto/keys/lazy_keybase_keyring.go @@ -32,9 +32,8 @@ type lazyKeybaseKeyring struct { userInput io.Reader } -// NewKeybaseKeyring creates a new instance of a lazy keybase using a Keyring as -// the persistence layer. -func NewKeybaseKeyring(name string, dir string, userInput io.Reader, test bool) Keybase { +// NewKeyring creates a new instance of a keyring. +func NewKeyring(name string, dir string, userInput io.Reader) Keybase { _, err := keyring.Open(keyring.Config{ ServiceName: name, }) @@ -42,7 +41,21 @@ func NewKeybaseKeyring(name string, dir string, userInput io.Reader, test bool) panic(err) } - return lazyKeybaseKeyring{name: name, dir: dir, userInput: userInput, test: test} + return lazyKeybaseKeyring{name: name, dir: dir, userInput: userInput, test: false} +} + +// NewTestKeyring creates a new instance of a keyring for +// testing purposes that does not prompt users for password. +func NewTestKeyring(name string, dir string) Keybase { + if _, err := keyring.Open(keyring.Config{ + AllowedBackends: []keyring.BackendType{"file"}, + ServiceName: name, + FileDir: dir, + }); err != nil { + panic(err) + } + + return lazyKeybaseKeyring{name: name, dir: dir, test: true} } func (lkb lazyKeybaseKeyring) lkbToKeyringConfig() keyring.Config { diff --git a/crypto/keys/lazy_keybase_keyring_test.go b/crypto/keys/lazy_keybase_keyring_test.go index e2866141be44..ebba4f7df858 100644 --- a/crypto/keys/lazy_keybase_keyring_test.go +++ b/crypto/keys/lazy_keybase_keyring_test.go @@ -9,30 +9,15 @@ import ( "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" - "github.com/99designs/keyring" - "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" ) -// New creates a new instance of a lazy keybase. -func newTestKeybaseKeyring(name string, dir string) Keybase { - if _, err := keyring.Open(keyring.Config{ - AllowedBackends: []keyring.BackendType{"file"}, - ServiceName: name, - FileDir: dir, - }); err != nil { - panic(err) - } - - return lazyKeybaseKeyring{name: name, dir: dir, test: true} -} - func TestNewTestKeybaseKeyring(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) lazykb, ok := kb.(lazyKeybaseKeyring) require.True(t, ok) require.Equal(t, lazykb.name, "keybasename") @@ -41,7 +26,7 @@ func TestNewTestKeybaseKeyring(t *testing.T) { func TestLazyKeyManagementKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) algo := Secp256k1 n1, n2, n3 := "personal", "business", "other" @@ -124,7 +109,7 @@ func TestLazyKeyManagementKeyRing(t *testing.T) { func TestLazySignVerifyKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) algo := Secp256k1 n1, n2, n3 := "some dude", "a dudette", "dude-ish" @@ -199,7 +184,7 @@ func TestLazySignVerifyKeyRing(t *testing.T) { func TestLazyExportImportKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) info, _, err := kb.CreateMnemonic("john", English, "secretcpw", Secp256k1) require.NoError(t, err) @@ -227,7 +212,7 @@ func TestLazyExportImportKeyRing(t *testing.T) { func TestLazyExportImportPubKeyKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) // CreateMnemonic a private-public key pair and ensure consistency notPasswd := "n9y25ah7" @@ -266,7 +251,7 @@ func TestLazyExportImportPubKeyKeyRing(t *testing.T) { func TestLazyExportPrivateKeyObjectKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) info, _, err := kb.CreateMnemonic("john", English, "secretcpw", Secp256k1) require.NoError(t, err) @@ -281,7 +266,7 @@ func TestLazyExportPrivateKeyObjectKeyRing(t *testing.T) { func TestLazyAdvancedKeyManagementKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) algo := Secp256k1 n1, n2 := "old-name", "new name" @@ -314,7 +299,7 @@ func TestLazyAdvancedKeyManagementKeyRing(t *testing.T) { func TestLazySeedPhraseKeyRing(t *testing.T) { dir, cleanup := tests.NewTestCaseDir(t) defer cleanup() - kb := newTestKeybaseKeyring("keybasename", dir) + kb := NewTestKeyring("keybasename", dir) algo := Secp256k1 n1, n2 := "lost-key", "found-again" diff --git a/types/config.go b/types/config.go index ce116352f2bb..b3e569bcb82f 100644 --- a/types/config.go +++ b/types/config.go @@ -4,6 +4,9 @@ import ( "sync" ) +// DefaultKeyringServiceName defines a default service name for the keyring. +const DefaultKeyringServiceName = "cosmos" + // Config is the structure that holds the SDK configuration parameters. // This could be used to initialize certain configuration parameters for the SDK. type Config struct { @@ -14,10 +17,17 @@ type Config struct { fullFundraiserPath string txEncoder TxEncoder addressVerifier func([]byte) error + keyringServiceName string } -var ( - // Initializing an instance of Config +// cosmos-sdk wide global singleton +var sdkConfig *Config + +// GetConfig returns the config instance for the SDK. +func GetConfig() *Config { + if sdkConfig != nil { + return sdkConfig + } sdkConfig = &Config{ sealed: false, bech32AddressPrefix: map[string]string{ @@ -31,11 +41,8 @@ var ( coinType: CoinType, fullFundraiserPath: FullFundraiserPath, txEncoder: nil, + keyringServiceName: DefaultKeyringServiceName, } -) - -// GetConfig returns the config instance for the SDK. -func GetConfig() *Config { return sdkConfig } @@ -97,6 +104,12 @@ func (config *Config) SetFullFundraiserPath(fullFundraiserPath string) { config.fullFundraiserPath = fullFundraiserPath } +// Set the keyringServiceName (BIP44Prefix) on the config +func (config *Config) SetKeyringServiceName(keyringServiceName string) { + config.assertNotSealed() + config.keyringServiceName = keyringServiceName +} + // Seal seals the config such that the config state could not be modified further func (config *Config) Seal() *Config { config.mtx.Lock() @@ -146,12 +159,17 @@ func (config *Config) GetAddressVerifier() func([]byte) error { return config.addressVerifier } -// Get the BIP-0044 CoinType code on the config +// GetCoinType returns the BIP-0044 CoinType code on the config. func (config *Config) GetCoinType() uint32 { return config.coinType } -// Get the FullFundraiserPath (BIP44Prefix) on the config +// GetFullFundraiserPath returns the BIP44Prefix. func (config *Config) GetFullFundraiserPath() string { return config.fullFundraiserPath } + +// GetKeyringServiceName returns the keyring service name from the config. +func (config *Config) GetKeyringServiceName() string { + return config.keyringServiceName +}