diff options
author | Péter Szilágyi <peterke@gmail.com> | 2017-02-13 21:03:16 +0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-02-13 21:03:16 +0800 |
commit | f8f428cc18c5f70814d7b3937128781bac14bffd (patch) | |
tree | d93d285d2ec22bd8ed646695c3db116c69fa3329 /accounts | |
parent | e23e86921b55cb1ee2fca6b6fb9ed91f5532f9fd (diff) | |
parent | e99c788155ddd754c73d2c81b6051dcbd42e6575 (diff) | |
download | go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.gz go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.bz2 go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.lz go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.xz go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.tar.zst go-tangerine-f8f428cc18c5f70814d7b3937128781bac14bffd.zip |
Merge pull request #3592 from karalabe/hw-wallets
accounts: initial support for Ledger hardware wallets
Diffstat (limited to 'accounts')
-rw-r--r-- | accounts/abi/bind/auth.go | 4 | ||||
-rw-r--r-- | accounts/account_manager.go | 350 | ||||
-rw-r--r-- | accounts/accounts.go | 155 | ||||
-rw-r--r-- | accounts/accounts_test.go | 224 | ||||
-rw-r--r-- | accounts/errors.go | 68 | ||||
-rw-r--r-- | accounts/hd.go | 130 | ||||
-rw-r--r-- | accounts/hd_test.go | 79 | ||||
-rw-r--r-- | accounts/keystore/account_cache.go (renamed from accounts/addrcache.go) | 91 | ||||
-rw-r--r-- | accounts/keystore/account_cache_test.go (renamed from accounts/addrcache_test.go) | 146 | ||||
-rw-r--r-- | accounts/keystore/key.go (renamed from accounts/key.go) | 11 | ||||
-rw-r--r-- | accounts/keystore/keystore.go | 494 | ||||
-rw-r--r-- | accounts/keystore/keystore_passphrase.go (renamed from accounts/key_store_passphrase.go) | 2 | ||||
-rw-r--r-- | accounts/keystore/keystore_passphrase_test.go (renamed from accounts/key_store_passphrase_test.go) | 2 | ||||
-rw-r--r-- | accounts/keystore/keystore_plain.go (renamed from accounts/key_store_plain.go) | 2 | ||||
-rw-r--r-- | accounts/keystore/keystore_plain_test.go (renamed from accounts/key_store_test.go) | 30 | ||||
-rw-r--r-- | accounts/keystore/keystore_test.go | 365 | ||||
-rw-r--r-- | accounts/keystore/keystore_wallet.go | 139 | ||||
-rw-r--r-- | accounts/keystore/presale.go (renamed from accounts/presale.go) | 11 | ||||
-rw-r--r-- | accounts/keystore/testdata/dupes/1 (renamed from accounts/testdata/dupes/1) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/dupes/2 (renamed from accounts/testdata/dupes/2) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/dupes/foo (renamed from accounts/testdata/dupes/foo) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/.hiddenfile (renamed from accounts/testdata/keystore/.hiddenfile) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/README (renamed from accounts/testdata/keystore/README) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 (renamed from accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/aaa (renamed from accounts/testdata/keystore/aaa) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/empty (renamed from accounts/testdata/keystore/empty) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e (renamed from accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/garbage (renamed from accounts/testdata/keystore/garbage) | bin | 300 -> 300 bytes | |||
-rw-r--r-- | accounts/keystore/testdata/keystore/no-address (renamed from accounts/testdata/keystore/no-address) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/zero (renamed from accounts/testdata/keystore/zero) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/keystore/zzz (renamed from accounts/testdata/keystore/zzz) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e (renamed from accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/v1_test_vector.json (renamed from accounts/testdata/v1_test_vector.json) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/v3_test_vector.json (renamed from accounts/testdata/v3_test_vector.json) | 0 | ||||
-rw-r--r-- | accounts/keystore/testdata/very-light-scrypt.json (renamed from accounts/testdata/very-light-scrypt.json) | 0 | ||||
-rw-r--r-- | accounts/keystore/watch.go (renamed from accounts/watch.go) | 6 | ||||
-rw-r--r-- | accounts/keystore/watch_fallback.go (renamed from accounts/watch_fallback.go) | 8 | ||||
-rw-r--r-- | accounts/manager.go | 198 | ||||
-rw-r--r-- | accounts/url.go | 79 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_hub.go | 209 | ||||
-rw-r--r-- | accounts/usbwallet/ledger_wallet.go | 945 | ||||
-rw-r--r-- | accounts/usbwallet/usbwallet.go | 29 | ||||
-rw-r--r-- | accounts/usbwallet/usbwallet_ios.go | 38 |
43 files changed, 3099 insertions, 716 deletions
diff --git a/accounts/abi/bind/auth.go b/accounts/abi/bind/auth.go index dbb235c14..e6bb0c3b5 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 01dd62e25..000000000 --- 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 <http://www.gnu.org/licenses/>. - -// 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 000000000..640de5220 --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,155 @@ +// 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 <http://www.gnu.org/licenses/>. + +// Package accounts implements high level Ethereum account management. +package accounts + +import ( + "math/big" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" +) + +// Account represents an Ethereum account located at a specific location defined +// by the optional URL field. +type Account struct { + Address common.Address `json:"address"` // Ethereum account address derived from the key + URL URL `json:"url"` // Optional resource locator within a backend +} + +// Wallet represents a software or hardware wallet that might contain one or more +// accounts (derived from the same seed). +type Wallet interface { + // URL retrieves the canonical path under which this wallet is reachable. It is + // user by upper layers to define a sorting order over all wallets from multiple + // backends. + URL() URL + + // Status returns a textual status to aid the user in the current state of the + // wallet. + Status() string + + // Open initializes access to a wallet instance. It is not meant to unlock or + // decrypt account keys, rather simply to establish a connection to hardware + // wallets and/or to access derivation seeds. + // + // The passphrase parameter may or may not be used by the implementation of a + // particular wallet instance. The reason there is no passwordless open method + // is to strive towards a uniform wallet handling, oblivious to the different + // backend providers. + // + // Please note, if you open a wallet, you must close it to release any allocated + // resources (especially important when working with hardware wallets). + Open(passphrase string) error + + // Close releases any resources held by an open wallet instance. + Close() error + + // Accounts retrieves the list of signing accounts the wallet is currently aware + // of. For hierarchical deterministic wallets, the list will not be exhaustive, + // rather only contain the accounts explicitly pinned during account derivation. + Accounts() []Account + + // Contains returns whether an account is part of this particular wallet or not. + Contains(account Account) bool + + // Derive attempts to explicitly derive a hierarchical deterministic account at + // the specified derivation path. If requested, the derived account will be added + // to the wallet's tracked account list. + Derive(path DerivationPath, pin bool) (Account, error) + + // SelfDerive sets a base account derivation path from which the wallet attempts + // to discover non zero accounts and automatically add them to list of tracked + // accounts. + // + // Note, self derivaton will increment the last component of the specified path + // opposed to decending into a child path to allow discovering accounts starting + // from non zero components. + // + // You can disable automatic account discovery by calling SelfDerive with a nil + // chain state reader. + SelfDerive(base DerivationPath, chain ethereum.ChainStateReader) + + // SignHash requests the wallet 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 wallet 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(account Account, hash []byte) ([]byte, error) + + // SignTx requests the wallet 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 wallet 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(account Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // SignHashWithPassphrase requests the wallet to sign the given hash 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(account Account, passphrase string, hash []byte) ([]byte, error) + + // SignTxWithPassphrase requests the wallet 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(account Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) +} + +// Backend is a "wallet provider" that may contain a batch of accounts they can +// sign transactions with and upon request, do so. +type Backend interface { + // Wallets retrieves the list of wallets the backend is currently aware of. + // + // The returned wallets are not opened by default. For software HD wallets this + // means that no base seeds are decrypted, and for hardware wallets that no actual + // connection is established. + // + // The resulting wallet list will be sorted alphabetically based on its internal + // URL assigned by the backend. Since wallets (especially hardware) may come and + // go, the same wallet might appear at a different positions in the list during + // subsequent retrievals. + Wallets() []Wallet + + // Subscribe creates an async subscription to receive notifications when the + // backend detects the arrival or departure of a wallet. + Subscribe(sink chan<- WalletEvent) event.Subscription +} + +// WalletEvent is an event fired by an account backend when a wallet arrival or +// departure is detected. +type WalletEvent struct { + Wallet Wallet // Wallet instance arrived or departed + Arrive bool // Whether the wallet was added or removed +} diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go deleted file mode 100644 index b3ab87d50..000000000 --- a/accounts/accounts_test.go +++ /dev/null @@ -1,224 +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 <http://www.gnu.org/licenses/>. - -package accounts - -import ( - "io/ioutil" - "os" - "runtime" - "strings" - "testing" - "time" - - "github.com/ethereum/go-ethereum/common" -) - -var testSigData = make([]byte, 32) - -func TestManager(t *testing.T) { - dir, am := tmpManager(t, true) - defer os.RemoveAll(dir) - - a, err := am.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) - } - stat, err := os.Stat(a.File) - if err != nil { - t.Fatalf("account file %s doesn't exist (%v)", a.File, 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) { - t.Errorf("HasAccount(%x) should've returned true", a.Address) - } - if err := am.Update(a, "foo", "bar"); err != nil { - t.Errorf("Update error: %v", err) - } - if err := am.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 am.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) - defer os.RemoveAll(dir) - - pass := "" // not used but required by API - a1, err := am.NewAccount(pass) - if err != nil { - t.Fatal(err) - } - if err := am.Unlock(a1, ""); err != nil { - t.Fatal(err) - } - if _, err := am.Sign(a1.Address, testSigData); err != nil { - t.Fatal(err) - } -} - -func TestSignWithPassphrase(t *testing.T) { - dir, am := tmpManager(t, true) - defer os.RemoveAll(dir) - - pass := "passwd" - acc, err := am.NewAccount(pass) - if err != nil { - t.Fatal(err) - } - - if _, unlocked := am.unlocked[acc.Address]; unlocked { - t.Fatal("expected account to be locked") - } - - _, err = am.SignWithPassphrase(acc, pass, testSigData) - if err != nil { - t.Fatal(err) - } - - if _, unlocked := am.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") - } -} - -func TestTimedUnlock(t *testing.T) { - dir, am := tmpManager(t, true) - defer os.RemoveAll(dir) - - pass := "foo" - a1, err := am.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) - } - - // Signing with passphrase works - if err = am.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) - 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) - } -} - -func TestOverrideUnlock(t *testing.T) { - dir, am := tmpManager(t, false) - defer os.RemoveAll(dir) - - pass := "foo" - a1, err := am.NewAccount(pass) - if err != nil { - t.Fatal(err) - } - - // Unlock indefinitely. - if err = am.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) - 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 { - t.Fatal(err) - } - - // Signing without passphrase still works because account is temp unlocked - _, err = am.Sign(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) - } -} - -// This test should fail under -race if signing races the expiration goroutine. -func TestSignRace(t *testing.T) { - dir, am := tmpManager(t, false) - defer os.RemoveAll(dir) - - // Create a test account. - a1, err := am.NewAccount("") - if err != nil { - t.Fatal("could not create the test account", err) - } - - if err := am.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 { - return - } else if err != nil { - t.Errorf("Sign error: %v", err) - return - } - time.Sleep(1 * time.Millisecond) - } - t.Errorf("Account did not lock within the timeout") -} - -func tmpManager(t *testing.T, encrypted bool) (string, *Manager) { - d, err := ioutil.TempDir("", "eth-keystore-test") - if err != nil { - t.Fatal(err) - } - new := NewPlaintextManager - if encrypted { - new = func(kd string) *Manager { return NewManager(kd, veryLightScryptN, veryLightScryptP) } - } - return d, new(d) -} diff --git a/accounts/errors.go b/accounts/errors.go new file mode 100644 index 000000000..9ecc1eafd --- /dev/null +++ b/accounts/errors.go @@ -0,0 +1,68 @@ +// 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 <http://www.gnu.org/licenses/>. + +package accounts + +import ( + "errors" + "fmt" +) + +// ErrUnknownAccount is returned for any requested operation for which no backend +// provides the specified account. +var ErrUnknownAccount = errors.New("unknown account") + +// ErrUnknownWallet is returned for any requested operation for which no backend +// provides the specified wallet. +var ErrUnknownWallet = errors.New("unknown wallet") + +// ErrNotSupported is returned when an operation is requested from an account +// backend that it does not support. +var ErrNotSupported = errors.New("not supported") + +// ErrInvalidPassphrase is returned when a decryption operation receives a bad +// passphrase. +var ErrInvalidPassphrase = errors.New("invalid passphrase") + +// ErrWalletAlreadyOpen is returned if a wallet is attempted to be opened the +// secodn time. +var ErrWalletAlreadyOpen = errors.New("wallet already open") + +// ErrWalletClosed is returned if a wallet is attempted to be opened the +// secodn time. +var ErrWalletClosed = errors.New("wallet closed") + +// 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/hd.go b/accounts/hd.go new file mode 100644 index 000000000..e8bc191af --- /dev/null +++ b/accounts/hd.go @@ -0,0 +1,130 @@ +// 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 <http://www.gnu.org/licenses/>. + +package accounts + +import ( + "errors" + "fmt" + "math" + "math/big" + "strings" +) + +// DefaultRootDerivationPath is the root path to which custom derivation endpoints +// are appended. As such, the first account will be at m/44'/60'/0'/0, the second +// at m/44'/60'/0'/1, etc. +var DefaultRootDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0} + +// DefaultBaseDerivationPath is the base path from which custom derivation endpoints +// are incremented. As such, the first account will be at m/44'/60'/0'/0, the second +// at m/44'/60'/0'/1, etc. +var DefaultBaseDerivationPath = DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} + +// DerivationPath represents the computer friendly version of a hierarchical +// deterministic wallet account derivaion path. +// +// The BIP-32 spec https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +// defines derivation paths to be of the form: +// +// m / purpose' / coin_type' / account' / change / address_index +// +// The BIP-44 spec https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki +// defines that the `purpose` be 44' (or 0x8000002C) for crypto currencies, and +// SLIP-44 https://github.com/satoshilabs/slips/blob/master/slip-0044.md assigns +// the `coin_type` 60' (or 0x8000003C) to Ethereum. +// +// The root path for Ethereum is m/44'/60'/0'/0 according to the specification +// from https://github.com/ethereum/EIPs/issues/84, albeit it's not set in stone +// yet whether accounts should increment the last component or the children of +// that. We will go with the simpler approach of incrementing the last component. +type DerivationPath []uint32 + +// ParseDerivationPath converts a user specified derivation path string to the +// internal binary representation. +// +// Full derivation paths need to start with the `m/` prefix, relative derivation +// paths (which will get appended to the default root path) must not have prefixes +// in front of the first element. Whitespace is ignored. +func ParseDerivationPath(path string) (DerivationPath, error) { + var result DerivationPath + + // Handle absolute or relative paths + components := strings.Split(path, "/") + switch { + case len(components) == 0: + return nil, errors.New("empty derivation path") + + case strings.TrimSpace(components[0]) == "": + return nil, errors.New("ambiguous path: use 'm/' prefix for absolute paths, or no leading '/' for relative ones") + + case strings.TrimSpace(components[0]) == "m": + components = components[1:] + + default: + result = append(result, DefaultRootDerivationPath...) + } + // All remaining components are relative, append one by one + if len(components) == 0 { + return nil, errors.New("empty derivation path") // Empty relative paths + } + for _, component := range components { + // Ignore any user added whitespace + component = strings.TrimSpace(component) + var value uint32 + + // Handle hardened paths + if strings.HasSuffix(component, "'") { + value = 0x80000000 + component = strings.TrimSpace(strings.TrimSuffix(component, "'")) + } + // Handle the non hardened component + bigval, ok := new(big.Int).SetString(component, 0) + if !ok { + return nil, fmt.Errorf("invalid component: %s", component) + } + max := math.MaxUint32 - value + if bigval.Sign() < 0 || bigval.Cmp(big.NewInt(int64(max))) > 0 { + if value == 0 { + return nil, fmt.Errorf("component %v out of allowed range [0, %d]", bigval, max) + } + return nil, fmt.Errorf("component %v out of allowed hardened range [0, %d]", bigval, max) + } + value += uint32(bigval.Uint64()) + + // Append and repeat + result = append(result, value) + } + return result, nil +} + +// String implements the stringer interface, converting a binary derivation path +// to its canonical representation. +func (path DerivationPath) String() string { + result := "m" + for _, component := range path { + var hardened bool + if component >= 0x80000000 { + component -= 0x80000000 + hardened = true + } + result = fmt.Sprintf("%s/%d", result, component) + if hardened { + result += "'" + } + } + return result +} diff --git a/accounts/hd_test.go b/accounts/hd_test.go new file mode 100644 index 000000000..83ec34adb --- /dev/null +++ b/accounts/hd_test.go @@ -0,0 +1,79 @@ +// 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 <http://www.gnu.org/licenses/>. + +package accounts + +import ( + "reflect" + "testing" +) + +// Tests that HD derivation paths can be correctly parsed into our internal binary +// representation. +func TestHDPathParsing(t *testing.T) { + tests := []struct { + input string + output DerivationPath + }{ + // Plain absolute derivation paths + {"m/44'/60'/0'/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/44'/60'/0'/128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, + {"m/44'/60'/0'/0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + {"m/44'/60'/0'/128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, + {"m/2147483692/2147483708/2147483648/0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/2147483692/2147483708/2147483648/2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + + // Plain relative derivation paths + {"0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"128", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, + {"0'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + {"128'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, + {"2147483648", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + + // Hexadecimal absolute derivation paths + {"m/0x2C'/0x3c'/0x00'/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/0x2C'/0x3c'/0x00'/0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, + {"m/0x2C'/0x3c'/0x00'/0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + {"m/0x2C'/0x3c'/0x00'/0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, + {"m/0x8000002C/0x8000003c/0x80000000/0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"m/0x8000002C/0x8000003c/0x80000000/0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + + // Hexadecimal relative derivation paths + {"0x00", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + {"0x80", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 128}}, + {"0x00'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + {"0x80'", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 128}}, + {"0x80000000", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0x80000000 + 0}}, + + // Weird inputs just to ensure they work + {" m / 44 '\n/\n 60 \n\n\t' /\n0 ' /\t\t 0", DerivationPath{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0}}, + + // Invaid derivation paths + {"", nil}, // Empty relative derivation path + {"m", nil}, // Empty absolute derivation path + {"m/", nil}, // Missing last derivation component + {"/44'/60'/0'/0", nil}, // Absolute path without m prefix, might be user error + {"m/2147483648'", nil}, // Overflows 32 bit integer + {"m/-1'", nil}, // Cannot contain negative number + } + for i, tt := range tests { + if path, err := ParseDerivationPath(tt.input); !reflect.DeepEqual(path, tt.output) { + t.Errorf("test %d: parse mismatch: have %v (%v), want %v", i, path, err, tt.output) + } else if path == nil && err == nil { + t.Errorf("test %d: nil path and error: %v", i, err) + } + } +} diff --git a/accounts/addrcache.go b/accounts/keystore/account_cache.go index a99f23606..3fae3ef5b 100644 --- a/accounts/addrcache.go +++ b/accounts/keystore/account_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 <http://www.gnu.org/licenses/>. -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 accountsByURL []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) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s accountsByURL) Len() int { return len(s) } +func (s accountsByURL) Less(i, j int) bool { return s[i].URL.Cmp(s[j].URL) < 0 } +func (s accountsByURL) 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.Path if i < len(err.Matches)-1 { files += ", " } @@ -62,60 +63,63 @@ 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 { +// accountCache is a live index of all accounts in the keystore. +type accountCache struct { keydir string watcher *watcher mu sync.Mutex - all accountsByFile - byAddr map[common.Address][]Account + all accountsByURL + byAddr map[common.Address][]accounts.Account throttle *time.Timer + notify chan struct{} } -func newAddrCache(keydir string) *addrCache { - ac := &addrCache{ +func newAccountCache(keydir string) (*accountCache, chan struct{}) { + ac := &accountCache{ keydir: keydir, - byAddr: make(map[common.Address][]Account), + byAddr: make(map[common.Address][]accounts.Account), + notify: make(chan struct{}, 1), } ac.watcher = newWatcher(ac) - return ac + return ac, ac.notify } -func (ac *addrCache) accounts() []Account { +func (ac *accountCache) 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 *accountCache) 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 *accountCache) 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.Cmp(newAccount.URL) >= 0 }) 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 *accountCache) delete(removed accounts.Account) { ac.mu.Lock() defer ac.mu.Unlock() + ac.all = removeAccount(ac.all, removed) if ba := removeAccount(ac.byAddr[removed.Address], removed); len(ba) == 0 { delete(ac.byAddr, removed.Address) @@ -124,7 +128,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,43 +138,44 @@ 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 *accountCache) 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.Path != "" { // 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.Path, filepath.Separator) { + a.URL.Path = filepath.Join(ac.keydir, a.URL.Path) } 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 *accountCache) maybeReload() { ac.mu.Lock() defer ac.mu.Unlock() + if ac.watcher.running { return // A watcher is running and will keep the cache up-to-date. } @@ -188,18 +193,22 @@ func (ac *addrCache) maybeReload() { ac.throttle.Reset(minReloadInterval) } -func (ac *addrCache) close() { +func (ac *accountCache) close() { ac.mu.Lock() ac.watcher.close() if ac.throttle != nil { ac.throttle.Stop() } + if ac.notify != nil { + close(ac.notify) + ac.notify = nil + } ac.mu.Unlock() } // reload caches addresses of existing accounts. // Callers must hold ac.mu. -func (ac *addrCache) reload() { +func (ac *accountCache) reload() { accounts, err := ac.scan() if err != nil && glog.V(logger.Debug) { glog.Errorf("can't load keys: %v", err) @@ -212,10 +221,14 @@ func (ac *addrCache) reload() { for _, a := range accounts { ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a) } + select { + case ac.notify <- struct{}{}: + default: + } glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all)) } -func (ac *addrCache) scan() ([]Account, error) { +func (ac *accountCache) scan() ([]accounts.Account, error) { files, err := ioutil.ReadDir(ac.keydir) if err != nil { return nil, err @@ -223,7 +236,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 +263,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: accounts.URL{Scheme: KeyStoreScheme, Path: path}}) } fd.Close() } diff --git a/accounts/addrcache_test.go b/accounts/keystore/account_cache_test.go index e5f08cffc..554196321 100644 --- a/accounts/addrcache_test.go +++ b/accounts/keystore/account_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 <http://www.gnu.org/licenses/>. -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: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8")}, }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: filepath.Join(cachetestDir, "aaa"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - File: filepath.Join(cachetestDir, "zzz"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, }, } ) @@ -52,29 +53,36 @@ var ( func TestWatchNewFile(t *testing.T) { t.Parallel() - dir, am := tmpManager(t, false) + dir, ks := tmpKeyStore(t, false) defer os.RemoveAll(dir) // Ensure the watcher is started before adding any files. - am.Accounts() + ks.Accounts() 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)) - wantAccounts[i] = a - if err := cp.CopyFile(a.File, cachetestAccounts[i].File); err != nil { + wantAccounts[i] = accounts.Account{ + Address: cachetestAccounts[i].Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, filepath.Base(cachetestAccounts[i].URL.Path))}, + } + if err := cp.CopyFile(wantAccounts[i].URL.Path, cachetestAccounts[i].URL.Path); err != nil { t.Fatal(err) } } - // am should see the accounts. - var list []Account + // ks should see the accounts. + var list []accounts.Account for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { - list = am.Accounts() + list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { + // ks should have also received change notifications + select { + case <-ks.changes: + default: + t.Fatalf("wasn't notified of new accounts") + } return } time.Sleep(d) @@ -85,12 +93,12 @@ func TestWatchNewFile(t *testing.T) { func TestWatchNoDir(t *testing.T) { t.Parallel() - // Create am but not the directory that it watches. + // Create ks 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) + ks := NewKeyStore(dir, LightScryptN, LightScryptP) - list := am.Accounts() + list := ks.Accounts() if len(list) > 0 { t.Error("initial account list not empty:", list) } @@ -100,16 +108,22 @@ 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.Path); err != nil { t.Fatal(err) } - // am should see the account. - wantAccounts := []Account{cachetestAccounts[0]} - wantAccounts[0].File = file + // ks should see the account. + wantAccounts := []accounts.Account{cachetestAccounts[0]} + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { - list = am.Accounts() + list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { + // ks should have also received change notifications + select { + case <-ks.changes: + default: + t.Fatalf("wasn't notified of new accounts") + } return } time.Sleep(d) @@ -118,7 +132,7 @@ func TestWatchNoDir(t *testing.T) { } func TestCacheInitialReload(t *testing.T) { - cache := newAddrCache(cachetestDir) + cache, _ := newAccountCache(cachetestDir) accounts := cache.accounts() if !reflect.DeepEqual(accounts, cachetestAccounts) { t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) @@ -126,55 +140,55 @@ func TestCacheInitialReload(t *testing.T) { } func TestCacheAddDeleteOrder(t *testing.T) { - cache := newAddrCache("testdata/no-such-dir") + cache, _ := newAccountCache("testdata/no-such-dir") cache.watcher.running = true // prevent unexpected reloads - accounts := []Account{ + accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - File: "-309830980", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - File: "ggg", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, }, { Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - File: "zzzzzz-the-very-last-one.keyXXX", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: "SOMETHING.key", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, }, { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - File: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"}, }, { Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: "aaa", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - File: "zzz", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "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) - sort.Sort(accountsByFile(wantAccounts)) + wantAccounts := make([]accounts.Account, len(accs)) + copy(wantAccounts, accs) + sort.Sort(accountsByURL(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 +198,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: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) // Check content again after deletion. - wantAccountsAfterDelete := []Account{ + wantAccountsAfterDelete := []accounts.Account{ wantAccounts[1], wantAccounts[3], wantAccounts[5], @@ -211,63 +225,63 @@ func TestCacheAddDeleteOrder(t *testing.T) { func TestCacheFind(t *testing.T) { dir := filepath.Join("testdata", "dir") - cache := newAddrCache(dir) + cache, _ := newAccountCache(dir) cache.watcher.running = true // prevent unexpected reloads - accounts := []Account{ + accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - File: filepath.Join(dir, "a.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - File: filepath.Join(dir, "b.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: filepath.Join(dir, "c.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: filepath.Join(dir, "c2.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: 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: accounts.URL{Scheme: KeyStoreScheme, Path: 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: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, 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: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(nomatchAccount.URL.Path)}}, 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 index dbcb49dcf..e2bdf09fe 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 <http://www.gnu.org/licenses/>. -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: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(keyFileName(key.Address))}} + if err := ks.StoreKey(a.URL.Path, 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 000000000..a01ff17e3 --- /dev/null +++ b/accounts/keystore/keystore.go @@ -0,0 +1,494 @@ +// 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 <http://www.gnu.org/licenses/>. + +// 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" + "github.com/ethereum/go-ethereum/event" +) + +var ( + ErrLocked = 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") +) + +// KeyStoreType is the reflect type of a keystore backend. +var KeyStoreType = reflect.TypeOf(&KeyStore{}) + +// KeyStoreScheme is the protocol scheme prefixing account and wallet URLs. +var KeyStoreScheme = "keystore" + +// Maximum time between wallet refreshes (if filesystem notifications don't work). +const walletRefreshCycle = 3 * time.Second + +// KeyStore manages a key storage directory on disk. +type KeyStore struct { + storage keyStore // Storage backend, might be cleartext or encrypted + cache *accountCache // In-memory account cache over the filesystem storage + changes chan struct{} // Channel receiving change notifications from the cache + unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys) + + wallets []accounts.Wallet // Wallet wrappers around the individual key files + updateFeed event.Feed // Event feed to notify wallet additions/removals + updateScope event.SubscriptionScope // Subscription scope tracking current live listeners + updating bool // Whether the event notification loop is running + + mu sync.RWMutex +} + +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{storage: &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{storage: &keyStorePlain{keydir}} + ks.init(keydir) + return ks +} + +func (ks *KeyStore) init(keydir string) { + // Lock the mutex since the account cache might call back with events + ks.mu.Lock() + defer ks.mu.Unlock() + + // Initialize the set of unlocked keys and the account cache + ks.unlocked = make(map[common.Address]*unlocked) + ks.cache, ks.changes = newAccountCache(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() + }) + // Create the initial list of wallets from the cache + accs := ks.cache.accounts() + ks.wallets = make([]accounts.Wallet, len(accs)) + for i := 0; i < len(accs); i++ { + ks.wallets[i] = &keystoreWallet{account: accs[i], keystore: ks} + } +} + +// Wallets implements accounts.Backend, returning all single-key wallets from the +// keystore directory. +func (ks *KeyStore) Wallets() []accounts.Wallet { + // Make sure the list of wallets is in sync with the account cache + ks.refreshWallets() + + ks.mu.RLock() + defer ks.mu.RUnlock() + + cpy := make([]accounts.Wallet, len(ks.wallets)) + copy(cpy, ks.wallets) + return cpy +} + +// refreshWallets retrieves the current account list and based on that does any +// necessary wallet refreshes. +func (ks *KeyStore) refreshWallets() { + // Retrieve the current list of accounts + ks.mu.Lock() + accs := ks.cache.accounts() + + // Transform the current list of wallets into the new one + wallets := make([]accounts.Wallet, 0, len(accs)) + events := []accounts.WalletEvent{} + + for _, account := range accs { + // Drop wallets while they were in front of the next account + for len(ks.wallets) > 0 && ks.wallets[0].URL().Cmp(account.URL) < 0 { + events = append(events, accounts.WalletEvent{Wallet: ks.wallets[0], Arrive: false}) + ks.wallets = ks.wallets[1:] + } + // If there are no more wallets or the account is before the next, wrap new wallet + if len(ks.wallets) == 0 || ks.wallets[0].URL().Cmp(account.URL) > 0 { + wallet := &keystoreWallet{account: account, keystore: ks} + + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) + wallets = append(wallets, wallet) + continue + } + // If the account is the same as the first wallet, keep it + if ks.wallets[0].Accounts()[0] == account { + wallets = append(wallets, ks.wallets[0]) + ks.wallets = ks.wallets[1:] + continue + } + } + // Drop any leftover wallets and set the new batch + for _, wallet := range ks.wallets { + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) + } + ks.wallets = wallets + ks.mu.Unlock() + + // Fire all wallet events and return + for _, event := range events { + ks.updateFeed.Send(event) + } +} + +// Subscribe implements accounts.Backend, creating an async subscription to +// receive notifications on the addition or removal of keystore wallets. +func (ks *KeyStore) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + // We need the mutex to reliably start/stop the update loop + ks.mu.Lock() + defer ks.mu.Unlock() + + // Subscribe the caller and track the subscriber count + sub := ks.updateScope.Track(ks.updateFeed.Subscribe(sink)) + + // Subscribers require an active notification loop, start it + if !ks.updating { + ks.updating = true + go ks.updater() + } + return sub +} + +// updater is responsible for maintaining an up-to-date list of wallets stored in +// the keystore, and for firing wallet addition/removal events. It listens for +// account change events from the underlying account cache, and also periodically +// forces a manual refresh (only triggers for systems where the filesystem notifier +// is not running). +func (ks *KeyStore) updater() { + for { + // Wait for an account update or a refresh timeout + select { + case <-ks.changes: + case <-time.After(walletRefreshCycle): + } + // Run the wallet refresher + ks.refreshWallets() + + // If all our subscribers left, stop the updater + ks.mu.Lock() + if ks.updateScope.Count() == 0 { + ks.updating = false + ks.mu.Unlock() + return + } + ks.mu.Unlock() + } +} + +// 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.Path) + if err == nil { + ks.cache.delete(a) + ks.refreshWallets() + } + 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, ErrLocked + } + // 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, ErrLocked + } + // 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 + } + // 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.storage.GetKey(a.Address, a.URL.Path, 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.storage, 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) + ks.refreshWallets() + 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.storage.(*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: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}} + if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil { + return accounts.Account{}, err + } + ks.cache.add(a) + ks.refreshWallets() + 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.storage.StoreKey(a.URL.Path, 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.storage, keyJSON, passphrase) + if err != nil { + return a, err + } + ks.cache.add(a) + ks.refreshWallets() + 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 index 4a777956d..8ef510fcf 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 index 217393fa5..086addbc1 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 <http://www.gnu.org/licenses/>. -package accounts +package keystore import ( "io/ioutil" diff --git a/accounts/key_store_plain.go b/accounts/keystore/keystore_plain.go index 2cbaa94df..b490ca72b 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 <http://www.gnu.org/licenses/>. -package accounts +package keystore import ( "encoding/json" diff --git a/accounts/key_store_test.go b/accounts/keystore/keystore_plain_test.go index d0713caa0..8c0eb52ea 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 <http://www.gnu.org/licenses/>. -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.Path, 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.Path, 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.Path, "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.Path, 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/keystore/keystore_test.go b/accounts/keystore/keystore_test.go new file mode 100644 index 000000000..60f2606ee --- /dev/null +++ b/accounts/keystore/keystore_test.go @@ -0,0 +1,365 @@ +// 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 <http://www.gnu.org/licenses/>. + +package keystore + +import ( + "io/ioutil" + "math/rand" + "os" + "runtime" + "sort" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/event" +) + +var testSigData = make([]byte, 32) + +func TestKeyStore(t *testing.T) { + dir, ks := tmpKeyStore(t, true) + defer os.RemoveAll(dir) + + a, err := ks.NewAccount("foo") + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(a.URL.Path, dir) { + t.Errorf("account file %s doesn't have dir prefix", a.URL) + } + stat, err := os.Stat(a.URL.Path) + if err != nil { + 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 !ks.HasAddress(a.Address) { + t.Errorf("HasAccount(%x) should've returned true", a.Address) + } + if err := ks.Update(a, "foo", "bar"); err != nil { + t.Errorf("Update error: %v", err) + } + if err := ks.Delete(a, "bar"); err != nil { + t.Errorf("Delete error: %v", err) + } + if common.FileExist(a.URL.Path) { + t.Errorf("account file %s should be gone after Delete", a.URL) + } + if ks.HasAddress(a.Address) { + t.Errorf("HasAccount(%x) should've returned true after Delete", a.Address) + } +} + +func TestSign(t *testing.T) { + dir, ks := tmpKeyStore(t, true) + defer os.RemoveAll(dir) + + pass := "" // not used but required by API + a1, err := ks.NewAccount(pass) + if err != nil { + t.Fatal(err) + } + if err := ks.Unlock(a1, ""); err != nil { + t.Fatal(err) + } + if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err != nil { + t.Fatal(err) + } +} + +func TestSignWithPassphrase(t *testing.T) { + dir, ks := tmpKeyStore(t, true) + defer os.RemoveAll(dir) + + pass := "passwd" + acc, err := ks.NewAccount(pass) + if err != nil { + t.Fatal(err) + } + + if _, unlocked := ks.unlocked[acc.Address]; unlocked { + t.Fatal("expected account to be locked") + } + + _, err = ks.SignHashWithPassphrase(acc, pass, testSigData) + if err != nil { + t.Fatal(err) + } + + if _, unlocked := ks.unlocked[acc.Address]; unlocked { + t.Fatal("expected account to be locked") + } + + 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, ks := tmpKeyStore(t, true) + defer os.RemoveAll(dir) + + pass := "foo" + a1, err := ks.NewAccount(pass) + if err != nil { + t.Fatal(err) + } + + // Signing without passphrase fails because account is locked + _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err) + } + + // Signing with passphrase works + if err = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { + t.Fatal(err) + } + + // Signing without passphrase works because account is temp unlocked + _, 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 = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) + } +} + +func TestOverrideUnlock(t *testing.T) { + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + pass := "foo" + a1, err := ks.NewAccount(pass) + if err != nil { + t.Fatal(err) + } + + // Unlock indefinitely. + if err = ks.TimedUnlock(a1, pass, 5*time.Minute); err != nil { + t.Fatal(err) + } + + // Signing without passphrase works because account is temp unlocked + _, 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 = ks.TimedUnlock(a1, pass, 100*time.Millisecond); err != nil { + t.Fatal(err) + } + + // Signing without passphrase still works because account is temp unlocked + _, 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 = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) + } +} + +// This test should fail under -race if signing races the expiration goroutine. +func TestSignRace(t *testing.T) { + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Create a test account. + a1, err := ks.NewAccount("") + if err != nil { + t.Fatal("could not create the test account", err) + } + + 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 := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked { + return + } else if err != nil { + t.Errorf("Sign error: %v", err) + return + } + time.Sleep(1 * time.Millisecond) + } + t.Errorf("Account did not lock within the timeout") +} + +// Tests that the wallet notifier loop starts and stops correctly based on the +// addition and removal of wallet event subscriptions. +func TestWalletNotifierLifecycle(t *testing.T) { + // Create a temporary kesytore to test with + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Ensure that the notification updater is not running yet + time.Sleep(250 * time.Millisecond) + ks.mu.RLock() + updating := ks.updating + ks.mu.RUnlock() + + if updating { + t.Errorf("wallet notifier running without subscribers") + } + // Subscribe to the wallet feed and ensure the updater boots up + updates := make(chan accounts.WalletEvent) + + subs := make([]event.Subscription, 2) + for i := 0; i < len(subs); i++ { + // Create a new subscription + subs[i] = ks.Subscribe(updates) + + // Ensure the notifier comes online + time.Sleep(250 * time.Millisecond) + ks.mu.RLock() + updating = ks.updating + ks.mu.RUnlock() + + if !updating { + t.Errorf("sub %d: wallet notifier not running after subscription", i) + } + } + // Unsubscribe and ensure the updater terminates eventually + for i := 0; i < len(subs); i++ { + // Close an existing subscription + subs[i].Unsubscribe() + + // Ensure the notifier shuts down at and only at the last close + for k := 0; k < int(walletRefreshCycle/(250*time.Millisecond))+2; k++ { + ks.mu.RLock() + updating = ks.updating + ks.mu.RUnlock() + + if i < len(subs)-1 && !updating { + t.Fatalf("sub %d: event notifier stopped prematurely", i) + } + if i == len(subs)-1 && !updating { + return + } + time.Sleep(250 * time.Millisecond) + } + } + t.Errorf("wallet notifier didn't terminate after unsubscribe") +} + +// Tests that wallet notifications and correctly fired when accounts are added +// or deleted from the keystore. +func TestWalletNotifications(t *testing.T) { + // Create a temporary kesytore to test with + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Subscribe to the wallet feed + updates := make(chan accounts.WalletEvent, 1) + sub := ks.Subscribe(updates) + defer sub.Unsubscribe() + + // Randomly add and remove account and make sure events and wallets are in sync + live := make(map[common.Address]accounts.Account) + for i := 0; i < 1024; i++ { + // Execute a creation or deletion and ensure event arrival + if create := len(live) == 0 || rand.Int()%4 > 0; create { + // Add a new account and ensure wallet notifications arrives + account, err := ks.NewAccount("") + if err != nil { + t.Fatalf("failed to create test account: %v", err) + } + select { + case event := <-updates: + if !event.Arrive { + t.Errorf("departure event on account creation") + } + if event.Wallet.Accounts()[0] != account { + t.Errorf("account mismatch on created wallet: have %v, want %v", event.Wallet.Accounts()[0], account) + } + default: + t.Errorf("wallet arrival event not fired on account creation") + } + live[account.Address] = account + } else { + // Select a random account to delete (crude, but works) + var account accounts.Account + for _, a := range live { + account = a + break + } + // Remove an account and ensure wallet notifiaction arrives + if err := ks.Delete(account, ""); err != nil { + t.Fatalf("failed to delete test account: %v", err) + } + select { + case event := <-updates: + if event.Arrive { + t.Errorf("arrival event on account deletion") + } + if event.Wallet.Accounts()[0] != account { + t.Errorf("account mismatch on deleted wallet: have %v, want %v", event.Wallet.Accounts()[0], account) + } + default: + t.Errorf("wallet departure event not fired on account creation") + } + delete(live, account.Address) + } + // Retrieve the list of wallets and ensure it matches with our required live set + liveList := make([]accounts.Account, 0, len(live)) + for _, account := range live { + liveList = append(liveList, account) + } + sort.Sort(accountsByURL(liveList)) + + wallets := ks.Wallets() + if len(liveList) != len(wallets) { + t.Errorf("wallet list doesn't match required accounts: have %v, want %v", wallets, liveList) + } else { + for j, wallet := range wallets { + if accs := wallet.Accounts(); len(accs) != 1 { + t.Errorf("wallet %d: contains invalid number of accounts: have %d, want 1", j, len(accs)) + } else if accs[0] != liveList[j] { + t.Errorf("wallet %d: account mismatch: have %v, want %v", j, accs[0], liveList[j]) + } + } + } + } +} + +func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) { + d, err := ioutil.TempDir("", "eth-keystore-test") + if err != nil { + t.Fatal(err) + } + new := NewPlaintextKeyStore + if encrypted { + new = func(kd string) *KeyStore { return NewKeyStore(kd, veryLightScryptN, veryLightScryptP) } + } + return d, new(d) +} diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go new file mode 100644 index 000000000..7165d2821 --- /dev/null +++ b/accounts/keystore/keystore_wallet.go @@ -0,0 +1,139 @@ +// 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 <http://www.gnu.org/licenses/>. + +package keystore + +import ( + "math/big" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/core/types" +) + +// keystoreWallet implements the accounts.Wallet interface for the original +// keystore. +type keystoreWallet struct { + account accounts.Account // Single account contained in this wallet + keystore *KeyStore // Keystore where the account originates from +} + +// URL implements accounts.Wallet, returning the URL of the account within. +func (w *keystoreWallet) URL() accounts.URL { + return w.account.URL +} + +// Status implements accounts.Wallet, always returning "open", since there is no +// concept of open/close for plain keystore accounts. +func (w *keystoreWallet) Status() string { + w.keystore.mu.RLock() + defer w.keystore.mu.RUnlock() + + if _, ok := w.keystore.unlocked[w.account.Address]; ok { + return "Unlocked" + } + return "Locked" +} + +// Open implements accounts.Wallet, but is a noop for plain wallets since there +// is no connection or decryption step necessary to access the list of accounts. +func (w *keystoreWallet) Open(passphrase string) error { return nil } + +// Close implements accounts.Wallet, but is a noop for plain wallets since is no +// meaningful open operation. +func (w *keystoreWallet) Close() error { return nil } + +// Accounts implements accounts.Wallet, returning an account list consisting of +// a single account that the plain kestore wallet contains. +func (w *keystoreWallet) Accounts() []accounts.Account { + return []accounts.Account{w.account} +} + +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not wrapped by this wallet instance. +func (w *keystoreWallet) Contains(account accounts.Account) bool { + return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL) +} + +// Derive implements accounts.Wallet, but is a noop for plain wallets since there +// is no notion of hierarchical account derivation for plain keystore accounts. +func (w *keystoreWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { + return accounts.Account{}, accounts.ErrNotSupported +} + +// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since +// there is no notion of hierarchical account derivation for plain keystore accounts. +func (w *keystoreWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) {} + +// SignHash implements accounts.Wallet, attempting to sign the given hash with +// the given account. If the wallet does not wrap this particular account, an +// error is returned to avoid account leakage (even though in theory we may be +// able to sign via our shared keystore backend). +func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHash(account, hash) +} + +// SignTx implements accounts.Wallet, attempting to sign the given transaction +// with the given account. If the wallet does not wrap this particular account, +// an error is returned to avoid account leakage (even though in theory we may +// be able to sign via our shared keystore backend). +func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignTx(account, tx, chainID) +} + +// SignHashWithPassphrase implements accounts.Wallet, attempting to sign the +// given hash with the given account using passphrase as extra authentication. +func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHashWithPassphrase(account, passphrase, hash) +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Make sure the requested account is contained within + if account.Address != w.account.Address { + return nil, accounts.ErrUnknownAccount + } + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignTxWithPassphrase(account, passphrase, tx, chainID) +} diff --git a/accounts/presale.go b/accounts/keystore/presale.go index f00b4f502..5b883c45f 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 <http://www.gnu.org/licenses/>. -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: accounts.URL{Scheme: KeyStoreScheme, Path: keyStore.JoinPath(keyFileName(key.Address))}} + err = keyStore.StoreKey(a.URL.Path, key, password) return a, key, err } diff --git a/accounts/testdata/dupes/1 b/accounts/keystore/testdata/dupes/1 index a3868ec6d..a3868ec6d 100644 --- a/accounts/testdata/dupes/1 +++ b/accounts/keystore/testdata/dupes/1 diff --git a/accounts/testdata/dupes/2 b/accounts/keystore/testdata/dupes/2 index a3868ec6d..a3868ec6d 100644 --- a/accounts/testdata/dupes/2 +++ b/accounts/keystore/testdata/dupes/2 diff --git a/accounts/testdata/dupes/foo b/accounts/keystore/testdata/dupes/foo index c57060aea..c57060aea 100644 --- a/accounts/testdata/dupes/foo +++ b/accounts/keystore/testdata/dupes/foo diff --git a/accounts/testdata/keystore/.hiddenfile b/accounts/keystore/testdata/keystore/.hiddenfile index d91faccde..d91faccde 100644 --- a/accounts/testdata/keystore/.hiddenfile +++ b/accounts/keystore/testdata/keystore/.hiddenfile diff --git a/accounts/testdata/keystore/README b/accounts/keystore/testdata/keystore/README index a5a86f964..a5a86f964 100644 --- a/accounts/testdata/keystore/README +++ b/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 index c57060aea..c57060aea 100644 --- 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 diff --git a/accounts/testdata/keystore/aaa b/accounts/keystore/testdata/keystore/aaa index a3868ec6d..a3868ec6d 100644 --- a/accounts/testdata/keystore/aaa +++ b/accounts/keystore/testdata/keystore/aaa diff --git a/accounts/testdata/keystore/empty b/accounts/keystore/testdata/keystore/empty index e69de29bb..e69de29bb 100644 --- a/accounts/testdata/keystore/empty +++ b/accounts/keystore/testdata/keystore/empty diff --git a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e index 309841e52..309841e52 100644 --- a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e +++ b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e diff --git a/accounts/testdata/keystore/garbage b/accounts/keystore/testdata/keystore/garbage Binary files differindex ff45091e7..ff45091e7 100644 --- a/accounts/testdata/keystore/garbage +++ b/accounts/keystore/testdata/keystore/garbage diff --git a/accounts/testdata/keystore/no-address b/accounts/keystore/testdata/keystore/no-address index ad51269ea..ad51269ea 100644 --- a/accounts/testdata/keystore/no-address +++ b/accounts/keystore/testdata/keystore/no-address diff --git a/accounts/testdata/keystore/zero b/accounts/keystore/testdata/keystore/zero index b52617f8a..b52617f8a 100644 --- a/accounts/testdata/keystore/zero +++ b/accounts/keystore/testdata/keystore/zero diff --git a/accounts/testdata/keystore/zzz b/accounts/keystore/testdata/keystore/zzz index cfd8a4701..cfd8a4701 100644 --- a/accounts/testdata/keystore/zzz +++ b/accounts/keystore/testdata/keystore/zzz diff --git a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e index 498d8131e..498d8131e 100644 --- a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e +++ b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e diff --git a/accounts/testdata/v1_test_vector.json b/accounts/keystore/testdata/v1_test_vector.json index 3d09b55b5..3d09b55b5 100644 --- a/accounts/testdata/v1_test_vector.json +++ b/accounts/keystore/testdata/v1_test_vector.json diff --git a/accounts/testdata/v3_test_vector.json b/accounts/keystore/testdata/v3_test_vector.json index 1e7f790c0..1e7f790c0 100644 --- a/accounts/testdata/v3_test_vector.json +++ b/accounts/keystore/testdata/v3_test_vector.json diff --git a/accounts/testdata/very-light-scrypt.json b/accounts/keystore/testdata/very-light-scrypt.json index d23b9b2b9..d23b9b2b9 100644 --- a/accounts/testdata/very-light-scrypt.json +++ b/accounts/keystore/testdata/very-light-scrypt.json diff --git a/accounts/watch.go b/accounts/keystore/watch.go index 472be2df7..0b4401255 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 *accountCache starting bool running bool ev chan notify.EventInfo quit chan struct{} } -func newWatcher(ac *addrCache) *watcher { +func newWatcher(ac *accountCache) *watcher { return &watcher{ ac: ac, ev: make(chan notify.EventInfo, 10), diff --git a/accounts/watch_fallback.go b/accounts/keystore/watch_fallback.go index bf971cb1b..7c5e9cb2e 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(*accountCache) *watcher { return new(watcher) } +func (*watcher) start() {} +func (*watcher) close() {} diff --git a/accounts/manager.go b/accounts/manager.go new file mode 100644 index 000000000..12a5bfcd9 --- /dev/null +++ b/accounts/manager.go @@ -0,0 +1,198 @@ +// 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 <http://www.gnu.org/licenses/>. + +package accounts + +import ( + "reflect" + "sort" + "sync" + + "github.com/ethereum/go-ethereum/event" +) + +// Manager is an overarching account manager that can communicate with various +// backends for signing transactions. +type Manager struct { + backends map[reflect.Type][]Backend // Index of backends currently registered + updaters []event.Subscription // Wallet update subscriptions for all backends + updates chan WalletEvent // Subscription sink for backend wallet changes + wallets []Wallet // Cache of all wallets from all registered backends + + feed event.Feed // Wallet feed notifying of arrivals/departures + + quit chan chan error + lock sync.RWMutex +} + +// NewManager creates a generic account manager to sign transaction via various +// supported backends. +func NewManager(backends ...Backend) *Manager { + // Subscribe to wallet notifications from all backends + updates := make(chan WalletEvent, 4*len(backends)) + + subs := make([]event.Subscription, len(backends)) + for i, backend := range backends { + subs[i] = backend.Subscribe(updates) + } + // Retrieve the initial list of wallets from the backends and sort by URL + var wallets []Wallet + for _, backend := range backends { + wallets = merge(wallets, backend.Wallets()...) + } + // Assemble the account manager and return + am := &Manager{ + backends: make(map[reflect.Type][]Backend), + updaters: subs, + updates: updates, + wallets: wallets, + quit: make(chan chan error), + } + for _, backend := range backends { + kind := reflect.TypeOf(backend) + am.backends[kind] = append(am.backends[kind], backend) + } + go am.update() + + return am +} + +// Close terminates the account manager's internal notification processes. +func (am *Manager) Close() error { + errc := make(chan error) + am.quit <- errc + return <-errc +} + +// update is the wallet event loop listening for notifications from the backends +// and updating the cache of wallets. +func (am *Manager) update() { + // Close all subscriptions when the manager terminates + defer func() { + am.lock.Lock() + for _, sub := range am.updaters { + sub.Unsubscribe() + } + am.updaters = nil + am.lock.Unlock() + }() + + // Loop until termination + for { + select { + case event := <-am.updates: + // Wallet event arrived, update local cache + am.lock.Lock() + if event.Arrive { + am.wallets = merge(am.wallets, event.Wallet) + } else { + am.wallets = drop(am.wallets, event.Wallet) + } + am.lock.Unlock() + + // Notify any listeners of the event + am.feed.Send(event) + + case errc := <-am.quit: + // Manager terminating, return + errc <- nil + return + } + } +} + +// Backends retrieves the backend(s) with the given type from the account manager. +func (am *Manager) Backends(kind reflect.Type) []Backend { + return am.backends[kind] +} + +// Wallets returns all signer accounts registered under this account manager. +func (am *Manager) Wallets() []Wallet { + am.lock.RLock() + defer am.lock.RUnlock() + + cpy := make([]Wallet, len(am.wallets)) + copy(cpy, am.wallets) + return cpy +} + +// Wallet retrieves the wallet associated with a particular URL. +func (am *Manager) Wallet(url string) (Wallet, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + parsed, err := parseURL(url) + if err != nil { + return nil, err + } + for _, wallet := range am.Wallets() { + if wallet.URL() == parsed { + return wallet, nil + } + } + return nil, ErrUnknownWallet +} + +// Find attempts to locate the wallet corresponding to a specific account. Since +// accounts can be dynamically added to and removed from wallets, this method has +// a linear runtime in the number of wallets. +func (am *Manager) Find(account Account) (Wallet, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + for _, wallet := range am.wallets { + if wallet.Contains(account) { + return wallet, nil + } + } + return nil, ErrUnknownAccount +} + +// Subscribe creates an async subscription to receive notifications when the +// manager detects the arrival or departure of a wallet from any of its backends. +func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { + return am.feed.Subscribe(sink) +} + +// merge is a sorted analogue of append for wallets, where the ordering of the +// origin list is preserved by inserting new wallets at the correct position. +// +// The original slice is assumed to be already sorted by URL. +func merge(slice []Wallet, wallets ...Wallet) []Wallet { + for _, wallet := range wallets { + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) + if n == len(slice) { + slice = append(slice, wallet) + continue + } + slice = append(slice[:n], append([]Wallet{wallet}, slice[n:]...)...) + } + return slice +} + +// drop is the couterpart of merge, which looks up wallets from within the sorted +// cache and removes the ones specified. +func drop(slice []Wallet, wallets ...Wallet) []Wallet { + for _, wallet := range wallets { + n := sort.Search(len(slice), func(i int) bool { return slice[i].URL().Cmp(wallet.URL()) >= 0 }) + if n == len(slice) { + // Wallet not found, may happen during startup + continue + } + slice = append(slice[:n], slice[n+1:]...) + } + return slice +} diff --git a/accounts/url.go b/accounts/url.go new file mode 100644 index 000000000..a2d00c1c6 --- /dev/null +++ b/accounts/url.go @@ -0,0 +1,79 @@ +// 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 <http://www.gnu.org/licenses/>. + +package accounts + +import ( + "encoding/json" + "errors" + "fmt" + "strings" +) + +// URL represents the canonical identification URL of a wallet or account. +// +// It is a simplified version of url.URL, with the important limitations (which +// are considered features here) that it contains value-copyable components only, +// as well as that it doesn't do any URL encoding/decoding of special characters. +// +// The former is important to allow an account to be copied without leaving live +// references to the original version, whereas the latter is important to ensure +// one single canonical form opposed to many allowed ones by the RFC 3986 spec. +// +// As such, these URLs should not be used outside of the scope of an Ethereum +// wallet or account. +type URL struct { + Scheme string // Protocol scheme to identify a capable account backend + Path string // Path for the backend to identify a unique entity +} + +// parseURL converts a user supplied URL into the accounts specific structure. +func parseURL(url string) (URL, error) { + parts := strings.Split(url, "://") + if len(parts) != 2 || parts[0] == "" { + return URL{}, errors.New("protocol scheme missing") + } + return URL{ + Scheme: parts[0], + Path: parts[1], + }, nil +} + +// String implements the stringer interface. +func (u URL) String() string { + if u.Scheme != "" { + return fmt.Sprintf("%s://%s", u.Scheme, u.Path) + } + return u.Path +} + +// MarshalJSON implements the json.Marshaller interface. +func (u URL) MarshalJSON() ([]byte, error) { + return json.Marshal(u.String()) +} + +// Cmp compares x and y and returns: +// +// -1 if x < y +// 0 if x == y +// +1 if x > y +// +func (u URL) Cmp(url URL) int { + if u.Scheme == url.Scheme { + return strings.Compare(u.Path, url.Path) + } + return strings.Compare(u.Scheme, url.Scheme) +} diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go new file mode 100644 index 000000000..ad5940cd4 --- /dev/null +++ b/accounts/usbwallet/ledger_hub.go @@ -0,0 +1,209 @@ +// 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 <http://www.gnu.org/licenses/>. + +// This file contains the implementation for interacting with the Ledger hardware +// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: +// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc + +// +build !ios + +package usbwallet + +import ( + "fmt" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/event" + "github.com/karalabe/gousb/usb" +) + +// LedgerScheme is the protocol scheme prefixing account and wallet URLs. +var LedgerScheme = "ledger" + +// ledgerDeviceIDs are the known device IDs that Ledger wallets use. +var ledgerDeviceIDs = []deviceID{ + {Vendor: 0x2c97, Product: 0x0000}, // Ledger Blue + {Vendor: 0x2c97, Product: 0x0001}, // Ledger Nano S +} + +// Maximum time between wallet refreshes (if USB hotplug notifications don't work). +const ledgerRefreshCycle = time.Second + +// Minimum time between wallet refreshes to avoid USB trashing. +const ledgerRefreshThrottling = 500 * time.Millisecond + +// LedgerHub is a accounts.Backend that can find and handle Ledger hardware wallets. +type LedgerHub struct { + ctx *usb.Context // Context interfacing with a libusb instance + + refreshed time.Time // Time instance when the list of wallets was last refreshed + wallets []accounts.Wallet // List of Ledger devices currently tracking + updateFeed event.Feed // Event feed to notify wallet additions/removals + updateScope event.SubscriptionScope // Subscription scope tracking current live listeners + updating bool // Whether the event notification loop is running + + quit chan chan error + lock sync.RWMutex +} + +// NewLedgerHub creates a new hardware wallet manager for Ledger devices. +func NewLedgerHub() (*LedgerHub, error) { + // Initialize the USB library to access Ledgers through + ctx, err := usb.NewContext() + if err != nil { + return nil, err + } + // Create the USB hub, start and return it + hub := &LedgerHub{ + ctx: ctx, + quit: make(chan chan error), + } + hub.refreshWallets() + + return hub, nil +} + +// Wallets implements accounts.Backend, returning all the currently tracked USB +// devices that appear to be Ledger hardware wallets. +func (hub *LedgerHub) Wallets() []accounts.Wallet { + // Make sure the list of wallets is up to date + hub.refreshWallets() + + hub.lock.RLock() + defer hub.lock.RUnlock() + + cpy := make([]accounts.Wallet, len(hub.wallets)) + copy(cpy, hub.wallets) + return cpy +} + +// refreshWallets scans the USB devices attached to the machine and updates the +// list of wallets based on the found devices. +func (hub *LedgerHub) refreshWallets() { + // Don't scan the USB like crazy it the user fetches wallets in a loop + hub.lock.RLock() + elapsed := time.Since(hub.refreshed) + hub.lock.RUnlock() + + if elapsed < ledgerRefreshThrottling { + return + } + // Retrieve the current list of Ledger devices + var devIDs []deviceID + var busIDs []uint16 + + hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { + // Gather Ledger devices, don't connect any just yet + for _, id := range ledgerDeviceIDs { + if desc.Vendor == id.Vendor && desc.Product == id.Product { + devIDs = append(devIDs, deviceID{Vendor: desc.Vendor, Product: desc.Product}) + busIDs = append(busIDs, uint16(desc.Bus)<<8+uint16(desc.Address)) + return false + } + } + // Not ledger, ignore and don't connect either + return false + }) + // Transform the current list of wallets into the new one + hub.lock.Lock() + + wallets := make([]accounts.Wallet, 0, len(devIDs)) + events := []accounts.WalletEvent{} + + for i := 0; i < len(devIDs); i++ { + devID, busID := devIDs[i], busIDs[i] + + url := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)} + + // Drop wallets in front of the next device or those that failed for some reason + for len(hub.wallets) > 0 && (hub.wallets[0].URL().Cmp(url) < 0 || hub.wallets[0].(*ledgerWallet).failed()) { + events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) + hub.wallets = hub.wallets[1:] + } + // If there are no more wallets or the device is before the next, wrap new wallet + if len(hub.wallets) == 0 || hub.wallets[0].URL().Cmp(url) > 0 { + wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: &url} + + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: true}) + wallets = append(wallets, wallet) + continue + } + // If the device is the same as the first wallet, keep it + if hub.wallets[0].URL().Cmp(url) == 0 { + wallets = append(wallets, hub.wallets[0]) + hub.wallets = hub.wallets[1:] + continue + } + } + // Drop any leftover wallets and set the new batch + for _, wallet := range hub.wallets { + events = append(events, accounts.WalletEvent{Wallet: wallet, Arrive: false}) + } + hub.refreshed = time.Now() + hub.wallets = wallets + hub.lock.Unlock() + + // Fire all wallet events and return + for _, event := range events { + hub.updateFeed.Send(event) + } +} + +// Subscribe implements accounts.Backend, creating an async subscription to +// receive notifications on the addition or removal of Ledger wallets. +func (hub *LedgerHub) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + // We need the mutex to reliably start/stop the update loop + hub.lock.Lock() + defer hub.lock.Unlock() + + // Subscribe the caller and track the subscriber count + sub := hub.updateScope.Track(hub.updateFeed.Subscribe(sink)) + + // Subscribers require an active notification loop, start it + if !hub.updating { + hub.updating = true + go hub.updater() + } + return sub +} + +// updater is responsible for maintaining an up-to-date list of wallets stored in +// the keystore, and for firing wallet addition/removal events. It listens for +// account change events from the underlying account cache, and also periodically +// forces a manual refresh (only triggers for systems where the filesystem notifier +// is not running). +func (hub *LedgerHub) updater() { + for { + // Wait for a USB hotplug event (not supported yet) or a refresh timeout + select { + //case <-hub.changes: // reenable on hutplug implementation + case <-time.After(ledgerRefreshCycle): + } + // Run the wallet refresher + hub.refreshWallets() + + // If all our subscribers left, stop the updater + hub.lock.Lock() + if hub.updateScope.Count() == 0 { + hub.updating = false + hub.lock.Unlock() + return + } + hub.lock.Unlock() + } +} diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go new file mode 100644 index 000000000..a667f580a --- /dev/null +++ b/accounts/usbwallet/ledger_wallet.go @@ -0,0 +1,945 @@ +// 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 <http://www.gnu.org/licenses/>. + +// This file contains the implementation for interacting with the Ledger hardware +// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: +// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc + +// +build !ios + +package usbwallet + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io" + "math/big" + "sync" + "time" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rlp" + "github.com/karalabe/gousb/usb" + "golang.org/x/net/context" +) + +// Maximum time between wallet health checks to detect USB unplugs. +const ledgerHeartbeatCycle = time.Second + +// Minimum time to wait between self derivation attempts, even it the user is +// requesting accounts like crazy. +const ledgerSelfDeriveThrottling = time.Second + +// ledgerOpcode is an enumeration encoding the supported Ledger opcodes. +type ledgerOpcode byte + +// ledgerParam1 is an enumeration encoding the supported Ledger parameters for +// specific opcodes. The same parameter values may be reused between opcodes. +type ledgerParam1 byte + +// ledgerParam2 is an enumeration encoding the supported Ledger parameters for +// specific opcodes. The same parameter values may be reused between opcodes. +type ledgerParam2 byte + +const ( + ledgerOpRetrieveAddress ledgerOpcode = 0x02 // Returns the public key and Ethereum address for a given BIP 32 path + ledgerOpSignTransaction ledgerOpcode = 0x04 // Signs an Ethereum transaction after having the user validate the parameters + ledgerOpGetConfiguration ledgerOpcode = 0x06 // Returns specific wallet application configuration + + ledgerP1DirectlyFetchAddress ledgerParam1 = 0x00 // Return address directly from the wallet + ledgerP1ConfirmFetchAddress ledgerParam1 = 0x01 // Require a user confirmation before returning the address + ledgerP1InitTransactionData ledgerParam1 = 0x00 // First transaction data block for signing + ledgerP1ContTransactionData ledgerParam1 = 0x80 // Subsequent transaction data block for signing + ledgerP2DiscardAddressChainCode ledgerParam2 = 0x00 // Do not return the chain code along with the address + ledgerP2ReturnAddressChainCode ledgerParam2 = 0x01 // Require a user confirmation before returning the address +) + +// errReplyInvalidHeader is the error message returned by a Ledfer data exchange +// if the device replies with a mismatching header. This usually means the device +// is in browser mode. +var errReplyInvalidHeader = errors.New("invalid reply header") + +// ledgerWallet represents a live USB Ledger hardware wallet. +type ledgerWallet struct { + context *usb.Context // USB context to interface libusb through + hardwareID deviceID // USB identifiers to identify this device type + locationID uint16 // USB bus and address to identify this device instance + url *accounts.URL // Textual URL uniquely identifying this wallet + + device *usb.Device // USB device advertising itself as a Ledger wallet + input usb.Endpoint // Input endpoint to send data to this device + output usb.Endpoint // Output endpoint to receive data from this device + failure error // Any failure that would make the device unusable + + version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) + browser bool // Flag whether the Ledger is in browser mode (reply channel mismatch) + accounts []accounts.Account // List of derive accounts pinned on the Ledger + paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations + + deriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery + deriveNextAddr common.Address // Next derived account address for auto-discovery + deriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with + deriveReq chan chan struct{} // Channel to request a self-derivation on + deriveQuit chan chan error // Channel to terminate the self-deriver with + + healthQuit chan chan error + + // Locking a hardware wallet is a bit special. Since hardware devices are lower + // performing, any communication with them might take a non negligible amount of + // time. Worse still, waiting for user confirmation can take arbitrarily long, + // but exclusive communication must be upheld during. Locking the entire wallet + // in the mean time however would stall any parts of the system that don't want + // to communicate, just read some state (e.g. list the accounts). + // + // As such, a hardware wallet needs two locks to function correctly. A state + // lock can be used to protect the wallet's software-side internal state, which + // must not be held exlusively during hardware communication. A communication + // lock can be used to achieve exclusive access to the device itself, this one + // however should allow "skipping" waiting for operations that might want to + // use the device, but can live without too (e.g. account self-derivation). + // + // Since we have two locks, it's important to know how to properly use them: + // - Communication requires the `device` to not change, so obtaining the + // commsLock should be done after having a stateLock. + // - Communication must not disable read access to the wallet state, so it + // must only ever hold a *read* lock to stateLock. + commsLock chan struct{} // Mutex (buf=1) for the USB comms without keeping the state locked + stateLock sync.RWMutex // Protects read and write access to the wallet struct fields +} + +// URL implements accounts.Wallet, returning the URL of the Ledger device. +func (w *ledgerWallet) URL() accounts.URL { + return *w.url // Immutable, no need for a lock +} + +// Status implements accounts.Wallet, always whether the Ledger is opened, closed +// or whether the Ethereum app was not started on it. +func (w *ledgerWallet) Status() string { + w.stateLock.RLock() // No device communication, state lock is enough + defer w.stateLock.RUnlock() + + if w.failure != nil { + return fmt.Sprintf("Failed: %v", w.failure) + } + if w.device == nil { + return "Closed" + } + if w.browser { + return "Ethereum app in browser mode" + } + if w.offline() { + return "Ethereum app offline" + } + return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2]) +} + +// offline returns whether the wallet and the Ethereum app is offline or not. +// +// The method assumes that the state lock is held! +func (w *ledgerWallet) offline() bool { + return w.version == [3]byte{0, 0, 0} +} + +// failed returns if the USB device wrapped by the wallet failed for some reason. +// This is used by the device scanner to report failed wallets as departed. +// +// The method assumes that the state lock is *not* held! +func (w *ledgerWallet) failed() bool { + w.stateLock.RLock() // No device communication, state lock is enough + defer w.stateLock.RUnlock() + + return w.failure != nil +} + +// Open implements accounts.Wallet, attempting to open a USB connection to the +// Ledger hardware wallet. The Ledger does not require a user passphrase, so that +// parameter is silently discarded. +func (w *ledgerWallet) Open(passphrase string) error { + w.stateLock.Lock() // State lock is enough since there's no connection yet at this point + defer w.stateLock.Unlock() + + // If the wallet was already opened, don't try to open again + if w.device != nil { + return accounts.ErrWalletAlreadyOpen + } + // Otherwise iterate over all USB devices and find this again (no way to directly do this) + // Iterate over all attached devices and fetch those seemingly Ledger + devices, err := w.context.ListDevices(func(desc *usb.Descriptor) bool { + // Only open this single specific device + return desc.Vendor == w.hardwareID.Vendor && desc.Product == w.hardwareID.Product && + uint16(desc.Bus)<<8+uint16(desc.Address) == w.locationID + }) + if err != nil { + return err + } + if len(devices) == 0 { + return accounts.ErrUnknownWallet + } + // Device opened, attach to the input and output endpoints + device := devices[0] + + var invalid string + switch { + case len(device.Descriptor.Configs) == 0: + invalid = "no endpoint config available" + case len(device.Descriptor.Configs[0].Interfaces) == 0: + invalid = "no endpoint interface available" + case len(device.Descriptor.Configs[0].Interfaces[0].Setups) == 0: + invalid = "no endpoint setup available" + case len(device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints) < 2: + invalid = "not enough IO endpoints available" + } + if invalid != "" { + device.Close() + return fmt.Errorf("ledger wallet [%s] invalid: %s", w.url, invalid) + } + // Open the input and output endpoints to the device + input, err := device.OpenEndpoint( + device.Descriptor.Configs[0].Config, + device.Descriptor.Configs[0].Interfaces[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[1].Address, + ) + if err != nil { + device.Close() + return fmt.Errorf("ledger wallet [%s] input open failed: %v", w.url, err) + } + output, err := device.OpenEndpoint( + device.Descriptor.Configs[0].Config, + device.Descriptor.Configs[0].Interfaces[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Number, + device.Descriptor.Configs[0].Interfaces[0].Setups[0].Endpoints[0].Address, + ) + if err != nil { + device.Close() + return fmt.Errorf("ledger wallet [%s] output open failed: %v", w.url, err) + } + // Wallet seems to be successfully opened, guess if the Ethereum app is running + w.device, w.input, w.output = device, input, output + w.commsLock = make(chan struct{}, 1) + w.commsLock <- struct{}{} // Enable lock + + w.paths = make(map[common.Address]accounts.DerivationPath) + + w.deriveReq = make(chan chan struct{}) + w.deriveQuit = make(chan chan error) + w.healthQuit = make(chan chan error) + + defer func() { + go w.heartbeat() + go w.selfDerive() + }() + + if _, err = w.ledgerDerive(accounts.DefaultBaseDerivationPath); err != nil { + // Ethereum app is not running or in browser mode, nothing more to do, return + if err == errReplyInvalidHeader { + w.browser = true + } + return nil + } + // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 + if w.version, err = w.ledgerVersion(); err != nil { + w.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 + } + return nil +} + +// heartbeat is a health check loop for the Ledger wallets to periodically verify +// whether they are still present or if they malfunctioned. It is needed because: +// - libusb on Windows doesn't support hotplug, so we can't detect USB unplugs +// - communication timeout on the Ledger requires a device power cycle to fix +func (w *ledgerWallet) heartbeat() { + glog.V(logger.Debug).Infof("%s health-check started", w.url.String()) + defer glog.V(logger.Debug).Infof("%s health-check stopped", w.url.String()) + + // Execute heartbeat checks until termination or error + var ( + errc chan error + err error + ) + for errc == nil && err == nil { + // Wait until termination is requested or the heartbeat cycle arrives + select { + case errc = <-w.healthQuit: + // Termination requested + continue + case <-time.After(ledgerHeartbeatCycle): + // Heartbeat time + } + // Execute a tiny data exchange to see responsiveness + w.stateLock.RLock() + if w.device == nil { + // Terminated while waiting for the lock + w.stateLock.RUnlock() + continue + } + <-w.commsLock // Don't lock state while resolving version + _, err = w.ledgerVersion() + w.commsLock <- struct{}{} + w.stateLock.RUnlock() + + if err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { + w.stateLock.Lock() // Lock state to tear the wallet down + w.failure = err + w.close() + w.stateLock.Unlock() + } + // Ignore uninteresting errors + err = nil + } + // In case of error, wait for termination + if err != nil { + glog.V(logger.Debug).Infof("%s health-check failed: %v", w.url.String(), err) + errc = <-w.healthQuit + } + errc <- err +} + +// Close implements accounts.Wallet, closing the USB connection to the Ledger. +func (w *ledgerWallet) Close() error { + // Ensure the wallet was opened + w.stateLock.RLock() + hQuit, dQuit := w.healthQuit, w.deriveQuit + w.stateLock.RUnlock() + + // Terminate the health checks + var herr error + if hQuit != nil { + errc := make(chan error) + hQuit <- errc + herr = <-errc // Save for later, we *must* close the USB + } + // Terminate the self-derivations + var derr error + if dQuit != nil { + errc := make(chan error) + dQuit <- errc + derr = <-errc // Save for later, we *must* close the USB + } + // Terminate the device connection + w.stateLock.Lock() + defer w.stateLock.Unlock() + + w.healthQuit = nil + w.deriveQuit = nil + w.deriveReq = nil + + if err := w.close(); err != nil { + return err + } + if herr != nil { + return herr + } + return derr +} + +// close is the internal wallet closer that terminates the USB connection and +// resets all the fields to their defaults. +// +// Note, close assumes the state lock is held! +func (w *ledgerWallet) close() error { + // Allow duplicate closes, especially for health-check failures + if w.device == nil { + return nil + } + // Close the device, clear everything, then return + err := w.device.Close() + + w.device, w.input, w.output = nil, nil, nil + w.browser, w.version = false, [3]byte{} + w.accounts, w.paths = nil, nil + + return err +} + +// Accounts implements accounts.Wallet, returning the list of accounts pinned to +// the Ledger hardware wallet. If self-derivation was enabled, the account list +// is periodically expanded based on current chain state. +func (w *ledgerWallet) Accounts() []accounts.Account { + // Attempt self-derivation if it's running + reqc := make(chan struct{}, 1) + select { + case w.deriveReq <- reqc: + // Self-derivation request accepted, wait for it + <-reqc + default: + // Self-derivation offline, throttled or busy, skip + } + // Return whatever account list we ended up with + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy +} + +// selfDerive is an account derivation loop that upon request attempts to find +// new non-zero accounts. +func (w *ledgerWallet) selfDerive() { + glog.V(logger.Debug).Infof("%s self-derivation started", w.url.String()) + defer glog.V(logger.Debug).Infof("%s self-derivation stopped", w.url.String()) + + // Execute self-derivations until termination or error + var ( + reqc chan struct{} + errc chan error + err error + ) + for errc == nil && err == nil { + // Wait until either derivation or termination is requested + select { + case errc = <-w.deriveQuit: + // Termination requested + continue + case reqc = <-w.deriveReq: + // Account discovery requested + } + // Derivation needs a chain and device access, skip if either unavailable + w.stateLock.RLock() + if w.device == nil || w.deriveChain == nil || w.offline() { + w.stateLock.RUnlock() + reqc <- struct{}{} + continue + } + select { + case <-w.commsLock: + default: + w.stateLock.RUnlock() + reqc <- struct{}{} + continue + } + // Device lock obtained, derive the next batch of accounts + var ( + accs []accounts.Account + paths []accounts.DerivationPath + + nextAddr = w.deriveNextAddr + nextPath = w.deriveNextPath + + context = context.Background() + ) + for empty := false; !empty; { + // Retrieve the next derived Ethereum account + if nextAddr == (common.Address{}) { + if nextAddr, err = w.ledgerDerive(nextPath); err != nil { + glog.V(logger.Warn).Infof("%s self-derivation failed: %v", w.url.String(), err) + break + } + } + // Check the account's status against the current chain state + var ( + balance *big.Int + nonce uint64 + ) + balance, err = w.deriveChain.BalanceAt(context, nextAddr, nil) + if err != nil { + glog.V(logger.Warn).Infof("%s self-derivation balance retrieval failed: %v", w.url.String(), err) + break + } + nonce, err = w.deriveChain.NonceAt(context, nextAddr, nil) + if err != nil { + glog.V(logger.Warn).Infof("%s self-derivation nonce retrieval failed: %v", w.url.String(), err) + break + } + // If the next account is empty, stop self-derivation, but add it nonetheless + if balance.BitLen() == 0 && nonce == 0 { + empty = true + } + // We've just self-derived a new account, start tracking it locally + path := make(accounts.DerivationPath, len(nextPath)) + copy(path[:], nextPath[:]) + paths = append(paths, path) + + account := accounts.Account{ + Address: nextAddr, + URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, + } + accs = append(accs, account) + + // Display a log message to the user for new (or previously empty accounts) + if _, known := w.paths[nextAddr]; !known || (!empty && nextAddr == w.deriveNextAddr) { + glog.V(logger.Info).Infof("%s discovered %s (balance %22v, nonce %4d) at %s", w.url.String(), nextAddr.Hex(), balance, nonce, path) + } + // Fetch the next potential account + if !empty { + nextAddr = common.Address{} + nextPath[len(nextPath)-1]++ + } + } + // Self derivation complete, release device lock + w.commsLock <- struct{}{} + w.stateLock.RUnlock() + + // Insert any accounts successfully derived + w.stateLock.Lock() + for i := 0; i < len(accs); i++ { + if _, ok := w.paths[accs[i].Address]; !ok { + w.accounts = append(w.accounts, accs[i]) + w.paths[accs[i].Address] = paths[i] + } + } + // Shift the self-derivation forward + // TODO(karalabe): don't overwrite changes from wallet.SelfDerive + w.deriveNextAddr = nextAddr + w.deriveNextPath = nextPath + w.stateLock.Unlock() + + // Notify the user of termination and loop after a bit of time (to avoid trashing) + reqc <- struct{}{} + if err == nil { + select { + case errc = <-w.deriveQuit: + // Termination requested, abort + case <-time.After(ledgerSelfDeriveThrottling): + // Waited enough, willing to self-derive again + } + } + } + // In case of error, wait for termination + if err != nil { + glog.V(logger.Debug).Infof("%s self-derivation failed: %s", w.url.String(), err) + errc = <-w.deriveQuit + } + errc <- err +} + +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not pinned into this Ledger instance. Although we could attempt to resolve +// unpinned accounts, that would be an non-negligible hardware operation. +func (w *ledgerWallet) Contains(account accounts.Account) bool { + w.stateLock.RLock() + defer w.stateLock.RUnlock() + + _, exists := w.paths[account.Address] + return exists +} + +// Derive implements accounts.Wallet, deriving a new account at the specific +// derivation path. If pin is set to true, the account will be added to the list +// of tracked accounts. +func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { + // Try to derive the actual account and update its URL if successful + w.stateLock.RLock() // Avoid device disappearing during derivation + + if w.device == nil || w.offline() { + w.stateLock.RUnlock() + return accounts.Account{}, accounts.ErrWalletClosed + } + <-w.commsLock // Avoid concurrent hardware access + address, err := w.ledgerDerive(path) + w.commsLock <- struct{}{} + + w.stateLock.RUnlock() + + // If an error occurred or no pinning was requested, return + if err != nil { + return accounts.Account{}, err + } + account := accounts.Account{ + Address: address, + URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, + } + if !pin { + return account, nil + } + // Pinning needs to modify the state + w.stateLock.Lock() + defer w.stateLock.Unlock() + + if _, ok := w.paths[address]; !ok { + w.accounts = append(w.accounts, account) + w.paths[address] = path + } + return account, nil +} + +// SelfDerive implements accounts.Wallet, trying to discover accounts that the +// user used previously (based on the chain state), but ones that he/she did not +// explicitly pin to the wallet manually. To avoid chain head monitoring, self +// derivation only runs during account listing (and even then throttled). +func (w *ledgerWallet) SelfDerive(base accounts.DerivationPath, chain ethereum.ChainStateReader) { + w.stateLock.Lock() + defer w.stateLock.Unlock() + + w.deriveNextPath = make(accounts.DerivationPath, len(base)) + copy(w.deriveNextPath[:], base[:]) + + w.deriveNextAddr = common.Address{} + w.deriveChain = chain +} + +// SignHash implements accounts.Wallet, however signing arbitrary data is not +// supported for Ledger wallets, so this method will always return an error. +func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTx implements accounts.Wallet. It sends the transaction over to the Ledger +// wallet to request a confirmation from the user. It returns either the signed +// transaction or a failure if the user denied the transaction. +// +// Note, if the version of the Ethereum application running on the Ledger wallet is +// too old to sign EIP-155 transactions, but such is requested nonetheless, an error +// will be returned opposed to silently signing in Homestead mode. +func (w *ledgerWallet) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + w.stateLock.RLock() // Comms have own mutex, this is for the state fields + defer w.stateLock.RUnlock() + + // If the wallet is closed, or the Ethereum app doesn't run, abort + if w.device == nil || w.offline() { + return nil, accounts.ErrWalletClosed + } + // Make sure the requested account is contained within + path, ok := w.paths[account.Address] + if !ok { + return nil, accounts.ErrUnknownAccount + } + // Ensure the wallet is capable of signing the given transaction + if chainID != nil && w.version[0] <= 1 && w.version[1] <= 0 && w.version[2] <= 2 { + return nil, fmt.Errorf("Ledger v%d.%d.%d doesn't support signing this transaction, please update to v1.0.3 at least", w.version[0], w.version[1], w.version[2]) + } + // All infos gathered and metadata checks out, request signing + <-w.commsLock + defer func() { w.commsLock <- struct{}{} }() + + return w.ledgerSign(path, account.Address, tx, chainID) +} + +// SignHashWithPassphrase implements accounts.Wallet, however signing arbitrary +// data is not supported for Ledger wallets, so this method will always return +// an error. +func (w *ledgerWallet) SignHashWithPassphrase(account accounts.Account, passphrase string, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +// Since the Ledger does not support extra passphrases, it is silently ignored. +func (w *ledgerWallet) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return w.SignTx(account, tx, chainID) +} + +// ledgerVersion retrieves the current version of the Ethereum wallet app running +// on the Ledger wallet. +// +// The version retrieval protocol is defined as follows: +// +// CLA | INS | P1 | P2 | Lc | Le +// ----+-----+----+----+----+--- +// E0 | 06 | 00 | 00 | 00 | 04 +// +// With no input data, and the output data being: +// +// Description | Length +// ---------------------------------------------------+-------- +// Flags 01: arbitrary data signature enabled by user | 1 byte +// Application major version | 1 byte +// Application minor version | 1 byte +// Application patch version | 1 byte +func (w *ledgerWallet) ledgerVersion() ([3]byte, error) { + // Send the request and wait for the response + reply, err := w.ledgerExchange(ledgerOpGetConfiguration, 0, 0, nil) + if err != nil { + return [3]byte{}, err + } + if len(reply) != 4 { + return [3]byte{}, errors.New("reply not of correct size") + } + // Cache the version for future reference + var version [3]byte + copy(version[:], reply[1:]) + return version, nil +} + +// ledgerDerive retrieves the currently active Ethereum address from a Ledger +// wallet at the specified derivation path. +// +// The address derivation protocol is defined as follows: +// +// CLA | INS | P1 | P2 | Lc | Le +// ----+-----+----+----+-----+--- +// E0 | 02 | 00 return address +// 01 display address and confirm before returning +// | 00: do not return the chain code +// | 01: return the chain code +// | var | 00 +// +// Where the input data is: +// +// Description | Length +// -------------------------------------------------+-------- +// Number of BIP 32 derivations to perform (max 10) | 1 byte +// First derivation index (big endian) | 4 bytes +// ... | 4 bytes +// Last derivation index (big endian) | 4 bytes +// +// And the output data is: +// +// Description | Length +// ------------------------+------------------- +// Public Key length | 1 byte +// Uncompressed Public Key | arbitrary +// Ethereum address length | 1 byte +// Ethereum address | 40 bytes hex ascii +// Chain code if requested | 32 bytes +func (w *ledgerWallet) ledgerDerive(derivationPath []uint32) (common.Address, error) { + // Flatten the derivation path into the Ledger request + path := make([]byte, 1+4*len(derivationPath)) + path[0] = byte(len(derivationPath)) + for i, component := range derivationPath { + binary.BigEndian.PutUint32(path[1+4*i:], component) + } + // Send the request and wait for the response + reply, err := w.ledgerExchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) + if err != nil { + return common.Address{}, err + } + // Discard the public key, we don't need that for now + if len(reply) < 1 || len(reply) < 1+int(reply[0]) { + return common.Address{}, errors.New("reply lacks public key entry") + } + reply = reply[1+int(reply[0]):] + + // Extract the Ethereum hex address string + if len(reply) < 1 || len(reply) < 1+int(reply[0]) { + return common.Address{}, errors.New("reply lacks address entry") + } + hexstr := reply[1 : 1+int(reply[0])] + + // Decode the hex sting into an Ethereum address and return + var address common.Address + hex.Decode(address[:], hexstr) + return address, nil +} + +// ledgerSign sends the transaction to the Ledger wallet, and waits for the user +// to confirm or deny the transaction. +// +// The transaction signing protocol is defined as follows: +// +// CLA | INS | P1 | P2 | Lc | Le +// ----+-----+----+----+-----+--- +// E0 | 04 | 00: first transaction data block +// 80: subsequent transaction data block +// | 00 | variable | variable +// +// Where the input for the first transaction block (first 255 bytes) is: +// +// Description | Length +// -------------------------------------------------+---------- +// Number of BIP 32 derivations to perform (max 10) | 1 byte +// First derivation index (big endian) | 4 bytes +// ... | 4 bytes +// Last derivation index (big endian) | 4 bytes +// RLP transaction chunk | arbitrary +// +// And the input for subsequent transaction blocks (first 255 bytes) are: +// +// Description | Length +// ----------------------+---------- +// RLP transaction chunk | arbitrary +// +// And the output data is: +// +// Description | Length +// ------------+--------- +// signature V | 1 byte +// signature R | 32 bytes +// signature S | 32 bytes +func (w *ledgerWallet) ledgerSign(derivationPath []uint32, address common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // We need to modify the timeouts to account for user feedback + defer func(old time.Duration) { w.device.ReadTimeout = old }(w.device.ReadTimeout) + w.device.ReadTimeout = time.Hour * 24 * 30 // Timeout requires a Ledger power cycle, only if you must + + // Flatten the derivation path into the Ledger request + path := make([]byte, 1+4*len(derivationPath)) + path[0] = byte(len(derivationPath)) + for i, component := range derivationPath { + binary.BigEndian.PutUint32(path[1+4*i:], component) + } + // Create the transaction RLP based on whether legacy or EIP155 signing was requeste + var ( + txrlp []byte + err error + ) + if chainID == nil { + if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data()}); err != nil { + return nil, err + } + } else { + if txrlp, err = rlp.EncodeToBytes([]interface{}{tx.Nonce(), tx.GasPrice(), tx.Gas(), tx.To(), tx.Value(), tx.Data(), chainID, big.NewInt(0), big.NewInt(0)}); err != nil { + return nil, err + } + } + payload := append(path, txrlp...) + + // Send the request and wait for the response + var ( + op = ledgerP1InitTransactionData + reply []byte + ) + for len(payload) > 0 { + // Calculate the size of the next data chunk + chunk := 255 + if chunk > len(payload) { + chunk = len(payload) + } + // Send the chunk over, ensuring it's processed correctly + reply, err = w.ledgerExchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) + if err != nil { + return nil, err + } + // Shift the payload and ensure subsequent chunks are marked as such + payload = payload[chunk:] + op = ledgerP1ContTransactionData + } + // Extract the Ethereum signature and do a sanity validation + if len(reply) != 65 { + return nil, errors.New("reply lacks signature") + } + signature := append(reply[1:], reply[0]) + + // Create the correct signer and signature transform based on the chain ID + var signer types.Signer + if chainID == nil { + signer = new(types.HomesteadSigner) + } else { + signer = types.NewEIP155Signer(chainID) + signature[64] = signature[64] - byte(chainID.Uint64()*2+35) + } + // Inject the final signature into the transaction and sanity check the sender + signed, err := tx.WithSignature(signer, signature) + if err != nil { + return nil, err + } + sender, err := types.Sender(signer, signed) + if err != nil { + return nil, err + } + if sender != address { + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", address.Hex(), sender.Hex()) + } + return signed, nil +} + +// ledgerExchange performs a data exchange with the Ledger wallet, sending it a +// message and retrieving the response. +// +// The common transport header is defined as follows: +// +// Description | Length +// --------------------------------------+---------- +// Communication channel ID (big endian) | 2 bytes +// Command tag | 1 byte +// Packet sequence index (big endian) | 2 bytes +// Payload | arbitrary +// +// The Communication channel ID allows commands multiplexing over the same +// physical link. It is not used for the time being, and should be set to 0101 +// to avoid compatibility issues with implementations ignoring a leading 00 byte. +// +// The Command tag describes the message content. Use TAG_APDU (0x05) for standard +// APDU payloads, or TAG_PING (0x02) for a simple link test. +// +// The Packet sequence index describes the current sequence for fragmented payloads. +// The first fragment index is 0x00. +// +// APDU Command payloads are encoded as follows: +// +// Description | Length +// ----------------------------------- +// APDU length (big endian) | 2 bytes +// APDU CLA | 1 byte +// APDU INS | 1 byte +// APDU P1 | 1 byte +// APDU P2 | 1 byte +// APDU length | 1 byte +// Optional APDU data | arbitrary +func (w *ledgerWallet) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { + // Construct the message payload, possibly split into multiple chunks + apdu := make([]byte, 2, 7+len(data)) + + binary.BigEndian.PutUint16(apdu, uint16(5+len(data))) + apdu = append(apdu, []byte{0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))}...) + apdu = append(apdu, data...) + + // Stream all the chunks to the device + header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended + chunk := make([]byte, 64) + space := len(chunk) - len(header) + + for i := 0; len(apdu) > 0; i++ { + // Construct the new message to stream + chunk = append(chunk[:0], header...) + binary.BigEndian.PutUint16(chunk[3:], uint16(i)) + + if len(apdu) > space { + chunk = append(chunk, apdu[:space]...) + apdu = apdu[space:] + } else { + chunk = append(chunk, apdu...) + apdu = nil + } + // Send over to the device + if glog.V(logger.Detail) { + glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, chunk) + } + if _, err := w.input.Write(chunk); err != nil { + return nil, err + } + } + // Stream the reply back from the wallet in 64 byte chunks + var reply []byte + chunk = chunk[:64] // Yeah, we surely have enough space + for { + // Read the next chunk from the Ledger wallet + if _, err := io.ReadFull(w.output, chunk); err != nil { + return nil, err + } + if glog.V(logger.Detail) { + glog.Infof("<- %03d.%03d: %x", w.device.Bus, w.device.Address, chunk) + } + // Make sure the transport header matches + if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { + return nil, errReplyInvalidHeader + } + // If it's the first chunk, retrieve the total message length + var payload []byte + + if chunk[3] == 0x00 && chunk[4] == 0x00 { + reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7]))) + payload = chunk[7:] + } else { + payload = chunk[5:] + } + // Append to the reply and stop when filled up + if left := cap(reply) - len(reply); left > len(payload) { + reply = append(reply, payload...) + } else { + reply = append(reply, payload[:left]...) + break + } + } + return reply[:len(reply)-2], nil +} diff --git a/accounts/usbwallet/usbwallet.go b/accounts/usbwallet/usbwallet.go new file mode 100644 index 000000000..3989f3d02 --- /dev/null +++ b/accounts/usbwallet/usbwallet.go @@ -0,0 +1,29 @@ +// 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 <http://www.gnu.org/licenses/>. + +// +build !ios + +// Package usbwallet implements support for USB hardware wallets. +package usbwallet + +import "github.com/karalabe/gousb/usb" + +// deviceID is a combined vendor/product identifier to uniquely identify a USB +// hardware device. +type deviceID struct { + Vendor usb.ID // The Vendor identifer + Product usb.ID // The Product identifier +} diff --git a/accounts/usbwallet/usbwallet_ios.go b/accounts/usbwallet/usbwallet_ios.go new file mode 100644 index 000000000..17d342903 --- /dev/null +++ b/accounts/usbwallet/usbwallet_ios.go @@ -0,0 +1,38 @@ +// 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 <http://www.gnu.org/licenses/>. + +// This file contains the implementation for interacting with the Ledger hardware +// wallets. The wire protocol spec can be found in the Ledger Blue GitHub repo: +// https://raw.githubusercontent.com/LedgerHQ/blue-app-eth/master/doc/ethapp.asc + +// +build ios + +package usbwallet + +import ( + "errors" + + "github.com/ethereum/go-ethereum/accounts" +) + +// Here be dragons! There is no USB support on iOS. + +// ErrIOSNotSupported is returned for all USB hardware backends on iOS. +var ErrIOSNotSupported = errors.New("no USB support on iOS") + +func NewLedgerHub() (accounts.Backend, error) { + return nil, ErrIOSNotSupported +} |