diff --git a/CHANGELOG.md b/CHANGELOG.md index 22a49edab308..5c59ec9a9418 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,8 @@ if the provided arguments are invalid. `StdTx.Signatures` to get back the array of StdSignatures `[]StdSignature`. * (modules) [\#5299](https://github.com/cosmos/cosmos-sdk/pull/5299) `HandleDoubleSign` along with params `MaxEvidenceAge` and `DoubleSignJailEndTime` have moved from the `x/slashing` module to the `x/evidence` module. +* (keys) [\#4941](https://github.com/cosmos/cosmos-sdk/issues/4941) Initializing a new keybase through `NewKeyringFromHomeFlag`, `NewKeyringFromDir`, `NewKeyBaseFromHomeFlag`, `NewKeyBaseFromDir`, or `NewInMemory` functions now accept optional parameters of type `KeybaseOption`. These optional parameters are also added on the keys subcommands functions, which are now public, and allows these options to be set on the commands or ignored to default to previous behavior. + * The option introduced in this PR is `WithKeygenFunc` which allows a custom bytes to key implementation to be defined when keys are created. ### Client Breaking Changes @@ -146,6 +148,7 @@ that allows for arbitrary vesting periods. * `IncrementSequenceDecorator`: Increments the account sequence for each signer to prevent replay attacks. * (cli) [\#5223](https://github.com/cosmos/cosmos-sdk/issues/5223) Cosmos Ledger App v2.0.0 is now supported. The changes are backwards compatible and App v1.5.x is still supported. * (modules) [\#5249](https://github.com/cosmos/cosmos-sdk/pull/5249) Funds are now allowed to be directly sent to the community pool (via the distribution module account). +* (keys) [\#4941](https://github.com/cosmos/cosmos-sdk/issues/4941) Introduce keybase option to allow overriding the default private key implementation of a key generated through the `keys add` cli command. ### Improvements diff --git a/client/keys/add.go b/client/keys/add.go index f55572887b12..791ba6bc8b3a 100644 --- a/client/keys/add.go +++ b/client/keys/add.go @@ -5,6 +5,7 @@ import ( "bytes" "errors" "fmt" + "io" "sort" bip39 "github.com/bartekn/go-bip39" @@ -36,7 +37,8 @@ const ( DefaultKeyPass = "12345678" ) -func addKeyCommand() *cobra.Command { +// AddKeyCommand defines a keys command to add a generated or recovered private key to keybase. +func AddKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "add ", Short: "Add an encrypted private key (either newly generated or recovered), encrypt it, and save to disk", @@ -75,6 +77,24 @@ the flag --nosort is set. return cmd } +func getKeybase(transient bool, buf io.Reader) (keys.Keybase, error) { + if transient { + return keys.NewInMemory(), nil + } + + return NewKeyringFromHomeFlag(buf) +} + +func runAddCmd(cmd *cobra.Command, args []string) error { + inBuf := bufio.NewReader(cmd.InOrStdin()) + kb, err := getKeybase(viper.GetBool(flagDryRun), inBuf) + if err != nil { + return err + } + + return RunAddCmd(cmd, args, kb, inBuf) +} + /* input - bip39 mnemonic @@ -84,26 +104,15 @@ input output - armor encrypted private key (saved to file) */ -func runAddCmd(cmd *cobra.Command, args []string) error { - var kb keys.Keybase +func RunAddCmd(cmd *cobra.Command, args []string, kb keys.Keybase, inBuf *bufio.Reader) error { var err error - inBuf := bufio.NewReader(cmd.InOrStdin()) name := args[0] interactive := viper.GetBool(flagInteractive) showMnemonic := !viper.GetBool(flagNoBackup) - if viper.GetBool(flagDryRun) { - // we throw this away, so don't enforce args, - // we want to get a new random seed phrase quickly - kb = keys.NewInMemory() - } else { - kb, err = NewKeyringFromHomeFlag(cmd.InOrStdin()) - if err != nil { - return err - } - + if !viper.GetBool(flagDryRun) { _, err = kb.Get(name) if err == nil { // account exists, ask for user confirmation @@ -273,9 +282,9 @@ func printCreate(cmd *cobra.Command, info keys.Info, showMnemonic bool, mnemonic var jsonString []byte if viper.GetBool(flags.FlagIndentResponse) { - jsonString, err = cdc.MarshalJSONIndent(out, "", " ") + jsonString, err = KeysCdc.MarshalJSONIndent(out, "", " ") } else { - jsonString, err = cdc.MarshalJSON(out) + jsonString, err = KeysCdc.MarshalJSON(out) } if err != nil { diff --git a/client/keys/add_ledger_test.go b/client/keys/add_ledger_test.go index d33fd4a814d6..184e99597137 100644 --- a/client/keys/add_ledger_test.go +++ b/client/keys/add_ledger_test.go @@ -33,7 +33,7 @@ func Test_runAddCmdLedgerWithCustomCoinType(t *testing.T) { config.SetBech32PrefixForValidator(bech32PrefixValAddr, bech32PrefixValPub) config.SetBech32PrefixForConsensusNode(bech32PrefixConsAddr, bech32PrefixConsPub) - cmd := addKeyCommand() + cmd := AddKeyCommand() require.NotNil(t, cmd) // Prepare a keybase @@ -80,7 +80,7 @@ func Test_runAddCmdLedgerWithCustomCoinType(t *testing.T) { func Test_runAddCmdLedger(t *testing.T) { runningUnattended := isRunningUnattended() - cmd := addKeyCommand() + cmd := AddKeyCommand() require.NotNil(t, cmd) mockIn, _, _ := tests.ApplyMockIO(cmd) diff --git a/client/keys/add_test.go b/client/keys/add_test.go index 3f332a393e4a..d1a7dd04b3e0 100644 --- a/client/keys/add_test.go +++ b/client/keys/add_test.go @@ -15,7 +15,7 @@ import ( func Test_runAddCmdBasic(t *testing.T) { runningUnattended := isRunningUnattended() - cmd := addKeyCommand() + cmd := AddKeyCommand() assert.NotNil(t, cmd) mockIn, _, _ := tests.ApplyMockIO(cmd) diff --git a/client/keys/codec.go b/client/keys/codec.go index eae45446e784..e47d3b231afc 100644 --- a/client/keys/codec.go +++ b/client/keys/codec.go @@ -4,20 +4,21 @@ import ( "github.com/cosmos/cosmos-sdk/codec" ) -var cdc *codec.Codec +// KeysCdc defines codec to be used with key operations +var KeysCdc *codec.Codec func init() { - cdc = codec.New() - codec.RegisterCrypto(cdc) - cdc.Seal() + KeysCdc = codec.New() + codec.RegisterCrypto(KeysCdc) + KeysCdc.Seal() } // marshal keys func MarshalJSON(o interface{}) ([]byte, error) { - return cdc.MarshalJSON(o) + return KeysCdc.MarshalJSON(o) } // unmarshal json func UnmarshalJSON(bz []byte, ptr interface{}) error { - return cdc.UnmarshalJSON(bz, ptr) + return KeysCdc.UnmarshalJSON(bz, ptr) } diff --git a/client/keys/delete.go b/client/keys/delete.go index aadc831fe129..8e607a319ba4 100644 --- a/client/keys/delete.go +++ b/client/keys/delete.go @@ -16,7 +16,8 @@ const ( flagForce = "force" ) -func deleteKeyCommand() *cobra.Command { +// DeleteKeyCommand deletes a key from the key store. +func DeleteKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "delete ...", Short: "Delete the given keys", diff --git a/client/keys/delete_test.go b/client/keys/delete_test.go index 537c8073f88a..7593b7f010a3 100644 --- a/client/keys/delete_test.go +++ b/client/keys/delete_test.go @@ -14,7 +14,7 @@ import ( func Test_runDeleteCmd(t *testing.T) { runningUnattended := isRunningUnattended() - deleteKeyCommand := deleteKeyCommand() + deleteKeyCommand := DeleteKeyCommand() mockIn, _, _ := tests.ApplyMockIO(deleteKeyCommand) yesF, _ := deleteKeyCommand.Flags().GetBool(flagYes) diff --git a/client/keys/export.go b/client/keys/export.go index dc6be244beec..761cdc561c37 100644 --- a/client/keys/export.go +++ b/client/keys/export.go @@ -8,7 +8,8 @@ import ( "github.com/cosmos/cosmos-sdk/client/input" ) -func exportKeyCommand() *cobra.Command { +// ExportKeyCommand exports private keys from the key store. +func ExportKeyCommand() *cobra.Command { return &cobra.Command{ Use: "export ", Short: "Export private keys", diff --git a/client/keys/export_test.go b/client/keys/export_test.go index 8132f06a6d55..1279ae537a74 100644 --- a/client/keys/export_test.go +++ b/client/keys/export_test.go @@ -12,7 +12,7 @@ import ( func Test_runExportCmd(t *testing.T) { runningUnattended := isRunningUnattended() - exportKeyCommand := exportKeyCommand() + exportKeyCommand := ExportKeyCommand() mockIn, _, _ := tests.ApplyMockIO(exportKeyCommand) // Now add a temporary keybase diff --git a/client/keys/import.go b/client/keys/import.go index 1e754b7852f9..7fb323c77478 100644 --- a/client/keys/import.go +++ b/client/keys/import.go @@ -9,7 +9,8 @@ import ( "github.com/cosmos/cosmos-sdk/client/input" ) -func importKeyCommand() *cobra.Command { +// ImportKeyCommand imports private keys from a keyfile. +func ImportKeyCommand() *cobra.Command { return &cobra.Command{ Use: "import ", Short: "Import private keys into the local keybase", diff --git a/client/keys/import_test.go b/client/keys/import_test.go index 3579f968634a..aad96b83e8fe 100644 --- a/client/keys/import_test.go +++ b/client/keys/import_test.go @@ -14,7 +14,7 @@ import ( func Test_runImportCmd(t *testing.T) { runningUnattended := isRunningUnattended() - importKeyCommand := importKeyCommand() + importKeyCommand := ImportKeyCommand() mockIn, _, _ := tests.ApplyMockIO(importKeyCommand) // Now add a temporary keybase diff --git a/client/keys/list.go b/client/keys/list.go index 45d29a942699..1954a46de35c 100644 --- a/client/keys/list.go +++ b/client/keys/list.go @@ -9,7 +9,8 @@ import ( const flagListNames = "list-names" -func listKeysCmd() *cobra.Command { +// ListKeysCmd lists all keys in the key store. +func ListKeysCmd() *cobra.Command { cmd := &cobra.Command{ Use: "list", Short: "List all keys", diff --git a/client/keys/list_test.go b/client/keys/list_test.go index 8f4a5b6411b6..129255fd541b 100644 --- a/client/keys/list_test.go +++ b/client/keys/list_test.go @@ -18,7 +18,7 @@ func Test_runListCmd(t *testing.T) { args []string } - cmdBasic := listKeysCmd() + cmdBasic := ListKeysCmd() // Prepare some keybases kbHome1, cleanUp1 := tests.NewTestCaseDir(t) diff --git a/client/keys/migrate.go b/client/keys/migrate.go index 63f4e33b0262..1b7c19f24c24 100644 --- a/client/keys/migrate.go +++ b/client/keys/migrate.go @@ -18,7 +18,8 @@ import ( // is not needed for importing into the Keyring keystore. const migratePassphrase = "NOOP_PASSPHRASE" -func migrateCommand() *cobra.Command { +// MigrateCommand migrates key information from legacy keybase to OS secret store. +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", diff --git a/client/keys/migrate_test.go b/client/keys/migrate_test.go index 61a55ef90804..0ad9226d309d 100644 --- a/client/keys/migrate_test.go +++ b/client/keys/migrate_test.go @@ -13,7 +13,7 @@ import ( ) func Test_runMigrateCmd(t *testing.T) { - cmd := addKeyCommand() + cmd := AddKeyCommand() assert.NotNil(t, cmd) mockIn, _, _ := tests.ApplyMockIO(cmd) @@ -29,7 +29,7 @@ func Test_runMigrateCmd(t *testing.T) { assert.NoError(t, err) viper.Set(flags.FlagDryRun, true) - cmd = migrateCommand() + cmd = MigrateCommand() mockIn, _, _ = tests.ApplyMockIO(cmd) mockIn.Reset("test1234\n") assert.NoError(t, runMigrateCmd(cmd, []string{})) diff --git a/client/keys/mnemonic.go b/client/keys/mnemonic.go index c5b7f9a08b4e..01d7416f0536 100644 --- a/client/keys/mnemonic.go +++ b/client/keys/mnemonic.go @@ -17,7 +17,8 @@ const ( mnemonicEntropySize = 256 ) -func mnemonicKeyCommand() *cobra.Command { +// MnemonicKeyCommand computes the bip39 memonic for input entropy. +func MnemonicKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "mnemonic", Short: "Compute the bip39 mnemonic for some input entropy", diff --git a/client/keys/mnemonic_test.go b/client/keys/mnemonic_test.go index e532c42ddccb..7097ef0a3ca5 100644 --- a/client/keys/mnemonic_test.go +++ b/client/keys/mnemonic_test.go @@ -11,13 +11,13 @@ import ( ) func Test_RunMnemonicCmdNormal(t *testing.T) { - cmdBasic := mnemonicKeyCommand() + cmdBasic := MnemonicKeyCommand() err := runMnemonicCmd(cmdBasic, []string{}) require.NoError(t, err) } func Test_RunMnemonicCmdUser(t *testing.T) { - cmdUser := mnemonicKeyCommand() + cmdUser := MnemonicKeyCommand() err := cmdUser.Flags().Set(flagUserEntropy, "1") assert.NoError(t, err) diff --git a/client/keys/parse.go b/client/keys/parse.go index b76b530ab6cd..79ccca739f0a 100644 --- a/client/keys/parse.go +++ b/client/keys/parse.go @@ -67,7 +67,8 @@ func (bo bech32Output) String() string { return fmt.Sprintf("Bech32 Formats:\n%s", strings.Join(out, "\n")) } -func parseKeyStringCommand() *cobra.Command { +// ParseKeyStringCommand parses an address from hex to bech32 and vice versa. +func ParseKeyStringCommand() *cobra.Command { cmd := &cobra.Command{ Use: "parse ", Short: "Parse address from hex to bech32 and vice versa", @@ -124,9 +125,9 @@ func displayParseKeyInfo(stringer fmt.Stringer) { case OutputFormatJSON: if viper.GetBool(flags.FlagIndentResponse) { - out, err = cdc.MarshalJSONIndent(stringer, "", " ") + out, err = KeysCdc.MarshalJSONIndent(stringer, "", " ") } else { - out = cdc.MustMarshalJSON(stringer) + out = KeysCdc.MustMarshalJSON(stringer) } } diff --git a/client/keys/root.go b/client/keys/root.go index 5e54a5263ed4..b7f70059c015 100644 --- a/client/keys/root.go +++ b/client/keys/root.go @@ -20,17 +20,17 @@ func Commands() *cobra.Command { needs to sign with a private key.`, } cmd.AddCommand( - mnemonicKeyCommand(), - addKeyCommand(), - exportKeyCommand(), - importKeyCommand(), - listKeysCmd(), - showKeysCmd(), + MnemonicKeyCommand(), + AddKeyCommand(), + ExportKeyCommand(), + ImportKeyCommand(), + ListKeysCmd(), + ShowKeysCmd(), flags.LineBreak, - deleteKeyCommand(), - updateKeyCommand(), - parseKeyStringCommand(), - migrateCommand(), + DeleteKeyCommand(), + UpdateKeyCommand(), + ParseKeyStringCommand(), + MigrateCommand(), ) cmd.PersistentFlags().String(flags.FlagKeyringBackend, flags.DefaultKeyringBackend, "Select keyring's backend (os|file|test)") viper.BindPFlag(flags.FlagKeyringBackend, cmd.Flags().Lookup(flags.FlagKeyringBackend)) diff --git a/client/keys/show.go b/client/keys/show.go index fa37f372f90a..e848a2208bb3 100644 --- a/client/keys/show.go +++ b/client/keys/show.go @@ -32,7 +32,8 @@ const ( defaultMultiSigKeyName = "multi" ) -func showKeysCmd() *cobra.Command { +// ShowKeysCmd shows key information for a given key name. +func ShowKeysCmd() *cobra.Command { cmd := &cobra.Command{ Use: "show [name [name...]]", Short: "Show key info for the given name", diff --git a/client/keys/show_test.go b/client/keys/show_test.go index 6fb97d97f441..0483e06d1de3 100644 --- a/client/keys/show_test.go +++ b/client/keys/show_test.go @@ -28,7 +28,7 @@ func Test_multiSigKey_Properties(t *testing.T) { } func Test_showKeysCmd(t *testing.T) { - cmd := showKeysCmd() + cmd := ShowKeysCmd() require.NotNil(t, cmd) require.Equal(t, "false", cmd.Flag(FlagAddress).DefValue) require.Equal(t, "false", cmd.Flag(FlagPublicKey).DefValue) @@ -36,7 +36,7 @@ func Test_showKeysCmd(t *testing.T) { func Test_runShowCmd(t *testing.T) { runningUnattended := isRunningUnattended() - cmd := showKeysCmd() + cmd := ShowKeysCmd() mockIn, _, _ := tests.ApplyMockIO(cmd) require.EqualError(t, runShowCmd(cmd, []string{"invalid"}), "The specified item could not be found in the keyring") require.EqualError(t, runShowCmd(cmd, []string{"invalid1", "invalid2"}), "The specified item could not be found in the keyring") diff --git a/client/keys/update.go b/client/keys/update.go index 64fdbe7e5f91..79b3ad4498ae 100644 --- a/client/keys/update.go +++ b/client/keys/update.go @@ -8,7 +8,9 @@ import ( "github.com/cosmos/cosmos-sdk/client/input" ) -func updateKeyCommand() *cobra.Command { +// UpdateKeyCommand changes the password of a key in the keybase. +// It takes no effect on keys managed by new the keyring-based keybase implementation. +func UpdateKeyCommand() *cobra.Command { cmd := &cobra.Command{ Use: "update ", Short: "Change the password used to protect private key", diff --git a/client/keys/update_test.go b/client/keys/update_test.go index 43e5dc98f0e2..6cfd46d92f38 100644 --- a/client/keys/update_test.go +++ b/client/keys/update_test.go @@ -11,7 +11,7 @@ import ( ) func Test_updateKeyCommand(t *testing.T) { - cmd := updateKeyCommand() + cmd := UpdateKeyCommand() assert.NotNil(t, cmd) // No flags or defaults to validate } @@ -20,7 +20,7 @@ func Test_runUpdateCmd(t *testing.T) { fakeKeyName1 := "runUpdateCmd_Key1" fakeKeyName2 := "runUpdateCmd_Key2" - cmd := updateKeyCommand() + cmd := UpdateKeyCommand() // fails because it requests a password assert.EqualError(t, runUpdateCmd(cmd, []string{fakeKeyName1}), "EOF") diff --git a/client/keys/utils.go b/client/keys/utils.go index 1096ae6e0790..498758d2c7bb 100644 --- a/client/keys/utils.go +++ b/client/keys/utils.go @@ -26,30 +26,33 @@ const ( type bechKeyOutFn func(keyInfo keys.Info) (keys.KeyOutput, error) -// NewKeyBaseFromHomeFlag initializes a Keybase based on the configuration. -func NewKeyBaseFromHomeFlag() (keys.Keybase, error) { +// NewKeyBaseFromHomeFlag initializes a Keybase based on the configuration. Keybase +// options can be applied when generating this new Keybase. +func NewKeyBaseFromHomeFlag(opts ...keys.KeybaseOption) (keys.Keybase, error) { rootDir := viper.GetString(flags.FlagHome) - return NewKeyBaseFromDir(rootDir) + return NewKeyBaseFromDir(rootDir, opts...) } -// NewKeyBaseFromDir initializes a keybase at a particular dir. -func NewKeyBaseFromDir(rootDir string) (keys.Keybase, error) { - return getLazyKeyBaseFromDir(rootDir) +// NewKeyBaseFromDir initializes a keybase at the rootDir directory. Keybase +// options can be applied when generating this new Keybase. +func NewKeyBaseFromDir(rootDir string, opts ...keys.KeybaseOption) (keys.Keybase, error) { + return getLazyKeyBaseFromDir(rootDir, opts...) } // NewInMemoryKeyBase returns a storage-less keybase. func NewInMemoryKeyBase() keys.Keybase { return keys.NewInMemory() } -// NewKeyBaseFromHomeFlag initializes a keyring based on configuration. -func NewKeyringFromHomeFlag(input io.Reader) (keys.Keybase, error) { - return NewKeyringFromDir(viper.GetString(flags.FlagHome), input) +// NewKeyBaseFromHomeFlag initializes a keyring based on configuration. Keybase +// options can be applied when generating this new Keybase. +func NewKeyringFromHomeFlag(input io.Reader, opts ...keys.KeybaseOption) (keys.Keybase, error) { + return NewKeyringFromDir(viper.GetString(flags.FlagHome), input, opts...) } // NewKeyBaseFromDir initializes a keyring at the given directory. // If the viper flag flags.FlagKeyringBackend is set to file, it returns an on-disk keyring with // CLI prompt support only. If flags.FlagKeyringBackend is set to test it will return an on-disk, // password-less keyring that could be used for testing purposes. -func NewKeyringFromDir(rootDir string, input io.Reader) (keys.Keybase, error) { +func NewKeyringFromDir(rootDir string, input io.Reader, opts ...keys.KeybaseOption) (keys.Keybase, error) { keyringBackend := viper.GetString(flags.FlagKeyringBackend) switch keyringBackend { case flags.KeyringBackendTest: @@ -62,8 +65,8 @@ func NewKeyringFromDir(rootDir string, input io.Reader) (keys.Keybase, error) { return nil, fmt.Errorf("unknown keyring backend %q", keyringBackend) } -func getLazyKeyBaseFromDir(rootDir string) (keys.Keybase, error) { - return keys.New(defaultKeyDBName, filepath.Join(rootDir, "keys")), nil +func getLazyKeyBaseFromDir(rootDir string, opts ...keys.KeybaseOption) (keys.Keybase, error) { + return keys.New(defaultKeyDBName, filepath.Join(rootDir, "keys"), opts...), nil } func printKeyInfo(keyInfo keys.Info, bechKeyOut bechKeyOutFn) { @@ -80,9 +83,9 @@ func printKeyInfo(keyInfo keys.Info, bechKeyOut bechKeyOutFn) { var out []byte var err error if viper.GetBool(flags.FlagIndentResponse) { - out, err = cdc.MarshalJSONIndent(ko, "", " ") + out, err = KeysCdc.MarshalJSONIndent(ko, "", " ") } else { - out, err = cdc.MarshalJSON(ko) + out, err = KeysCdc.MarshalJSON(ko) } if err != nil { panic(err) @@ -107,9 +110,9 @@ func printInfos(infos []keys.Info) { var err error if viper.GetBool(flags.FlagIndentResponse) { - out, err = cdc.MarshalJSONIndent(kos, "", " ") + out, err = KeysCdc.MarshalJSONIndent(kos, "", " ") } else { - out, err = cdc.MarshalJSON(kos) + out, err = KeysCdc.MarshalJSON(kos) } if err != nil { diff --git a/crypto/keys/codec.go b/crypto/keys/codec.go index 3181a9ef3e60..c3b3e76a3a8b 100644 --- a/crypto/keys/codec.go +++ b/crypto/keys/codec.go @@ -7,16 +7,22 @@ import ( "github.com/cosmos/cosmos-sdk/crypto/keys/hd" ) -var cdc *codec.Codec +// CryptoCdc defines the codec required for keys and info +var CryptoCdc *codec.Codec func init() { - cdc = codec.New() - cryptoAmino.RegisterAmino(cdc) + CryptoCdc = codec.New() + cryptoAmino.RegisterAmino(CryptoCdc) + RegisterCodec(CryptoCdc) + CryptoCdc.Seal() +} + +// RegisterCodec registers concrete types and interfaces on the given codec. +func RegisterCodec(cdc *codec.Codec) { cdc.RegisterInterface((*Info)(nil), nil) cdc.RegisterConcrete(hd.BIP44Params{}, "crypto/keys/hd/BIP44Params", nil) cdc.RegisterConcrete(localInfo{}, "crypto/keys/localInfo", nil) cdc.RegisterConcrete(ledgerInfo{}, "crypto/keys/ledgerInfo", nil) cdc.RegisterConcrete(offlineInfo{}, "crypto/keys/offlineInfo", nil) cdc.RegisterConcrete(multiInfo{}, "crypto/keys/multiInfo", nil) - cdc.Seal() } diff --git a/crypto/keys/keybase.go b/crypto/keys/keybase.go index 00ac5c2373b5..d1b67e914248 100644 --- a/crypto/keys/keybase.go +++ b/crypto/keys/keybase.go @@ -75,16 +75,17 @@ type dbKeybase struct { // newDBKeybase creates a new dbKeybase instance using the provided DB for // reading and writing keys. -func newDBKeybase(db dbm.DB) Keybase { +func newDBKeybase(db dbm.DB, opts ...KeybaseOption) Keybase { return dbKeybase{ - base: baseKeybase{}, + base: newBaseKeybase(opts...), db: db, } } // NewInMemory creates a transient keybase on top of in-memory storage // instance useful for testing purposes and on-the-fly key generation. -func NewInMemory() Keybase { return newDBKeybase(dbm.NewMemDB()) } +// Keybase options can be applied when generating this new Keybase. +func NewInMemory(opts ...KeybaseOption) Keybase { return newDBKeybase(dbm.NewMemDB(), opts...) } // CreateMnemonic generates a new key and persists it to storage, encrypted // using the provided password. It returns the generated mnemonic and the key Info. diff --git a/crypto/keys/keybase_base.go b/crypto/keys/keybase_base.go index 55dd9bbffece..ea3a60bb9b75 100644 --- a/crypto/keys/keybase_base.go +++ b/crypto/keys/keybase_base.go @@ -16,9 +16,15 @@ import ( ) type ( + kbOptions struct { + keygenFunc PrivKeyGenFunc + } + // baseKeybase is an auxiliary type that groups Keybase storage agnostic features // together. - baseKeybase struct{} + baseKeybase struct { + options kbOptions + } keyWriter interface { writeLocalKeyer @@ -34,6 +40,30 @@ type ( } ) +// WithKeygenFunc applies an overridden key generation function to generate the private key. +func WithKeygenFunc(f PrivKeyGenFunc) KeybaseOption { + return func(o *kbOptions) { + o.keygenFunc = f + } +} + +// newBaseKeybase generates the base keybase with defaulting to tendermint SECP256K1 key type +func newBaseKeybase(optionsFns ...KeybaseOption) baseKeybase { + // Default options for keybase + options := kbOptions{keygenFunc: baseSecpPrivKeyGen} + + for _, optionFn := range optionsFns { + optionFn(&options) + } + + return baseKeybase{options: options} +} + +// baseSecpPrivKeyGen generates a secp256k1 private key from the given bytes +func baseSecpPrivKeyGen(bz [32]byte) tmcrypto.PrivKey { + return secp256k1.PrivKeySecp256k1(bz) +} + // SignWithLedger signs a binary message with the ledger device referenced by an Info object // and returns the signed bytes and the public key. It returns an error if the device could // not be queried or it returned an error. @@ -72,7 +102,7 @@ func (kb baseKeybase) DecodeSignature(info Info, msg []byte) (sig []byte, pub tm return nil, nil, err } - if err := cdc.UnmarshalBinaryLengthPrefixed([]byte(signed), sig); err != nil { + if err := CryptoCdc.UnmarshalBinaryLengthPrefixed([]byte(signed), sig); err != nil { return nil, nil, errors.Wrap(err, "failed to decode signature") } @@ -101,9 +131,9 @@ func (kb baseKeybase) persistDerivedKey( var info Info if passwd != "" { - info = keyWriter.writeLocalKey(name, secp256k1.PrivKeySecp256k1(derivedPriv), passwd) + info = keyWriter.writeLocalKey(name, kb.options.keygenFunc(derivedPriv), passwd) } else { - info = kb.writeOfflineKey(keyWriter, name, secp256k1.PrivKeySecp256k1(derivedPriv).PubKey()) + info = kb.writeOfflineKey(keyWriter, name, kb.options.keygenFunc(derivedPriv).PubKey()) } return info, nil diff --git a/crypto/keys/keybase_test.go b/crypto/keys/keybase_test.go index 2d4752fe3c31..f74d7c0c5785 100644 --- a/crypto/keys/keybase_test.go +++ b/crypto/keys/keybase_test.go @@ -10,6 +10,7 @@ import ( "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/cosmos/cosmos-sdk/crypto/keys/mintkey" @@ -401,7 +402,8 @@ func TestSeedPhrase(t *testing.T) { func ExampleNew() { // Select the encryption and storage for your cryptostore - cstore := NewInMemory() + customKeyGenFunc := func(bz [32]byte) crypto.PrivKey { return secp256k1.PrivKeySecp256k1(bz) } + cstore := NewInMemory(WithKeygenFunc(customKeyGenFunc)) sec := Secp256k1 diff --git a/crypto/keys/keyring.go b/crypto/keys/keyring.go index 28d9aaec8ff1..b7c7aa0daa84 100644 --- a/crypto/keys/keyring.go +++ b/crypto/keys/keyring.go @@ -42,14 +42,17 @@ type keyringKeybase struct { var maxPassphraseEntryAttempts = 3 -// NewKeyring creates a new instance of a keyring. -func NewKeyring(name string, dir string, userInput io.Reader) (Keybase, error) { +// NewKeyring creates a new instance of a keyring. Keybase +// options can be applied when generating this new Keybase. +func NewKeyring( + name string, dir string, userInput io.Reader, opts ...KeybaseOption, +) (Keybase, error) { db, err := keyring.Open(lkbToKeyringConfig(name, dir, userInput, false)) if err != nil { return nil, err } - return newKeyringKeybase(db), nil + return newKeyringKeybase(db, opts...), nil } // NewKeyringFile creates a new instance of an encrypted file-backed keyring. @@ -64,13 +67,13 @@ func NewKeyringFile(name string, dir string, userInput io.Reader) (Keybase, erro // NewTestKeyring creates a new instance of an on-disk keyring for // testing purposes that does not prompt users for password. -func NewTestKeyring(name string, dir string) (Keybase, error) { +func NewTestKeyring(name string, dir string, opts ...KeybaseOption) (Keybase, error) { db, err := keyring.Open(lkbToKeyringConfig(name, dir, nil, true)) if err != nil { return nil, err } - return newKeyringKeybase(db), nil + return newKeyringKeybase(db, opts...), nil } // CreateMnemonic generates a new key and persists it to storage, encrypted @@ -572,9 +575,9 @@ func fakePrompt(prompt string) (string, error) { return "test", nil } -func newKeyringKeybase(db keyring.Keyring) Keybase { +func newKeyringKeybase(db keyring.Keyring, opts ...KeybaseOption) Keybase { return keyringKeybase{ db: db, - base: baseKeybase{}, + base: newBaseKeybase(opts...), } } diff --git a/crypto/keys/lazy_keybase.go b/crypto/keys/lazy_keybase.go index f362c9a1a61e..0c9b61c8588d 100644 --- a/crypto/keys/lazy_keybase.go +++ b/crypto/keys/lazy_keybase.go @@ -14,17 +14,18 @@ var _ Keybase = lazyKeybase{} // NOTE: lazyKeybase will be deprecated in favor of lazyKeybaseKeyring. type lazyKeybase struct { - name string - dir string + name string + dir string + options []KeybaseOption } // New creates a new instance of a lazy keybase. -func New(name, dir string) Keybase { +func New(name, dir string, opts ...KeybaseOption) Keybase { if err := cmn.EnsureDir(dir, 0700); err != nil { panic(fmt.Sprintf("failed to create Keybase directory: %s", err)) } - return lazyKeybase{name: name, dir: dir} + return lazyKeybase{name: name, dir: dir, options: opts} } func (lkb lazyKeybase) List() ([]Info, error) { @@ -34,7 +35,7 @@ func (lkb lazyKeybase) List() ([]Info, error) { } defer db.Close() - return newDBKeybase(db).List() + return newDBKeybase(db, lkb.options...).List() } func (lkb lazyKeybase) Get(name string) (Info, error) { @@ -44,7 +45,7 @@ func (lkb lazyKeybase) Get(name string) (Info, error) { } defer db.Close() - return newDBKeybase(db).Get(name) + return newDBKeybase(db, lkb.options...).Get(name) } func (lkb lazyKeybase) GetByAddress(address sdk.AccAddress) (Info, error) { @@ -54,7 +55,7 @@ func (lkb lazyKeybase) GetByAddress(address sdk.AccAddress) (Info, error) { } defer db.Close() - return newDBKeybase(db).GetByAddress(address) + return newDBKeybase(db, lkb.options...).GetByAddress(address) } func (lkb lazyKeybase) Delete(name, passphrase string, skipPass bool) error { @@ -64,7 +65,7 @@ func (lkb lazyKeybase) Delete(name, passphrase string, skipPass bool) error { } defer db.Close() - return newDBKeybase(db).Delete(name, passphrase, skipPass) + return newDBKeybase(db, lkb.options...).Delete(name, passphrase, skipPass) } func (lkb lazyKeybase) Sign(name, passphrase string, msg []byte) ([]byte, crypto.PubKey, error) { @@ -74,7 +75,7 @@ func (lkb lazyKeybase) Sign(name, passphrase string, msg []byte) ([]byte, crypto } defer db.Close() - return newDBKeybase(db).Sign(name, passphrase, msg) + return newDBKeybase(db, lkb.options...).Sign(name, passphrase, msg) } func (lkb lazyKeybase) CreateMnemonic(name string, language Language, passwd string, algo SigningAlgo) (info Info, seed string, err error) { @@ -84,7 +85,7 @@ func (lkb lazyKeybase) CreateMnemonic(name string, language Language, passwd str } defer db.Close() - return newDBKeybase(db).CreateMnemonic(name, language, passwd, algo) + return newDBKeybase(db, lkb.options...).CreateMnemonic(name, language, passwd, algo) } func (lkb lazyKeybase) CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd string, account uint32, index uint32) (Info, error) { @@ -94,7 +95,8 @@ func (lkb lazyKeybase) CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd } defer db.Close() - return newDBKeybase(db).CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd, account, index) + return newDBKeybase(db, + lkb.options...).CreateAccount(name, mnemonic, bip39Passwd, encryptPasswd, account, index) } func (lkb lazyKeybase) Derive(name, mnemonic, bip39Passwd, encryptPasswd string, params hd.BIP44Params) (Info, error) { @@ -104,7 +106,7 @@ func (lkb lazyKeybase) Derive(name, mnemonic, bip39Passwd, encryptPasswd string, } defer db.Close() - return newDBKeybase(db).Derive(name, mnemonic, bip39Passwd, encryptPasswd, params) + return newDBKeybase(db, lkb.options...).Derive(name, mnemonic, bip39Passwd, encryptPasswd, params) } func (lkb lazyKeybase) CreateLedger(name string, algo SigningAlgo, hrp string, account, index uint32) (info Info, err error) { @@ -114,7 +116,7 @@ func (lkb lazyKeybase) CreateLedger(name string, algo SigningAlgo, hrp string, a } defer db.Close() - return newDBKeybase(db).CreateLedger(name, algo, hrp, account, index) + return newDBKeybase(db, lkb.options...).CreateLedger(name, algo, hrp, account, index) } func (lkb lazyKeybase) CreateOffline(name string, pubkey crypto.PubKey) (info Info, err error) { @@ -124,7 +126,7 @@ func (lkb lazyKeybase) CreateOffline(name string, pubkey crypto.PubKey) (info In } defer db.Close() - return newDBKeybase(db).CreateOffline(name, pubkey) + return newDBKeybase(db, lkb.options...).CreateOffline(name, pubkey) } func (lkb lazyKeybase) CreateMulti(name string, pubkey crypto.PubKey) (info Info, err error) { @@ -134,7 +136,7 @@ func (lkb lazyKeybase) CreateMulti(name string, pubkey crypto.PubKey) (info Info } defer db.Close() - return newDBKeybase(db).CreateMulti(name, pubkey) + return newDBKeybase(db, lkb.options...).CreateMulti(name, pubkey) } func (lkb lazyKeybase) Update(name, oldpass string, getNewpass func() (string, error)) error { @@ -144,7 +146,7 @@ func (lkb lazyKeybase) Update(name, oldpass string, getNewpass func() (string, e } defer db.Close() - return newDBKeybase(db).Update(name, oldpass, getNewpass) + return newDBKeybase(db, lkb.options...).Update(name, oldpass, getNewpass) } func (lkb lazyKeybase) Import(name string, armor string) (err error) { @@ -154,7 +156,7 @@ func (lkb lazyKeybase) Import(name string, armor string) (err error) { } defer db.Close() - return newDBKeybase(db).Import(name, armor) + return newDBKeybase(db, lkb.options...).Import(name, armor) } func (lkb lazyKeybase) ImportPrivKey(name string, armor string, passphrase string) error { @@ -164,7 +166,7 @@ func (lkb lazyKeybase) ImportPrivKey(name string, armor string, passphrase strin } defer db.Close() - return newDBKeybase(db).ImportPrivKey(name, armor, passphrase) + return newDBKeybase(db, lkb.options...).ImportPrivKey(name, armor, passphrase) } func (lkb lazyKeybase) ImportPubKey(name string, armor string) (err error) { @@ -174,7 +176,7 @@ func (lkb lazyKeybase) ImportPubKey(name string, armor string) (err error) { } defer db.Close() - return newDBKeybase(db).ImportPubKey(name, armor) + return newDBKeybase(db, lkb.options...).ImportPubKey(name, armor) } func (lkb lazyKeybase) Export(name string) (armor string, err error) { @@ -184,7 +186,7 @@ func (lkb lazyKeybase) Export(name string) (armor string, err error) { } defer db.Close() - return newDBKeybase(db).Export(name) + return newDBKeybase(db, lkb.options...).Export(name) } func (lkb lazyKeybase) ExportPubKey(name string) (armor string, err error) { @@ -194,7 +196,7 @@ func (lkb lazyKeybase) ExportPubKey(name string) (armor string, err error) { } defer db.Close() - return newDBKeybase(db).ExportPubKey(name) + return newDBKeybase(db, lkb.options...).ExportPubKey(name) } func (lkb lazyKeybase) ExportPrivateKeyObject(name string, passphrase string) (crypto.PrivKey, error) { @@ -204,7 +206,7 @@ func (lkb lazyKeybase) ExportPrivateKeyObject(name string, passphrase string) (c } defer db.Close() - return newDBKeybase(db).ExportPrivateKeyObject(name, passphrase) + return newDBKeybase(db, lkb.options...).ExportPrivateKeyObject(name, passphrase) } func (lkb lazyKeybase) ExportPrivKey(name string, decryptPassphrase string, @@ -216,7 +218,7 @@ func (lkb lazyKeybase) ExportPrivKey(name string, decryptPassphrase string, } defer db.Close() - return newDBKeybase(db).ExportPrivKey(name, decryptPassphrase, encryptPassphrase) + return newDBKeybase(db, lkb.options...).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 1dabc9b8ebe8..31a9cf242f72 100644 --- a/crypto/keys/lazy_keybase_test.go +++ b/crypto/keys/lazy_keybase_test.go @@ -5,9 +5,12 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + amino "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + tmamino "github.com/tendermint/tendermint/crypto/encoding/amino" + "github.com/cosmos/cosmos-sdk/codec" "github.com/cosmos/cosmos-sdk/crypto/keys/hd" "github.com/cosmos/cosmos-sdk/tests" sdk "github.com/cosmos/cosmos-sdk/types" @@ -371,3 +374,77 @@ func TestLazySeedPhrase(t *testing.T) { require.Equal(t, info.GetPubKey().Address(), newInfo.GetPubKey().Address()) require.Equal(t, info.GetPubKey(), newInfo.GetPubKey()) } + +var _ crypto.PrivKey = testPriv{} +var _ crypto.PubKey = testPub{} +var testCdc *amino.Codec + +type testPriv []byte + +func (privkey testPriv) PubKey() crypto.PubKey { return testPub{} } +func (privkey testPriv) Bytes() []byte { + return testCdc.MustMarshalBinaryBare(privkey) +} +func (privkey testPriv) Sign(msg []byte) ([]byte, error) { return []byte{}, nil } +func (privkey testPriv) Equals(other crypto.PrivKey) bool { return true } + +type testPub []byte + +func (key testPub) Address() crypto.Address { return crypto.Address{} } +func (key testPub) Bytes() []byte { + return testCdc.MustMarshalBinaryBare(key) +} +func (key testPub) VerifyBytes(msg []byte, sig []byte) bool { return true } +func (key testPub) Equals(other crypto.PubKey) bool { return true } + +func TestKeygenOverride(t *testing.T) { + dir, cleanup := tests.NewTestCaseDir(t) + defer cleanup() + + // Save existing codec and reset after test + cryptoCdc := CryptoCdc + defer func() { + CryptoCdc = cryptoCdc + }() + + // Setup testCdc encoding and decoding new key type + testCdc = codec.New() + RegisterCodec(testCdc) + tmamino.RegisterAmino(testCdc) + + // Set up codecs for using new key types + privName, pubName := "test/priv_name", "test/pub_name" + tmamino.RegisterKeyType(testPriv{}, privName) + tmamino.RegisterKeyType(testPub{}, pubName) + testCdc.RegisterConcrete(testPriv{}, privName, nil) + testCdc.RegisterConcrete(testPub{}, pubName, nil) + CryptoCdc = testCdc + + overrideCalled := false + dummyFunc := func(bz [32]byte) crypto.PrivKey { + overrideCalled = true + return testPriv(bz[:]) + } + + kb := New("keybasename", dir, WithKeygenFunc(dummyFunc)) + + testName, pw := "name", "testPassword" + + // create new key which will generate with + info, _, err := kb.CreateMnemonic(testName, English, pw, Secp256k1) + require.NoError(t, err) + require.Equal(t, info.GetName(), testName) + + // Assert overridden function was called + require.True(t, overrideCalled) + + // export private key object + exported, err := kb.ExportPrivateKeyObject(testName, pw) + require.Nil(t, err, "%+v", err) + + // require that the key type is the new key + _, ok := exported.(testPriv) + require.True(t, ok) + + require.True(t, exported.PubKey().Equals(info.GetPubKey())) +} diff --git a/crypto/keys/types.go b/crypto/keys/types.go index d968dce59716..739250f98ef8 100644 --- a/crypto/keys/types.go +++ b/crypto/keys/types.go @@ -306,11 +306,19 @@ func (i multiInfo) GetPath() (*hd.BIP44Params, error) { // encoding info func marshalInfo(i Info) []byte { - return cdc.MustMarshalBinaryLengthPrefixed(i) + return CryptoCdc.MustMarshalBinaryLengthPrefixed(i) } // decoding info func unmarshalInfo(bz []byte) (info Info, err error) { - err = cdc.UnmarshalBinaryLengthPrefixed(bz, &info) + err = CryptoCdc.UnmarshalBinaryLengthPrefixed(bz, &info) return } + +type ( + // PrivKeyGenFunc defines the function to convert derived key bytes to a tendermint private key + PrivKeyGenFunc func(bz [32]byte) crypto.PrivKey + + // KeybaseOption overrides options for the db + KeybaseOption func(*kbOptions) +)