From 641d2fd91f03191016f74ea32a0ae9592b884896 Mon Sep 17 00:00:00 2001 From: Jeff Thompson Date: Wed, 16 Oct 2024 14:51:17 +0200 Subject: [PATCH] feat(tm2/crypto/keys): In the gnokey CLI, add command to update the password (#2700) The `Keybase` API supports a method to [change the password of a key](https://github.com/gnolang/gno/blob/8a62a28f672d3311163bee75f5e8f10ba3d4d52b/tm2/pkg/crypto/keys/keybase.go#L450). It is currently called `Update` which is confusing. This PR renames the API function to `Rotate` and adds the "rotate" command to the gnokey CLI. BREAKING CHANGE: The Keybase API function `Update` is renamed to `Rotate`. (Note: I haven't seen code using this function, so it should be minimal impact.)
Contributors' checklist... - [x] Added new tests, or not needed, or not feasible - [x] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [x] Updated the official documentation or not needed - [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description
--------- Signed-off-by: Jeff Thompson Co-authored-by: Morgan --- .../cli/gnokey/working-with-key-pairs.md | 13 +++ gno.land/pkg/keyscli/root.go | 1 + tm2/pkg/crypto/keys/client/root.go | 1 + tm2/pkg/crypto/keys/client/rotate.go | 75 +++++++++++++++ tm2/pkg/crypto/keys/client/rotate_test.go | 95 +++++++++++++++++++ tm2/pkg/crypto/keys/keybase.go | 4 +- tm2/pkg/crypto/keys/keybase_test.go | 12 +-- tm2/pkg/crypto/keys/lazy_keybase.go | 4 +- tm2/pkg/crypto/keys/types.go | 2 +- 9 files changed, 196 insertions(+), 11 deletions(-) create mode 100644 tm2/pkg/crypto/keys/client/rotate.go create mode 100644 tm2/pkg/crypto/keys/client/rotate_test.go diff --git a/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md b/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md index ba03ca569b4..9bc29da6a18 100644 --- a/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md +++ b/docs/gno-tooling/cli/gnokey/working-with-key-pairs.md @@ -38,6 +38,7 @@ gno.land keychain & client SUBCOMMANDS add adds key to the keybase delete deletes a key from the keybase + rotate rotate the password of a key in the keybase to a new password generate generates a bip39 mnemonic export exports private key armor import imports encrypted private key armor @@ -161,6 +162,18 @@ you can recover it using the key's mnemonic, or by importing it if it was export at a previous point in time. ::: + +## Rotating the password of a private key to a new password +To rotate the password of a private key from the `gnokey` keystore to a new password, we need to know the name or +address of the key to remove. +After we have this information, we can run the following command: + +```bash +gnokey rotate MyKey +``` + +After entering the current key decryption password and the new password, the password of the key will be updated in the keystore. + ## Exporting a private key Private keys stored in the `gnokey` keystore can be exported to a desired place on the user's filesystem. diff --git a/gno.land/pkg/keyscli/root.go b/gno.land/pkg/keyscli/root.go index 19513fc0de6..c910e01b82c 100644 --- a/gno.land/pkg/keyscli/root.go +++ b/gno.land/pkg/keyscli/root.go @@ -30,6 +30,7 @@ func NewRootCmd(io commands.IO, base client.BaseOptions) *commands.Command { cmd.AddSubCommands( client.NewAddCmd(cfg, io), client.NewDeleteCmd(cfg, io), + client.NewRotateCmd(cfg, io), client.NewGenerateCmd(cfg, io), client.NewExportCmd(cfg, io), client.NewImportCmd(cfg, io), diff --git a/tm2/pkg/crypto/keys/client/root.go b/tm2/pkg/crypto/keys/client/root.go index f69155ace85..8dcd9210a50 100644 --- a/tm2/pkg/crypto/keys/client/root.go +++ b/tm2/pkg/crypto/keys/client/root.go @@ -43,6 +43,7 @@ func NewRootCmdWithBaseConfig(io commands.IO, base BaseOptions) *commands.Comman NewExportCmd(cfg, io), NewImportCmd(cfg, io), NewListCmd(cfg, io), + NewRotateCmd(cfg, io), NewSignCmd(cfg, io), NewVerifyCmd(cfg, io), NewQueryCmd(cfg, io), diff --git a/tm2/pkg/crypto/keys/client/rotate.go b/tm2/pkg/crypto/keys/client/rotate.go new file mode 100644 index 00000000000..876e9f40b70 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/rotate.go @@ -0,0 +1,75 @@ +package client + +import ( + "context" + "flag" + "fmt" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" +) + +type RotateCfg struct { + RootCfg *BaseCfg + + Force bool +} + +func NewRotateCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { + cfg := &RotateCfg{ + RootCfg: rootCfg, + } + + return commands.NewCommand( + commands.Metadata{ + Name: "rotate", + ShortUsage: "rotate [flags] ", + ShortHelp: "rotate the password of a key in the keybase to a new password", + }, + cfg, + func(_ context.Context, args []string) error { + return execRotate(cfg, args, io) + }, + ) +} + +func (c *RotateCfg) RegisterFlags(fs *flag.FlagSet) { +} + +func execRotate(cfg *RotateCfg, args []string, io commands.IO) error { + if len(args) != 1 { + return flag.ErrHelp + } + + nameOrBech32 := args[0] + + kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.Home) + if err != nil { + return err + } + + oldpass, err := io.GetPassword("Enter the current password:", cfg.RootCfg.InsecurePasswordStdin) + if err != nil { + return err + } + + newpass, err := io.GetCheckPassword( + [2]string{ + "Enter the new password to encrypt your key to disk:", + "Repeat the password:", + }, + cfg.RootCfg.InsecurePasswordStdin, + ) + if err != nil { + return fmt.Errorf("unable to parse provided password, %w", err) + } + + getNewpass := func() (string, error) { return newpass, nil } + err = kb.Rotate(nameOrBech32, oldpass, getNewpass) + if err != nil { + return err + } + io.ErrPrintln("Password rotated") + + return nil +} diff --git a/tm2/pkg/crypto/keys/client/rotate_test.go b/tm2/pkg/crypto/keys/client/rotate_test.go new file mode 100644 index 00000000000..f365359d943 --- /dev/null +++ b/tm2/pkg/crypto/keys/client/rotate_test.go @@ -0,0 +1,95 @@ +package client + +import ( + "strings" + "testing" + + "github.com/gnolang/gno/tm2/pkg/commands" + "github.com/gnolang/gno/tm2/pkg/crypto/keys" + "github.com/gnolang/gno/tm2/pkg/testutils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_execRotate(t *testing.T) { + t.Parallel() + + // make new test dir + kbHome, kbCleanUp := testutils.NewTestCaseDir(t) + defer kbCleanUp() + + // initialize test options + cfg := &RotateCfg{ + RootCfg: &BaseCfg{ + BaseOptions: BaseOptions{ + Home: kbHome, + InsecurePasswordStdin: true, + }, + }, + } + + io := commands.NewTestIO() + + // Add test accounts to keybase. + kb, err := keys.NewKeyBaseFromDir(kbHome) + assert.NoError(t, err) + + keyName := "rotateApp_Key1" + p1, p2 := "1234", "foobar" + mnemonic := "equip will roof matter pink blind book anxiety banner elbow sun young" + + _, err = kb.CreateAccount(keyName, mnemonic, "", p1, 0, 0) + assert.NoError(t, err) + + { + // test: Key not found + args := []string{"blah"} + io.SetIn(strings.NewReader(p1 + "\n" + p2 + "\n" + p2 + "\n")) + err = execRotate(cfg, args, io) + require.Error(t, err) + require.Equal(t, "Key blah not found", err.Error()) + } + + { + // test: Wrong password + args := []string{keyName} + io.SetIn(strings.NewReader("blah" + "\n" + p2 + "\n" + p2 + "\n")) + err = execRotate(cfg, args, io) + require.Error(t, err) + require.Equal(t, "invalid account password", err.Error()) + } + + { + // test: New passwords don't match + args := []string{keyName} + io.SetIn(strings.NewReader(p1 + "\n" + p2 + "\n" + "blah" + "\n")) + err = execRotate(cfg, args, io) + require.Error(t, err) + require.Equal(t, "unable to parse provided password, passphrases don't match", err.Error()) + } + + { + // Rotate the password + args := []string{keyName} + io.SetIn(strings.NewReader(p1 + "\n" + p2 + "\n" + p2 + "\n")) + err = execRotate(cfg, args, io) + require.NoError(t, err) + } + + { + // test: The old password shouldn't work + args := []string{keyName} + io.SetIn(strings.NewReader(p1 + "\n" + p1 + "\n" + p1 + "\n")) + err = execRotate(cfg, args, io) + require.Error(t, err) + require.Equal(t, "invalid account password", err.Error()) + } + + { + // Updating the new password to itself should work + args := []string{keyName} + io.SetIn(strings.NewReader(p2 + "\n" + p2 + "\n" + p2 + "\n")) + err = execRotate(cfg, args, io) + require.NoError(t, err) + } +} diff --git a/tm2/pkg/crypto/keys/keybase.go b/tm2/pkg/crypto/keys/keybase.go index 2dc7d41be0b..c28fd1ef952 100644 --- a/tm2/pkg/crypto/keys/keybase.go +++ b/tm2/pkg/crypto/keys/keybase.go @@ -441,13 +441,13 @@ func (kb dbKeybase) Delete(nameOrBech32, passphrase string, skipPass bool) error return nil } -// Update changes the passphrase with which an already stored key is +// Rotate changes the passphrase with which an already stored key is // encrypted. // // oldpass must be the current passphrase used for encryption, // getNewpass is a function to get the passphrase to permanently replace // the current passphrase -func (kb dbKeybase) Update(nameOrBech32, oldpass string, getNewpass func() (string, error)) error { +func (kb dbKeybase) Rotate(nameOrBech32, oldpass string, getNewpass func() (string, error)) error { info, err := kb.GetByNameOrAddress(nameOrBech32) if err != nil { return err diff --git a/tm2/pkg/crypto/keys/keybase_test.go b/tm2/pkg/crypto/keys/keybase_test.go index 32cc8788b52..afcc1c56197 100644 --- a/tm2/pkg/crypto/keys/keybase_test.go +++ b/tm2/pkg/crypto/keys/keybase_test.go @@ -199,9 +199,9 @@ func assertPassword(t *testing.T, cstore Keybase, name, pass, badpass string) { t.Helper() getNewpass := func() (string, error) { return pass, nil } - err := cstore.Update(name, badpass, getNewpass) + err := cstore.Rotate(name, badpass, getNewpass) require.NotNil(t, err) - err = cstore.Update(name, pass, getNewpass) + err = cstore.Rotate(name, pass, getNewpass) require.Nil(t, err, "%+v", err) } @@ -280,7 +280,7 @@ func TestExportImportPubKey(t *testing.T) { require.NotNil(t, err) } -// TestAdvancedKeyManagement verifies update, import, export functionality +// TestAdvancedKeyManagement verifies rotate, import, export functionality func TestAdvancedKeyManagement(t *testing.T) { t.Parallel() @@ -297,14 +297,14 @@ func TestAdvancedKeyManagement(t *testing.T) { require.Nil(t, err, "%+v", err) assertPassword(t, cstore, n1, p1, p2) - // update password requires the existing password + // rotate password requires the existing password getNewpass := func() (string, error) { return p2, nil } - err = cstore.Update(n1, "jkkgkg", getNewpass) + err = cstore.Rotate(n1, "jkkgkg", getNewpass) require.NotNil(t, err) assertPassword(t, cstore, n1, p1, p2) // then it changes the password when correct - err = cstore.Update(n1, p1, getNewpass) + err = cstore.Rotate(n1, p1, getNewpass) require.NoError(t, err) // p2 is now the proper one! assertPassword(t, cstore, n1, p2, p1) diff --git a/tm2/pkg/crypto/keys/lazy_keybase.go b/tm2/pkg/crypto/keys/lazy_keybase.go index eb9c0f3b551..38cec501135 100644 --- a/tm2/pkg/crypto/keys/lazy_keybase.go +++ b/tm2/pkg/crypto/keys/lazy_keybase.go @@ -179,14 +179,14 @@ func (lkb lazyKeybase) CreateMulti(name string, pubkey crypto.PubKey) (info Info return NewDBKeybase(db).CreateMulti(name, pubkey) } -func (lkb lazyKeybase) Update(name, oldpass string, getNewpass func() (string, error)) error { +func (lkb lazyKeybase) Rotate(name, oldpass string, getNewpass func() (string, error)) error { db, err := db.NewDB(lkb.name, dbBackend, lkb.dir) if err != nil { return err } defer db.Close() - return NewDBKeybase(db).Update(name, oldpass, getNewpass) + return NewDBKeybase(db).Rotate(name, oldpass, getNewpass) } func (lkb lazyKeybase) Import(name string, armor string) (err error) { diff --git a/tm2/pkg/crypto/keys/types.go b/tm2/pkg/crypto/keys/types.go index c5d33023a0a..3865951168e 100644 --- a/tm2/pkg/crypto/keys/types.go +++ b/tm2/pkg/crypto/keys/types.go @@ -43,7 +43,7 @@ type Keybase interface { CreateMulti(name string, pubkey crypto.PubKey) (info Info, err error) // The following operations will *only* work on locally-stored keys - Update(name, oldpass string, getNewpass func() (string, error)) error + Rotate(name, oldpass string, getNewpass func() (string, error)) error Import(name string, armor string) (err error) ImportPrivKey(name, armor, decryptPassphrase, encryptPassphrase string) error ImportPrivKeyUnsafe(name, armor, encryptPassphrase string) error