Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: In the gnokey CLI, add command to update the password #2700

Merged
merged 8 commits into from
Oct 16, 2024
13 changes: 13 additions & 0 deletions docs/gno-tooling/cli/gnokey/working-with-key-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions gno.land/pkg/keyscli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
1 change: 1 addition & 0 deletions tm2/pkg/crypto/keys/client/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
75 changes: 75 additions & 0 deletions tm2/pkg/crypto/keys/client/rotate.go
Original file line number Diff line number Diff line change
@@ -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] <key-name>",
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)
},

Check warning on line 32 in tm2/pkg/crypto/keys/client/rotate.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/crypto/keys/client/rotate.go#L31-L32

Added lines #L31 - L32 were not covered by tests
)
}

func (c *RotateCfg) RegisterFlags(fs *flag.FlagSet) {
}

func execRotate(cfg *RotateCfg, args []string, io commands.IO) error {
if len(args) != 1 {
return flag.ErrHelp

Check warning on line 41 in tm2/pkg/crypto/keys/client/rotate.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/crypto/keys/client/rotate.go#L41

Added line #L41 was not covered by tests
}

nameOrBech32 := args[0]

kb, err := keys.NewKeyBaseFromDir(cfg.RootCfg.Home)
if err != nil {
return err

Check warning on line 48 in tm2/pkg/crypto/keys/client/rotate.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/crypto/keys/client/rotate.go#L48

Added line #L48 was not covered by tests
}

oldpass, err := io.GetPassword("Enter the current password:", cfg.RootCfg.InsecurePasswordStdin)
if err != nil {
return err

Check warning on line 53 in tm2/pkg/crypto/keys/client/rotate.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/crypto/keys/client/rotate.go#L53

Added line #L53 was not covered by tests
}

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
}
95 changes: 95 additions & 0 deletions tm2/pkg/crypto/keys/client/rotate_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
4 changes: 2 additions & 2 deletions tm2/pkg/crypto/keys/keybase.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
12 changes: 6 additions & 6 deletions tm2/pkg/crypto/keys/keybase_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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()

Expand All @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions tm2/pkg/crypto/keys/lazy_keybase.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,14 @@
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 {

Check warning on line 182 in tm2/pkg/crypto/keys/lazy_keybase.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/crypto/keys/lazy_keybase.go#L182

Added line #L182 was not covered by tests
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)

Check warning on line 189 in tm2/pkg/crypto/keys/lazy_keybase.go

View check run for this annotation

Codecov / codecov/patch

tm2/pkg/crypto/keys/lazy_keybase.go#L189

Added line #L189 was not covered by tests
}

func (lkb lazyKeybase) Import(name string, armor string) (err error) {
Expand Down
2 changes: 1 addition & 1 deletion tm2/pkg/crypto/keys/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading