diff --git a/accounts/abi/bind/auth.go b/accounts/abi/bind/auth.go index dbb235c144..e6bb0c3b52 100644 --- a/accounts/abi/bind/auth.go +++ b/accounts/abi/bind/auth.go @@ -22,7 +22,7 @@ import ( "io" "io/ioutil" - "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/crypto" @@ -35,7 +35,7 @@ func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) { if err != nil { return nil, err } - key, err := accounts.DecryptKey(json, passphrase) + key, err := keystore.DecryptKey(json, passphrase) if err != nil { return nil, err } diff --git a/accounts/account_manager.go b/accounts/account_manager.go deleted file mode 100644 index 01dd62e25b..0000000000 --- a/accounts/account_manager.go +++ /dev/null @@ -1,350 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Package accounts implements encrypted storage of secp256k1 private keys. -// -// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification. -// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information. -package accounts - -import ( - "crypto/ecdsa" - crand "crypto/rand" - "encoding/json" - "errors" - "fmt" - "os" - "path/filepath" - "runtime" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" -) - -var ( - ErrLocked = errors.New("account is locked") - ErrNoMatch = errors.New("no key for given address or file") - ErrDecrypt = errors.New("could not decrypt key with given passphrase") -) - -// Account represents a stored key. -// When used as an argument, it selects a unique key file to act on. -type Account struct { - Address common.Address // Ethereum account address derived from the key - - // File contains the key file name. - // When Acccount is used as an argument to select a key, File can be left blank to - // select just by address or set to the basename or absolute path of a file in the key - // directory. Accounts returned by Manager will always contain an absolute path. - File string -} - -func (acc *Account) MarshalJSON() ([]byte, error) { - return []byte(`"` + acc.Address.Hex() + `"`), nil -} - -func (acc *Account) UnmarshalJSON(raw []byte) error { - return json.Unmarshal(raw, &acc.Address) -} - -// Manager manages a key storage directory on disk. -type Manager struct { - cache *addrCache - keyStore keyStore - mu sync.RWMutex - unlocked map[common.Address]*unlocked -} - -type unlocked struct { - *Key - abort chan struct{} -} - -// NewManager creates a manager for the given directory. -func NewManager(keydir string, scryptN, scryptP int) *Manager { - keydir, _ = filepath.Abs(keydir) - am := &Manager{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} - am.init(keydir) - return am -} - -// NewPlaintextManager creates a manager for the given directory. -// Deprecated: Use NewManager. -func NewPlaintextManager(keydir string) *Manager { - keydir, _ = filepath.Abs(keydir) - am := &Manager{keyStore: &keyStorePlain{keydir}} - am.init(keydir) - return am -} - -func (am *Manager) init(keydir string) { - am.unlocked = make(map[common.Address]*unlocked) - am.cache = newAddrCache(keydir) - // TODO: In order for this finalizer to work, there must be no references - // to am. addrCache doesn't keep a reference but unlocked keys do, - // so the finalizer will not trigger until all timed unlocks have expired. - runtime.SetFinalizer(am, func(m *Manager) { - m.cache.close() - }) -} - -// HasAddress reports whether a key with the given address is present. -func (am *Manager) HasAddress(addr common.Address) bool { - return am.cache.hasAddress(addr) -} - -// Accounts returns all key files present in the directory. -func (am *Manager) Accounts() []Account { - return am.cache.accounts() -} - -// Delete deletes the key matched by account if the passphrase is correct. -// If the account contains no filename, the address must match a unique key. -func (am *Manager) Delete(a Account, passphrase string) error { - // Decrypting the key isn't really necessary, but we do - // it anyway to check the password and zero out the key - // immediately afterwards. - a, key, err := am.getDecryptedKey(a, passphrase) - if key != nil { - zeroKey(key.PrivateKey) - } - if err != nil { - return err - } - // The order is crucial here. The key is dropped from the - // cache after the file is gone so that a reload happening in - // between won't insert it into the cache again. - err = os.Remove(a.File) - if err == nil { - am.cache.delete(a) - } - return err -} - -// Sign calculates a ECDSA signature for the given hash. The produced signature -// is in the [R || S || V] format where V is 0 or 1. -func (am *Manager) Sign(addr common.Address, hash []byte) ([]byte, error) { - am.mu.RLock() - defer am.mu.RUnlock() - - unlockedKey, found := am.unlocked[addr] - if !found { - return nil, ErrLocked - } - return crypto.Sign(hash, unlockedKey.PrivateKey) -} - -// SignWithPassphrase signs hash if the private key matching the given address -// can be decrypted with the given passphrase. The produced signature is in the -// [R || S || V] format where V is 0 or 1. -func (am *Manager) SignWithPassphrase(a Account, passphrase string, hash []byte) (signature []byte, err error) { - _, key, err := am.getDecryptedKey(a, passphrase) - if err != nil { - return nil, err - } - defer zeroKey(key.PrivateKey) - return crypto.Sign(hash, key.PrivateKey) -} - -// Unlock unlocks the given account indefinitely. -func (am *Manager) Unlock(a Account, passphrase string) error { - return am.TimedUnlock(a, passphrase, 0) -} - -// Lock removes the private key with the given address from memory. -func (am *Manager) Lock(addr common.Address) error { - am.mu.Lock() - if unl, found := am.unlocked[addr]; found { - am.mu.Unlock() - am.expire(addr, unl, time.Duration(0)*time.Nanosecond) - } else { - am.mu.Unlock() - } - return nil -} - -// TimedUnlock unlocks the given account with the passphrase. The account -// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account -// until the program exits. The account must match a unique key file. -// -// If the account address is already unlocked for a duration, TimedUnlock extends or -// shortens the active unlock timeout. If the address was previously unlocked -// indefinitely the timeout is not altered. -func (am *Manager) TimedUnlock(a Account, passphrase string, timeout time.Duration) error { - a, key, err := am.getDecryptedKey(a, passphrase) - if err != nil { - return err - } - - am.mu.Lock() - defer am.mu.Unlock() - u, found := am.unlocked[a.Address] - if found { - if u.abort == nil { - // The address was unlocked indefinitely, so unlocking - // it with a timeout would be confusing. - zeroKey(key.PrivateKey) - return nil - } else { - // Terminate the expire goroutine and replace it below. - close(u.abort) - } - } - if timeout > 0 { - u = &unlocked{Key: key, abort: make(chan struct{})} - go am.expire(a.Address, u, timeout) - } else { - u = &unlocked{Key: key} - } - am.unlocked[a.Address] = u - return nil -} - -// Find resolves the given account into a unique entry in the keystore. -func (am *Manager) Find(a Account) (Account, error) { - am.cache.maybeReload() - am.cache.mu.Lock() - a, err := am.cache.find(a) - am.cache.mu.Unlock() - return a, err -} - -func (am *Manager) getDecryptedKey(a Account, auth string) (Account, *Key, error) { - a, err := am.Find(a) - if err != nil { - return a, nil, err - } - key, err := am.keyStore.GetKey(a.Address, a.File, auth) - return a, key, 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.mu.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.mu.Unlock() - } -} - -// NewAccount generates a new key and stores it into the key directory, -// encrypting it with the passphrase. -func (am *Manager) NewAccount(passphrase string) (Account, error) { - _, account, err := storeNewKey(am.keyStore, crand.Reader, passphrase) - if err != nil { - return Account{}, err - } - // Add the account to the cache immediately rather - // than waiting for file system notifications to pick it up. - am.cache.add(account) - return account, nil -} - -// AccountByIndex returns the ith account. -func (am *Manager) AccountByIndex(i int) (Account, error) { - accounts := am.Accounts() - if i < 0 || i >= len(accounts) { - return Account{}, fmt.Errorf("account index %d out of range [0, %d]", i, len(accounts)-1) - } - return accounts[i], nil -} - -// Export exports as a JSON key, encrypted with newPassphrase. -func (am *Manager) Export(a Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { - _, key, err := am.getDecryptedKey(a, passphrase) - if err != nil { - return nil, err - } - var N, P int - if store, ok := am.keyStore.(*keyStorePassphrase); ok { - N, P = store.scryptN, store.scryptP - } else { - N, P = StandardScryptN, StandardScryptP - } - return EncryptKey(key, newPassphrase, N, P) -} - -// Import stores the given encrypted JSON key into the key directory. -func (am *Manager) Import(keyJSON []byte, passphrase, newPassphrase string) (Account, error) { - key, err := DecryptKey(keyJSON, passphrase) - if key != nil && key.PrivateKey != nil { - defer zeroKey(key.PrivateKey) - } - if err != nil { - return Account{}, err - } - return am.importKey(key, newPassphrase) -} - -// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase. -func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Account, error) { - key := newKeyFromECDSA(priv) - if am.cache.hasAddress(key.Address) { - return Account{}, fmt.Errorf("account already exists") - } - - return am.importKey(key, passphrase) -} - -func (am *Manager) importKey(key *Key, passphrase string) (Account, error) { - a := Account{Address: key.Address, File: am.keyStore.JoinPath(keyFileName(key.Address))} - if err := am.keyStore.StoreKey(a.File, key, passphrase); err != nil { - return Account{}, err - } - am.cache.add(a) - return a, nil -} - -// Update changes the passphrase of an existing account. -func (am *Manager) Update(a Account, passphrase, newPassphrase string) error { - a, key, err := am.getDecryptedKey(a, passphrase) - if err != nil { - return err - } - return am.keyStore.StoreKey(a.File, key, newPassphrase) -} - -// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores -// a key file in the key directory. The key file is encrypted with the same passphrase. -func (am *Manager) ImportPreSaleKey(keyJSON []byte, passphrase string) (Account, error) { - a, _, err := importPreSaleKey(am.keyStore, keyJSON, passphrase) - if err != nil { - return a, err - } - am.cache.add(a) - return a, nil -} - -// zeroKey zeroes a private key in memory. -func zeroKey(k *ecdsa.PrivateKey) { - b := k.D.Bits() - for i := range b { - b[i] = 0 - } -} diff --git a/accounts/accounts.go b/accounts/accounts.go new file mode 100644 index 0000000000..234b6e4565 --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,179 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package accounts implements high level Ethereum account management. +package accounts + +import ( + "encoding/json" + "errors" + "math/big" + "reflect" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// ErrUnknownAccount is returned for any requested operation for which no backend +// provides the specified account. +var ErrUnknownAccount = errors.New("unknown account") + +// ErrNotSupported is returned when an operation is requested from an account +// backend that it does not support. +var ErrNotSupported = errors.New("not supported") + +// Account represents a stored key. +// When used as an argument, it selects a unique key to act on. +type Account struct { + Address common.Address // Ethereum account address derived from the key + URL string // Optional resource locator within a backend + backend Backend // Backend where this account originates from +} + +func (acc *Account) MarshalJSON() ([]byte, error) { + return []byte(`"` + acc.Address.Hex() + `"`), nil +} + +func (acc *Account) UnmarshalJSON(raw []byte) error { + return json.Unmarshal(raw, &acc.Address) +} + +// Manager is an overarching account manager that can communicate with various +// backends for signing transactions. +type Manager struct { + backends []Backend // List of currently registered backends (ordered by registration) + index map[reflect.Type]Backend // Set of currently registered backends + lock sync.RWMutex +} + +// NewManager creates a generic account manager to sign transaction via various +// supported backends. +func NewManager(backends ...Backend) *Manager { + am := &Manager{ + backends: backends, + index: make(map[reflect.Type]Backend), + } + for _, backend := range backends { + am.index[reflect.TypeOf(backend)] = backend + } + return am +} + +// Backend retrieves the backend with the given type from the account manager. +func (am *Manager) Backend(backend reflect.Type) Backend { + return am.index[backend] +} + +// Accounts returns all signer accounts registered under this account manager. +func (am *Manager) Accounts() []Account { + am.lock.RLock() + defer am.lock.RUnlock() + + var all []Account + for _, backend := range am.backends { // TODO(karalabe): cache these after subscriptions are in + accounts := backend.Accounts() + for i := 0; i < len(accounts); i++ { + accounts[i].backend = backend + } + all = append(all, accounts...) + } + return all +} + +// HasAddress reports whether a key with the given address is present. +func (am *Manager) HasAddress(addr common.Address) bool { + am.lock.RLock() + defer am.lock.RUnlock() + + for _, backend := range am.backends { + if backend.HasAddress(addr) { + return true + } + } + return false +} + +// SignHash requests the account manager to get the hash signed with an arbitrary +// signing backend holding the authorization for the specified account. +func (am *Manager) SignHash(acc Account, hash []byte) ([]byte, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignHash(acc, hash) +} + +// SignTx requests the account manager to get the transaction signed with an +// arbitrary signing backend holding the authorization for the specified account. +func (am *Manager) SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignTx(acc, tx, chainID) +} + +// SignHashWithPassphrase requests the account manager to get the hash signed with +// an arbitrary signing backend holding the authorization for the specified account. +func (am *Manager) SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignHashWithPassphrase(acc, passphrase, hash) +} + +// SignTxWithPassphrase requests the account manager to get the transaction signed +// with an arbitrary signing backend holding the authorization for the specified +// account. +func (am *Manager) SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignTxWithPassphrase(acc, passphrase, tx, chainID) +} + +// ensureBackend ensures that the account has a correctly set backend and that +// it is still alive. +// +// Please note, this method assumes the manager lock is held! +func (am *Manager) ensureBackend(acc *Account) error { + // If we have a backend, make sure it's still live + if acc.backend != nil { + if _, exists := am.index[reflect.TypeOf(acc.backend)]; !exists { + return ErrUnknownAccount + } + return nil + } + // If we don't have a known backend, look up one that can service it + for _, backend := range am.backends { + if backend.HasAddress(acc.Address) { // TODO(karalabe): this assumes unique addresses per backend + acc.backend = backend + return nil + } + } + return ErrUnknownAccount +} diff --git a/accounts/backend.go b/accounts/backend.go new file mode 100644 index 0000000000..5f7ac07170 --- /dev/null +++ b/accounts/backend.go @@ -0,0 +1,88 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Backend is an "account provider" that can specify a batch of accounts it can +// sign transactions with and upon request, do so. +type Backend interface { + // Accounts retrieves the list of signing accounts the backend is currently aware of. + Accounts() []Account + + // HasAddress reports whether an account with the given address is present. + HasAddress(addr common.Address) bool + + // SignHash requests the backend to sign the given hash. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the backend requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code o verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignHashWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignHash(acc Account, hash []byte) ([]byte, error) + + // SignTx requests the backend to sign the given transaction. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the backend requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code o verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // SignHashWithPassphrase requests the backend to sign the given transaction with + // the given passphrase as extra authentication information. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) + + // SignTxWithPassphrase requests the backend to sign the given transaction, with the + // given passphrase as extra authentication information. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // TODO(karalabe,fjl): watching and caching needs the Go subscription system + // Watch requests the backend to send a notification to the specified channel whenever + // an new account appears or an existing one disappears. + //Watch(chan AccountEvent) error + + // Unwatch requests the backend stop sending notifications to the given channel. + //Unwatch(chan AccountEvent) error +} + +// TODO(karalabe,fjl): watching and caching needs the Go subscription system +// type AccountEvent struct { +// Account Account +// Added bool +// } diff --git a/accounts/errors.go b/accounts/errors.go new file mode 100644 index 0000000000..d6f578b528 --- /dev/null +++ b/accounts/errors.go @@ -0,0 +1,41 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import "fmt" + +// AuthNeededError is returned by backends for signing requests where the user +// is required to provide further authentication before signing can succeed. +// +// This usually means either that a password needs to be supplied, or perhaps a +// one time PIN code displayed by some hardware device. +type AuthNeededError struct { + Needed string // Extra authentication the user needs to provide +} + +// NewAuthNeededError creates a new authentication error with the extra details +// about the needed fields set. +func NewAuthNeededError(needed string) error { + return &AuthNeededError{ + Needed: needed, + } +} + +// Error implements the standard error interfacel. +func (err *AuthNeededError) Error() string { + return fmt.Sprintf("authentication needed: %s", err.Needed) +} diff --git a/accounts/addrcache.go b/accounts/keystore/address_cache.go similarity index 77% rename from accounts/addrcache.go rename to accounts/keystore/address_cache.go index a99f23606d..eb3e3263bd 100644 --- a/accounts/addrcache.go +++ b/accounts/keystore/address_cache.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "bufio" @@ -28,6 +28,7 @@ import ( "sync" "time" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/logger" "github.com/ethereum/go-ethereum/logger/glog" @@ -38,23 +39,23 @@ import ( // exist yet, the code will attempt to create a watcher at most this often. const minReloadInterval = 2 * time.Second -type accountsByFile []Account +type accountsByFile []accounts.Account func (s accountsByFile) Len() int { return len(s) } -func (s accountsByFile) Less(i, j int) bool { return s[i].File < s[j].File } +func (s accountsByFile) Less(i, j int) bool { return s[i].URL < s[j].URL } func (s accountsByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // AmbiguousAddrError is returned when attempting to unlock // an address for which more than one file exists. type AmbiguousAddrError struct { Addr common.Address - Matches []Account + Matches []accounts.Account } func (err *AmbiguousAddrError) Error() string { files := "" for i, a := range err.Matches { - files += a.File + files += a.URL if i < len(err.Matches)-1 { files += ", " } @@ -62,58 +63,58 @@ func (err *AmbiguousAddrError) Error() string { return fmt.Sprintf("multiple keys match address (%s)", files) } -// addrCache is a live index of all accounts in the keystore. -type addrCache struct { +// addressCache is a live index of all accounts in the keystore. +type addressCache struct { keydir string watcher *watcher mu sync.Mutex all accountsByFile - byAddr map[common.Address][]Account + byAddr map[common.Address][]accounts.Account throttle *time.Timer } -func newAddrCache(keydir string) *addrCache { - ac := &addrCache{ +func newAddrCache(keydir string) *addressCache { + ac := &addressCache{ keydir: keydir, - byAddr: make(map[common.Address][]Account), + byAddr: make(map[common.Address][]accounts.Account), } ac.watcher = newWatcher(ac) return ac } -func (ac *addrCache) accounts() []Account { +func (ac *addressCache) accounts() []accounts.Account { ac.maybeReload() ac.mu.Lock() defer ac.mu.Unlock() - cpy := make([]Account, len(ac.all)) + cpy := make([]accounts.Account, len(ac.all)) copy(cpy, ac.all) return cpy } -func (ac *addrCache) hasAddress(addr common.Address) bool { +func (ac *addressCache) hasAddress(addr common.Address) bool { ac.maybeReload() ac.mu.Lock() defer ac.mu.Unlock() return len(ac.byAddr[addr]) > 0 } -func (ac *addrCache) add(newAccount Account) { +func (ac *addressCache) add(newAccount accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].File >= newAccount.File }) + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL }) if i < len(ac.all) && ac.all[i] == newAccount { return } // newAccount is not in the cache. - ac.all = append(ac.all, Account{}) + ac.all = append(ac.all, accounts.Account{}) copy(ac.all[i+1:], ac.all[i:]) ac.all[i] = newAccount ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) } // note: removed needs to be unique here (i.e. both File and Address must be set). -func (ac *addrCache) delete(removed Account) { +func (ac *addressCache) delete(removed accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() ac.all = removeAccount(ac.all, removed) @@ -124,7 +125,7 @@ func (ac *addrCache) delete(removed Account) { } } -func removeAccount(slice []Account, elem Account) []Account { +func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { for i := range slice { if slice[i] == elem { return append(slice[:i], slice[i+1:]...) @@ -134,41 +135,41 @@ func removeAccount(slice []Account, elem Account) []Account { } // find returns the cached account for address if there is a unique match. -// The exact matching rules are explained by the documentation of Account. +// The exact matching rules are explained by the documentation of accounts.Account. // Callers must hold ac.mu. -func (ac *addrCache) find(a Account) (Account, error) { +func (ac *addressCache) find(a accounts.Account) (accounts.Account, error) { // Limit search to address candidates if possible. matches := ac.all if (a.Address != common.Address{}) { matches = ac.byAddr[a.Address] } - if a.File != "" { + if a.URL != "" { // If only the basename is specified, complete the path. - if !strings.ContainsRune(a.File, filepath.Separator) { - a.File = filepath.Join(ac.keydir, a.File) + if !strings.ContainsRune(a.URL, filepath.Separator) { + a.URL = filepath.Join(ac.keydir, a.URL) } for i := range matches { - if matches[i].File == a.File { + if matches[i].URL == a.URL { return matches[i], nil } } if (a.Address == common.Address{}) { - return Account{}, ErrNoMatch + return accounts.Account{}, ErrNoMatch } } switch len(matches) { case 1: return matches[0], nil case 0: - return Account{}, ErrNoMatch + return accounts.Account{}, ErrNoMatch default: - err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]Account, len(matches))} + err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} copy(err.Matches, matches) - return Account{}, err + return accounts.Account{}, err } } -func (ac *addrCache) maybeReload() { +func (ac *addressCache) maybeReload() { ac.mu.Lock() defer ac.mu.Unlock() if ac.watcher.running { @@ -188,7 +189,7 @@ func (ac *addrCache) maybeReload() { ac.throttle.Reset(minReloadInterval) } -func (ac *addrCache) close() { +func (ac *addressCache) close() { ac.mu.Lock() ac.watcher.close() if ac.throttle != nil { @@ -199,7 +200,7 @@ func (ac *addrCache) close() { // reload caches addresses of existing accounts. // Callers must hold ac.mu. -func (ac *addrCache) reload() { +func (ac *addressCache) reload() { accounts, err := ac.scan() if err != nil && glog.V(logger.Debug) { glog.Errorf("can't load keys: %v", err) @@ -215,7 +216,7 @@ func (ac *addrCache) reload() { glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all)) } -func (ac *addrCache) scan() ([]Account, error) { +func (ac *addressCache) scan() ([]accounts.Account, error) { files, err := ioutil.ReadDir(ac.keydir) if err != nil { return nil, err @@ -223,7 +224,7 @@ func (ac *addrCache) scan() ([]Account, error) { var ( buf = new(bufio.Reader) - addrs []Account + addrs []accounts.Account keyJSON struct { Address string `json:"address"` } @@ -250,7 +251,7 @@ func (ac *addrCache) scan() ([]Account, error) { case (addr == common.Address{}): glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) default: - addrs = append(addrs, Account{Address: addr, File: path}) + addrs = append(addrs, accounts.Account{Address: addr, URL: path}) } fd.Close() } diff --git a/accounts/addrcache_test.go b/accounts/keystore/address_cache_test.go similarity index 72% rename from accounts/addrcache_test.go rename to accounts/keystore/address_cache_test.go index e5f08cffc4..68af743384 100644 --- a/accounts/addrcache_test.go +++ b/accounts/keystore/address_cache_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "fmt" @@ -28,23 +28,24 @@ import ( "github.com/cespare/cp" "github.com/davecgh/go-spew/spew" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" ) var ( cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore")) - cachetestAccounts = []Account{ + cachetestAccounts = []accounts.Account{ { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - File: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: filepath.Join(cachetestDir, "aaa"), + URL: filepath.Join(cachetestDir, "aaa"), }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - File: filepath.Join(cachetestDir, "zzz"), + URL: filepath.Join(cachetestDir, "zzz"), }, } ) @@ -52,7 +53,7 @@ var ( func TestWatchNewFile(t *testing.T) { t.Parallel() - dir, am := tmpManager(t, false) + dir, am := tmpKeyStore(t, false) defer os.RemoveAll(dir) // Ensure the watcher is started before adding any files. @@ -60,18 +61,18 @@ func TestWatchNewFile(t *testing.T) { time.Sleep(200 * time.Millisecond) // Move in the files. - wantAccounts := make([]Account, len(cachetestAccounts)) + wantAccounts := make([]accounts.Account, len(cachetestAccounts)) for i := range cachetestAccounts { a := cachetestAccounts[i] - a.File = filepath.Join(dir, filepath.Base(a.File)) + a.URL = filepath.Join(dir, filepath.Base(a.URL)) wantAccounts[i] = a - if err := cp.CopyFile(a.File, cachetestAccounts[i].File); err != nil { + if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { t.Fatal(err) } } // am should see the accounts. - var list []Account + var list []accounts.Account for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { list = am.Accounts() if reflect.DeepEqual(list, wantAccounts) { @@ -88,7 +89,7 @@ func TestWatchNoDir(t *testing.T) { // Create am but not the directory that it watches. rand.Seed(time.Now().UnixNano()) dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) - am := NewManager(dir, LightScryptN, LightScryptP) + am := NewKeyStore(dir, LightScryptN, LightScryptP) list := am.Accounts() if len(list) > 0 { @@ -100,13 +101,13 @@ func TestWatchNoDir(t *testing.T) { os.MkdirAll(dir, 0700) defer os.RemoveAll(dir) file := filepath.Join(dir, "aaa") - if err := cp.CopyFile(file, cachetestAccounts[0].File); err != nil { + if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { t.Fatal(err) } // am should see the account. - wantAccounts := []Account{cachetestAccounts[0]} - wantAccounts[0].File = file + wantAccounts := []accounts.Account{cachetestAccounts[0]} + wantAccounts[0].URL = file for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { list = am.Accounts() if reflect.DeepEqual(list, wantAccounts) { @@ -129,52 +130,52 @@ func TestCacheAddDeleteOrder(t *testing.T) { cache := newAddrCache("testdata/no-such-dir") cache.watcher.running = true // prevent unexpected reloads - accounts := []Account{ + accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - File: "-309830980", + URL: "-309830980", }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - File: "ggg", + URL: "ggg", }, { Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - File: "zzzzzz-the-very-last-one.keyXXX", + URL: "zzzzzz-the-very-last-one.keyXXX", }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: "SOMETHING.key", + URL: "SOMETHING.key", }, { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - File: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: "aaa", + URL: "aaa", }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - File: "zzz", + URL: "zzz", }, } - for _, a := range accounts { + for _, a := range accs { cache.add(a) } // Add some of them twice to check that they don't get reinserted. - cache.add(accounts[0]) - cache.add(accounts[2]) + cache.add(accs[0]) + cache.add(accs[2]) // Check that the account list is sorted by filename. - wantAccounts := make([]Account, len(accounts)) - copy(wantAccounts, accounts) + wantAccounts := make([]accounts.Account, len(accs)) + copy(wantAccounts, accs) sort.Sort(accountsByFile(wantAccounts)) list := cache.accounts() if !reflect.DeepEqual(list, wantAccounts) { - t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accounts), spew.Sdump(wantAccounts)) + t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) } - for _, a := range accounts { + for _, a := range accs { if !cache.hasAddress(a.Address) { t.Errorf("expected hasAccount(%x) to return true", a.Address) } @@ -184,13 +185,13 @@ func TestCacheAddDeleteOrder(t *testing.T) { } // Delete a few keys from the cache. - for i := 0; i < len(accounts); i += 2 { + for i := 0; i < len(accs); i += 2 { cache.delete(wantAccounts[i]) } - cache.delete(Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), File: "something"}) + cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) // Check content again after deletion. - wantAccountsAfterDelete := []Account{ + wantAccountsAfterDelete := []accounts.Account{ wantAccounts[1], wantAccounts[3], wantAccounts[5], @@ -214,60 +215,60 @@ func TestCacheFind(t *testing.T) { cache := newAddrCache(dir) cache.watcher.running = true // prevent unexpected reloads - accounts := []Account{ + accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - File: filepath.Join(dir, "a.key"), + URL: filepath.Join(dir, "a.key"), }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - File: filepath.Join(dir, "b.key"), + URL: filepath.Join(dir, "b.key"), }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: filepath.Join(dir, "c.key"), + URL: filepath.Join(dir, "c.key"), }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: filepath.Join(dir, "c2.key"), + URL: filepath.Join(dir, "c2.key"), }, } - for _, a := range accounts { + for _, a := range accs { cache.add(a) } - nomatchAccount := Account{ + nomatchAccount := accounts.Account{ Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: filepath.Join(dir, "something"), + URL: filepath.Join(dir, "something"), } tests := []struct { - Query Account - WantResult Account + Query accounts.Account + WantResult accounts.Account WantError error }{ // by address - {Query: Account{Address: accounts[0].Address}, WantResult: accounts[0]}, + {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, // by file - {Query: Account{File: accounts[0].File}, WantResult: accounts[0]}, + {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, // by basename - {Query: Account{File: filepath.Base(accounts[0].File)}, WantResult: accounts[0]}, + {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, // by file and address - {Query: accounts[0], WantResult: accounts[0]}, + {Query: accs[0], WantResult: accs[0]}, // ambiguous address, tie resolved by file - {Query: accounts[2], WantResult: accounts[2]}, + {Query: accs[2], WantResult: accs[2]}, // ambiguous address error { - Query: Account{Address: accounts[2].Address}, + Query: accounts.Account{Address: accs[2].Address}, WantError: &AmbiguousAddrError{ - Addr: accounts[2].Address, - Matches: []Account{accounts[2], accounts[3]}, + Addr: accs[2].Address, + Matches: []accounts.Account{accs[2], accs[3]}, }, }, // no match error {Query: nomatchAccount, WantError: ErrNoMatch}, - {Query: Account{File: nomatchAccount.File}, WantError: ErrNoMatch}, - {Query: Account{File: filepath.Base(nomatchAccount.File)}, WantError: ErrNoMatch}, - {Query: Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch}, + {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, } for i, test := range tests { a, err := cache.find(test.Query) diff --git a/accounts/key.go b/accounts/keystore/key.go similarity index 95% rename from accounts/key.go rename to accounts/keystore/key.go index dbcb49dcff..1cf5f93662 100644 --- a/accounts/key.go +++ b/accounts/keystore/key.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "bytes" @@ -29,6 +29,7 @@ import ( "strings" "time" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/secp256k1" @@ -175,13 +176,13 @@ func newKey(rand io.Reader) (*Key, error) { return newKeyFromECDSA(privateKeyECDSA), nil } -func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, Account, error) { +func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { key, err := newKey(rand) if err != nil { - return nil, Account{}, err + return nil, accounts.Account{}, err } - a := Account{Address: key.Address, File: ks.JoinPath(keyFileName(key.Address))} - if err := ks.StoreKey(a.File, key, auth); err != nil { + a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))} + if err := ks.StoreKey(a.URL, key, auth); err != nil { zeroKey(key.PrivateKey) return nil, a, err } diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go new file mode 100644 index 0000000000..d125f7d625 --- /dev/null +++ b/accounts/keystore/keystore.go @@ -0,0 +1,362 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package keystore implements encrypted storage of secp256k1 private keys. +// +// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification. +// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information. +package keystore + +import ( + "crypto/ecdsa" + crand "crypto/rand" + "errors" + "fmt" + "math/big" + "os" + "path/filepath" + "reflect" + "runtime" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + ErrNeedPasswordOrUnlock = accounts.NewAuthNeededError("password or unlock") + ErrNoMatch = errors.New("no key for given address or file") + ErrDecrypt = errors.New("could not decrypt key with given passphrase") +) + +// BackendType can be used to query the account manager for encrypted keystores. +var BackendType = reflect.TypeOf(new(KeyStore)) + +// KeyStore manages a key storage directory on disk. +type KeyStore struct { + cache *addressCache + keyStore keyStore + mu sync.RWMutex + unlocked map[common.Address]*unlocked +} + +type unlocked struct { + *Key + abort chan struct{} +} + +// NewKeyStore creates a keystore for the given directory. +func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { + keydir, _ = filepath.Abs(keydir) + ks := &KeyStore{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} + ks.init(keydir) + return ks +} + +// NewPlaintextKeyStore creates a keystore for the given directory. +// Deprecated: Use NewKeyStore. +func NewPlaintextKeyStore(keydir string) *KeyStore { + keydir, _ = filepath.Abs(keydir) + ks := &KeyStore{keyStore: &keyStorePlain{keydir}} + ks.init(keydir) + return ks +} + +func (ks *KeyStore) init(keydir string) { + ks.unlocked = make(map[common.Address]*unlocked) + ks.cache = newAddrCache(keydir) + // TODO: In order for this finalizer to work, there must be no references + // to ks. addressCache doesn't keep a reference but unlocked keys do, + // so the finalizer will not trigger until all timed unlocks have expired. + runtime.SetFinalizer(ks, func(m *KeyStore) { + m.cache.close() + }) +} + +// HasAddress reports whether a key with the given address is present. +func (ks *KeyStore) HasAddress(addr common.Address) bool { + return ks.cache.hasAddress(addr) +} + +// Accounts returns all key files present in the directory. +func (ks *KeyStore) Accounts() []accounts.Account { + return ks.cache.accounts() +} + +// Delete deletes the key matched by account if the passphrase is correct. +// If the account contains no filename, the address must match a unique key. +func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { + // Decrypting the key isn't really necessary, but we do + // it anyway to check the password and zero out the key + // immediately afterwards. + a, key, err := ks.getDecryptedKey(a, passphrase) + if key != nil { + zeroKey(key.PrivateKey) + } + if err != nil { + return err + } + // The order is crucial here. The key is dropped from the + // cache after the file is gone so that a reload happening in + // between won't insert it into the cache again. + err = os.Remove(a.URL) + if err == nil { + ks.cache.delete(a) + } + return err +} + +// SignHash calculates a ECDSA signature for the given hash. The produced +// signature is in the [R || S || V] format where V is 0 or 1. +func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrNeedPasswordOrUnlock + } + // Sign the hash using plain ECDSA operations + return crypto.Sign(hash, unlockedKey.PrivateKey) +} + +// SignTx signs the given transaction with the requested account. +func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrNeedPasswordOrUnlock + } + // Depending on the presence of the chain ID, sign with EIP155 or homestead + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey) +} + +// SignHashWithPassphrase signs hash if the private key matching the given address +// can be decrypted with the given passphrase. The produced signature is in the +// [R || S || V] format where V is 0 or 1. +func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + defer zeroKey(key.PrivateKey) + return crypto.Sign(hash, key.PrivateKey) +} + +// SignTxWithPassphrase signs the transaction if the private key matching the +// given address can be decrypted with the given passphrase. +func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + defer zeroKey(key.PrivateKey) + + // Depending on the presence of the chain ID, sign with EIP155 or homestead + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey) +} + +// Unlock unlocks the given account indefinitely. +func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error { + return ks.TimedUnlock(a, passphrase, 0) +} + +// Lock removes the private key with the given address from memory. +func (ks *KeyStore) Lock(addr common.Address) error { + ks.mu.Lock() + if unl, found := ks.unlocked[addr]; found { + ks.mu.Unlock() + ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) + } else { + ks.mu.Unlock() + } + return nil +} + +// TimedUnlock unlocks the given account with the passphrase. The account +// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account +// until the program exits. The account must match a unique key file. +// +// If the account address is already unlocked for a duration, TimedUnlock extends or +// shortens the active unlock timeout. If the address was previously unlocked +// indefinitely the timeout is not altered. +func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + + ks.mu.Lock() + defer ks.mu.Unlock() + u, found := ks.unlocked[a.Address] + if found { + if u.abort == nil { + // The address was unlocked indefinitely, so unlocking + // it with a timeout would be confusing. + zeroKey(key.PrivateKey) + return nil + } else { + // Terminate the expire goroutine and replace it below. + close(u.abort) + } + } + if timeout > 0 { + u = &unlocked{Key: key, abort: make(chan struct{})} + go ks.expire(a.Address, u, timeout) + } else { + u = &unlocked{Key: key} + } + ks.unlocked[a.Address] = u + return nil +} + +// Find resolves the given account into a unique entry in the keystore. +func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { + ks.cache.maybeReload() + ks.cache.mu.Lock() + a, err := ks.cache.find(a) + ks.cache.mu.Unlock() + return a, err +} + +func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { + a, err := ks.Find(a) + if err != nil { + return a, nil, err + } + key, err := ks.keyStore.GetKey(a.Address, a.URL, auth) + return a, key, err +} + +func (ks *KeyStore) 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: + ks.mu.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 ks.unlocked[addr] == u { + zeroKey(u.PrivateKey) + delete(ks.unlocked, addr) + } + ks.mu.Unlock() + } +} + +// NewAccount generates a new key and stores it into the key directory, +// encrypting it with the passphrase. +func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { + _, account, err := storeNewKey(ks.keyStore, crand.Reader, passphrase) + if err != nil { + return accounts.Account{}, err + } + // Add the account to the cache immediately rather + // than waiting for file system notifications to pick it up. + ks.cache.add(account) + return account, nil +} + +// Export exports as a JSON key, encrypted with newPassphrase. +func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + var N, P int + if store, ok := ks.keyStore.(*keyStorePassphrase); ok { + N, P = store.scryptN, store.scryptP + } else { + N, P = StandardScryptN, StandardScryptP + } + return EncryptKey(key, newPassphrase, N, P) +} + +// Import stores the given encrypted JSON key into the key directory. +func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { + key, err := DecryptKey(keyJSON, passphrase) + if key != nil && key.PrivateKey != nil { + defer zeroKey(key.PrivateKey) + } + if err != nil { + return accounts.Account{}, err + } + return ks.importKey(key, newPassphrase) +} + +// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase. +func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { + key := newKeyFromECDSA(priv) + if ks.cache.hasAddress(key.Address) { + return accounts.Account{}, fmt.Errorf("account already exists") + } + + return ks.importKey(key, passphrase) +} + +func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { + a := accounts.Account{Address: key.Address, URL: ks.keyStore.JoinPath(keyFileName(key.Address))} + if err := ks.keyStore.StoreKey(a.URL, key, passphrase); err != nil { + return accounts.Account{}, err + } + ks.cache.add(a) + return a, nil +} + +// Update changes the passphrase of an existing account. +func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + return ks.keyStore.StoreKey(a.URL, key, newPassphrase) +} + +// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores +// a key file in the key directory. The key file is encrypted with the same passphrase. +func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { + a, _, err := importPreSaleKey(ks.keyStore, keyJSON, passphrase) + if err != nil { + return a, err + } + ks.cache.add(a) + return a, nil +} + +// zeroKey zeroes a private key in memory. +func zeroKey(k *ecdsa.PrivateKey) { + b := k.D.Bits() + for i := range b { + b[i] = 0 + } +} diff --git a/accounts/key_store_passphrase.go b/accounts/keystore/keystore_passphrase.go similarity index 99% rename from accounts/key_store_passphrase.go rename to accounts/keystore/keystore_passphrase.go index 4a777956d6..8ef510fcfa 100644 --- a/accounts/key_store_passphrase.go +++ b/accounts/keystore/keystore_passphrase.go @@ -23,7 +23,7 @@ The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-St */ -package accounts +package keystore import ( "bytes" diff --git a/accounts/key_store_passphrase_test.go b/accounts/keystore/keystore_passphrase_test.go similarity index 99% rename from accounts/key_store_passphrase_test.go rename to accounts/keystore/keystore_passphrase_test.go index 217393fa5e..086addbc14 100644 --- a/accounts/key_store_passphrase_test.go +++ b/accounts/keystore/keystore_passphrase_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "io/ioutil" diff --git a/accounts/key_store_plain.go b/accounts/keystore/keystore_plain.go similarity index 99% rename from accounts/key_store_plain.go rename to accounts/keystore/keystore_plain.go index 2cbaa94df5..b490ca72b8 100644 --- a/accounts/key_store_plain.go +++ b/accounts/keystore/keystore_plain.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "encoding/json" diff --git a/accounts/key_store_test.go b/accounts/keystore/keystore_plain_test.go similarity index 88% rename from accounts/key_store_test.go rename to accounts/keystore/keystore_plain_test.go index d0713caa05..dcb2ab901a 100644 --- a/accounts/key_store_test.go +++ b/accounts/keystore/keystore_plain_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "crypto/rand" @@ -30,7 +30,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" ) -func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) { +func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) { d, err := ioutil.TempDir("", "geth-keystore-test") if err != nil { t.Fatal(err) @@ -44,7 +44,7 @@ func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) { } func TestKeyStorePlain(t *testing.T) { - dir, ks := tmpKeyStore(t, false) + dir, ks := tmpKeyStoreIface(t, false) defer os.RemoveAll(dir) pass := "" // not used but required by API @@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.File, pass) + k2, err := ks.GetKey(k1.Address, account.URL, pass) if err != nil { t.Fatal(err) } @@ -65,7 +65,7 @@ func TestKeyStorePlain(t *testing.T) { } func TestKeyStorePassphrase(t *testing.T) { - dir, ks := tmpKeyStore(t, true) + dir, ks := tmpKeyStoreIface(t, true) defer os.RemoveAll(dir) pass := "foo" @@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.File, pass) + k2, err := ks.GetKey(k1.Address, account.URL, pass) if err != nil { t.Fatal(err) } @@ -86,7 +86,7 @@ func TestKeyStorePassphrase(t *testing.T) { } func TestKeyStorePassphraseDecryptionFail(t *testing.T) { - dir, ks := tmpKeyStore(t, true) + dir, ks := tmpKeyStoreIface(t, true) defer os.RemoveAll(dir) pass := "foo" @@ -94,13 +94,13 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = ks.GetKey(k1.Address, account.File, "bar"); err != ErrDecrypt { + if _, err = ks.GetKey(k1.Address, account.URL, "bar"); err != ErrDecrypt { t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt) } } func TestImportPreSaleKey(t *testing.T) { - dir, ks := tmpKeyStore(t, true) + dir, ks := tmpKeyStoreIface(t, true) defer os.RemoveAll(dir) // file content of a presale key file generated with: @@ -115,8 +115,8 @@ func TestImportPreSaleKey(t *testing.T) { if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { t.Errorf("imported account has wrong address %x", account.Address) } - if !strings.HasPrefix(account.File, dir) { - t.Errorf("imported account file not in keystore directory: %q", account.File) + if !strings.HasPrefix(account.URL, dir) { + t.Errorf("imported account file not in keystore directory: %q", account.URL) } } @@ -142,19 +142,19 @@ func TestV3_PBKDF2_1(t *testing.T) { func TestV3_PBKDF2_2(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t) testDecryptV3(tests["test1"], t) } func TestV3_PBKDF2_3(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t) testDecryptV3(tests["python_generated_test_with_odd_iv"], t) } func TestV3_PBKDF2_4(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t) testDecryptV3(tests["evilnonce"], t) } @@ -166,7 +166,7 @@ func TestV3_Scrypt_1(t *testing.T) { func TestV3_Scrypt_2(t *testing.T) { t.Parallel() - tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t) testDecryptV3(tests["test2"], t) } diff --git a/accounts/accounts_test.go b/accounts/keystore/keystore_test.go similarity index 56% rename from accounts/accounts_test.go rename to accounts/keystore/keystore_test.go index b3ab87d503..af2140c31a 100644 --- a/accounts/accounts_test.go +++ b/accounts/keystore/keystore_test.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "io/ioutil" @@ -24,183 +24,184 @@ import ( "testing" "time" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" ) var testSigData = make([]byte, 32) -func TestManager(t *testing.T) { - dir, am := tmpManager(t, true) +func TestKeyStore(t *testing.T) { + dir, ks := tmpKeyStore(t, true) defer os.RemoveAll(dir) - a, err := am.NewAccount("foo") + a, err := ks.NewAccount("foo") if err != nil { t.Fatal(err) } - if !strings.HasPrefix(a.File, dir) { - t.Errorf("account file %s doesn't have dir prefix", a.File) + if !strings.HasPrefix(a.URL, dir) { + t.Errorf("account file %s doesn't have dir prefix", a.URL) } - stat, err := os.Stat(a.File) + stat, err := os.Stat(a.URL) if err != nil { - t.Fatalf("account file %s doesn't exist (%v)", a.File, err) + t.Fatalf("account file %s doesn't exist (%v)", a.URL, err) } if runtime.GOOS != "windows" && stat.Mode() != 0600 { t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600) } - if !am.HasAddress(a.Address) { + if !ks.HasAddress(a.Address) { t.Errorf("HasAccount(%x) should've returned true", a.Address) } - if err := am.Update(a, "foo", "bar"); err != nil { + if err := ks.Update(a, "foo", "bar"); err != nil { t.Errorf("Update error: %v", err) } - if err := am.Delete(a, "bar"); err != nil { + if err := ks.Delete(a, "bar"); err != nil { t.Errorf("Delete error: %v", err) } - if common.FileExist(a.File) { - t.Errorf("account file %s should be gone after Delete", a.File) + if common.FileExist(a.URL) { + t.Errorf("account file %s should be gone after Delete", a.URL) } - if am.HasAddress(a.Address) { + if ks.HasAddress(a.Address) { t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address) } } func TestSign(t *testing.T) { - dir, am := tmpManager(t, true) + dir, ks := tmpKeyStore(t, true) defer os.RemoveAll(dir) pass := "" // not used but required by API - a1, err := am.NewAccount(pass) + a1, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) } - if err := am.Unlock(a1, ""); err != nil { + if err := ks.Unlock(a1, ""); err != nil { t.Fatal(err) } - if _, err := am.Sign(a1.Address, testSigData); err != nil { + if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil { t.Fatal(err) } } func TestSignWithPassphrase(t *testing.T) { - dir, am := tmpManager(t, true) + dir, ks := tmpKeyStore(t, true) defer os.RemoveAll(dir) pass := "passwd" - acc, err := am.NewAccount(pass) + acc, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) } - if _, unlocked := am.unlocked[acc.Address]; unlocked { + if _, unlocked := ks.unlocked[acc.Address]; unlocked { t.Fatal("expected account to be locked") } - _, err = am.SignWithPassphrase(acc, pass, testSigData) + _, err = ks.SignHashWithPassphrase(acc, pass, testSigData) if err != nil { t.Fatal(err) } - if _, unlocked := am.unlocked[acc.Address]; unlocked { + if _, unlocked := ks.unlocked[acc.Address]; unlocked { t.Fatal("expected account to be locked") } - if _, err = am.SignWithPassphrase(acc, "invalid passwd", testSigData); err == nil { - t.Fatal("expected SignHash to fail with invalid password") + if _, err = ks.SignHashWithPassphrase(acc, "invalid passwd", testSigData); err == nil { + t.Fatal("expected SignHashWithPassphrase to fail with invalid password") } } func TestTimedUnlock(t *testing.T) { - dir, am := tmpManager(t, true) + dir, ks := tmpKeyStore(t, true) defer os.RemoveAll(dir) pass := "foo" - a1, err := am.NewAccount(pass) + a1, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) } // Signing without passphrase fails because account is locked - _, err = am.Sign(a1.Address, testSigData) - if err != ErrLocked { - t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) + if err != ErrNeedPasswordOrUnlock { + t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock before unlocking, got ", err) } // Signing with passphrase works - if err = am.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { + if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { t.Fatal(err) } // Signing without passphrase works because account is temp unlocked - _, err = am.Sign(a1.Address, testSigData) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) if err != nil { t.Fatal("Signing shouldn't return an error after unlocking, got ", err) } // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) - _, err = am.Sign(a1.Address, testSigData) - if err != ErrLocked { - t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) + if err != ErrNeedPasswordOrUnlock { + t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) } } func TestOverrideUnlock(t *testing.T) { - dir, am := tmpManager(t, false) + dir, ks := tmpKeyStore(t, false) defer os.RemoveAll(dir) pass := "foo" - a1, err := am.NewAccount(pass) + a1, err := ks.NewAccount(pass) if err != nil { t.Fatal(err) } // Unlock indefinitely. - if err = am.TimedUnlock(a1, pass, 5*time.Minute); err != nil { + if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil { t.Fatal(err) } // Signing without passphrase works because account is temp unlocked - _, err = am.Sign(a1.Address, testSigData) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) 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, pass, 100*time.Millisecond); err != nil { + if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { t.Fatal(err) } // Signing without passphrase still works because account is temp unlocked - _, err = am.Sign(a1.Address, testSigData) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) if err != nil { t.Fatal("Signing shouldn't return an error after unlocking, got ", err) } // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) - _, err = am.Sign(a1.Address, testSigData) - if err != ErrLocked { - t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) + if err != ErrNeedPasswordOrUnlock { + t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) } } // This test should fail under -race if signing races the expiration goroutine. func TestSignRace(t *testing.T) { - dir, am := tmpManager(t, false) + dir, ks := tmpKeyStore(t, false) defer os.RemoveAll(dir) // Create a test account. - a1, err := am.NewAccount("") + a1, err := ks.NewAccount("") if err != nil { t.Fatal("could not create the test account", err) } - if err := am.TimedUnlock(a1, "", 15*time.Millisecond); err != nil { + if err := ks.TimedUnlock(a1, "", 15*time.Millisecond); err != nil { t.Fatal("could not unlock the test account", err) } end := time.Now().Add(500 * time.Millisecond) for time.Now().Before(end) { - if _, err := am.Sign(a1.Address, testSigData); err == ErrLocked { + if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrNeedPasswordOrUnlock { return } else if err != nil { t.Errorf("Sign error: %v", err) @@ -211,14 +212,14 @@ func TestSignRace(t *testing.T) { t.Errorf("Account did not lock within the timeout") } -func tmpManager(t *testing.T, encrypted bool) (string, *Manager) { +func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) { d, err := ioutil.TempDir("", "eth-keystore-test") if err != nil { t.Fatal(err) } - new := NewPlaintextManager + new := NewPlaintextKeyStore if encrypted { - new = func(kd string) *Manager { return NewManager(kd, veryLightScryptN, veryLightScryptP) } + new = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) } } return d, new(d) } diff --git a/accounts/presale.go b/accounts/keystore/presale.go similarity index 93% rename from accounts/presale.go rename to accounts/keystore/presale.go index f00b4f5020..948320e0a8 100644 --- a/accounts/presale.go +++ b/accounts/keystore/presale.go @@ -14,7 +14,7 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -package accounts +package keystore import ( "crypto/aes" @@ -25,20 +25,21 @@ import ( "errors" "fmt" + "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/crypto" "github.com/pborman/uuid" "golang.org/x/crypto/pbkdf2" ) // creates a Key and stores that in the given KeyStore by decrypting a presale key JSON -func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (Account, *Key, error) { +func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accounts.Account, *Key, error) { key, err := decryptPreSaleKey(keyJSON, password) if err != nil { - return Account{}, nil, err + return accounts.Account{}, nil, err } key.Id = uuid.NewRandom() - a := Account{Address: key.Address, File: keyStore.JoinPath(keyFileName(key.Address))} - err = keyStore.StoreKey(a.File, key, password) + a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))} + err = keyStore.StoreKey(a.URL, key, password) return a, key, err } diff --git a/accounts/testdata/dupes/1 b/accounts/keystore/testdata/dupes/1 similarity index 100% rename from accounts/testdata/dupes/1 rename to accounts/keystore/testdata/dupes/1 diff --git a/accounts/testdata/dupes/2 b/accounts/keystore/testdata/dupes/2 similarity index 100% rename from accounts/testdata/dupes/2 rename to accounts/keystore/testdata/dupes/2 diff --git a/accounts/testdata/dupes/foo b/accounts/keystore/testdata/dupes/foo similarity index 100% rename from accounts/testdata/dupes/foo rename to accounts/keystore/testdata/dupes/foo diff --git a/accounts/testdata/keystore/.hiddenfile b/accounts/keystore/testdata/keystore/.hiddenfile similarity index 100% rename from accounts/testdata/keystore/.hiddenfile rename to accounts/keystore/testdata/keystore/.hiddenfile diff --git a/accounts/testdata/keystore/README b/accounts/keystore/testdata/keystore/README similarity index 100% rename from accounts/testdata/keystore/README rename to accounts/keystore/testdata/keystore/README diff --git a/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 similarity index 100% rename from accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 rename to accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 diff --git a/accounts/testdata/keystore/aaa b/accounts/keystore/testdata/keystore/aaa similarity index 100% rename from accounts/testdata/keystore/aaa rename to accounts/keystore/testdata/keystore/aaa diff --git a/accounts/testdata/keystore/empty b/accounts/keystore/testdata/keystore/empty similarity index 100% rename from accounts/testdata/keystore/empty rename to accounts/keystore/testdata/keystore/empty diff --git a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e similarity index 100% rename from accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e rename to accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e diff --git a/accounts/testdata/keystore/garbage b/accounts/keystore/testdata/keystore/garbage similarity index 100% rename from accounts/testdata/keystore/garbage rename to accounts/keystore/testdata/keystore/garbage diff --git a/accounts/testdata/keystore/no-address b/accounts/keystore/testdata/keystore/no-address similarity index 100% rename from accounts/testdata/keystore/no-address rename to accounts/keystore/testdata/keystore/no-address diff --git a/accounts/testdata/keystore/zero b/accounts/keystore/testdata/keystore/zero similarity index 100% rename from accounts/testdata/keystore/zero rename to accounts/keystore/testdata/keystore/zero diff --git a/accounts/testdata/keystore/zzz b/accounts/keystore/testdata/keystore/zzz similarity index 100% rename from accounts/testdata/keystore/zzz rename to accounts/keystore/testdata/keystore/zzz diff --git a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e similarity index 100% rename from accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e rename to accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e diff --git a/accounts/testdata/v1_test_vector.json b/accounts/keystore/testdata/v1_test_vector.json similarity index 100% rename from accounts/testdata/v1_test_vector.json rename to accounts/keystore/testdata/v1_test_vector.json diff --git a/accounts/testdata/v3_test_vector.json b/accounts/keystore/testdata/v3_test_vector.json similarity index 100% rename from accounts/testdata/v3_test_vector.json rename to accounts/keystore/testdata/v3_test_vector.json diff --git a/accounts/testdata/very-light-scrypt.json b/accounts/keystore/testdata/very-light-scrypt.json similarity index 100% rename from accounts/testdata/very-light-scrypt.json rename to accounts/keystore/testdata/very-light-scrypt.json diff --git a/accounts/watch.go b/accounts/keystore/watch.go similarity index 96% rename from accounts/watch.go rename to accounts/keystore/watch.go index 472be2df76..04a87b12ec 100644 --- a/accounts/watch.go +++ b/accounts/keystore/watch.go @@ -16,7 +16,7 @@ // +build darwin,!ios freebsd linux,!arm64 netbsd solaris -package accounts +package keystore import ( "time" @@ -27,14 +27,14 @@ import ( ) type watcher struct { - ac *addrCache + ac *addressCache starting bool running bool ev chan notify.EventInfo quit chan struct{} } -func newWatcher(ac *addrCache) *watcher { +func newWatcher(ac *addressCache) *watcher { return &watcher{ ac: ac, ev: make(chan notify.EventInfo, 10), diff --git a/accounts/watch_fallback.go b/accounts/keystore/watch_fallback.go similarity index 85% rename from accounts/watch_fallback.go rename to accounts/keystore/watch_fallback.go index bf971cb1ba..6412f3b33b 100644 --- a/accounts/watch_fallback.go +++ b/accounts/keystore/watch_fallback.go @@ -19,10 +19,10 @@ // This is the fallback implementation of directory watching. // It is used on unsupported platforms. -package accounts +package keystore type watcher struct{ running bool } -func newWatcher(*addrCache) *watcher { return new(watcher) } -func (*watcher) start() {} -func (*watcher) close() {} +func newWatcher(*addressCache) *watcher { return new(watcher) } +func (*watcher) start() {} +func (*watcher) close() {} diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go index 237af99eb5..3a0eb13e81 100644 --- a/cmd/geth/accountcmd.go +++ b/cmd/geth/accountcmd.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/console" "github.com/ethereum/go-ethereum/crypto" @@ -181,30 +182,30 @@ nodes. func accountList(ctx *cli.Context) error { stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) for i, acct := range stack.AccountManager().Accounts() { - fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.File) + fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.URL) } return nil } // tries unlocking the specified account a few times. -func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (accounts.Account, string) { - account, err := utils.MakeAddress(accman, address) +func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) { + account, err := utils.MakeAddress(ks, address) if err != nil { utils.Fatalf("Could not list accounts: %v", err) } 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) - err = accman.Unlock(account, password) + err = ks.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 { + if err, ok := err.(*keystore.AmbiguousAddrError); ok { glog.V(logger.Info).Infof("Unlocked account %x", account.Address) - return ambiguousAddrRecovery(accman, err, password), password + return ambiguousAddrRecovery(ks, err, password), password } - if err != accounts.ErrDecrypt { + if err != keystore.ErrDecrypt { // No need to prompt again if the error is not decryption-related. break } @@ -244,15 +245,15 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) return password } -func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account { +func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.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(" ", a.URL) } 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 { + if err := ks.Unlock(a, auth); err == nil { match = &a break } @@ -260,11 +261,11 @@ func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrErro if match == nil { utils.Fatalf("None of the listed files could be unlocked.") } - fmt.Printf("Your passphrase unlocked %s\n", match.File) + fmt.Printf("Your passphrase unlocked %s\n", match.URL) 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) + fmt.Println(" ", a.URL) } } return *match @@ -275,7 +276,8 @@ func accountCreate(ctx *cli.Context) error { stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - account, err := stack.AccountManager().NewAccount(password) + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + account, err := ks.NewAccount(password) if err != nil { utils.Fatalf("Failed to create account: %v", err) } @@ -290,9 +292,11 @@ func accountUpdate(ctx *cli.Context) error { utils.Fatalf("No accounts specified to update") } stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) - account, oldPassword := unlockAccount(ctx, stack.AccountManager(), ctx.Args().First(), 0, nil) + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + + account, oldPassword := unlockAccount(ctx, ks, ctx.Args().First(), 0, nil) newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) - if err := stack.AccountManager().Update(account, oldPassword, newPassword); err != nil { + if err := ks.Update(account, oldPassword, newPassword); err != nil { utils.Fatalf("Could not update the account: %v", err) } return nil @@ -310,7 +314,9 @@ func importWallet(ctx *cli.Context) error { stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx)) - acct, err := stack.AccountManager().ImportPreSaleKey(keyJson, passphrase) + + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + acct, err := ks.ImportPreSaleKey(keyJson, passphrase) if err != nil { utils.Fatalf("%v", err) } @@ -329,7 +335,9 @@ func accountImport(ctx *cli.Context) error { } stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - acct, err := stack.AccountManager().ImportECDSA(key, passphrase) + + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + acct, err := ks.ImportECDSA(key, passphrase) if err != nil { utils.Fatalf("Could not create the account: %v", err) } diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go index 113df983e2..7e03d7548d 100644 --- a/cmd/geth/accountcmd_test.go +++ b/cmd/geth/accountcmd_test.go @@ -35,7 +35,7 @@ import ( func tmpDatadirWithKeystore(t *testing.T) string { datadir := tmpdir(t) keystore := filepath.Join(datadir, "keystore") - source := filepath.Join("..", "..", "accounts", "testdata", "keystore") + source := filepath.Join("..", "..", "accounts", "keystore", "testdata", "keystore") if err := cp.CopyAll(keystore, source); err != nil { t.Fatal(err) } @@ -230,7 +230,7 @@ Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase) } func TestUnlockFlagAmbiguous(t *testing.T) { - store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes") geth := runGeth(t, "--keystore", store, "--nat", "none", "--nodiscover", "--dev", "--unlock", "f466859ead1932d743d622cb74fc058882e8648a", @@ -267,7 +267,7 @@ In order to avoid this warning, you need to remove the following duplicate key f } func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) { - store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes") geth := runGeth(t, "--keystore", store, "--nat", "none", "--nodiscover", "--dev", "--unlock", "f466859ead1932d743d622cb74fc058882e8648a") diff --git a/cmd/geth/main.go b/cmd/geth/main.go index d7e4cc7b53..2c4963cac0 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console" @@ -245,12 +246,13 @@ func startNode(ctx *cli.Context, stack *node.Node) { utils.StartNode(stack) // Unlock any account specifically requested - accman := stack.AccountManager() + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + passwords := utils.MakePasswordList(ctx) accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",") for i, account := range accounts { if trimmed := strings.TrimSpace(account); trimmed != "" { - unlockAccount(ctx, accman, trimmed, i, passwords) + unlockAccount(ctx, ks, trimmed, i, passwords) } } // Start auxiliary services if enabled diff --git a/cmd/gethrpctest/main.go b/cmd/gethrpctest/main.go index 850bf8eb22..348eeebce0 100644 --- a/cmd/gethrpctest/main.go +++ b/cmd/gethrpctest/main.go @@ -23,6 +23,7 @@ import ( "os" "os/signal" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/ethdb" @@ -99,17 +100,18 @@ func MakeSystemNode(privkey string, test *tests.BlockTest) (*node.Node, error) { return nil, err } // Create the keystore and inject an unlocked account if requested - accman := stack.AccountManager() + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + if len(privkey) > 0 { key, err := crypto.HexToECDSA(privkey) if err != nil { return nil, err } - a, err := accman.ImportECDSA(key, "") + a, err := ks.ImportECDSA(key, "") if err != nil { return nil, err } - if err := accman.Unlock(a, ""); err != nil { + if err := ks.Unlock(a, ""); err != nil { return nil, err } } diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go index 546c646f11..466b17f300 100644 --- a/cmd/swarm/main.go +++ b/cmd/swarm/main.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console" @@ -327,29 +328,36 @@ func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { return key } // Otherwise try getting it from the keystore. - return decryptStoreAccount(stack.AccountManager(), keyid) + am := stack.AccountManager() + ks := am.Backend(keystore.BackendType).(*keystore.KeyStore) + + return decryptStoreAccount(ks, keyid) } -func decryptStoreAccount(accman *accounts.Manager, account string) *ecdsa.PrivateKey { +func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKey { var a accounts.Account var err error if common.IsHexAddress(account) { - a, err = accman.Find(accounts.Account{Address: common.HexToAddress(account)}) - } else if ix, ixerr := strconv.Atoi(account); ixerr == nil { - a, err = accman.AccountByIndex(ix) + a, err = ks.Find(accounts.Account{Address: common.HexToAddress(account)}) + } else if ix, ixerr := strconv.Atoi(account); ixerr == nil && ix > 0 { + if accounts := ks.Accounts(); len(accounts) > ix { + a = accounts[ix] + } else { + err = fmt.Errorf("index %d higher than number of accounts %d", ix, len(accounts)) + } } else { utils.Fatalf("Can't find swarm account key %s", account) } if err != nil { utils.Fatalf("Can't find swarm account key: %v", err) } - keyjson, err := ioutil.ReadFile(a.File) + keyjson, err := ioutil.ReadFile(a.URL) if err != nil { utils.Fatalf("Can't load swarm account key: %v", err) } for i := 1; i <= 3; i++ { passphrase := promptPassphrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i)) - key, err := accounts.DecryptKey(keyjson, passphrase) + key, err := keystore.DecryptKey(keyjson, passphrase) if err == nil { return key.PrivateKey } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9ba33df809..1196a64b58 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" @@ -587,23 +588,27 @@ func MakeDatabaseHandles() int { // MakeAddress converts an account specified directly as a hex encoded string or // a key index in the key store to an internal account representation. -func MakeAddress(accman *accounts.Manager, account string) (accounts.Account, error) { +func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error) { // If the specified account is a valid address, return it if common.IsHexAddress(account) { return accounts.Account{Address: common.HexToAddress(account)}, nil } // Otherwise try to interpret the account as a keystore index index, err := strconv.Atoi(account) - if err != nil { + if err != nil || index < 0 { return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account) } - return accman.AccountByIndex(index) + accs := ks.Accounts() + if len(accs) <= index { + return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs)) + } + return accs[index], nil } // MakeEtherbase retrieves the etherbase either from the directly specified // command line flags or from the keystore if CLI indexed. -func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { - accounts := accman.Accounts() +func MakeEtherbase(ks *keystore.KeyStore, ctx *cli.Context) common.Address { + accounts := ks.Accounts() if !ctx.GlobalIsSet(EtherbaseFlag.Name) && len(accounts) == 0 { glog.V(logger.Error).Infoln("WARNING: No etherbase set and no accounts found as default") return common.Address{} @@ -613,7 +618,7 @@ func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { return common.Address{} } // If the specified etherbase is a valid address, return it - account, err := MakeAddress(accman, etherbase) + account, err := MakeAddress(ks, etherbase) if err != nil { Fatalf("Option %q: %v", EtherbaseFlag.Name, err) } @@ -721,9 +726,10 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) { if networks > 1 { Fatalf("The %v flags are mutually exclusive", netFlags) } + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) ethConf := ð.Config{ - Etherbase: MakeEtherbase(stack.AccountManager(), ctx), + Etherbase: MakeEtherbase(ks, ctx), ChainConfig: MakeChainConfig(ctx, stack), FastSync: ctx.GlobalBool(FastSyncFlag.Name), LightMode: ctx.GlobalBool(LightModeFlag.Name), diff --git a/eth/backend.go b/eth/backend.go index af120cbadd..004be7e26d 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -361,15 +361,13 @@ func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { } func (s *Ethereum) Etherbase() (eb common.Address, err error) { - eb = s.etherbase - if (eb == common.Address{}) { - firstAccount, err := s.AccountManager().AccountByIndex(0) - eb = firstAccount.Address - if err != nil { - return eb, fmt.Errorf("etherbase address must be explicitly specified") - } + if s.etherbase != (common.Address{}) { + return s.etherbase, nil + } + if accounts := s.AccountManager().Accounts(); len(accounts) > 0 { + return accounts[0].Address, nil } - return eb, nil + return common.Address{}, fmt.Errorf("etherbase address must be explicitly specified") } // set in js console via admin interface or wrapper from cli flags diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4c8e784c5d..7962f8b171 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" @@ -219,13 +220,18 @@ func (s *PrivateAccountAPI) ListAccounts() []common.Address { // NewAccount will create a new account and returns the address for the new account. func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error) { - acc, err := s.am.NewAccount(password) + acc, err := fetchKeystore(s.am).NewAccount(password) if err == nil { return acc.Address, nil } return common.Address{}, err } +// fetchKeystore retrives the encrypted keystore from the account manager. +func fetchKeystore(am *accounts.Manager) *keystore.KeyStore { + return am.Backend(keystore.BackendType).(*keystore.KeyStore) +} + // ImportRawKey stores the given hex encoded ECDSA key into the key directory, // encrypting it with the passphrase. func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (common.Address, error) { @@ -234,7 +240,7 @@ func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (commo return common.Address{}, err } - acc, err := s.am.ImportECDSA(crypto.ToECDSA(hexkey), password) + acc, err := fetchKeystore(s.am).ImportECDSA(crypto.ToECDSA(hexkey), password) return acc.Address, err } @@ -251,13 +257,13 @@ func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, } else { d = time.Duration(*duration) * time.Second } - err := s.am.TimedUnlock(accounts.Account{Address: addr}, password, d) + err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d) return err == nil, err } // LockAccount will lock the account associated with the given address when it's unlocked. func (s *PrivateAccountAPI) LockAccount(addr common.Address) bool { - return s.am.Lock(addr) == nil + return fetchKeystore(s.am).Lock(addr) == nil } // SendTransaction will create a transaction from the given arguments and @@ -268,13 +274,16 @@ func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs return common.Hash{}, err } tx := args.toTransaction() - signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number()) - signature, err := s.am.SignWithPassphrase(accounts.Account{Address: args.From}, passwd, signer.Hash(tx).Bytes()) + + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := s.am.SignTxWithPassphrase(accounts.Account{Address: args.From}, passwd, tx, chainID) if err != nil { return common.Hash{}, err } - - return submitTransaction(ctx, s.b, tx, signature) + return submitTransaction(ctx, s.b, signed) } // signHash is a helper function that calculates a hash for the given message that can be @@ -299,7 +308,7 @@ func signHash(data []byte) []byte { // // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) { - signature, err := s.b.AccountManager().SignWithPassphrase(accounts.Account{Address: addr}, passwd, signHash(data)) + signature, err := s.b.AccountManager().SignHashWithPassphrase(accounts.Account{Address: addr}, passwd, signHash(data)) if err != nil { return nil, err } @@ -1023,13 +1032,11 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(txHash common.Hash) (ma // sign is a helper function that signs a transaction with the private key of the given address. func (s *PublicTransactionPoolAPI) sign(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { - signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number()) - - signature, err := s.b.AccountManager().Sign(addr, signer.Hash(tx).Bytes()) - if err != nil { - return nil, err + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId } - return tx.WithSignature(signer, signature) + return s.b.AccountManager().SignTx(accounts.Account{Address: addr}, tx, chainID) } // SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool. @@ -1076,27 +1083,19 @@ func (args *SendTxArgs) toTransaction() *types.Transaction { } // submitTransaction is a helper function that submits tx to txPool and logs a message. -func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction, signature []byte) (common.Hash, error) { - signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) - - signedTx, err := tx.WithSignature(signer, signature) - if err != nil { +func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { + if err := b.SendTx(ctx, tx); err != nil { return common.Hash{}, err } - - if err := b.SendTx(ctx, signedTx); err != nil { - return common.Hash{}, err - } - - if signedTx.To() == nil { - from, _ := types.Sender(signer, signedTx) - addr := crypto.CreateAddress(from, signedTx.Nonce()) - glog.V(logger.Info).Infof("Tx(%s) created: %s\n", signedTx.Hash().Hex(), addr.Hex()) + if tx.To() == nil { + signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) + from, _ := types.Sender(signer, tx) + addr := crypto.CreateAddress(from, tx.Nonce()) + glog.V(logger.Info).Infof("Tx(%s) created: %s\n", tx.Hash().Hex(), addr.Hex()) } else { - glog.V(logger.Info).Infof("Tx(%s) to: %s\n", signedTx.Hash().Hex(), tx.To().Hex()) + glog.V(logger.Info).Infof("Tx(%s) to: %s\n", tx.Hash().Hex(), tx.To().Hex()) } - - return signedTx.Hash(), nil + return tx.Hash(), nil } // SendTransaction creates a transaction for the given argument, sign it and submit it to the @@ -1106,12 +1105,16 @@ func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args Sen return common.Hash{}, err } tx := args.toTransaction() - signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number()) - signature, err := s.b.AccountManager().Sign(args.From, signer.Hash(tx).Bytes()) + + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := s.b.AccountManager().SignTx(accounts.Account{Address: args.From}, tx, chainID) if err != nil { return common.Hash{}, err } - return submitTransaction(ctx, s.b, tx, signature) + return submitTransaction(ctx, s.b, signed) } // SendRawTransaction will add the signed transaction to the transaction pool. @@ -1151,7 +1154,7 @@ func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encod // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { - signature, err := s.b.AccountManager().Sign(addr, signHash(data)) + signature, err := s.b.AccountManager().SignHash(accounts.Account{Address: addr}, signHash(data)) if err == nil { signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper } diff --git a/internal/guide/guide_test.go b/internal/guide/guide_test.go index 8f89037bd6..9c7ad16d18 100644 --- a/internal/guide/guide_test.go +++ b/internal/guide/guide_test.go @@ -24,13 +24,14 @@ package guide import ( "io/ioutil" + "math/big" "os" "path/filepath" "testing" "time" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/core/types" ) // Tests that the account management snippets work correctly. @@ -42,59 +43,59 @@ func TestAccountManagement(t *testing.T) { } defer os.RemoveAll(workdir) - // Create an encrypted keystore manager with standard crypto parameters - am := accounts.NewManager(filepath.Join(workdir, "keystore"), accounts.StandardScryptN, accounts.StandardScryptP) + // Create an encrypted keystore with standard crypto parameters + ks := keystore.NewKeyStore(filepath.Join(workdir, "keystore"), keystore.StandardScryptN, keystore.StandardScryptP) // Create a new account with the specified encryption passphrase - newAcc, err := am.NewAccount("Creation password") + newAcc, err := ks.NewAccount("Creation password") if err != nil { t.Fatalf("Failed to create new account: %v", err) } // Export the newly created account with a different passphrase. The returned // data from this method invocation is a JSON encoded, encrypted key-file - jsonAcc, err := am.Export(newAcc, "Creation password", "Export password") + jsonAcc, err := ks.Export(newAcc, "Creation password", "Export password") if err != nil { t.Fatalf("Failed to export account: %v", err) } // Update the passphrase on the account created above inside the local keystore - if err := am.Update(newAcc, "Creation password", "Update password"); err != nil { + if err := ks.Update(newAcc, "Creation password", "Update password"); err != nil { t.Fatalf("Failed to update account: %v", err) } // Delete the account updated above from the local keystore - if err := am.Delete(newAcc, "Update password"); err != nil { + if err := ks.Delete(newAcc, "Update password"); err != nil { t.Fatalf("Failed to delete account: %v", err) } // Import back the account we've exported (and then deleted) above with yet // again a fresh passphrase - if _, err := am.Import(jsonAcc, "Export password", "Import password"); err != nil { + if _, err := ks.Import(jsonAcc, "Export password", "Import password"); err != nil { t.Fatalf("Failed to import account: %v", err) } // Create a new account to sign transactions with - signer, err := am.NewAccount("Signer password") + signer, err := ks.NewAccount("Signer password") if err != nil { t.Fatalf("Failed to create signer account: %v", err) } - txHash := common.HexToHash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + tx, chain := new(types.Transaction), big.NewInt(1) // Sign a transaction with a single authorization - if _, err := am.SignWithPassphrase(signer, "Signer password", txHash.Bytes()); err != nil { + if _, err := ks.SignTxWithPassphrase(signer, "Signer password", tx, chain); err != nil { t.Fatalf("Failed to sign with passphrase: %v", err) } // Sign a transaction with multiple manually cancelled authorizations - if err := am.Unlock(signer, "Signer password"); err != nil { + if err := ks.Unlock(signer, "Signer password"); err != nil { t.Fatalf("Failed to unlock account: %v", err) } - if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil { + if _, err := ks.SignTx(signer, tx, chain); err != nil { t.Fatalf("Failed to sign with unlocked account: %v", err) } - if err := am.Lock(signer.Address); err != nil { + if err := ks.Lock(signer.Address); err != nil { t.Fatalf("Failed to lock account: %v", err) } // Sign a transaction with multiple automatically cancelled authorizations - if err := am.TimedUnlock(signer, "Signer password", time.Second); err != nil { + if err := ks.TimedUnlock(signer, "Signer password", time.Second); err != nil { t.Fatalf("Failed to time unlock account: %v", err) } - if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil { + if _, err := ks.SignTx(signer, tx, chain); err != nil { t.Fatalf("Failed to sign with time unlocked account: %v", err) } } diff --git a/mobile/accounts.go b/mobile/accounts.go index 621be4d7a3..897549a6b9 100644 --- a/mobile/accounts.go +++ b/mobile/accounts.go @@ -24,24 +24,25 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" ) const ( // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptN = int(accounts.StandardScryptN) + StandardScryptN = int(keystore.StandardScryptN) // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptP = int(accounts.StandardScryptP) + StandardScryptP = int(keystore.StandardScryptP) // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptN = int(accounts.LightScryptN) + LightScryptN = int(keystore.LightScryptN) // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptP = int(accounts.LightScryptP) + LightScryptP = int(keystore.LightScryptP) ) // Account represents a stored key. @@ -79,57 +80,73 @@ func (a *Account) GetAddress() *Address { // GetFile retrieves the path of the file containing the account key. func (a *Account) GetFile() string { - return a.account.File + return a.account.URL } -// AccountManager manages a key storage directory on disk. -type AccountManager struct{ manager *accounts.Manager } +// KeyStore manages a key storage directory on disk. +type KeyStore struct{ keystore *keystore.KeyStore } -// NewAccountManager creates a manager for the given directory. -func NewAccountManager(keydir string, scryptN, scryptP int) *AccountManager { - return &AccountManager{manager: accounts.NewManager(keydir, scryptN, scryptP)} +// NewKeyStore creates a keystore for the given directory. +func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { + return &KeyStore{keystore: keystore.NewKeyStore(keydir, scryptN, scryptP)} } // HasAddress reports whether a key with the given address is present. -func (am *AccountManager) HasAddress(address *Address) bool { - return am.manager.HasAddress(address.address) +func (ks *KeyStore) HasAddress(address *Address) bool { + return ks.keystore.HasAddress(address.address) } // GetAccounts returns all key files present in the directory. -func (am *AccountManager) GetAccounts() *Accounts { - return &Accounts{am.manager.Accounts()} +func (ks *KeyStore) GetAccounts() *Accounts { + return &Accounts{ks.keystore.Accounts()} } // DeleteAccount deletes the key matched by account if the passphrase is correct. // If a contains no filename, the address must match a unique key. -func (am *AccountManager) DeleteAccount(account *Account, passphrase string) error { - return am.manager.Delete(accounts.Account{ - Address: account.account.Address, - File: account.account.File, - }, passphrase) +func (ks *KeyStore) DeleteAccount(account *Account, passphrase string) error { + return ks.keystore.Delete(account.account, passphrase) } -// Sign calculates a ECDSA signature for the given hash. The produced signature +// SignHash calculates a ECDSA signature for the given hash. The produced signature // is in the [R || S || V] format where V is 0 or 1. -func (am *AccountManager) Sign(address *Address, hash []byte) (signature []byte, _ error) { - return am.manager.Sign(address.address, hash) +func (ks *KeyStore) SignHash(address *Address, hash []byte) (signature []byte, _ error) { + return ks.keystore.SignHash(accounts.Account{Address: address.address}, hash) } -// SignPassphrase signs hash if the private key matching the given address can +// SignTx signs the given transaction with the requested account. +func (ks *KeyStore) SignTx(account *Account, tx *Transaction, chainID *BigInt) (*Transaction, error) { + signed, err := ks.keystore.SignTx(account.account, tx.tx, chainID.bigint) + if err != nil { + return nil, err + } + return &Transaction{signed}, nil +} + +// SignHashPassphrase signs hash if the private key matching the given address can // be decrypted with the given passphrase. The produced signature is in the // [R || S || V] format where V is 0 or 1. -func (am *AccountManager) SignPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) { - return am.manager.SignWithPassphrase(account.account, passphrase, hash) +func (ks *KeyStore) SignHashPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) { + return ks.keystore.SignHashWithPassphrase(account.account, passphrase, hash) +} + +// SignTxPassphrase signs the transaction if the private key matching the +// given address can be decrypted with the given passphrase. +func (ks *KeyStore) SignTxPassphrase(account *Account, passphrase string, tx *Transaction, chainID *BigInt) (*Transaction, error) { + signed, err := ks.keystore.SignTxWithPassphrase(account.account, passphrase, tx.tx, chainID.bigint) + if err != nil { + return nil, err + } + return &Transaction{signed}, nil } // Unlock unlocks the given account indefinitely. -func (am *AccountManager) Unlock(account *Account, passphrase string) error { - return am.manager.TimedUnlock(account.account, passphrase, 0) +func (ks *KeyStore) Unlock(account *Account, passphrase string) error { + return ks.keystore.TimedUnlock(account.account, passphrase, 0) } // Lock removes the private key with the given address from memory. -func (am *AccountManager) Lock(address *Address) error { - return am.manager.Lock(address.address) +func (ks *KeyStore) Lock(address *Address) error { + return ks.keystore.Lock(address.address) } // TimedUnlock unlocks the given account with the passphrase. The account stays @@ -139,14 +156,14 @@ func (am *AccountManager) Lock(address *Address) error { // If the account address is already unlocked for a duration, TimedUnlock extends or // shortens the active unlock timeout. If the address was previously unlocked // indefinitely the timeout is not altered. -func (am *AccountManager) TimedUnlock(account *Account, passphrase string, timeout int64) error { - return am.manager.TimedUnlock(account.account, passphrase, time.Duration(timeout)) +func (ks *KeyStore) TimedUnlock(account *Account, passphrase string, timeout int64) error { + return ks.keystore.TimedUnlock(account.account, passphrase, time.Duration(timeout)) } // NewAccount generates a new key and stores it into the key directory, // encrypting it with the passphrase. -func (am *AccountManager) NewAccount(passphrase string) (*Account, error) { - account, err := am.manager.NewAccount(passphrase) +func (ks *KeyStore) NewAccount(passphrase string) (*Account, error) { + account, err := ks.keystore.NewAccount(passphrase) if err != nil { return nil, err } @@ -154,13 +171,13 @@ func (am *AccountManager) NewAccount(passphrase string) (*Account, error) { } // ExportKey exports as a JSON key, encrypted with newPassphrase. -func (am *AccountManager) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) { - return am.manager.Export(account.account, passphrase, newPassphrase) +func (ks *KeyStore) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) { + return ks.keystore.Export(account.account, passphrase, newPassphrase) } // ImportKey stores the given encrypted JSON key into the key directory. -func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) { - acc, err := am.manager.Import(keyJSON, passphrase, newPassphrase) +func (ks *KeyStore) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) { + acc, err := ks.keystore.Import(keyJSON, passphrase, newPassphrase) if err != nil { return nil, err } @@ -168,14 +185,14 @@ func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase st } // UpdateAccount changes the passphrase of an existing account. -func (am *AccountManager) UpdateAccount(account *Account, passphrase, newPassphrase string) error { - return am.manager.Update(account.account, passphrase, newPassphrase) +func (ks *KeyStore) UpdateAccount(account *Account, passphrase, newPassphrase string) error { + return ks.keystore.Update(account.account, passphrase, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores // a key file in the key directory. The key file is encrypted with the same passphrase. -func (am *AccountManager) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) { - account, err := am.manager.ImportPreSaleKey(keyJSON, passphrase) +func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) { + account, err := ks.keystore.ImportPreSaleKey(keyJSON, passphrase) if err != nil { return nil, err } diff --git a/mobile/android_test.go b/mobile/android_test.go index 3776f82911..6f5076d64e 100644 --- a/mobile/android_test.go +++ b/mobile/android_test.go @@ -43,42 +43,46 @@ public class AndroidTest extends InstrumentationTestCase { public AndroidTest() {} public void testAccountManagement() { - // Create an encrypted keystore manager with light crypto parameters. - AccountManager am = new AccountManager(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP); + // Create an encrypted keystore with light crypto parameters. + KeyStore ks = new KeyStore(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP); try { // Create a new account with the specified encryption passphrase. - Account newAcc = am.newAccount("Creation password"); + Account newAcc = ks.newAccount("Creation password"); // Export the newly created account with a different passphrase. The returned // data from this method invocation is a JSON encoded, encrypted key-file. - byte[] jsonAcc = am.exportKey(newAcc, "Creation password", "Export password"); + byte[] jsonAcc = ks.exportKey(newAcc, "Creation password", "Export password"); // Update the passphrase on the account created above inside the local keystore. - am.updateAccount(newAcc, "Creation password", "Update password"); + ks.updateAccount(newAcc, "Creation password", "Update password"); // Delete the account updated above from the local keystore. - am.deleteAccount(newAcc, "Update password"); + ks.deleteAccount(newAcc, "Update password"); // Import back the account we've exported (and then deleted) above with yet // again a fresh passphrase. - Account impAcc = am.importKey(jsonAcc, "Export password", "Import password"); + Account impAcc = ks.importKey(jsonAcc, "Export password", "Import password"); // Create a new account to sign transactions with - Account signer = am.newAccount("Signer password"); - Hash txHash = new Hash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + Account signer = ks.newAccount("Signer password"); + + Transaction tx = new Transaction( + 1, new Address("0x0000000000000000000000000000000000000000"), + new BigInt(0), new BigInt(0), new BigInt(1), null); // Random empty transaction + BigInt chain = new BigInt(1); // Chain identifier of the main net // Sign a transaction with a single authorization - byte[] signature = am.signPassphrase(signer, "Signer password", txHash.getBytes()); + Transaction signed = ks.signTxPassphrase(signer, "Signer password", tx, chain); // Sign a transaction with multiple manually cancelled authorizations - am.unlock(signer, "Signer password"); - signature = am.sign(signer.getAddress(), txHash.getBytes()); - am.lock(signer.getAddress()); + ks.unlock(signer, "Signer password"); + signed = ks.signTx(signer, tx, chain); + ks.lock(signer.getAddress()); // Sign a transaction with multiple automatically cancelled authorizations - am.timedUnlock(signer, "Signer password", 1000000000); - signature = am.sign(signer.getAddress(), txHash.getBytes()); + ks.timedUnlock(signer, "Signer password", 1000000000); + signed = ks.signTx(signer, tx, chain); } catch (Exception e) { fail(e.toString()); } diff --git a/mobile/types.go b/mobile/types.go index 9ea70ea9b1..a9c8cf68c6 100644 --- a/mobile/types.go +++ b/mobile/types.go @@ -132,6 +132,11 @@ type Transaction struct { tx *types.Transaction } +// NewTransaction creates a new transaction with the given properties. +func NewTransaction(nonce int64, to *Address, amount, gasLimit, gasPrice *BigInt, data []byte) *Transaction { + return &Transaction{types.NewTransaction(uint64(nonce), to.address, amount.bigint, gasLimit.bigint, gasPrice.bigint, data)} +} + func (tx *Transaction) GetData() []byte { return tx.tx.Data() } func (tx *Transaction) GetGas() int64 { return tx.tx.Gas().Int64() } func (tx *Transaction) GetGasPrice() *BigInt { return &BigInt{tx.tx.GasPrice()} } diff --git a/node/config.go b/node/config.go index 8d75e441b7..f3b4dcc731 100644 --- a/node/config.go +++ b/node/config.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/logger" @@ -401,11 +402,11 @@ func (c *Config) parsePersistentNodes(path string) []*discover.Node { } func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore string, err error) { - scryptN := accounts.StandardScryptN - scryptP := accounts.StandardScryptP + scryptN := keystore.StandardScryptN + scryptP := keystore.StandardScryptP if conf.UseLightweightKDF { - scryptN = accounts.LightScryptN - scryptP = accounts.LightScryptP + scryptN = keystore.LightScryptN + scryptP = keystore.LightScryptP } var keydir string @@ -431,6 +432,6 @@ func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore s if err := os.MkdirAll(keydir, 0700); err != nil { return nil, "", err } - - return accounts.NewManager(keydir, scryptN, scryptP), ephemeralKeystore, nil + ks := keystore.NewKeyStore(keydir, scryptN, scryptP) + return accounts.NewManager(ks), ephemeralKeystore, nil }