From 1d72aaa0cd3a94e95c892a8b8b88a8a1ef59847e Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 18 Jun 2015 15:12:39 +0100 Subject: [PATCH 1/8] simplify account unlocking --- accounts/account_manager.go | 98 ++++++++++++++++--------------------- accounts/accounts_test.go | 43 +++++++++++++++- 2 files changed, 85 insertions(+), 56 deletions(-) diff --git a/accounts/account_manager.go b/accounts/account_manager.go index 13f16296a..e79ec51a2 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -49,11 +49,6 @@ var ( ErrNoKeys = errors.New("no keys in store") ) -const ( - // Default unlock duration (in seconds) when an account is unlocked from the console - DefaultAccountUnlockDuration = 300 -) - type Account struct { Address common.Address } @@ -114,28 +109,58 @@ func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) return signature, err } -// TimedUnlock unlocks the account with the given address. -// When timeout has passed, the account will be locked again. +// unlock indefinitely +func (am *Manager) Unlock(addr common.Address, keyAuth string) error { + return am.TimedUnlock(addr, keyAuth, 0) +} + +// Unlock unlocks the account with the given address. The account +// stays unlocked for the duration of timeout +// it timeout is 0 the account is unlocked for the entire session func (am *Manager) TimedUnlock(addr common.Address, keyAuth string, timeout time.Duration) error { key, err := am.keyStore.GetKey(addr, keyAuth) if err != nil { return err } - u := am.addUnlocked(addr, key) - go am.dropLater(addr, u, timeout) + var u *unlocked + am.mutex.Lock() + defer am.mutex.Unlock() + var found bool + u, found = am.unlocked[addr] + if found { + // terminate dropLater for this key to avoid unexpected drops. + if u.abort != nil { + close(u.abort) + } + } + if timeout > 0 { + u = &unlocked{Key: key, abort: make(chan struct{})} + go am.expire(addr, u, timeout) + } else { + u = &unlocked{Key: key} + } + am.unlocked[addr] = u return nil } -// Unlock unlocks the account with the given address. The account -// stays unlocked until the program exits or until a TimedUnlock -// timeout (started after the call to Unlock) expires. -func (am *Manager) Unlock(addr common.Address, keyAuth string) error { - key, err := am.keyStore.GetKey(addr, keyAuth) - if err != nil { - return err +func (am *Manager) expire(addr common.Address, u *unlocked, timeout time.Duration) { + t := time.NewTimer(timeout) + defer t.Stop() + select { + case <-u.abort: + // just quit + case <-t.C: + am.mutex.Lock() + // only drop if it's still the same key instance that dropLater + // was launched with. we can check that using pointer equality + // because the map stores a new pointer every time the key is + // unlocked. + if am.unlocked[addr] == u { + zeroKey(u.PrivateKey) + delete(am.unlocked, addr) + } + am.mutex.Unlock() } - am.addUnlocked(addr, key) - return nil } func (am *Manager) NewAccount(auth string) (Account, error) { @@ -162,43 +187,6 @@ func (am *Manager) Accounts() ([]Account, error) { return accounts, err } -func (am *Manager) addUnlocked(addr common.Address, key *crypto.Key) *unlocked { - u := &unlocked{Key: key, abort: make(chan struct{})} - am.mutex.Lock() - prev, found := am.unlocked[addr] - if found { - // terminate dropLater for this key to avoid unexpected drops. - close(prev.abort) - // the key is zeroed here instead of in dropLater because - // there might not actually be a dropLater running for this - // key, i.e. when Unlock was used. - zeroKey(prev.PrivateKey) - } - am.unlocked[addr] = u - am.mutex.Unlock() - return u -} - -func (am *Manager) dropLater(addr common.Address, u *unlocked, timeout time.Duration) { - t := time.NewTimer(timeout) - defer t.Stop() - select { - case <-u.abort: - // just quit - case <-t.C: - am.mutex.Lock() - // only drop if it's still the same key instance that dropLater - // was launched with. we can check that using pointer equality - // because the map stores a new pointer every time the key is - // unlocked. - if am.unlocked[addr] == u { - zeroKey(u.PrivateKey) - delete(am.unlocked, addr) - } - am.mutex.Unlock() - } -} - // zeroKey zeroes a private key in memory. func zeroKey(k *ecdsa.PrivateKey) { b := k.D.Bits() diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 427114cbd..6065fa8e4 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -18,7 +18,7 @@ func TestSign(t *testing.T) { pass := "" // not used but required by API a1, err := am.NewAccount(pass) toSign := randentropy.GetEntropyCSPRNG(32) - am.Unlock(a1.Address, "") + am.Unlock(a1.Address, "", 0) _, err = am.Sign(a1, toSign) if err != nil { @@ -58,6 +58,47 @@ func TestTimedUnlock(t *testing.T) { if err != ErrLocked { t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } + +} + +func TestOverrideUnlock(t *testing.T) { + dir, ks := tmpKeyStore(t, crypto.NewKeyStorePassphrase) + defer os.RemoveAll(dir) + + am := NewManager(ks) + pass := "foo" + a1, err := am.NewAccount(pass) + toSign := randentropy.GetEntropyCSPRNG(32) + + // Unlock indefinitely + if err = am.Unlock(a1.Address, pass); err != nil { + t.Fatal(err) + } + + // Signing without passphrase works because account is temp unlocked + _, err = am.Sign(a1, toSign) + if err != nil { + t.Fatal("Signing shouldn't return an error after unlocking, got ", err) + } + + // reset unlock to a shorter period, invalidates the previous unlock + if err = am.TimedUnlock(a1.Address, pass, 100*time.Millisecond); err != nil { + t.Fatal(err) + } + + // Signing without passphrase still works because account is temp unlocked + _, err = am.Sign(a1, toSign) + if err != nil { + t.Fatal("Signing shouldn't return an error after unlocking, got ", err) + } + + // Signing fails again after automatic locking + time.Sleep(150 * time.Millisecond) + _, err = am.Sign(a1, toSign) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) + } + } func tmpKeyStore(t *testing.T, new func(string) crypto.KeyStore2) (string, crypto.KeyStore2) { From fc2e33c594449e38b90bad2bd7b5c50f03b7f69d Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 18 Jun 2015 16:20:00 +0100 Subject: [PATCH 2/8] unlock multiple passes and obsolete primary * multiple passwords allowed in password file * split on "\n", sideeffect: chop trailing slashes. fixes common mistake <(echo 'pass') * remove accounts.Primary method * do not fall back to primary account for mining --- accounts/account_manager.go | 13 ------------- accounts/accounts_test.go | 2 +- cmd/geth/js_test.go | 2 +- cmd/geth/main.go | 38 +++++++++++++++++++++---------------- cmd/utils/flags.go | 2 +- eth/backend.go | 11 ++--------- 6 files changed, 27 insertions(+), 41 deletions(-) diff --git a/accounts/account_manager.go b/accounts/account_manager.go index e79ec51a2..4519c8420 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -81,19 +81,6 @@ func (am *Manager) HasAccount(addr common.Address) bool { return false } -func (am *Manager) Primary() (addr common.Address, err error) { - addrs, err := am.keyStore.GetKeyAddresses() - if os.IsNotExist(err) { - return common.Address{}, ErrNoKeys - } else if err != nil { - return common.Address{}, err - } - if len(addrs) == 0 { - return common.Address{}, ErrNoKeys - } - return addrs[0], nil -} - func (am *Manager) DeleteAccount(address common.Address, auth string) error { return am.keyStore.DeleteKey(address, auth) } diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 6065fa8e4..8bd70880c 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -18,7 +18,7 @@ func TestSign(t *testing.T) { pass := "" // not used but required by API a1, err := am.NewAccount(pass) toSign := randentropy.GetEntropyCSPRNG(32) - am.Unlock(a1.Address, "", 0) + am.Unlock(a1.Address, "") _, err = am.Sign(a1, toSign) if err != nil { diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go index cfbe26bee..fc2444a7b 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -140,7 +140,7 @@ func TestAccounts(t *testing.T) { defer os.RemoveAll(tmp) checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`"]`) - checkEvalJSON(t, repl, `eth.coinbase`, `"`+testAddress+`"`) + checkEvalJSON(t, repl, `eth.coinbase`, `"`+common.Address{}.Hex()+`"`) val, err := repl.re.Run(`personal.newAccount("password")`) if err != nil { diff --git a/cmd/geth/main.go b/cmd/geth/main.go index be40d5137..b20c6d85d 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -153,9 +153,12 @@ Note that exporting your key in unencrypted format is NOT supported. Keys are stored under /keys. It is safe to transfer the entire directory or the individual keys therein -between ethereum nodes. +between ethereum nodes by simply copying. Make sure you backup your keys regularly. +In order to use your account to send transactions, you need to unlock them using the +'--unlock' option. The argument is a comma + And finally. DO NOT FORGET YOUR PASSWORD. `, Subcommands: []cli.Command{ @@ -430,7 +433,7 @@ func execJSFiles(ctx *cli.Context) { ethereum.WaitForShutdown() } -func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string) (passphrase string) { +func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string, i int) { var err error // Load startup keys. XXX we are going to need a different format @@ -441,7 +444,7 @@ func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string) (pass attempts := 3 for tries := 0; tries < attempts; tries++ { msg := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", account, tries+1, attempts) - passphrase = getPassPhrase(ctx, msg, false) + passphrase := getPassPhrase(ctx, msg, false, i) err = am.Unlock(common.HexToAddress(account), passphrase) if err == nil { break @@ -451,7 +454,6 @@ func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string) (pass utils.Fatalf("Unlock account failed '%v'", err) } fmt.Printf("Account '%s' unlocked.\n", account) - return } func blockRecovery(ctx *cli.Context) { @@ -492,16 +494,12 @@ func startEth(ctx *cli.Context, eth *eth.Ethereum) { account := ctx.GlobalString(utils.UnlockedAccountFlag.Name) accounts := strings.Split(account, " ") - for _, account := range accounts { + for i, account := range accounts { if len(account) > 0 { if account == "primary" { - primaryAcc, err := am.Primary() - if err != nil { - utils.Fatalf("no primary account: %v", err) - } - account = primaryAcc.Hex() + utils.Fatalf("the 'primary' keyword is deprecated. You can use indexes, but the indexes are not permanent, they can change if you add external keys, export your keys or copy your keystore to another node.") } - unlockAccount(ctx, am, account) + unlockAccount(ctx, am, account, i) } } // Start auxiliary services if enabled. @@ -535,7 +533,7 @@ func accountList(ctx *cli.Context) { } } -func getPassPhrase(ctx *cli.Context, desc string, confirmation bool) (passphrase string) { +func getPassPhrase(ctx *cli.Context, desc string, confirmation bool, i int) (passphrase string) { passfile := ctx.GlobalString(utils.PasswordFileFlag.Name) if len(passfile) == 0 { fmt.Println(desc) @@ -559,14 +557,22 @@ func getPassPhrase(ctx *cli.Context, desc string, confirmation bool) (passphrase if err != nil { utils.Fatalf("Unable to read password file '%s': %v", passfile, err) } - passphrase = string(passbytes) + // this is backwards compatible if the same password unlocks several accounts + // it also has the consequence that trailing newlines will not count as part + // of the password, so --password <(echo -n 'pass') will now work without -n + passphrases := strings.Split(string(passbytes), "\n") + if i >= len(passphrases) { + passphrase = passphrases[len(passphrases)-1] + } else { + passphrase = passphrases[i] + } } return } func accountCreate(ctx *cli.Context) { am := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase(ctx, "Your new account is locked with a password. Please give a password. Do not forget this password.", true) + passphrase := getPassPhrase(ctx, "Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0) acct, err := am.NewAccount(passphrase) if err != nil { utils.Fatalf("Could not create the account: %v", err) @@ -585,7 +591,7 @@ func importWallet(ctx *cli.Context) { } am := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase(ctx, "", false) + passphrase := getPassPhrase(ctx, "", false, 0) acct, err := am.ImportPreSaleKey(keyJson, passphrase) if err != nil { @@ -600,7 +606,7 @@ func accountImport(ctx *cli.Context) { utils.Fatalf("keyfile must be given as argument") } am := utils.MakeAccountManager(ctx) - passphrase := getPassPhrase(ctx, "Your new account is locked with a password. Please give a password. Do not forget this password.", true) + passphrase := getPassPhrase(ctx, "Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0) acct, err := am.Import(keyfile, passphrase) if err != nil { utils.Fatalf("Could not create the account: %v", err) diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 6f319eb40..7460f51e1 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -133,7 +133,7 @@ var ( UnlockedAccountFlag = cli.StringFlag{ Name: "unlock", - Usage: "Unlock the account given until this program exits (prompts for password). '--unlock primary' unlocks the primary account", + Usage: "Unlock the account given until this program exits (prompts for password). '--unlock n' unlocks the n-th account in order or creation.", Value: "", } PasswordFileFlag = cli.StringFlag{ diff --git a/eth/backend.go b/eth/backend.go index d6ad3381d..ce774ba1b 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -464,15 +464,8 @@ func (s *Ethereum) StartMining(threads int) error { func (s *Ethereum) Etherbase() (eb common.Address, err error) { eb = s.etherbase if (eb == common.Address{}) { - primary, err := s.accountManager.Primary() - if err != nil { - return eb, err - } - if (primary == common.Address{}) { - err = fmt.Errorf("no accounts found") - return eb, err - } - eb = primary + err = fmt.Errorf("no accounts found") + return eb, err } return eb, nil } From 65a26e40a886c48031a7936d3cc9bf341e7165f4 Mon Sep 17 00:00:00 2001 From: zelig Date: Wed, 17 Jun 2015 11:25:42 +0100 Subject: [PATCH 3/8] require explicit etherbase address for mining. Falling back to primary is risky given it is inconsistent if keys are imported/merged/created or copied/transfered --- eth/backend.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/eth/backend.go b/eth/backend.go index ce774ba1b..8195110de 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -464,10 +464,9 @@ func (s *Ethereum) StartMining(threads int) error { func (s *Ethereum) Etherbase() (eb common.Address, err error) { eb = s.etherbase if (eb == common.Address{}) { - err = fmt.Errorf("no accounts found") - return eb, err + err = fmt.Errorf("etherbase address must be explicitly specified") } - return eb, nil + return } func (s *Ethereum) StopMining() { s.miner.Stop() } From 09b69831758cb1001027fbb59dff9b3fbe20bbb2 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 21 Jun 2015 04:01:12 +0100 Subject: [PATCH 4/8] no primary when listing accounts --- cmd/geth/main.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index b20c6d85d..673a08d45 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -497,7 +497,7 @@ func startEth(ctx *cli.Context, eth *eth.Ethereum) { for i, account := range accounts { if len(account) > 0 { if account == "primary" { - utils.Fatalf("the 'primary' keyword is deprecated. You can use indexes, but the indexes are not permanent, they can change if you add external keys, export your keys or copy your keystore to another node.") + utils.Fatalf("the 'primary' keyword is deprecated. You can use integer indexes, but the indexes are not permanent, they can change if you add external keys, export your keys or copy your keystore to another node.") } unlockAccount(ctx, am, account, i) } @@ -526,10 +526,8 @@ func accountList(ctx *cli.Context) { if err != nil { utils.Fatalf("Could not list accounts: %v", err) } - name := "Primary" for i, acct := range accts { - fmt.Printf("%s #%d: %x\n", name, i, acct) - name = "Account" + fmt.Printf("Account #%d: %x\n", i, acct) } } From eb82ca4563cf80bef9b520673d3bd18283da3a1f Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 21 Jun 2015 20:33:51 +0100 Subject: [PATCH 5/8] rpc/js coinbase returns null if no etherbase set --- cmd/geth/js_test.go | 4 +--- xeth/xeth.go | 5 ++++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go index fc2444a7b..5bdfb7048 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -140,7 +140,7 @@ func TestAccounts(t *testing.T) { defer os.RemoveAll(tmp) checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`"]`) - checkEvalJSON(t, repl, `eth.coinbase`, `"`+common.Address{}.Hex()+`"`) + checkEvalJSON(t, repl, `eth.coinbase`, `null`) val, err := repl.re.Run(`personal.newAccount("password")`) if err != nil { @@ -151,9 +151,7 @@ func TestAccounts(t *testing.T) { t.Errorf("address not hex: %q", addr) } - // skip until order fixed #824 // checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`", "`+addr+`"]`) - // checkEvalJSON(t, repl, `eth.coinbase`, `"`+testAddress+`"`) } func TestBlockChain(t *testing.T) { diff --git a/xeth/xeth.go b/xeth/xeth.go index 2a1366fe1..84d58a49f 100644 --- a/xeth/xeth.go +++ b/xeth/xeth.go @@ -477,7 +477,10 @@ func (self *XEth) IsListening() bool { } func (self *XEth) Coinbase() string { - eb, _ := self.backend.Etherbase() + eb, err := self.backend.Etherbase() + if err != nil { + return "0x0" + } return eb.Hex() } From a4df9d74eabb3bef8449744c4fe966572586dc39 Mon Sep 17 00:00:00 2001 From: zelig Date: Sun, 21 Jun 2015 22:17:17 +0100 Subject: [PATCH 6/8] accounts order by keyfile ctime --- cmd/geth/js_test.go | 6 +++--- crypto/key_store_plain.go | 27 +++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go index 5bdfb7048..61e85d399 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -9,6 +9,7 @@ import ( "runtime" "strconv" "testing" + "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -20,8 +21,8 @@ import ( "github.com/ethereum/go-ethereum/core/state" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" - "github.com/ethereum/go-ethereum/rpc/comms" "github.com/ethereum/go-ethereum/rpc/codec" + "github.com/ethereum/go-ethereum/rpc/comms" ) const ( @@ -141,7 +142,6 @@ func TestAccounts(t *testing.T) { checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`"]`) checkEvalJSON(t, repl, `eth.coinbase`, `null`) - val, err := repl.re.Run(`personal.newAccount("password")`) if err != nil { t.Errorf("expected no error, got %v", err) @@ -151,7 +151,7 @@ func TestAccounts(t *testing.T) { t.Errorf("address not hex: %q", addr) } - // checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`", "`+addr+`"]`) + checkEvalJSON(t, repl, `eth.accounts`, `["`+testAddress+`","`+addr+`"]`) } func TestBlockChain(t *testing.T) { diff --git a/crypto/key_store_plain.go b/crypto/key_store_plain.go index 6a8afe27d..e3150e9a9 100644 --- a/crypto/key_store_plain.go +++ b/crypto/key_store_plain.go @@ -27,11 +27,15 @@ import ( "encoding/hex" "encoding/json" "fmt" - "github.com/ethereum/go-ethereum/common" "io" "io/ioutil" "os" "path/filepath" + "sort" + "syscall" + "time" + + "github.com/ethereum/go-ethereum/common" ) // TODO: rename to KeyStore when replacing existing KeyStore @@ -118,8 +122,15 @@ func GetKeyAddresses(keysDirPath string) (addresses []common.Address, err error) if err != nil { return nil, err } + var kfis keyFileInfos for _, fileInfo := range fileInfos { - address, err := hex.DecodeString(fileInfo.Name()) + stat := fileInfo.Sys().(*syscall.Stat_t) + ctime := time.Unix(int64(stat.Ctimespec.Sec), int64(stat.Ctimespec.Nsec)) + kfis = append(kfis, keyFileInfo{fileInfo.Name(), ctime}) + } + sort.Sort(kfis) + for _, kfi := range kfis { + address, err := hex.DecodeString(kfi.name) if err != nil { continue } @@ -127,3 +138,15 @@ func GetKeyAddresses(keysDirPath string) (addresses []common.Address, err error) } return addresses, err } + +type keyFileInfo struct { + name string + ctime time.Time +} +type keyFileInfos []keyFileInfo + +func (a keyFileInfos) Len() int { return len(a) } +func (a keyFileInfos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a keyFileInfos) Less(i, j int) bool { + return a[i].ctime.Before(a[j].ctime) +} From fc17a527bc2bd07fc30e16d161059a441042d5f1 Mon Sep 17 00:00:00 2001 From: zelig Date: Thu, 2 Jul 2015 22:58:00 +0100 Subject: [PATCH 7/8] fix account ordering * chronological order of creation * new naming scheme keystore/UTC---
* KeyStore2 -> KeyStore * backward compatibility * refactor keyStore methods --- accounts/account_manager.go | 6 +- accounts/accounts_test.go | 5 +- cmd/geth/js_test.go | 2 +- crypto/crypto.go | 2 +- crypto/key_store_passphrase.go | 36 ++++----- crypto/key_store_plain.go | 135 ++++++++++++++++++++------------- 6 files changed, 105 insertions(+), 81 deletions(-) diff --git a/accounts/account_manager.go b/accounts/account_manager.go index 4519c8420..eb2672a7d 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -26,7 +26,7 @@ This abstracts part of a user's interaction with an account she controls. It's not an abstraction of core Ethereum accounts data type / logic - for that see the core processing code of blocks / txs. -Currently this is pretty much a passthrough to the KeyStore2 interface, +Currently this is pretty much a passthrough to the KeyStore interface, and accounts persistence is derived from stored keys' addresses */ @@ -54,7 +54,7 @@ type Account struct { } type Manager struct { - keyStore crypto.KeyStore2 + keyStore crypto.KeyStore unlocked map[common.Address]*unlocked mutex sync.RWMutex } @@ -64,7 +64,7 @@ type unlocked struct { abort chan struct{} } -func NewManager(keyStore crypto.KeyStore2) *Manager { +func NewManager(keyStore crypto.KeyStore) *Manager { return &Manager{ keyStore: keyStore, unlocked: make(map[common.Address]*unlocked), diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 8bd70880c..4b94b78fd 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -98,10 +98,11 @@ func TestOverrideUnlock(t *testing.T) { if err != ErrLocked { t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } - } -func tmpKeyStore(t *testing.T, new func(string) crypto.KeyStore2) (string, crypto.KeyStore2) { +// + +func tmpKeyStore(t *testing.T, new func(string) crypto.KeyStore) (string, crypto.KeyStore) { d, err := ioutil.TempDir("", "eth-keystore-test") if err != nil { t.Fatal(err) diff --git a/cmd/geth/js_test.go b/cmd/geth/js_test.go index 61e85d399..480f77c91 100644 --- a/cmd/geth/js_test.go +++ b/cmd/geth/js_test.go @@ -9,7 +9,6 @@ import ( "runtime" "strconv" "testing" - "time" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -128,6 +127,7 @@ func TestNodeInfo(t *testing.T) { } defer ethereum.Stop() defer os.RemoveAll(tmp) + want := `{"DiscPort":0,"IP":"0.0.0.0","ListenAddr":"","Name":"test","NodeID":"4cb2fc32924e94277bf94b5e4c983beedb2eabd5a0bc941db32202735c6625d020ca14a5963d1738af43b6ac0a711d61b1a06de931a499fe2aa0b1a132a902b5","NodeUrl":"enode://4cb2fc32924e94277bf94b5e4c983beedb2eabd5a0bc941db32202735c6625d020ca14a5963d1738af43b6ac0a711d61b1a06de931a499fe2aa0b1a132a902b5@0.0.0.0:0","TCPPort":0,"Td":"131072"}` checkEvalJSON(t, repl, `admin.nodeInfo`, want) } diff --git a/crypto/crypto.go b/crypto/crypto.go index 153bbbc5d..deef67415 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -209,7 +209,7 @@ func ImportBlockTestKey(privKeyBytes []byte) error { } // creates a Key and stores that in the given KeyStore by decrypting a presale key JSON -func ImportPreSaleKey(keyStore KeyStore2, keyJSON []byte, password string) (*Key, error) { +func ImportPreSaleKey(keyStore KeyStore, keyJSON []byte, password string) (*Key, error) { key, err := decryptPreSaleKey(keyJSON, password) if err != nil { return nil, err diff --git a/crypto/key_store_passphrase.go b/crypto/key_store_passphrase.go index 2000a2438..d26e3407f 100644 --- a/crypto/key_store_passphrase.go +++ b/crypto/key_store_passphrase.go @@ -41,8 +41,6 @@ import ( "errors" "fmt" "io" - "os" - "path/filepath" "reflect" "code.google.com/p/go-uuid/uuid" @@ -65,7 +63,7 @@ type keyStorePassphrase struct { keysDirPath string } -func NewKeyStorePassphrase(path string) KeyStore2 { +func NewKeyStorePassphrase(path string) KeyStore { return &keyStorePassphrase{path} } @@ -74,7 +72,7 @@ func (ks keyStorePassphrase) GenerateNewKey(rand io.Reader, auth string) (key *K } func (ks keyStorePassphrase) GetKey(keyAddr common.Address, auth string) (key *Key, err error) { - keyBytes, keyId, err := DecryptKeyFromFile(ks, keyAddr, auth) + keyBytes, keyId, err := decryptKeyFromFile(ks, keyAddr, auth) if err != nil { return nil, err } @@ -87,7 +85,7 @@ func (ks keyStorePassphrase) GetKey(keyAddr common.Address, auth string) (key *K } func (ks keyStorePassphrase) GetKeyAddresses() (addresses []common.Address, err error) { - return GetKeyAddresses(ks.keysDirPath) + return getKeyAddresses(ks.keysDirPath) } func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { @@ -139,42 +137,40 @@ func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { return err } - return WriteKeyFile(key.Address, ks.keysDirPath, keyJSON) + return writeKeyFile(key.Address, ks.keysDirPath, keyJSON) } func (ks keyStorePassphrase) DeleteKey(keyAddr common.Address, auth string) (err error) { // only delete if correct passphrase is given - _, _, err = DecryptKeyFromFile(ks, keyAddr, auth) + _, _, err = decryptKeyFromFile(ks, keyAddr, auth) if err != nil { return err } - keyDirPath := filepath.Join(ks.keysDirPath, hex.EncodeToString(keyAddr[:])) - return os.RemoveAll(keyDirPath) + return deleteKey(ks.keysDirPath, keyAddr) } -func DecryptKeyFromFile(ks keyStorePassphrase, keyAddr common.Address, auth string) (keyBytes []byte, keyId []byte, err error) { - fileContent, err := GetKeyFile(ks.keysDirPath, keyAddr) - if err != nil { - return nil, nil, err - } - +func decryptKeyFromFile(ks keyStorePassphrase, keyAddr common.Address, auth string) (keyBytes []byte, keyId []byte, err error) { m := make(map[string]interface{}) - err = json.Unmarshal(fileContent, &m) + err = getKey(ks.keysDirPath, keyAddr, &m) + if err != nil { + fmt.Printf("get key error: %v\n", err) + return + } v := reflect.ValueOf(m["version"]) if v.Kind() == reflect.String && v.String() == "1" { k := new(encryptedKeyJSONV1) - err := json.Unmarshal(fileContent, k) + getKey(ks.keysDirPath, keyAddr, &k) if err != nil { - return nil, nil, err + return } return decryptKeyV1(k, auth) } else { k := new(encryptedKeyJSONV3) - err := json.Unmarshal(fileContent, k) + getKey(ks.keysDirPath, keyAddr, &k) if err != nil { - return nil, nil, err + return } return decryptKeyV3(k, auth) } diff --git a/crypto/key_store_plain.go b/crypto/key_store_plain.go index e3150e9a9..d785fdf68 100644 --- a/crypto/key_store_plain.go +++ b/crypto/key_store_plain.go @@ -31,18 +31,15 @@ import ( "io/ioutil" "os" "path/filepath" - "sort" - "syscall" "time" "github.com/ethereum/go-ethereum/common" ) -// TODO: rename to KeyStore when replacing existing KeyStore -type KeyStore2 interface { +type KeyStore interface { // create new key using io.Reader entropy source and optionally using auth string GenerateNewKey(io.Reader, string) (*Key, error) - GetKey(common.Address, string) (*Key, error) // key from addr and auth string + GetKey(common.Address, string) (*Key, error) // get key from addr and auth string GetKeyAddresses() ([]common.Address, error) // get all addresses StoreKey(*Key, string) error // store key optionally using auth string DeleteKey(common.Address, string) error // delete key by addr and auth string @@ -52,7 +49,7 @@ type keyStorePlain struct { keysDirPath string } -func NewKeyStorePlain(path string) KeyStore2 { +func NewKeyStorePlain(path string) KeyStore { return &keyStorePlain{path} } @@ -60,7 +57,7 @@ func (ks keyStorePlain) GenerateNewKey(rand io.Reader, auth string) (key *Key, e return GenerateNewKeyDefault(ks, rand, auth) } -func GenerateNewKeyDefault(ks KeyStore2, rand io.Reader, auth string) (key *Key, err error) { +func GenerateNewKeyDefault(ks KeyStore, rand io.Reader, auth string) (key *Key, err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("GenerateNewKey error: %v", r) @@ -72,81 +69,111 @@ func GenerateNewKeyDefault(ks KeyStore2, rand io.Reader, auth string) (key *Key, } func (ks keyStorePlain) GetKey(keyAddr common.Address, auth string) (key *Key, err error) { - fileContent, err := GetKeyFile(ks.keysDirPath, keyAddr) - if err != nil { - return nil, err - } - key = new(Key) - err = json.Unmarshal(fileContent, key) - return key, err + err = getKey(ks.keysDirPath, keyAddr, key) + return +} + +func getKey(keysDirPath string, keyAddr common.Address, content interface{}) (err error) { + fileContent, err := getKeyFile(keysDirPath, keyAddr) + if err != nil { + return + } + return json.Unmarshal(fileContent, content) } func (ks keyStorePlain) GetKeyAddresses() (addresses []common.Address, err error) { - return GetKeyAddresses(ks.keysDirPath) + return getKeyAddresses(ks.keysDirPath) } func (ks keyStorePlain) StoreKey(key *Key, auth string) (err error) { keyJSON, err := json.Marshal(key) if err != nil { - return err + return } - err = WriteKeyFile(key.Address, ks.keysDirPath, keyJSON) - return err + err = writeKeyFile(key.Address, ks.keysDirPath, keyJSON) + return } func (ks keyStorePlain) DeleteKey(keyAddr common.Address, auth string) (err error) { - keyDirPath := filepath.Join(ks.keysDirPath, keyAddr.Hex()) - err = os.RemoveAll(keyDirPath) - return err + return deleteKey(ks.keysDirPath, keyAddr) } -func GetKeyFile(keysDirPath string, keyAddr common.Address) (fileContent []byte, err error) { - fileName := hex.EncodeToString(keyAddr[:]) - return ioutil.ReadFile(filepath.Join(keysDirPath, fileName, fileName)) +func deleteKey(keysDirPath string, keyAddr common.Address) (err error) { + var keyFilePath string + keyFilePath, err = getKeyFilePath(keysDirPath, keyAddr) + if err == nil { + err = os.Remove(keyFilePath) + } + return } -func WriteKeyFile(addr common.Address, keysDirPath string, content []byte) (err error) { - addrHex := hex.EncodeToString(addr[:]) - keyDirPath := filepath.Join(keysDirPath, addrHex) - keyFilePath := filepath.Join(keyDirPath, addrHex) - err = os.MkdirAll(keyDirPath, 0700) // read, write and dir search for user +func getKeyFilePath(keysDirPath string, keyAddr common.Address) (keyFilePath string, err error) { + addrHex := hex.EncodeToString(keyAddr[:]) + matches, err := filepath.Glob(filepath.Join(keysDirPath, fmt.Sprintf("*--%s", addrHex))) + if len(matches) > 0 { + if err == nil { + keyFilePath = matches[len(matches)-1] + } + return + } + keyFilePath = filepath.Join(keysDirPath, addrHex, addrHex) + _, err = os.Stat(keyFilePath) + return +} + +func getKeyFile(keysDirPath string, keyAddr common.Address) (fileContent []byte, err error) { + var keyFilePath string + keyFilePath, err = getKeyFilePath(keysDirPath, keyAddr) + if err == nil { + fileContent, err = ioutil.ReadFile(keyFilePath) + } + return +} + +func writeKeyFile(addr common.Address, keysDirPath string, content []byte) (err error) { + filename := keyFileName(addr) + // read, write and dir search for user + err = os.MkdirAll(keysDirPath, 0700) if err != nil { return err } - return ioutil.WriteFile(keyFilePath, content, 0600) // read, write for user + // read, write for user + return ioutil.WriteFile(filepath.Join(keysDirPath, filename), content, 0600) } -func GetKeyAddresses(keysDirPath string) (addresses []common.Address, err error) { +// keyFilePath implements the naming convention for keyfiles: +// UTC---
+func keyFileName(keyAddr common.Address) string { + ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) +} + +func toISO8601(t time.Time) string { + var tz string + name, offset := t.Zone() + if name == "UTC" { + tz = "Z" + } else { + tz = fmt.Sprintf("%03d00", offset/3600) + } + return fmt.Sprintf("%04d-%02d-%02dT%02d:%02d:%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) +} + +func getKeyAddresses(keysDirPath string) (addresses []common.Address, err error) { fileInfos, err := ioutil.ReadDir(keysDirPath) if err != nil { return nil, err } - var kfis keyFileInfos for _, fileInfo := range fileInfos { - stat := fileInfo.Sys().(*syscall.Stat_t) - ctime := time.Unix(int64(stat.Ctimespec.Sec), int64(stat.Ctimespec.Nsec)) - kfis = append(kfis, keyFileInfo{fileInfo.Name(), ctime}) - } - sort.Sort(kfis) - for _, kfi := range kfis { - address, err := hex.DecodeString(kfi.name) - if err != nil { - continue + filename := fileInfo.Name() + if len(filename) >= 40 { + addr := filename[len(filename)-40 : len(filename)] + address, err := hex.DecodeString(addr) + if err == nil { + addresses = append(addresses, common.BytesToAddress(address)) + } } - addresses = append(addresses, common.BytesToAddress(address)) } return addresses, err } - -type keyFileInfo struct { - name string - ctime time.Time -} -type keyFileInfos []keyFileInfo - -func (a keyFileInfos) Len() int { return len(a) } -func (a keyFileInfos) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a keyFileInfos) Less(i, j int) bool { - return a[i].ctime.Before(a[j].ctime) -} From 1959346793bdee469f68841843dd383cf801aba1 Mon Sep 17 00:00:00 2001 From: zelig Date: Fri, 3 Jul 2015 04:56:20 +0100 Subject: [PATCH 8/8] account update: migrate or change password * account.Update * KeyStore.Cleanup * fix dir rm for old format deleteKey --- accounts/account_manager.go | 28 ++++++++++++++ cmd/geth/main.go | 68 ++++++++++++++++++++++++++++++---- crypto/key_store_passphrase.go | 33 +++++++++-------- crypto/key_store_plain.go | 45 ++++++++++++++++++++-- 4 files changed, 149 insertions(+), 25 deletions(-) diff --git a/accounts/account_manager.go b/accounts/account_manager.go index eb2672a7d..17b128e9e 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -36,6 +36,7 @@ import ( "crypto/ecdsa" crand "crypto/rand" "errors" + "fmt" "os" "sync" "time" @@ -158,6 +159,20 @@ func (am *Manager) NewAccount(auth string) (Account, error) { return Account{Address: key.Address}, nil } +func (am *Manager) AddressByIndex(index int) (addr string, err error) { + var addrs []common.Address + addrs, err = am.keyStore.GetKeyAddresses() + if err != nil { + return + } + if index < 0 || index >= len(addrs) { + err = fmt.Errorf("index out of range: %d (should be 0-%d)", index, len(addrs)-1) + } else { + addr = addrs[index].Hex() + } + return +} + func (am *Manager) Accounts() ([]Account, error) { addresses, err := am.keyStore.GetKeyAddresses() if os.IsNotExist(err) { @@ -204,6 +219,19 @@ func (am *Manager) Import(path string, keyAuth string) (Account, error) { return Account{Address: key.Address}, nil } +func (am *Manager) Update(addr common.Address, authFrom, authTo string) (err error) { + var key *crypto.Key + key, err = am.keyStore.GetKey(addr, authFrom) + + if err == nil { + err = am.keyStore.StoreKey(key, authTo) + if err == nil { + am.keyStore.Cleanup(addr) + } + } + return +} + func (am *Manager) ImportPreSaleKey(keyJSON []byte, password string) (acc Account, err error) { var key *crypto.Key key, err = crypto.ImportPreSaleKey(am.keyStore, keyJSON, password) diff --git a/cmd/geth/main.go b/cmd/geth/main.go index 673a08d45..ffd26a7c2 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -189,6 +189,33 @@ Note, this is meant to be used for testing only, it is a bad idea to save your password to file or expose in any other way. `, }, + { + Action: accountUpdate, + Name: "update", + Usage: "update an existing account", + Description: ` + + ethereum account update
+ +Update an existing account. + +The account is saved in the newest version in encrypted format, you are prompted +for a passphrase to unlock the account and another to save the updated file. + +This same command can therefore be used to migrate an account of a deprecated +format to the newest format or change the password for an account. + +For non-interactive use the passphrase can be specified with the --password flag: + + ethereum --password account new + +Since only one password can be given, only format update can be performed, +changing your password is only possible interactively. + +Note that account update has the a side effect that the order of your accounts +changes. + `, + }, { Action: accountImport, Name: "import", @@ -433,19 +460,30 @@ func execJSFiles(ctx *cli.Context) { ethereum.WaitForShutdown() } -func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string, i int) { +func unlockAccount(ctx *cli.Context, am *accounts.Manager, addr string, i int) (addrHex, auth string) { var err error // Load startup keys. XXX we are going to need a different format - if !((len(account) == 40) || (len(account) == 42)) { // with or without 0x - utils.Fatalf("Invalid account address '%s'", account) + if !((len(addr) == 40) || (len(addr) == 42)) { // with or without 0x + var index int + index, err = strconv.Atoi(addr) + if err != nil { + utils.Fatalf("Invalid account address '%s'", addr) + } + + addrHex, err = am.AddressByIndex(index) + if err != nil { + utils.Fatalf("%v", err) + } + } else { + addrHex = addr } // Attempt to unlock the account 3 times attempts := 3 for tries := 0; tries < attempts; tries++ { - msg := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", account, tries+1, attempts) - passphrase := getPassPhrase(ctx, msg, false, i) - err = am.Unlock(common.HexToAddress(account), passphrase) + msg := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", addr, tries+1, attempts) + auth = getPassPhrase(ctx, msg, false, i) + err = am.Unlock(common.HexToAddress(addrHex), auth) if err == nil { break } @@ -453,7 +491,8 @@ func unlockAccount(ctx *cli.Context, am *accounts.Manager, account string, i int if err != nil { utils.Fatalf("Unlock account failed '%v'", err) } - fmt.Printf("Account '%s' unlocked.\n", account) + fmt.Printf("Account '%s' unlocked.\n", addr) + return } func blockRecovery(ctx *cli.Context) { @@ -578,6 +617,21 @@ func accountCreate(ctx *cli.Context) { fmt.Printf("Address: %x\n", acct) } +func accountUpdate(ctx *cli.Context) { + am := utils.MakeAccountManager(ctx) + arg := ctx.Args().First() + if len(arg) == 0 { + utils.Fatalf("account address or index must be given as argument") + } + + addr, authFrom := unlockAccount(ctx, am, arg, 0) + authTo := getPassPhrase(ctx, "Please give a new password. Do not forget this password.", true, 0) + err := am.Update(common.HexToAddress(addr), authFrom, authTo) + if err != nil { + utils.Fatalf("Could not update the account: %v", err) + } +} + func importWallet(ctx *cli.Context) { keyfile := ctx.Args().First() if len(keyfile) == 0 { diff --git a/crypto/key_store_passphrase.go b/crypto/key_store_passphrase.go index d26e3407f..47909bc76 100644 --- a/crypto/key_store_passphrase.go +++ b/crypto/key_store_passphrase.go @@ -72,16 +72,19 @@ func (ks keyStorePassphrase) GenerateNewKey(rand io.Reader, auth string) (key *K } func (ks keyStorePassphrase) GetKey(keyAddr common.Address, auth string) (key *Key, err error) { - keyBytes, keyId, err := decryptKeyFromFile(ks, keyAddr, auth) - if err != nil { - return nil, err + keyBytes, keyId, err := decryptKeyFromFile(ks.keysDirPath, keyAddr, auth) + if err == nil { + key = &Key{ + Id: uuid.UUID(keyId), + Address: keyAddr, + PrivateKey: ToECDSA(keyBytes), + } } - key = &Key{ - Id: uuid.UUID(keyId), - Address: keyAddr, - PrivateKey: ToECDSA(keyBytes), - } - return key, err + return +} + +func (ks keyStorePassphrase) Cleanup(keyAddr common.Address) (err error) { + return cleanup(ks.keysDirPath, keyAddr) } func (ks keyStorePassphrase) GetKeyAddresses() (addresses []common.Address, err error) { @@ -142,7 +145,7 @@ func (ks keyStorePassphrase) StoreKey(key *Key, auth string) (err error) { func (ks keyStorePassphrase) DeleteKey(keyAddr common.Address, auth string) (err error) { // only delete if correct passphrase is given - _, _, err = decryptKeyFromFile(ks, keyAddr, auth) + _, _, err = decryptKeyFromFile(ks.keysDirPath, keyAddr, auth) if err != nil { return err } @@ -150,25 +153,25 @@ func (ks keyStorePassphrase) DeleteKey(keyAddr common.Address, auth string) (err return deleteKey(ks.keysDirPath, keyAddr) } -func decryptKeyFromFile(ks keyStorePassphrase, keyAddr common.Address, auth string) (keyBytes []byte, keyId []byte, err error) { +func decryptKeyFromFile(keysDirPath string, keyAddr common.Address, auth string) (keyBytes []byte, keyId []byte, err error) { + fmt.Printf("%v\n", keyAddr.Hex()) m := make(map[string]interface{}) - err = getKey(ks.keysDirPath, keyAddr, &m) + err = getKey(keysDirPath, keyAddr, &m) if err != nil { - fmt.Printf("get key error: %v\n", err) return } v := reflect.ValueOf(m["version"]) if v.Kind() == reflect.String && v.String() == "1" { k := new(encryptedKeyJSONV1) - getKey(ks.keysDirPath, keyAddr, &k) + err = getKey(keysDirPath, keyAddr, &k) if err != nil { return } return decryptKeyV1(k, auth) } else { k := new(encryptedKeyJSONV3) - getKey(ks.keysDirPath, keyAddr, &k) + err = getKey(keysDirPath, keyAddr, &k) if err != nil { return } diff --git a/crypto/key_store_plain.go b/crypto/key_store_plain.go index d785fdf68..c13c5e7a4 100644 --- a/crypto/key_store_plain.go +++ b/crypto/key_store_plain.go @@ -43,6 +43,7 @@ type KeyStore interface { GetKeyAddresses() ([]common.Address, error) // get all addresses StoreKey(*Key, string) error // store key optionally using auth string DeleteKey(common.Address, string) error // delete key by addr and auth string + Cleanup(keyAddr common.Address) (err error) } type keyStorePlain struct { @@ -86,6 +87,10 @@ func (ks keyStorePlain) GetKeyAddresses() (addresses []common.Address, err error return getKeyAddresses(ks.keysDirPath) } +func (ks keyStorePlain) Cleanup(keyAddr common.Address) (err error) { + return cleanup(ks.keysDirPath, keyAddr) +} + func (ks keyStorePlain) StoreKey(key *Key, auth string) (err error) { keyJSON, err := json.Marshal(key) if err != nil { @@ -100,10 +105,14 @@ func (ks keyStorePlain) DeleteKey(keyAddr common.Address, auth string) (err erro } func deleteKey(keysDirPath string, keyAddr common.Address) (err error) { - var keyFilePath string - keyFilePath, err = getKeyFilePath(keysDirPath, keyAddr) + var path string + path, err = getKeyFilePath(keysDirPath, keyAddr) if err == nil { - err = os.Remove(keyFilePath) + addrHex := hex.EncodeToString(keyAddr[:]) + if path == filepath.Join(keysDirPath, addrHex, addrHex) { + path = filepath.Join(keysDirPath, addrHex) + } + err = os.RemoveAll(path) } return } @@ -122,6 +131,36 @@ func getKeyFilePath(keysDirPath string, keyAddr common.Address) (keyFilePath str return } +func cleanup(keysDirPath string, keyAddr common.Address) (err error) { + fileInfos, err := ioutil.ReadDir(keysDirPath) + if err != nil { + return + } + var paths []string + account := hex.EncodeToString(keyAddr[:]) + for _, fileInfo := range fileInfos { + path := filepath.Join(keysDirPath, fileInfo.Name()) + if len(path) >= 40 { + addr := path[len(path)-40 : len(path)] + if addr == account { + if path == filepath.Join(keysDirPath, addr, addr) { + path = filepath.Join(keysDirPath, addr) + } + paths = append(paths, path) + } + } + } + if len(paths) > 1 { + for i := 0; err == nil && i < len(paths)-1; i++ { + err = os.RemoveAll(paths[i]) + if err != nil { + break + } + } + } + return +} + func getKeyFile(keysDirPath string, keyAddr common.Address) (fileContent []byte, err error) { var keyFilePath string keyFilePath, err = getKeyFilePath(keysDirPath, keyAddr)