diff --git a/cli/wallet/wallet.go b/cli/wallet/wallet.go index be36f4016..0d62f1840 100644 --- a/cli/wallet/wallet.go +++ b/cli/wallet/wallet.go @@ -105,6 +105,18 @@ func NewCommands() []cli.Command { }, }, }, + { + Name: "change-password", + Usage: "change password for accounts", + Action: changePassword, + Flags: []cli.Flag{ + walletPathFlag, + flags.AddressFlag{ + Name: "address, a", + Usage: "address to change password for", + }, + }, + }, { Name: "convert", Usage: "convert addresses from existing NEO2 NEP6-wallet to NEO3 format", @@ -289,6 +301,55 @@ func claimGas(ctx *cli.Context) error { return nil } +func changePassword(ctx *cli.Context) error { + wall, err := openWallet(ctx.String("wallet")) + if err != nil { + return cli.NewExitError(err, 1) + } + addrFlag := ctx.Generic("address").(*flags.Address) + if addrFlag.IsSet { + // Check for account presence first before asking for password. + acc := wall.GetAccount(addrFlag.Uint160()) + if acc == nil { + return cli.NewExitError("account is missing", 1) + } + } + + oldPass, err := input.ReadPassword("Enter password > ") + if err != nil { + return cli.NewExitError(fmt.Errorf("Error reading old password: %w", err), 1) + } + + for i := range wall.Accounts { + if addrFlag.IsSet && wall.Accounts[i].Address != addrFlag.String() { + continue + } + err := wall.Accounts[i].Decrypt(oldPass, wall.Scrypt) + if err != nil { + return cli.NewExitError(fmt.Errorf("unable to decrypt account %s: %w", wall.Accounts[i].Address, err), 1) + } + } + + pass, err := readNewPassword() + if err != nil { + return cli.NewExitError(fmt.Errorf("Error reading new password: %w", err), 1) + } + for i := range wall.Accounts { + if addrFlag.IsSet && wall.Accounts[i].Address != addrFlag.String() { + continue + } + err := wall.Accounts[i].Encrypt(pass, wall.Scrypt) + if err != nil { + return cli.NewExitError(err, 1) + } + } + err = wall.Save() + if err != nil { + return cli.NewExitError(fmt.Errorf("Error saving the wallet: %w", err), 1) + } + return nil +} + func convertWallet(ctx *cli.Context) error { wall, err := newWalletV2FromFile(ctx.String("wallet")) if err != nil { @@ -656,22 +717,31 @@ func createWallet(ctx *cli.Context) error { } func readAccountInfo() (string, string, error) { - rawName, _ := input.ReadLine("Enter the name of the account > ") + name, err := input.ReadLine("Enter the name of the account > ") + if err != nil { + return "", "", err + } + phrase, err := readNewPassword() + if err != nil { + return "", "", err + } + return name, phrase, nil +} + +func readNewPassword() (string, error) { phrase, err := input.ReadPassword("Enter passphrase > ") if err != nil { - return "", "", fmt.Errorf("Error reading password: %w", err) + return "", fmt.Errorf("Error reading password: %w", err) } phraseCheck, err := input.ReadPassword("Confirm passphrase > ") if err != nil { - return "", "", fmt.Errorf("Error reading password: %w", err) + return "", fmt.Errorf("Error reading password: %w", err) } if phrase != phraseCheck { - return "", "", errPhraseMismatch + return "", errPhraseMismatch } - - name := strings.TrimRight(rawName, "\n") - return name, phrase, nil + return phrase, nil } func createAccount(wall *wallet.Wallet) error { diff --git a/cli/wallet_test.go b/cli/wallet_test.go index d3ee32da9..dc36cb5f0 100644 --- a/cli/wallet_test.go +++ b/cli/wallet_test.go @@ -45,6 +45,72 @@ func TestWalletAccountRemove(t *testing.T) { require.NoError(t, json.Unmarshal(rawWallet, new(wallet.Wallet))) } +func TestWalletChangePassword(t *testing.T) { + tmpDir := t.TempDir() + e := newExecutor(t, false) + + walletPath := filepath.Join(tmpDir, "wallet.json") + e.In.WriteString("acc1\r") + e.In.WriteString("pass\r") + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "init", "--wallet", walletPath, "--account") + + e.In.WriteString("acc2\r") + e.In.WriteString("pass\r") + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "create", "--wallet", walletPath) + + w, err := wallet.NewWalletFromFile(walletPath) + require.NoError(t, err) + + addr1 := w.Accounts[0].Address + addr2 := w.Accounts[1].Address + t.Run("bad old password", func(t *testing.T) { + e.In.WriteString("ssap\r") + e.In.WriteString("aaa\r") // Pretend for the password to be fine. + e.In.WriteString("aaa\r") + + e.RunWithError(t, "neo-go", "wallet", "change-password", "--wallet", walletPath) + }) + t.Run("no account", func(t *testing.T) { + e.RunWithError(t, "neo-go", "wallet", "change-password", "--wallet", walletPath, "--address", "NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq") + }) + t.Run("bad new password, multiaccount", func(t *testing.T) { + e.In.WriteString("pass\r") + e.In.WriteString("pass1\r") + e.In.WriteString("pass2\r") + e.RunWithError(t, "neo-go", "wallet", "change-password", "--wallet", walletPath) + }) + t.Run("good, multiaccount", func(t *testing.T) { + e.In.WriteString("pass\r") + e.In.WriteString("asdf\r") + e.In.WriteString("asdf\r") + e.Run(t, "neo-go", "wallet", "change-password", "--wallet", walletPath) + }) + t.Run("good, single account", func(t *testing.T) { + e.In.WriteString("asdf\r") + e.In.WriteString("jkl\r") + e.In.WriteString("jkl\r") + e.Run(t, "neo-go", "wallet", "change-password", "--wallet", walletPath, "--address", addr1) + }) + t.Run("bad, different passwords", func(t *testing.T) { + e.In.WriteString("jkl\r") + e.RunWithError(t, "neo-go", "wallet", "change-password", "--wallet", walletPath) + }) + t.Run("good, second account", func(t *testing.T) { + e.In.WriteString("asdf\r") + e.In.WriteString("jkl\r") + e.In.WriteString("jkl\r") + e.Run(t, "neo-go", "wallet", "change-password", "--wallet", walletPath, "--address", addr2) + }) + t.Run("good, second multiaccount", func(t *testing.T) { + e.In.WriteString("jkl\r") + e.In.WriteString("pass\r") + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "change-password", "--wallet", walletPath) + }) +} + func TestWalletInit(t *testing.T) { tmpDir := t.TempDir() e := newExecutor(t, false)