diff --git a/accounts/testdata/dupes/1 b/accounts/testdata/dupes/1 new file mode 100644 index 0000000000..a3868ec6d5 --- /dev/null +++ b/accounts/testdata/dupes/1 @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/dupes/2 b/accounts/testdata/dupes/2 new file mode 100644 index 0000000000..a3868ec6d5 --- /dev/null +++ b/accounts/testdata/dupes/2 @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/dupes/foo b/accounts/testdata/dupes/foo new file mode 100644 index 0000000000..c57060aea0 --- /dev/null +++ b/accounts/testdata/dupes/foo @@ -0,0 +1 @@ +{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go index 86175d05f6..0bd60d701e 100644 --- a/cmd/geth/accountcmd.go +++ b/cmd/geth/accountcmd.go @@ -181,10 +181,19 @@ func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i for trials := 0; trials < 3; trials++ { prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) password := getPassPhrase(prompt, false, i, passwords) - if err = accman.Unlock(account, password); err == nil { + err = accman.Unlock(account, password) + if err == nil { glog.V(logger.Info).Infof("Unlocked account %x", account.Address) return account, password } + if err, ok := err.(*accounts.AmbiguousAddrError); ok { + glog.V(logger.Info).Infof("Unlocked account %x", account.Address) + return ambiguousAddrRecovery(accman, err, password), password + } + if err != accounts.ErrDecrypt { + // No need to prompt again if the error is not decryption-related. + break + } } // All trials expended to unlock account, bail out utils.Fatalf("Failed to unlock account %s (%v)", address, err) @@ -221,6 +230,32 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) return password } +func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account { + fmt.Printf("Multiple key files exist for address %x:\n", err.Addr) + for _, a := range err.Matches { + fmt.Println(" ", a.File) + } + fmt.Println("Testing your passphrase against all of them...") + var match *accounts.Account + for _, a := range err.Matches { + if err := am.Unlock(a, auth); err == nil { + match = &a + break + } + } + if match == nil { + utils.Fatalf("None of the listed files could be unlocked.") + } + fmt.Printf("Your passphrase unlocked %s\n", match.File) + fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:") + for _, a := range err.Matches { + if a != *match { + fmt.Println(" ", a.File) + } + } + return *match +} + // accountCreate creates a new account into the keystore defined by the CLI flags. func accountCreate(ctx *cli.Context) { accman := utils.MakeAccountManager(ctx) diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go index 440a0cb0b3..b6abde6d89 100644 --- a/cmd/geth/accountcmd_test.go +++ b/cmd/geth/accountcmd_test.go @@ -228,3 +228,65 @@ func TestUnlockFlagPasswordFileWrongPassword(t *testing.T) { Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase) `) } + +func TestUnlockFlagAmbiguous(t *testing.T) { + store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + geth := runGeth(t, + "--keystore", store, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "f466859ead1932d743d622cb74fc058882e8648a", + "js", "testdata/empty.js") + defer geth.expectExit() + + // Helper for the expect template, returns absolute keystore path. + geth.setTemplateFunc("keypath", func(file string) string { + abs, _ := filepath.Abs(filepath.Join(store, file)) + return abs + }) + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "foobar"}} +Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: + {{keypath "1"}} + {{keypath "2"}} +Testing your passphrase against all of them... +Your passphrase unlocked {{keypath "1"}} +In order to avoid this warning, you need to remove the following duplicate key files: + {{keypath "2"}} +`) + geth.expectExit() + + wantMessages := []string{ + "Unlocked account f466859ead1932d743d622cb74fc058882e8648a", + } + for _, m := range wantMessages { + if strings.Index(geth.stderrText(), m) == -1 { + t.Errorf("stderr text does not contain %q", m) + } + } +} + +func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) { + store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + geth := runGeth(t, + "--keystore", store, "--nat", "none", "--nodiscover", "--dev", + "--unlock", "f466859ead1932d743d622cb74fc058882e8648a") + defer geth.expectExit() + + // Helper for the expect template, returns absolute keystore path. + geth.setTemplateFunc("keypath", func(file string) string { + abs, _ := filepath.Abs(filepath.Join(store, file)) + return abs + }) + geth.expect(` +Unlocking account f466859ead1932d743d622cb74fc058882e8648a | Attempt 1/3 +!! Unsupported terminal, password will be echoed. +Passphrase: {{.InputLine "wrong"}} +Multiple key files exist for address f466859ead1932d743d622cb74fc058882e8648a: + {{keypath "1"}} + {{keypath "2"}} +Testing your passphrase against all of them... +Fatal: None of the listed files could be unlocked. +`) + geth.expectExit() +} diff --git a/cmd/geth/run_test.go b/cmd/geth/run_test.go index a15d0bf28d..a82eb9d68a 100644 --- a/cmd/geth/run_test.go +++ b/cmd/geth/run_test.go @@ -45,6 +45,7 @@ type testgeth struct { // template variables for expect Datadir string Executable string + Func template.FuncMap removeDatadir bool cmd *exec.Cmd @@ -114,6 +115,13 @@ func (tt *testgeth) InputLine(s string) string { return "" } +func (tt *testgeth) setTemplateFunc(name string, fn interface{}) { + if tt.Func == nil { + tt.Func = make(map[string]interface{}) + } + tt.Func[name] = fn +} + // expect runs its argument as a template, then expects the // child process to output the result of the template within 5s. // @@ -121,7 +129,7 @@ func (tt *testgeth) InputLine(s string) string { // before matching. func (tt *testgeth) expect(tplsource string) { // Generate the expected output by running the template. - tpl := template.Must(template.New("").Parse(tplsource)) + tpl := template.Must(template.New("").Funcs(tt.Func).Parse(tplsource)) wantbuf := new(bytes.Buffer) if err := tpl.Execute(wantbuf, tt); err != nil { panic(err)