From 833e4d1319336fbb66fd8f1e473c24472d65b17b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Tue, 24 Jan 2017 11:49:20 +0200 Subject: accounts, cmd, eth, internal, mobile, node: split account backends --- accounts/abi/bind/auth.go | 4 +- accounts/account_manager.go | 350 -------------------- accounts/accounts.go | 179 ++++++++++ accounts/accounts_test.go | 224 ------------- accounts/addrcache.go | 270 --------------- accounts/addrcache_test.go | 283 ---------------- accounts/backend.go | 88 +++++ accounts/errors.go | 41 +++ accounts/key.go | 229 ------------- accounts/key_store_passphrase.go | 305 ----------------- accounts/key_store_passphrase_test.go | 60 ---- accounts/key_store_plain.go | 62 ---- accounts/key_store_test.go | 253 -------------- accounts/keystore/address_cache.go | 271 +++++++++++++++ accounts/keystore/address_cache_test.go | 284 ++++++++++++++++ accounts/keystore/key.go | 230 +++++++++++++ accounts/keystore/keystore.go | 362 +++++++++++++++++++++ accounts/keystore/keystore_passphrase.go | 305 +++++++++++++++++ accounts/keystore/keystore_passphrase_test.go | 60 ++++ accounts/keystore/keystore_plain.go | 62 ++++ accounts/keystore/keystore_plain_test.go | 253 ++++++++++++++ accounts/keystore/keystore_test.go | 225 +++++++++++++ accounts/keystore/presale.go | 137 ++++++++ accounts/keystore/testdata/dupes/1 | 1 + accounts/keystore/testdata/dupes/2 | 1 + accounts/keystore/testdata/dupes/foo | 1 + accounts/keystore/testdata/keystore/.hiddenfile | 1 + accounts/keystore/testdata/keystore/README | 21 ++ ...1759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 | 1 + accounts/keystore/testdata/keystore/aaa | 1 + accounts/keystore/testdata/keystore/empty | 0 .../foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e | 1 + accounts/keystore/testdata/keystore/garbage | Bin 0 -> 300 bytes accounts/keystore/testdata/keystore/no-address | 1 + accounts/keystore/testdata/keystore/zero | 1 + accounts/keystore/testdata/keystore/zzz | 1 + .../cb61d5a9c4896fb9658090b597ef0e7be6f7b67e | 1 + accounts/keystore/testdata/v1_test_vector.json | 28 ++ accounts/keystore/testdata/v3_test_vector.json | 97 ++++++ accounts/keystore/testdata/very-light-scrypt.json | 1 + accounts/keystore/watch.go | 113 +++++++ accounts/keystore/watch_fallback.go | 28 ++ accounts/presale.go | 136 -------- accounts/testdata/dupes/1 | 1 - accounts/testdata/dupes/2 | 1 - accounts/testdata/dupes/foo | 1 - accounts/testdata/keystore/.hiddenfile | 1 - accounts/testdata/keystore/README | 21 -- ...1759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 | 1 - accounts/testdata/keystore/aaa | 1 - accounts/testdata/keystore/empty | 0 .../foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e | 1 - accounts/testdata/keystore/garbage | Bin 300 -> 0 bytes accounts/testdata/keystore/no-address | 1 - accounts/testdata/keystore/zero | 1 - accounts/testdata/keystore/zzz | 1 - .../cb61d5a9c4896fb9658090b597ef0e7be6f7b67e | 1 - accounts/testdata/v1_test_vector.json | 28 -- accounts/testdata/v3_test_vector.json | 97 ------ accounts/testdata/very-light-scrypt.json | 1 - accounts/watch.go | 113 ------- accounts/watch_fallback.go | 28 -- 62 files changed, 2798 insertions(+), 2473 deletions(-) delete mode 100644 accounts/account_manager.go create mode 100644 accounts/accounts.go delete mode 100644 accounts/accounts_test.go delete mode 100644 accounts/addrcache.go delete mode 100644 accounts/addrcache_test.go create mode 100644 accounts/backend.go create mode 100644 accounts/errors.go delete mode 100644 accounts/key.go delete mode 100644 accounts/key_store_passphrase.go delete mode 100644 accounts/key_store_passphrase_test.go delete mode 100644 accounts/key_store_plain.go delete mode 100644 accounts/key_store_test.go create mode 100644 accounts/keystore/address_cache.go create mode 100644 accounts/keystore/address_cache_test.go create mode 100644 accounts/keystore/key.go create mode 100644 accounts/keystore/keystore.go create mode 100644 accounts/keystore/keystore_passphrase.go create mode 100644 accounts/keystore/keystore_passphrase_test.go create mode 100644 accounts/keystore/keystore_plain.go create mode 100644 accounts/keystore/keystore_plain_test.go create mode 100644 accounts/keystore/keystore_test.go create mode 100644 accounts/keystore/presale.go create mode 100644 accounts/keystore/testdata/dupes/1 create mode 100644 accounts/keystore/testdata/dupes/2 create mode 100644 accounts/keystore/testdata/dupes/foo create mode 100644 accounts/keystore/testdata/keystore/.hiddenfile create mode 100644 accounts/keystore/testdata/keystore/README create mode 100644 accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 create mode 100644 accounts/keystore/testdata/keystore/aaa create mode 100644 accounts/keystore/testdata/keystore/empty create mode 100644 accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e create mode 100644 accounts/keystore/testdata/keystore/garbage create mode 100644 accounts/keystore/testdata/keystore/no-address create mode 100644 accounts/keystore/testdata/keystore/zero create mode 100644 accounts/keystore/testdata/keystore/zzz create mode 100644 accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e create mode 100644 accounts/keystore/testdata/v1_test_vector.json create mode 100644 accounts/keystore/testdata/v3_test_vector.json create mode 100644 accounts/keystore/testdata/very-light-scrypt.json create mode 100644 accounts/keystore/watch.go create mode 100644 accounts/keystore/watch_fallback.go delete mode 100644 accounts/presale.go delete mode 100644 accounts/testdata/dupes/1 delete mode 100644 accounts/testdata/dupes/2 delete mode 100644 accounts/testdata/dupes/foo delete mode 100644 accounts/testdata/keystore/.hiddenfile delete mode 100644 accounts/testdata/keystore/README delete mode 100644 accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 delete mode 100644 accounts/testdata/keystore/aaa delete mode 100644 accounts/testdata/keystore/empty delete mode 100644 accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e delete mode 100644 accounts/testdata/keystore/garbage delete mode 100644 accounts/testdata/keystore/no-address delete mode 100644 accounts/testdata/keystore/zero delete mode 100644 accounts/testdata/keystore/zzz delete mode 100644 accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e delete mode 100644 accounts/testdata/v1_test_vector.json delete mode 100644 accounts/testdata/v3_test_vector.json delete mode 100644 accounts/testdata/very-light-scrypt.json delete mode 100644 accounts/watch.go delete mode 100644 accounts/watch_fallback.go (limited to 'accounts') 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 . - -// 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..234b6e456 --- /dev/null +++ b/accounts/accounts.go @@ -0,0 +1,179 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package accounts implements high level Ethereum account management. +package accounts + +import ( + "encoding/json" + "errors" + "math/big" + "reflect" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// ErrUnknownAccount is returned for any requested operation for which no backend +// provides the specified account. +var ErrUnknownAccount = errors.New("unknown account") + +// ErrNotSupported is returned when an operation is requested from an account +// backend that it does not support. +var ErrNotSupported = errors.New("not supported") + +// Account represents a stored key. +// When used as an argument, it selects a unique key to act on. +type Account struct { + Address common.Address // Ethereum account address derived from the key + URL string // Optional resource locator within a backend + backend Backend // Backend where this account originates from +} + +func (acc *Account) MarshalJSON() ([]byte, error) { + return []byte(`"` + acc.Address.Hex() + `"`), nil +} + +func (acc *Account) UnmarshalJSON(raw []byte) error { + return json.Unmarshal(raw, &acc.Address) +} + +// Manager is an overarching account manager that can communicate with various +// backends for signing transactions. +type Manager struct { + backends []Backend // List of currently registered backends (ordered by registration) + index map[reflect.Type]Backend // Set of currently registered backends + lock sync.RWMutex +} + +// NewManager creates a generic account manager to sign transaction via various +// supported backends. +func NewManager(backends ...Backend) *Manager { + am := &Manager{ + backends: backends, + index: make(map[reflect.Type]Backend), + } + for _, backend := range backends { + am.index[reflect.TypeOf(backend)] = backend + } + return am +} + +// Backend retrieves the backend with the given type from the account manager. +func (am *Manager) Backend(backend reflect.Type) Backend { + return am.index[backend] +} + +// Accounts returns all signer accounts registered under this account manager. +func (am *Manager) Accounts() []Account { + am.lock.RLock() + defer am.lock.RUnlock() + + var all []Account + for _, backend := range am.backends { // TODO(karalabe): cache these after subscriptions are in + accounts := backend.Accounts() + for i := 0; i < len(accounts); i++ { + accounts[i].backend = backend + } + all = append(all, accounts...) + } + return all +} + +// HasAddress reports whether a key with the given address is present. +func (am *Manager) HasAddress(addr common.Address) bool { + am.lock.RLock() + defer am.lock.RUnlock() + + for _, backend := range am.backends { + if backend.HasAddress(addr) { + return true + } + } + return false +} + +// SignHash requests the account manager to get the hash signed with an arbitrary +// signing backend holding the authorization for the specified account. +func (am *Manager) SignHash(acc Account, hash []byte) ([]byte, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignHash(acc, hash) +} + +// SignTx requests the account manager to get the transaction signed with an +// arbitrary signing backend holding the authorization for the specified account. +func (am *Manager) SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignTx(acc, tx, chainID) +} + +// SignHashWithPassphrase requests the account manager to get the hash signed with +// an arbitrary signing backend holding the authorization for the specified account. +func (am *Manager) SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignHashWithPassphrase(acc, passphrase, hash) +} + +// SignTxWithPassphrase requests the account manager to get the transaction signed +// with an arbitrary signing backend holding the authorization for the specified +// account. +func (am *Manager) SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + am.lock.RLock() + defer am.lock.RUnlock() + + if err := am.ensureBackend(&acc); err != nil { + return nil, err + } + return acc.backend.SignTxWithPassphrase(acc, passphrase, tx, chainID) +} + +// ensureBackend ensures that the account has a correctly set backend and that +// it is still alive. +// +// Please note, this method assumes the manager lock is held! +func (am *Manager) ensureBackend(acc *Account) error { + // If we have a backend, make sure it's still live + if acc.backend != nil { + if _, exists := am.index[reflect.TypeOf(acc.backend)]; !exists { + return ErrUnknownAccount + } + return nil + } + // If we don't have a known backend, look up one that can service it + for _, backend := range am.backends { + if backend.HasAddress(acc.Address) { // TODO(karalabe): this assumes unique addresses per backend + acc.backend = backend + return nil + } + } + return ErrUnknownAccount +} diff --git a/accounts/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 . - -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/addrcache.go b/accounts/addrcache.go deleted file mode 100644 index a99f23606..000000000 --- a/accounts/addrcache.go +++ /dev/null @@ -1,270 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "bufio" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" -) - -// Minimum amount of time between cache reloads. This limit applies if the platform does -// not support change notifications. It also applies if the keystore directory does not -// exist yet, the code will attempt to create a watcher at most this often. -const minReloadInterval = 2 * time.Second - -type accountsByFile []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] } - -// AmbiguousAddrError is returned when attempting to unlock -// an address for which more than one file exists. -type AmbiguousAddrError struct { - Addr common.Address - Matches []Account -} - -func (err *AmbiguousAddrError) Error() string { - files := "" - for i, a := range err.Matches { - files += a.File - if i < len(err.Matches)-1 { - files += ", " - } - } - return fmt.Sprintf("multiple keys match address (%s)", files) -} - -// addrCache is a live index of all accounts in the keystore. -type addrCache struct { - keydir string - watcher *watcher - mu sync.Mutex - all accountsByFile - byAddr map[common.Address][]Account - throttle *time.Timer -} - -func newAddrCache(keydir string) *addrCache { - ac := &addrCache{ - keydir: keydir, - byAddr: make(map[common.Address][]Account), - } - ac.watcher = newWatcher(ac) - return ac -} - -func (ac *addrCache) accounts() []Account { - ac.maybeReload() - ac.mu.Lock() - defer ac.mu.Unlock() - cpy := make([]Account, len(ac.all)) - copy(cpy, ac.all) - return cpy -} - -func (ac *addrCache) 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) { - ac.mu.Lock() - defer ac.mu.Unlock() - - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].File >= newAccount.File }) - if i < len(ac.all) && ac.all[i] == newAccount { - return - } - // newAccount is not in the cache. - ac.all = append(ac.all, 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) { - 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) - } else { - ac.byAddr[removed.Address] = ba - } -} - -func removeAccount(slice []Account, elem Account) []Account { - for i := range slice { - if slice[i] == elem { - return append(slice[:i], slice[i+1:]...) - } - } - return slice -} - -// find returns the cached account for address if there is a unique match. -// The exact matching rules are explained by the documentation of Account. -// Callers must hold ac.mu. -func (ac *addrCache) find(a Account) (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 only the basename is specified, complete the path. - if !strings.ContainsRune(a.File, filepath.Separator) { - a.File = filepath.Join(ac.keydir, a.File) - } - for i := range matches { - if matches[i].File == a.File { - return matches[i], nil - } - } - if (a.Address == common.Address{}) { - return Account{}, ErrNoMatch - } - } - switch len(matches) { - case 1: - return matches[0], nil - case 0: - return Account{}, ErrNoMatch - default: - err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]Account, len(matches))} - copy(err.Matches, matches) - return Account{}, err - } -} - -func (ac *addrCache) 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. - } - if ac.throttle == nil { - ac.throttle = time.NewTimer(0) - } else { - select { - case <-ac.throttle.C: - default: - return // The cache was reloaded recently. - } - } - ac.watcher.start() - ac.reload() - ac.throttle.Reset(minReloadInterval) -} - -func (ac *addrCache) close() { - ac.mu.Lock() - ac.watcher.close() - if ac.throttle != nil { - ac.throttle.Stop() - } - ac.mu.Unlock() -} - -// reload caches addresses of existing accounts. -// Callers must hold ac.mu. -func (ac *addrCache) reload() { - accounts, err := ac.scan() - if err != nil && glog.V(logger.Debug) { - glog.Errorf("can't load keys: %v", err) - } - ac.all = accounts - sort.Sort(ac.all) - for k := range ac.byAddr { - delete(ac.byAddr, k) - } - for _, a := range accounts { - ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a) - } - glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all)) -} - -func (ac *addrCache) scan() ([]Account, error) { - files, err := ioutil.ReadDir(ac.keydir) - if err != nil { - return nil, err - } - - var ( - buf = new(bufio.Reader) - addrs []Account - keyJSON struct { - Address string `json:"address"` - } - ) - for _, fi := range files { - path := filepath.Join(ac.keydir, fi.Name()) - if skipKeyFile(fi) { - glog.V(logger.Detail).Infof("ignoring file %s", path) - continue - } - fd, err := os.Open(path) - if err != nil { - glog.V(logger.Detail).Infoln(err) - continue - } - buf.Reset(fd) - // Parse the address. - keyJSON.Address = "" - err = json.NewDecoder(buf).Decode(&keyJSON) - addr := common.HexToAddress(keyJSON.Address) - switch { - case err != nil: - glog.V(logger.Debug).Infof("can't decode key %s: %v", path, err) - 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}) - } - fd.Close() - } - return addrs, err -} - -func skipKeyFile(fi os.FileInfo) bool { - // Skip editor backups and UNIX-style hidden files. - if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { - return true - } - // Skip misc special files, directories (yes, symlinks too). - if fi.IsDir() || fi.Mode()&os.ModeType != 0 { - return true - } - return false -} diff --git a/accounts/addrcache_test.go b/accounts/addrcache_test.go deleted file mode 100644 index e5f08cffc..000000000 --- a/accounts/addrcache_test.go +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "fmt" - "math/rand" - "os" - "path/filepath" - "reflect" - "sort" - "testing" - "time" - - "github.com/cespare/cp" - "github.com/davecgh/go-spew/spew" - "github.com/ethereum/go-ethereum/common" -) - -var ( - cachetestDir, _ = filepath.Abs(filepath.Join("testdata", "keystore")) - cachetestAccounts = []Account{ - { - Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - File: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - }, - { - Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: filepath.Join(cachetestDir, "aaa"), - }, - { - Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - File: filepath.Join(cachetestDir, "zzz"), - }, - } -) - -func TestWatchNewFile(t *testing.T) { - t.Parallel() - - dir, am := tmpManager(t, false) - defer os.RemoveAll(dir) - - // Ensure the watcher is started before adding any files. - am.Accounts() - time.Sleep(200 * time.Millisecond) - - // Move in the files. - wantAccounts := make([]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 { - t.Fatal(err) - } - } - - // am should see the accounts. - var list []Account - for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { - list = am.Accounts() - if reflect.DeepEqual(list, wantAccounts) { - return - } - time.Sleep(d) - } - t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts)) -} - -func TestWatchNoDir(t *testing.T) { - t.Parallel() - - // Create am but not the directory that it watches. - rand.Seed(time.Now().UnixNano()) - dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) - am := NewManager(dir, LightScryptN, LightScryptP) - - list := am.Accounts() - if len(list) > 0 { - t.Error("initial account list not empty:", list) - } - time.Sleep(100 * time.Millisecond) - - // Create the directory and copy a key file into it. - os.MkdirAll(dir, 0700) - defer os.RemoveAll(dir) - file := filepath.Join(dir, "aaa") - if err := cp.CopyFile(file, cachetestAccounts[0].File); err != nil { - t.Fatal(err) - } - - // am should see the account. - wantAccounts := []Account{cachetestAccounts[0]} - wantAccounts[0].File = file - for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { - list = am.Accounts() - if reflect.DeepEqual(list, wantAccounts) { - return - } - time.Sleep(d) - } - t.Errorf("\ngot %v\nwant %v", list, wantAccounts) -} - -func TestCacheInitialReload(t *testing.T) { - cache := newAddrCache(cachetestDir) - accounts := cache.accounts() - if !reflect.DeepEqual(accounts, cachetestAccounts) { - t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) - } -} - -func TestCacheAddDeleteOrder(t *testing.T) { - cache := newAddrCache("testdata/no-such-dir") - cache.watcher.running = true // prevent unexpected reloads - - accounts := []Account{ - { - Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - File: "-309830980", - }, - { - Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - File: "ggg", - }, - { - Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - File: "zzzzzz-the-very-last-one.keyXXX", - }, - { - Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: "SOMETHING.key", - }, - { - Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - File: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", - }, - { - Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: "aaa", - }, - { - Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - File: "zzz", - }, - } - for _, a := range accounts { - cache.add(a) - } - // Add some of them twice to check that they don't get reinserted. - cache.add(accounts[0]) - cache.add(accounts[2]) - - // Check that the account list is sorted by filename. - wantAccounts := make([]Account, len(accounts)) - copy(wantAccounts, accounts) - sort.Sort(accountsByFile(wantAccounts)) - list := cache.accounts() - if !reflect.DeepEqual(list, wantAccounts) { - t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accounts), spew.Sdump(wantAccounts)) - } - for _, a := range accounts { - if !cache.hasAddress(a.Address) { - t.Errorf("expected hasAccount(%x) to return true", a.Address) - } - } - if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { - t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) - } - - // Delete a few keys from the cache. - for i := 0; i < len(accounts); i += 2 { - cache.delete(wantAccounts[i]) - } - cache.delete(Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), File: "something"}) - - // Check content again after deletion. - wantAccountsAfterDelete := []Account{ - wantAccounts[1], - wantAccounts[3], - wantAccounts[5], - } - list = cache.accounts() - if !reflect.DeepEqual(list, wantAccountsAfterDelete) { - t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) - } - for _, a := range wantAccountsAfterDelete { - if !cache.hasAddress(a.Address) { - t.Errorf("expected hasAccount(%x) to return true", a.Address) - } - } - if cache.hasAddress(wantAccounts[0].Address) { - t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) - } -} - -func TestCacheFind(t *testing.T) { - dir := filepath.Join("testdata", "dir") - cache := newAddrCache(dir) - cache.watcher.running = true // prevent unexpected reloads - - accounts := []Account{ - { - Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - File: filepath.Join(dir, "a.key"), - }, - { - Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - File: filepath.Join(dir, "b.key"), - }, - { - Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: filepath.Join(dir, "c.key"), - }, - { - Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - File: filepath.Join(dir, "c2.key"), - }, - } - for _, a := range accounts { - cache.add(a) - } - - nomatchAccount := Account{ - Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - File: filepath.Join(dir, "something"), - } - tests := []struct { - Query Account - WantResult Account - WantError error - }{ - // by address - {Query: Account{Address: accounts[0].Address}, WantResult: accounts[0]}, - // by file - {Query: Account{File: accounts[0].File}, WantResult: accounts[0]}, - // by basename - {Query: Account{File: filepath.Base(accounts[0].File)}, WantResult: accounts[0]}, - // by file and address - {Query: accounts[0], WantResult: accounts[0]}, - // ambiguous address, tie resolved by file - {Query: accounts[2], WantResult: accounts[2]}, - // ambiguous address error - { - Query: Account{Address: accounts[2].Address}, - WantError: &AmbiguousAddrError{ - Addr: accounts[2].Address, - Matches: []Account{accounts[2], accounts[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}, - } - for i, test := range tests { - a, err := cache.find(test.Query) - if !reflect.DeepEqual(err, test.WantError) { - t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) - continue - } - if a != test.WantResult { - t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) - continue - } - } -} diff --git a/accounts/backend.go b/accounts/backend.go new file mode 100644 index 000000000..5f7ac0717 --- /dev/null +++ b/accounts/backend.go @@ -0,0 +1,88 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" +) + +// Backend is an "account provider" that can specify a batch of accounts it can +// sign transactions with and upon request, do so. +type Backend interface { + // Accounts retrieves the list of signing accounts the backend is currently aware of. + Accounts() []Account + + // HasAddress reports whether an account with the given address is present. + HasAddress(addr common.Address) bool + + // SignHash requests the backend to sign the given hash. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the backend requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code o verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignHashWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignHash(acc Account, hash []byte) ([]byte, error) + + // SignTx requests the backend to sign the given transaction. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + // + // If the backend requires additional authentication to sign the request (e.g. + // a password to decrypt the account, or a PIN code o verify the transaction), + // an AuthNeededError instance will be returned, containing infos for the user + // about which fields or actions are needed. The user may retry by providing + // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock + // the account in a keystore). + SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // SignHashWithPassphrase requests the backend to sign the given transaction with + // the given passphrase as extra authentication information. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) + + // SignTxWithPassphrase requests the backend to sign the given transaction, with the + // given passphrase as extra authentication information. + // + // It looks up the account specified either solely via its address contained within, + // or optionally with the aid of any location metadata from the embedded URL field. + SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) + + // TODO(karalabe,fjl): watching and caching needs the Go subscription system + // Watch requests the backend to send a notification to the specified channel whenever + // an new account appears or an existing one disappears. + //Watch(chan AccountEvent) error + + // Unwatch requests the backend stop sending notifications to the given channel. + //Unwatch(chan AccountEvent) error +} + +// TODO(karalabe,fjl): watching and caching needs the Go subscription system +// type AccountEvent struct { +// Account Account +// Added bool +// } diff --git a/accounts/errors.go b/accounts/errors.go new file mode 100644 index 000000000..d6f578b52 --- /dev/null +++ b/accounts/errors.go @@ -0,0 +1,41 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import "fmt" + +// AuthNeededError is returned by backends for signing requests where the user +// is required to provide further authentication before signing can succeed. +// +// This usually means either that a password needs to be supplied, or perhaps a +// one time PIN code displayed by some hardware device. +type AuthNeededError struct { + Needed string // Extra authentication the user needs to provide +} + +// NewAuthNeededError creates a new authentication error with the extra details +// about the needed fields set. +func NewAuthNeededError(needed string) error { + return &AuthNeededError{ + Needed: needed, + } +} + +// Error implements the standard error interfacel. +func (err *AuthNeededError) Error() string { + return fmt.Sprintf("authentication needed: %s", err.Needed) +} diff --git a/accounts/key.go b/accounts/key.go deleted file mode 100644 index dbcb49dcf..000000000 --- a/accounts/key.go +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright 2014 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "bytes" - "crypto/ecdsa" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "strings" - "time" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/secp256k1" - "github.com/pborman/uuid" -) - -const ( - version = 3 -) - -type Key struct { - Id uuid.UUID // Version 4 "random" for unique id not derived from key data - // to simplify lookups we also store the address - Address common.Address - // we only store privkey as pubkey/address can be derived from it - // privkey in this struct is always in plaintext - PrivateKey *ecdsa.PrivateKey -} - -type keyStore interface { - // Loads and decrypts the key from disk. - GetKey(addr common.Address, filename string, auth string) (*Key, error) - // Writes and encrypts the key. - StoreKey(filename string, k *Key, auth string) error - // Joins filename with the key directory unless it is already absolute. - JoinPath(filename string) string -} - -type plainKeyJSON struct { - Address string `json:"address"` - PrivateKey string `json:"privatekey"` - Id string `json:"id"` - Version int `json:"version"` -} - -type encryptedKeyJSONV3 struct { - Address string `json:"address"` - Crypto cryptoJSON `json:"crypto"` - Id string `json:"id"` - Version int `json:"version"` -} - -type encryptedKeyJSONV1 struct { - Address string `json:"address"` - Crypto cryptoJSON `json:"crypto"` - Id string `json:"id"` - Version string `json:"version"` -} - -type cryptoJSON struct { - Cipher string `json:"cipher"` - CipherText string `json:"ciphertext"` - CipherParams cipherparamsJSON `json:"cipherparams"` - KDF string `json:"kdf"` - KDFParams map[string]interface{} `json:"kdfparams"` - MAC string `json:"mac"` -} - -type cipherparamsJSON struct { - IV string `json:"iv"` -} - -type scryptParamsJSON struct { - N int `json:"n"` - R int `json:"r"` - P int `json:"p"` - DkLen int `json:"dklen"` - Salt string `json:"salt"` -} - -func (k *Key) MarshalJSON() (j []byte, err error) { - jStruct := plainKeyJSON{ - hex.EncodeToString(k.Address[:]), - hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)), - k.Id.String(), - version, - } - j, err = json.Marshal(jStruct) - return j, err -} - -func (k *Key) UnmarshalJSON(j []byte) (err error) { - keyJSON := new(plainKeyJSON) - err = json.Unmarshal(j, &keyJSON) - if err != nil { - return err - } - - u := new(uuid.UUID) - *u = uuid.Parse(keyJSON.Id) - k.Id = *u - addr, err := hex.DecodeString(keyJSON.Address) - if err != nil { - return err - } - - privkey, err := hex.DecodeString(keyJSON.PrivateKey) - if err != nil { - return err - } - - k.Address = common.BytesToAddress(addr) - k.PrivateKey = crypto.ToECDSA(privkey) - - return nil -} - -func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { - id := uuid.NewRandom() - key := &Key{ - Id: id, - Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), - PrivateKey: privateKeyECDSA, - } - return key -} - -// NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit -// into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we -// retry until the first byte is 0. -func NewKeyForDirectICAP(rand io.Reader) *Key { - randBytes := make([]byte, 64) - _, err := rand.Read(randBytes) - if err != nil { - panic("key generation: could not read from random source: " + err.Error()) - } - reader := bytes.NewReader(randBytes) - privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), reader) - if err != nil { - panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) - } - key := newKeyFromECDSA(privateKeyECDSA) - if !strings.HasPrefix(key.Address.Hex(), "0x00") { - return NewKeyForDirectICAP(rand) - } - return key -} - -func newKey(rand io.Reader) (*Key, error) { - privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), rand) - if err != nil { - return nil, err - } - return newKeyFromECDSA(privateKeyECDSA), nil -} - -func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, Account, error) { - key, err := newKey(rand) - if err != nil { - return nil, Account{}, err - } - a := Account{Address: key.Address, File: ks.JoinPath(keyFileName(key.Address))} - if err := ks.StoreKey(a.File, key, auth); err != nil { - zeroKey(key.PrivateKey) - return nil, a, err - } - return key, a, err -} - -func writeKeyFile(file string, content []byte) error { - // Create the keystore directory with appropriate permissions - // in case it is not present yet. - const dirPerm = 0700 - if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { - return err - } - // Atomic write: create a temporary hidden file first - // then move it into place. TempFile assigns mode 0600. - f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp") - if err != nil { - return err - } - if _, err := f.Write(content); err != nil { - f.Close() - os.Remove(f.Name()) - return err - } - f.Close() - return os.Rename(f.Name(), file) -} - -// keyFileName implements the naming convention for keyfiles: -// UTC---
-func keyFileName(keyAddr common.Address) string { - ts := time.Now().UTC() - return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) -} - -func toISO8601(t time.Time) string { - var tz string - name, offset := t.Zone() - if name == "UTC" { - tz = "Z" - } else { - tz = fmt.Sprintf("%03d00", offset/3600) - } - return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) -} diff --git a/accounts/key_store_passphrase.go b/accounts/key_store_passphrase.go deleted file mode 100644 index 4a777956d..000000000 --- a/accounts/key_store_passphrase.go +++ /dev/null @@ -1,305 +0,0 @@ -// Copyright 2014 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 . - -/* - -This key store behaves as KeyStorePlain with the difference that -the private key is encrypted and on disk uses another JSON encoding. - -The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition - -*/ - -package accounts - -import ( - "bytes" - "crypto/aes" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io/ioutil" - "path/filepath" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" - "github.com/ethereum/go-ethereum/crypto/randentropy" - "github.com/pborman/uuid" - "golang.org/x/crypto/pbkdf2" - "golang.org/x/crypto/scrypt" -) - -const ( - keyHeaderKDF = "scrypt" - - // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB - // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptN = 1 << 18 - - // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB - // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptP = 1 - - // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB - // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptN = 1 << 12 - - // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB - // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptP = 6 - - scryptR = 8 - scryptDKLen = 32 -) - -type keyStorePassphrase struct { - keysDirPath string - scryptN int - scryptP int -} - -func (ks keyStorePassphrase) GetKey(addr common.Address, filename, auth string) (*Key, error) { - // Load the key from the keystore and decrypt its contents - keyjson, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - key, err := DecryptKey(keyjson, auth) - if err != nil { - return nil, err - } - // Make sure we're really operating on the requested key (no swap attacks) - if key.Address != addr { - return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr) - } - return key, nil -} - -func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { - keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) - if err != nil { - return err - } - return writeKeyFile(filename, keyjson) -} - -func (ks keyStorePassphrase) JoinPath(filename string) string { - if filepath.IsAbs(filename) { - return filename - } else { - return filepath.Join(ks.keysDirPath, filename) - } -} - -// EncryptKey encrypts a key using the specified scrypt parameters into a json -// blob that can be decrypted later on. -func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { - authArray := []byte(auth) - salt := randentropy.GetEntropyCSPRNG(32) - derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen) - if err != nil { - return nil, err - } - encryptKey := derivedKey[:16] - keyBytes0 := crypto.FromECDSA(key.PrivateKey) - keyBytes := common.LeftPadBytes(keyBytes0, 32) - - iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16 - cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv) - if err != nil { - return nil, err - } - mac := crypto.Keccak256(derivedKey[16:32], cipherText) - - scryptParamsJSON := make(map[string]interface{}, 5) - scryptParamsJSON["n"] = scryptN - scryptParamsJSON["r"] = scryptR - scryptParamsJSON["p"] = scryptP - scryptParamsJSON["dklen"] = scryptDKLen - scryptParamsJSON["salt"] = hex.EncodeToString(salt) - - cipherParamsJSON := cipherparamsJSON{ - IV: hex.EncodeToString(iv), - } - - cryptoStruct := cryptoJSON{ - Cipher: "aes-128-ctr", - CipherText: hex.EncodeToString(cipherText), - CipherParams: cipherParamsJSON, - KDF: "scrypt", - KDFParams: scryptParamsJSON, - MAC: hex.EncodeToString(mac), - } - encryptedKeyJSONV3 := encryptedKeyJSONV3{ - hex.EncodeToString(key.Address[:]), - cryptoStruct, - key.Id.String(), - version, - } - return json.Marshal(encryptedKeyJSONV3) -} - -// DecryptKey decrypts a key from a json blob, returning the private key itself. -func DecryptKey(keyjson []byte, auth string) (*Key, error) { - // Parse the json into a simple map to fetch the key version - m := make(map[string]interface{}) - if err := json.Unmarshal(keyjson, &m); err != nil { - return nil, err - } - // Depending on the version try to parse one way or another - var ( - keyBytes, keyId []byte - err error - ) - if version, ok := m["version"].(string); ok && version == "1" { - k := new(encryptedKeyJSONV1) - if err := json.Unmarshal(keyjson, k); err != nil { - return nil, err - } - keyBytes, keyId, err = decryptKeyV1(k, auth) - } else { - k := new(encryptedKeyJSONV3) - if err := json.Unmarshal(keyjson, k); err != nil { - return nil, err - } - keyBytes, keyId, err = decryptKeyV3(k, auth) - } - // Handle any decryption errors and return the key - if err != nil { - return nil, err - } - key := crypto.ToECDSA(keyBytes) - return &Key{ - Id: uuid.UUID(keyId), - Address: crypto.PubkeyToAddress(key.PublicKey), - PrivateKey: key, - }, nil -} - -func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { - if keyProtected.Version != version { - return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) - } - - if keyProtected.Crypto.Cipher != "aes-128-ctr" { - return nil, nil, fmt.Errorf("Cipher not supported: %v", keyProtected.Crypto.Cipher) - } - - keyId = uuid.Parse(keyProtected.Id) - mac, err := hex.DecodeString(keyProtected.Crypto.MAC) - if err != nil { - return nil, nil, err - } - - iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) - if err != nil { - return nil, nil, err - } - - cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) - if err != nil { - return nil, nil, err - } - - derivedKey, err := getKDFKey(keyProtected.Crypto, auth) - if err != nil { - return nil, nil, err - } - - calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) - if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, ErrDecrypt - } - - plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) - if err != nil { - return nil, nil, err - } - return plainText, keyId, err -} - -func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { - keyId = uuid.Parse(keyProtected.Id) - mac, err := hex.DecodeString(keyProtected.Crypto.MAC) - if err != nil { - return nil, nil, err - } - - iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) - if err != nil { - return nil, nil, err - } - - cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) - if err != nil { - return nil, nil, err - } - - derivedKey, err := getKDFKey(keyProtected.Crypto, auth) - if err != nil { - return nil, nil, err - } - - calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) - if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, ErrDecrypt - } - - plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) - if err != nil { - return nil, nil, err - } - return plainText, keyId, err -} - -func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) { - authArray := []byte(auth) - salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) - if err != nil { - return nil, err - } - dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) - - if cryptoJSON.KDF == "scrypt" { - n := ensureInt(cryptoJSON.KDFParams["n"]) - r := ensureInt(cryptoJSON.KDFParams["r"]) - p := ensureInt(cryptoJSON.KDFParams["p"]) - return scrypt.Key(authArray, salt, n, r, p, dkLen) - - } else if cryptoJSON.KDF == "pbkdf2" { - c := ensureInt(cryptoJSON.KDFParams["c"]) - prf := cryptoJSON.KDFParams["prf"].(string) - if prf != "hmac-sha256" { - return nil, fmt.Errorf("Unsupported PBKDF2 PRF: %s", prf) - } - key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) - return key, nil - } - - return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) -} - -// TODO: can we do without this when unmarshalling dynamic JSON? -// why do integers in KDF params end up as float64 and not int after -// unmarshal? -func ensureInt(x interface{}) int { - res, ok := x.(int) - if !ok { - res = int(x.(float64)) - } - return res -} diff --git a/accounts/key_store_passphrase_test.go b/accounts/key_store_passphrase_test.go deleted file mode 100644 index 217393fa5..000000000 --- a/accounts/key_store_passphrase_test.go +++ /dev/null @@ -1,60 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "io/ioutil" - "testing" - - "github.com/ethereum/go-ethereum/common" -) - -const ( - veryLightScryptN = 2 - veryLightScryptP = 1 -) - -// Tests that a json key file can be decrypted and encrypted in multiple rounds. -func TestKeyEncryptDecrypt(t *testing.T) { - keyjson, err := ioutil.ReadFile("testdata/very-light-scrypt.json") - if err != nil { - t.Fatal(err) - } - password := "" - address := common.HexToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") - - // Do a few rounds of decryption and encryption - for i := 0; i < 3; i++ { - // Try a bad password first - if _, err := DecryptKey(keyjson, password+"bad"); err == nil { - t.Errorf("test %d: json key decrypted with bad password", i) - } - // Decrypt with the correct password - key, err := DecryptKey(keyjson, password) - if err != nil { - t.Errorf("test %d: json key failed to decrypt: %v", i, err) - } - if key.Address != address { - t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) - } - // Recrypt with a new password and start over - password += "new data appended" - if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { - t.Errorf("test %d: failed to recrypt key %v", i, err) - } - } -} diff --git a/accounts/key_store_plain.go b/accounts/key_store_plain.go deleted file mode 100644 index 2cbaa94df..000000000 --- a/accounts/key_store_plain.go +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2015 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "encoding/json" - "fmt" - "os" - "path/filepath" - - "github.com/ethereum/go-ethereum/common" -) - -type keyStorePlain struct { - keysDirPath string -} - -func (ks keyStorePlain) GetKey(addr common.Address, filename, auth string) (*Key, error) { - fd, err := os.Open(filename) - if err != nil { - return nil, err - } - defer fd.Close() - key := new(Key) - if err := json.NewDecoder(fd).Decode(key); err != nil { - return nil, err - } - if key.Address != addr { - return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) - } - return key, nil -} - -func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { - content, err := json.Marshal(key) - if err != nil { - return err - } - return writeKeyFile(filename, content) -} - -func (ks keyStorePlain) JoinPath(filename string) string { - if filepath.IsAbs(filename) { - return filename - } else { - return filepath.Join(ks.keysDirPath, filename) - } -} diff --git a/accounts/key_store_test.go b/accounts/key_store_test.go deleted file mode 100644 index d0713caa0..000000000 --- a/accounts/key_store_test.go +++ /dev/null @@ -1,253 +0,0 @@ -// Copyright 2014 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "crypto/rand" - "encoding/hex" - "fmt" - "io/ioutil" - "os" - "reflect" - "strings" - "testing" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/crypto" -) - -func tmpKeyStore(t *testing.T, encrypted bool) (dir string, ks keyStore) { - d, err := ioutil.TempDir("", "geth-keystore-test") - if err != nil { - t.Fatal(err) - } - if encrypted { - ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP} - } else { - ks = &keyStorePlain{d} - } - return d, ks -} - -func TestKeyStorePlain(t *testing.T) { - dir, ks := tmpKeyStore(t, false) - defer os.RemoveAll(dir) - - pass := "" // not used but required by API - k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } - k2, err := ks.GetKey(k1.Address, account.File, pass) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(k1.Address, k2.Address) { - t.Fatal(err) - } - if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - t.Fatal(err) - } -} - -func TestKeyStorePassphrase(t *testing.T) { - dir, ks := tmpKeyStore(t, true) - defer os.RemoveAll(dir) - - pass := "foo" - k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } - k2, err := ks.GetKey(k1.Address, account.File, pass) - if err != nil { - t.Fatal(err) - } - if !reflect.DeepEqual(k1.Address, k2.Address) { - t.Fatal(err) - } - if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { - t.Fatal(err) - } -} - -func TestKeyStorePassphraseDecryptionFail(t *testing.T) { - dir, ks := tmpKeyStore(t, true) - defer os.RemoveAll(dir) - - pass := "foo" - k1, account, err := storeNewKey(ks, rand.Reader, pass) - if err != nil { - t.Fatal(err) - } - if _, err = ks.GetKey(k1.Address, account.File, "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) - defer os.RemoveAll(dir) - - // file content of a presale key file generated with: - // python pyethsaletool.py genwallet - // with password "foo" - fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" - pass := "foo" - account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) - if err != nil { - t.Fatal(err) - } - 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) - } -} - -// Test and utils for the key store tests in the Ethereum JSON tests; -// testdataKeyStoreTests/basic_tests.json -type KeyStoreTestV3 struct { - Json encryptedKeyJSONV3 - Password string - Priv string -} - -type KeyStoreTestV1 struct { - Json encryptedKeyJSONV1 - Password string - Priv string -} - -func TestV3_PBKDF2_1(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t) -} - -func TestV3_PBKDF2_2(t *testing.T) { - t.Parallel() - 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) - 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) - testDecryptV3(tests["evilnonce"], t) -} - -func TestV3_Scrypt_1(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["wikipage_test_vector_scrypt"], t) -} - -func TestV3_Scrypt_2(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) - testDecryptV3(tests["test2"], t) -} - -func TestV1_1(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t) - testDecryptV1(tests["test1"], t) -} - -func TestV1_2(t *testing.T) { - t.Parallel() - ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP} - addr := common.HexToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") - file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" - k, err := ks.GetKey(addr, file, "g") - if err != nil { - t.Fatal(err) - } - privHex := hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)) - expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" - if privHex != expectedHex { - t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) - } -} - -func testDecryptV3(test KeyStoreTestV3, t *testing.T) { - privBytes, _, err := decryptKeyV3(&test.Json, test.Password) - if err != nil { - t.Fatal(err) - } - privHex := hex.EncodeToString(privBytes) - if test.Priv != privHex { - t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) - } -} - -func testDecryptV1(test KeyStoreTestV1, t *testing.T) { - privBytes, _, err := decryptKeyV1(&test.Json, test.Password) - if err != nil { - t.Fatal(err) - } - privHex := hex.EncodeToString(privBytes) - if test.Priv != privHex { - t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) - } -} - -func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 { - tests := make(map[string]KeyStoreTestV3) - err := common.LoadJSON(file, &tests) - if err != nil { - t.Fatal(err) - } - return tests -} - -func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 { - tests := make(map[string]KeyStoreTestV1) - err := common.LoadJSON(file, &tests) - if err != nil { - t.Fatal(err) - } - return tests -} - -func TestKeyForDirectICAP(t *testing.T) { - t.Parallel() - key := NewKeyForDirectICAP(rand.Reader) - if !strings.HasPrefix(key.Address.Hex(), "0x00") { - t.Errorf("Expected first address byte to be zero, have: %s", key.Address.Hex()) - } -} - -func TestV3_31_Byte_Key(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["31_byte_key"], t) -} - -func TestV3_30_Byte_Key(t *testing.T) { - t.Parallel() - tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) - testDecryptV3(tests["30_byte_key"], t) -} diff --git a/accounts/keystore/address_cache.go b/accounts/keystore/address_cache.go new file mode 100644 index 000000000..eb3e3263b --- /dev/null +++ b/accounts/keystore/address_cache.go @@ -0,0 +1,271 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "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" +) + +// Minimum amount of time between cache reloads. This limit applies if the platform does +// not support change notifications. It also applies if the keystore directory does not +// exist yet, the code will attempt to create a watcher at most this often. +const minReloadInterval = 2 * time.Second + +type accountsByFile []accounts.Account + +func (s accountsByFile) Len() int { return len(s) } +func (s accountsByFile) Less(i, j int) bool { return s[i].URL < s[j].URL } +func (s accountsByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +// AmbiguousAddrError is returned when attempting to unlock +// an address for which more than one file exists. +type AmbiguousAddrError struct { + Addr common.Address + Matches []accounts.Account +} + +func (err *AmbiguousAddrError) Error() string { + files := "" + for i, a := range err.Matches { + files += a.URL + if i < len(err.Matches)-1 { + files += ", " + } + } + return fmt.Sprintf("multiple keys match address (%s)", files) +} + +// addressCache is a live index of all accounts in the keystore. +type addressCache struct { + keydir string + watcher *watcher + mu sync.Mutex + all accountsByFile + byAddr map[common.Address][]accounts.Account + throttle *time.Timer +} + +func newAddrCache(keydir string) *addressCache { + ac := &addressCache{ + keydir: keydir, + byAddr: make(map[common.Address][]accounts.Account), + } + ac.watcher = newWatcher(ac) + return ac +} + +func (ac *addressCache) accounts() []accounts.Account { + ac.maybeReload() + ac.mu.Lock() + defer ac.mu.Unlock() + cpy := make([]accounts.Account, len(ac.all)) + copy(cpy, ac.all) + return cpy +} + +func (ac *addressCache) hasAddress(addr common.Address) bool { + ac.maybeReload() + ac.mu.Lock() + defer ac.mu.Unlock() + return len(ac.byAddr[addr]) > 0 +} + +func (ac *addressCache) add(newAccount accounts.Account) { + ac.mu.Lock() + defer ac.mu.Unlock() + + i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL }) + if i < len(ac.all) && ac.all[i] == newAccount { + return + } + // newAccount is not in the cache. + 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 *addressCache) 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) + } else { + ac.byAddr[removed.Address] = ba + } +} + +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:]...) + } + } + return slice +} + +// find returns the cached account for address if there is a unique match. +// The exact matching rules are explained by the documentation of accounts.Account. +// Callers must hold ac.mu. +func (ac *addressCache) find(a accounts.Account) (accounts.Account, error) { + // Limit search to address candidates if possible. + matches := ac.all + if (a.Address != common.Address{}) { + matches = ac.byAddr[a.Address] + } + if a.URL != "" { + // If only the basename is specified, complete the path. + if !strings.ContainsRune(a.URL, filepath.Separator) { + a.URL = filepath.Join(ac.keydir, a.URL) + } + for i := range matches { + if matches[i].URL == a.URL { + return matches[i], nil + } + } + if (a.Address == common.Address{}) { + return accounts.Account{}, ErrNoMatch + } + } + switch len(matches) { + case 1: + return matches[0], nil + case 0: + return accounts.Account{}, ErrNoMatch + default: + err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} + copy(err.Matches, matches) + return accounts.Account{}, err + } +} + +func (ac *addressCache) 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. + } + if ac.throttle == nil { + ac.throttle = time.NewTimer(0) + } else { + select { + case <-ac.throttle.C: + default: + return // The cache was reloaded recently. + } + } + ac.watcher.start() + ac.reload() + ac.throttle.Reset(minReloadInterval) +} + +func (ac *addressCache) close() { + ac.mu.Lock() + ac.watcher.close() + if ac.throttle != nil { + ac.throttle.Stop() + } + ac.mu.Unlock() +} + +// reload caches addresses of existing accounts. +// Callers must hold ac.mu. +func (ac *addressCache) reload() { + accounts, err := ac.scan() + if err != nil && glog.V(logger.Debug) { + glog.Errorf("can't load keys: %v", err) + } + ac.all = accounts + sort.Sort(ac.all) + for k := range ac.byAddr { + delete(ac.byAddr, k) + } + for _, a := range accounts { + ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a) + } + glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all)) +} + +func (ac *addressCache) scan() ([]accounts.Account, error) { + files, err := ioutil.ReadDir(ac.keydir) + if err != nil { + return nil, err + } + + var ( + buf = new(bufio.Reader) + addrs []accounts.Account + keyJSON struct { + Address string `json:"address"` + } + ) + for _, fi := range files { + path := filepath.Join(ac.keydir, fi.Name()) + if skipKeyFile(fi) { + glog.V(logger.Detail).Infof("ignoring file %s", path) + continue + } + fd, err := os.Open(path) + if err != nil { + glog.V(logger.Detail).Infoln(err) + continue + } + buf.Reset(fd) + // Parse the address. + keyJSON.Address = "" + err = json.NewDecoder(buf).Decode(&keyJSON) + addr := common.HexToAddress(keyJSON.Address) + switch { + case err != nil: + glog.V(logger.Debug).Infof("can't decode key %s: %v", path, err) + case (addr == common.Address{}): + glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) + default: + addrs = append(addrs, accounts.Account{Address: addr, URL: path}) + } + fd.Close() + } + return addrs, err +} + +func skipKeyFile(fi os.FileInfo) bool { + // Skip editor backups and UNIX-style hidden files. + if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { + return true + } + // Skip misc special files, directories (yes, symlinks too). + if fi.IsDir() || fi.Mode()&os.ModeType != 0 { + return true + } + return false +} diff --git a/accounts/keystore/address_cache_test.go b/accounts/keystore/address_cache_test.go new file mode 100644 index 000000000..68af74338 --- /dev/null +++ b/accounts/keystore/address_cache_test.go @@ -0,0 +1,284 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "fmt" + "math/rand" + "os" + "path/filepath" + "reflect" + "sort" + "testing" + "time" + + "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 = []accounts.Account{ + { + Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + }, + { + Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: filepath.Join(cachetestDir, "aaa"), + }, + { + Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), + URL: filepath.Join(cachetestDir, "zzz"), + }, + } +) + +func TestWatchNewFile(t *testing.T) { + t.Parallel() + + dir, am := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Ensure the watcher is started before adding any files. + am.Accounts() + time.Sleep(200 * time.Millisecond) + + // Move in the files. + wantAccounts := make([]accounts.Account, len(cachetestAccounts)) + for i := range cachetestAccounts { + a := cachetestAccounts[i] + a.URL = filepath.Join(dir, filepath.Base(a.URL)) + wantAccounts[i] = a + if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { + t.Fatal(err) + } + } + + // am should see the accounts. + var list []accounts.Account + for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { + list = am.Accounts() + if reflect.DeepEqual(list, wantAccounts) { + return + } + time.Sleep(d) + } + t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts)) +} + +func TestWatchNoDir(t *testing.T) { + t.Parallel() + + // Create am but not the directory that it watches. + rand.Seed(time.Now().UnixNano()) + dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) + am := NewKeyStore(dir, LightScryptN, LightScryptP) + + list := am.Accounts() + if len(list) > 0 { + t.Error("initial account list not empty:", list) + } + time.Sleep(100 * time.Millisecond) + + // Create the directory and copy a key file into it. + os.MkdirAll(dir, 0700) + defer os.RemoveAll(dir) + file := filepath.Join(dir, "aaa") + if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { + t.Fatal(err) + } + + // am should see the account. + wantAccounts := []accounts.Account{cachetestAccounts[0]} + wantAccounts[0].URL = file + for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { + list = am.Accounts() + if reflect.DeepEqual(list, wantAccounts) { + return + } + time.Sleep(d) + } + t.Errorf("\ngot %v\nwant %v", list, wantAccounts) +} + +func TestCacheInitialReload(t *testing.T) { + cache := newAddrCache(cachetestDir) + accounts := cache.accounts() + if !reflect.DeepEqual(accounts, cachetestAccounts) { + t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) + } +} + +func TestCacheAddDeleteOrder(t *testing.T) { + cache := newAddrCache("testdata/no-such-dir") + cache.watcher.running = true // prevent unexpected reloads + + accs := []accounts.Account{ + { + Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), + URL: "-309830980", + }, + { + Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), + URL: "ggg", + }, + { + Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), + URL: "zzzzzz-the-very-last-one.keyXXX", + }, + { + Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: "SOMETHING.key", + }, + { + Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + }, + { + Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: "aaa", + }, + { + Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), + URL: "zzz", + }, + } + for _, a := range accs { + cache.add(a) + } + // Add some of them twice to check that they don't get reinserted. + cache.add(accs[0]) + cache.add(accs[2]) + + // Check that the account list is sorted by filename. + wantAccounts := make([]accounts.Account, len(accs)) + copy(wantAccounts, accs) + sort.Sort(accountsByFile(wantAccounts)) + list := cache.accounts() + if !reflect.DeepEqual(list, wantAccounts) { + t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) + } + for _, a := range accs { + if !cache.hasAddress(a.Address) { + t.Errorf("expected hasAccount(%x) to return true", a.Address) + } + } + if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { + t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) + } + + // Delete a few keys from the cache. + for i := 0; i < len(accs); i += 2 { + cache.delete(wantAccounts[i]) + } + cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) + + // Check content again after deletion. + wantAccountsAfterDelete := []accounts.Account{ + wantAccounts[1], + wantAccounts[3], + wantAccounts[5], + } + list = cache.accounts() + if !reflect.DeepEqual(list, wantAccountsAfterDelete) { + t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) + } + for _, a := range wantAccountsAfterDelete { + if !cache.hasAddress(a.Address) { + t.Errorf("expected hasAccount(%x) to return true", a.Address) + } + } + if cache.hasAddress(wantAccounts[0].Address) { + t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) + } +} + +func TestCacheFind(t *testing.T) { + dir := filepath.Join("testdata", "dir") + cache := newAddrCache(dir) + cache.watcher.running = true // prevent unexpected reloads + + accs := []accounts.Account{ + { + Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), + URL: filepath.Join(dir, "a.key"), + }, + { + Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), + URL: filepath.Join(dir, "b.key"), + }, + { + Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: filepath.Join(dir, "c.key"), + }, + { + Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: filepath.Join(dir, "c2.key"), + }, + } + for _, a := range accs { + cache.add(a) + } + + nomatchAccount := accounts.Account{ + Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: filepath.Join(dir, "something"), + } + tests := []struct { + Query accounts.Account + WantResult accounts.Account + WantError error + }{ + // by address + {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, + // by file + {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, + // by basename + {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, + // by file and address + {Query: accs[0], WantResult: accs[0]}, + // ambiguous address, tie resolved by file + {Query: accs[2], WantResult: accs[2]}, + // ambiguous address error + { + Query: accounts.Account{Address: accs[2].Address}, + WantError: &AmbiguousAddrError{ + Addr: accs[2].Address, + Matches: []accounts.Account{accs[2], accs[3]}, + }, + }, + // no match error + {Query: nomatchAccount, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch}, + {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, + } + for i, test := range tests { + a, err := cache.find(test.Query) + if !reflect.DeepEqual(err, test.WantError) { + t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) + continue + } + if a != test.WantResult { + t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) + continue + } + } +} diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go new file mode 100644 index 000000000..1cf5f9366 --- /dev/null +++ b/accounts/keystore/key.go @@ -0,0 +1,230 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "bytes" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "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" + "github.com/pborman/uuid" +) + +const ( + version = 3 +) + +type Key struct { + Id uuid.UUID // Version 4 "random" for unique id not derived from key data + // to simplify lookups we also store the address + Address common.Address + // we only store privkey as pubkey/address can be derived from it + // privkey in this struct is always in plaintext + PrivateKey *ecdsa.PrivateKey +} + +type keyStore interface { + // Loads and decrypts the key from disk. + GetKey(addr common.Address, filename string, auth string) (*Key, error) + // Writes and encrypts the key. + StoreKey(filename string, k *Key, auth string) error + // Joins filename with the key directory unless it is already absolute. + JoinPath(filename string) string +} + +type plainKeyJSON struct { + Address string `json:"address"` + PrivateKey string `json:"privatekey"` + Id string `json:"id"` + Version int `json:"version"` +} + +type encryptedKeyJSONV3 struct { + Address string `json:"address"` + Crypto cryptoJSON `json:"crypto"` + Id string `json:"id"` + Version int `json:"version"` +} + +type encryptedKeyJSONV1 struct { + Address string `json:"address"` + Crypto cryptoJSON `json:"crypto"` + Id string `json:"id"` + Version string `json:"version"` +} + +type cryptoJSON struct { + Cipher string `json:"cipher"` + CipherText string `json:"ciphertext"` + CipherParams cipherparamsJSON `json:"cipherparams"` + KDF string `json:"kdf"` + KDFParams map[string]interface{} `json:"kdfparams"` + MAC string `json:"mac"` +} + +type cipherparamsJSON struct { + IV string `json:"iv"` +} + +type scryptParamsJSON struct { + N int `json:"n"` + R int `json:"r"` + P int `json:"p"` + DkLen int `json:"dklen"` + Salt string `json:"salt"` +} + +func (k *Key) MarshalJSON() (j []byte, err error) { + jStruct := plainKeyJSON{ + hex.EncodeToString(k.Address[:]), + hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)), + k.Id.String(), + version, + } + j, err = json.Marshal(jStruct) + return j, err +} + +func (k *Key) UnmarshalJSON(j []byte) (err error) { + keyJSON := new(plainKeyJSON) + err = json.Unmarshal(j, &keyJSON) + if err != nil { + return err + } + + u := new(uuid.UUID) + *u = uuid.Parse(keyJSON.Id) + k.Id = *u + addr, err := hex.DecodeString(keyJSON.Address) + if err != nil { + return err + } + + privkey, err := hex.DecodeString(keyJSON.PrivateKey) + if err != nil { + return err + } + + k.Address = common.BytesToAddress(addr) + k.PrivateKey = crypto.ToECDSA(privkey) + + return nil +} + +func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { + id := uuid.NewRandom() + key := &Key{ + Id: id, + Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), + PrivateKey: privateKeyECDSA, + } + return key +} + +// NewKeyForDirectICAP generates a key whose address fits into < 155 bits so it can fit +// into the Direct ICAP spec. for simplicity and easier compatibility with other libs, we +// retry until the first byte is 0. +func NewKeyForDirectICAP(rand io.Reader) *Key { + randBytes := make([]byte, 64) + _, err := rand.Read(randBytes) + if err != nil { + panic("key generation: could not read from random source: " + err.Error()) + } + reader := bytes.NewReader(randBytes) + privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), reader) + if err != nil { + panic("key generation: ecdsa.GenerateKey failed: " + err.Error()) + } + key := newKeyFromECDSA(privateKeyECDSA) + if !strings.HasPrefix(key.Address.Hex(), "0x00") { + return NewKeyForDirectICAP(rand) + } + return key +} + +func newKey(rand io.Reader) (*Key, error) { + privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), rand) + if err != nil { + return nil, err + } + return newKeyFromECDSA(privateKeyECDSA), nil +} + +func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Account, error) { + key, err := newKey(rand) + if err != nil { + return nil, accounts.Account{}, err + } + a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))} + if err := ks.StoreKey(a.URL, key, auth); err != nil { + zeroKey(key.PrivateKey) + return nil, a, err + } + return key, a, err +} + +func writeKeyFile(file string, content []byte) error { + // Create the keystore directory with appropriate permissions + // in case it is not present yet. + const dirPerm = 0700 + if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { + return err + } + // Atomic write: create a temporary hidden file first + // then move it into place. TempFile assigns mode 0600. + f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp") + if err != nil { + return err + } + if _, err := f.Write(content); err != nil { + f.Close() + os.Remove(f.Name()) + return err + } + f.Close() + return os.Rename(f.Name(), file) +} + +// keyFileName implements the naming convention for keyfiles: +// UTC---
+func keyFileName(keyAddr common.Address) string { + ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) +} + +func toISO8601(t time.Time) string { + var tz string + name, offset := t.Zone() + if name == "UTC" { + tz = "Z" + } else { + tz = fmt.Sprintf("%03d00", offset/3600) + } + return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) +} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go new file mode 100644 index 000000000..d125f7d62 --- /dev/null +++ b/accounts/keystore/keystore.go @@ -0,0 +1,362 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package keystore implements encrypted storage of secp256k1 private keys. +// +// Keys are stored as encrypted JSON files according to the Web3 Secret Storage specification. +// See https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition for more information. +package keystore + +import ( + "crypto/ecdsa" + crand "crypto/rand" + "errors" + "fmt" + "math/big" + "os" + "path/filepath" + "reflect" + "runtime" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +var ( + ErrNeedPasswordOrUnlock = accounts.NewAuthNeededError("password or unlock") + ErrNoMatch = errors.New("no key for given address or file") + ErrDecrypt = errors.New("could not decrypt key with given passphrase") +) + +// BackendType can be used to query the account manager for encrypted keystores. +var BackendType = reflect.TypeOf(new(KeyStore)) + +// KeyStore manages a key storage directory on disk. +type KeyStore struct { + cache *addressCache + keyStore keyStore + mu sync.RWMutex + unlocked map[common.Address]*unlocked +} + +type unlocked struct { + *Key + abort chan struct{} +} + +// NewKeyStore creates a keystore for the given directory. +func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { + keydir, _ = filepath.Abs(keydir) + ks := &KeyStore{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} + ks.init(keydir) + return ks +} + +// NewPlaintextKeyStore creates a keystore for the given directory. +// Deprecated: Use NewKeyStore. +func NewPlaintextKeyStore(keydir string) *KeyStore { + keydir, _ = filepath.Abs(keydir) + ks := &KeyStore{keyStore: &keyStorePlain{keydir}} + ks.init(keydir) + return ks +} + +func (ks *KeyStore) init(keydir string) { + ks.unlocked = make(map[common.Address]*unlocked) + ks.cache = newAddrCache(keydir) + // TODO: In order for this finalizer to work, there must be no references + // to ks. addressCache doesn't keep a reference but unlocked keys do, + // so the finalizer will not trigger until all timed unlocks have expired. + runtime.SetFinalizer(ks, func(m *KeyStore) { + m.cache.close() + }) +} + +// HasAddress reports whether a key with the given address is present. +func (ks *KeyStore) HasAddress(addr common.Address) bool { + return ks.cache.hasAddress(addr) +} + +// Accounts returns all key files present in the directory. +func (ks *KeyStore) Accounts() []accounts.Account { + return ks.cache.accounts() +} + +// Delete deletes the key matched by account if the passphrase is correct. +// If the account contains no filename, the address must match a unique key. +func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { + // Decrypting the key isn't really necessary, but we do + // it anyway to check the password and zero out the key + // immediately afterwards. + a, key, err := ks.getDecryptedKey(a, passphrase) + if key != nil { + zeroKey(key.PrivateKey) + } + if err != nil { + return err + } + // The order is crucial here. The key is dropped from the + // cache after the file is gone so that a reload happening in + // between won't insert it into the cache again. + err = os.Remove(a.URL) + if err == nil { + ks.cache.delete(a) + } + return err +} + +// SignHash calculates a ECDSA signature for the given hash. The produced +// signature is in the [R || S || V] format where V is 0 or 1. +func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrNeedPasswordOrUnlock + } + // Sign the hash using plain ECDSA operations + return crypto.Sign(hash, unlockedKey.PrivateKey) +} + +// SignTx signs the given transaction with the requested account. +func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrNeedPasswordOrUnlock + } + // Depending on the presence of the chain ID, sign with EIP155 or homestead + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey) +} + +// SignHashWithPassphrase signs hash if the private key matching the given address +// can be decrypted with the given passphrase. The produced signature is in the +// [R || S || V] format where V is 0 or 1. +func (ks *KeyStore) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + defer zeroKey(key.PrivateKey) + return crypto.Sign(hash, key.PrivateKey) +} + +// SignTxWithPassphrase signs the transaction if the private key matching the +// given address can be decrypted with the given passphrase. +func (ks *KeyStore) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + defer zeroKey(key.PrivateKey) + + // Depending on the presence of the chain ID, sign with EIP155 or homestead + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey) +} + +// Unlock unlocks the given account indefinitely. +func (ks *KeyStore) Unlock(a accounts.Account, passphrase string) error { + return ks.TimedUnlock(a, passphrase, 0) +} + +// Lock removes the private key with the given address from memory. +func (ks *KeyStore) Lock(addr common.Address) error { + ks.mu.Lock() + if unl, found := ks.unlocked[addr]; found { + ks.mu.Unlock() + ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) + } else { + ks.mu.Unlock() + } + return nil +} + +// TimedUnlock unlocks the given account with the passphrase. The account +// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account +// until the program exits. The account must match a unique key file. +// +// If the account address is already unlocked for a duration, TimedUnlock extends or +// shortens the active unlock timeout. If the address was previously unlocked +// indefinitely the timeout is not altered. +func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + + ks.mu.Lock() + defer ks.mu.Unlock() + u, found := ks.unlocked[a.Address] + if found { + if u.abort == nil { + // The address was unlocked indefinitely, so unlocking + // it with a timeout would be confusing. + zeroKey(key.PrivateKey) + return nil + } else { + // Terminate the expire goroutine and replace it below. + close(u.abort) + } + } + if timeout > 0 { + u = &unlocked{Key: key, abort: make(chan struct{})} + go ks.expire(a.Address, u, timeout) + } else { + u = &unlocked{Key: key} + } + ks.unlocked[a.Address] = u + return nil +} + +// Find resolves the given account into a unique entry in the keystore. +func (ks *KeyStore) Find(a accounts.Account) (accounts.Account, error) { + ks.cache.maybeReload() + ks.cache.mu.Lock() + a, err := ks.cache.find(a) + ks.cache.mu.Unlock() + return a, err +} + +func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { + a, err := ks.Find(a) + if err != nil { + return a, nil, err + } + key, err := ks.keyStore.GetKey(a.Address, a.URL, auth) + return a, key, err +} + +func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Duration) { + t := time.NewTimer(timeout) + defer t.Stop() + select { + case <-u.abort: + // just quit + case <-t.C: + ks.mu.Lock() + // only drop if it's still the same key instance that dropLater + // was launched with. we can check that using pointer equality + // because the map stores a new pointer every time the key is + // unlocked. + if ks.unlocked[addr] == u { + zeroKey(u.PrivateKey) + delete(ks.unlocked, addr) + } + ks.mu.Unlock() + } +} + +// NewAccount generates a new key and stores it into the key directory, +// encrypting it with the passphrase. +func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { + _, account, err := storeNewKey(ks.keyStore, crand.Reader, passphrase) + if err != nil { + return accounts.Account{}, err + } + // Add the account to the cache immediately rather + // than waiting for file system notifications to pick it up. + ks.cache.add(account) + return account, nil +} + +// Export exports as a JSON key, encrypted with newPassphrase. +func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + var N, P int + if store, ok := ks.keyStore.(*keyStorePassphrase); ok { + N, P = store.scryptN, store.scryptP + } else { + N, P = StandardScryptN, StandardScryptP + } + return EncryptKey(key, newPassphrase, N, P) +} + +// Import stores the given encrypted JSON key into the key directory. +func (ks *KeyStore) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { + key, err := DecryptKey(keyJSON, passphrase) + if key != nil && key.PrivateKey != nil { + defer zeroKey(key.PrivateKey) + } + if err != nil { + return accounts.Account{}, err + } + return ks.importKey(key, newPassphrase) +} + +// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase. +func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { + key := newKeyFromECDSA(priv) + if ks.cache.hasAddress(key.Address) { + return accounts.Account{}, fmt.Errorf("account already exists") + } + + return ks.importKey(key, passphrase) +} + +func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { + a := accounts.Account{Address: key.Address, URL: ks.keyStore.JoinPath(keyFileName(key.Address))} + if err := ks.keyStore.StoreKey(a.URL, key, passphrase); err != nil { + return accounts.Account{}, err + } + ks.cache.add(a) + return a, nil +} + +// Update changes the passphrase of an existing account. +func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + return ks.keyStore.StoreKey(a.URL, key, newPassphrase) +} + +// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores +// a key file in the key directory. The key file is encrypted with the same passphrase. +func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { + a, _, err := importPreSaleKey(ks.keyStore, keyJSON, passphrase) + if err != nil { + return a, err + } + ks.cache.add(a) + return a, nil +} + +// zeroKey zeroes a private key in memory. +func zeroKey(k *ecdsa.PrivateKey) { + b := k.D.Bits() + for i := range b { + b[i] = 0 + } +} diff --git a/accounts/keystore/keystore_passphrase.go b/accounts/keystore/keystore_passphrase.go new file mode 100644 index 000000000..8ef510fcf --- /dev/null +++ b/accounts/keystore/keystore_passphrase.go @@ -0,0 +1,305 @@ +// Copyright 2014 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 . + +/* + +This key store behaves as KeyStorePlain with the difference that +the private key is encrypted and on disk uses another JSON encoding. + +The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition + +*/ + +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/randentropy" + "github.com/pborman/uuid" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" +) + +const ( + keyHeaderKDF = "scrypt" + + // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptN = 1 << 18 + + // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptP = 1 + + // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptN = 1 << 12 + + // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptP = 6 + + scryptR = 8 + scryptDKLen = 32 +) + +type keyStorePassphrase struct { + keysDirPath string + scryptN int + scryptP int +} + +func (ks keyStorePassphrase) GetKey(addr common.Address, filename, auth string) (*Key, error) { + // Load the key from the keystore and decrypt its contents + keyjson, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + key, err := DecryptKey(keyjson, auth) + if err != nil { + return nil, err + } + // Make sure we're really operating on the requested key (no swap attacks) + if key.Address != addr { + return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, addr) + } + return key, nil +} + +func (ks keyStorePassphrase) StoreKey(filename string, key *Key, auth string) error { + keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) + if err != nil { + return err + } + return writeKeyFile(filename, keyjson) +} + +func (ks keyStorePassphrase) JoinPath(filename string) string { + if filepath.IsAbs(filename) { + return filename + } else { + return filepath.Join(ks.keysDirPath, filename) + } +} + +// EncryptKey encrypts a key using the specified scrypt parameters into a json +// blob that can be decrypted later on. +func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { + authArray := []byte(auth) + salt := randentropy.GetEntropyCSPRNG(32) + derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return nil, err + } + encryptKey := derivedKey[:16] + keyBytes0 := crypto.FromECDSA(key.PrivateKey) + keyBytes := common.LeftPadBytes(keyBytes0, 32) + + iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16 + cipherText, err := aesCTRXOR(encryptKey, keyBytes, iv) + if err != nil { + return nil, err + } + mac := crypto.Keccak256(derivedKey[16:32], cipherText) + + scryptParamsJSON := make(map[string]interface{}, 5) + scryptParamsJSON["n"] = scryptN + scryptParamsJSON["r"] = scryptR + scryptParamsJSON["p"] = scryptP + scryptParamsJSON["dklen"] = scryptDKLen + scryptParamsJSON["salt"] = hex.EncodeToString(salt) + + cipherParamsJSON := cipherparamsJSON{ + IV: hex.EncodeToString(iv), + } + + cryptoStruct := cryptoJSON{ + Cipher: "aes-128-ctr", + CipherText: hex.EncodeToString(cipherText), + CipherParams: cipherParamsJSON, + KDF: "scrypt", + KDFParams: scryptParamsJSON, + MAC: hex.EncodeToString(mac), + } + encryptedKeyJSONV3 := encryptedKeyJSONV3{ + hex.EncodeToString(key.Address[:]), + cryptoStruct, + key.Id.String(), + version, + } + return json.Marshal(encryptedKeyJSONV3) +} + +// DecryptKey decrypts a key from a json blob, returning the private key itself. +func DecryptKey(keyjson []byte, auth string) (*Key, error) { + // Parse the json into a simple map to fetch the key version + m := make(map[string]interface{}) + if err := json.Unmarshal(keyjson, &m); err != nil { + return nil, err + } + // Depending on the version try to parse one way or another + var ( + keyBytes, keyId []byte + err error + ) + if version, ok := m["version"].(string); ok && version == "1" { + k := new(encryptedKeyJSONV1) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV1(k, auth) + } else { + k := new(encryptedKeyJSONV3) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV3(k, auth) + } + // Handle any decryption errors and return the key + if err != nil { + return nil, err + } + key := crypto.ToECDSA(keyBytes) + return &Key{ + Id: uuid.UUID(keyId), + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: key, + }, nil +} + +func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byte, keyId []byte, err error) { + if keyProtected.Version != version { + return nil, nil, fmt.Errorf("Version not supported: %v", keyProtected.Version) + } + + if keyProtected.Crypto.Cipher != "aes-128-ctr" { + return nil, nil, fmt.Errorf("Cipher not supported: %v", keyProtected.Crypto.Cipher) + } + + keyId = uuid.Parse(keyProtected.Id) + mac, err := hex.DecodeString(keyProtected.Crypto.MAC) + if err != nil { + return nil, nil, err + } + + iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) + if err != nil { + return nil, nil, err + } + + cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) + if err != nil { + return nil, nil, err + } + + derivedKey, err := getKDFKey(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, nil, ErrDecrypt + } + + plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { + keyId = uuid.Parse(keyProtected.Id) + mac, err := hex.DecodeString(keyProtected.Crypto.MAC) + if err != nil { + return nil, nil, err + } + + iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) + if err != nil { + return nil, nil, err + } + + cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) + if err != nil { + return nil, nil, err + } + + derivedKey, err := getKDFKey(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, nil, ErrDecrypt + } + + plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) { + authArray := []byte(auth) + salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) + if err != nil { + return nil, err + } + dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) + + if cryptoJSON.KDF == "scrypt" { + n := ensureInt(cryptoJSON.KDFParams["n"]) + r := ensureInt(cryptoJSON.KDFParams["r"]) + p := ensureInt(cryptoJSON.KDFParams["p"]) + return scrypt.Key(authArray, salt, n, r, p, dkLen) + + } else if cryptoJSON.KDF == "pbkdf2" { + c := ensureInt(cryptoJSON.KDFParams["c"]) + prf := cryptoJSON.KDFParams["prf"].(string) + if prf != "hmac-sha256" { + return nil, fmt.Errorf("Unsupported PBKDF2 PRF: %s", prf) + } + key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) + return key, nil + } + + return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) +} + +// TODO: can we do without this when unmarshalling dynamic JSON? +// why do integers in KDF params end up as float64 and not int after +// unmarshal? +func ensureInt(x interface{}) int { + res, ok := x.(int) + if !ok { + res = int(x.(float64)) + } + return res +} diff --git a/accounts/keystore/keystore_passphrase_test.go b/accounts/keystore/keystore_passphrase_test.go new file mode 100644 index 000000000..086addbc1 --- /dev/null +++ b/accounts/keystore/keystore_passphrase_test.go @@ -0,0 +1,60 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "io/ioutil" + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +const ( + veryLightScryptN = 2 + veryLightScryptP = 1 +) + +// Tests that a json key file can be decrypted and encrypted in multiple rounds. +func TestKeyEncryptDecrypt(t *testing.T) { + keyjson, err := ioutil.ReadFile("testdata/very-light-scrypt.json") + if err != nil { + t.Fatal(err) + } + password := "" + address := common.HexToAddress("45dea0fb0bba44f4fcf290bba71fd57d7117cbb8") + + // Do a few rounds of decryption and encryption + for i := 0; i < 3; i++ { + // Try a bad password first + if _, err := DecryptKey(keyjson, password+"bad"); err == nil { + t.Errorf("test %d: json key decrypted with bad password", i) + } + // Decrypt with the correct password + key, err := DecryptKey(keyjson, password) + if err != nil { + t.Errorf("test %d: json key failed to decrypt: %v", i, err) + } + if key.Address != address { + t.Errorf("test %d: key address mismatch: have %x, want %x", i, key.Address, address) + } + // Recrypt with a new password and start over + password += "new data appended" + if keyjson, err = EncryptKey(key, password, veryLightScryptN, veryLightScryptP); err != nil { + t.Errorf("test %d: failed to recrypt key %v", i, err) + } + } +} diff --git a/accounts/keystore/keystore_plain.go b/accounts/keystore/keystore_plain.go new file mode 100644 index 000000000..b490ca72b --- /dev/null +++ b/accounts/keystore/keystore_plain.go @@ -0,0 +1,62 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/ethereum/go-ethereum/common" +) + +type keyStorePlain struct { + keysDirPath string +} + +func (ks keyStorePlain) GetKey(addr common.Address, filename, auth string) (*Key, error) { + fd, err := os.Open(filename) + if err != nil { + return nil, err + } + defer fd.Close() + key := new(Key) + if err := json.NewDecoder(fd).Decode(key); err != nil { + return nil, err + } + if key.Address != addr { + return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) + } + return key, nil +} + +func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { + content, err := json.Marshal(key) + if err != nil { + return err + } + return writeKeyFile(filename, content) +} + +func (ks keyStorePlain) JoinPath(filename string) string { + if filepath.IsAbs(filename) { + return filename + } else { + return filepath.Join(ks.keysDirPath, filename) + } +} diff --git a/accounts/keystore/keystore_plain_test.go b/accounts/keystore/keystore_plain_test.go new file mode 100644 index 000000000..dcb2ab901 --- /dev/null +++ b/accounts/keystore/keystore_plain_test.go @@ -0,0 +1,253 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "crypto/rand" + "encoding/hex" + "fmt" + "io/ioutil" + "os" + "reflect" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" +) + +func tmpKeyStoreIface(t *testing.T, encrypted bool) (dir string, ks keyStore) { + d, err := ioutil.TempDir("", "geth-keystore-test") + if err != nil { + t.Fatal(err) + } + if encrypted { + ks = &keyStorePassphrase{d, veryLightScryptN, veryLightScryptP} + } else { + ks = &keyStorePlain{d} + } + return d, ks +} + +func TestKeyStorePlain(t *testing.T) { + dir, ks := tmpKeyStoreIface(t, false) + defer os.RemoveAll(dir) + + pass := "" // not used but required by API + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + k2, err := ks.GetKey(k1.Address, account.URL, pass) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.Address, k2.Address) { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + t.Fatal(err) + } +} + +func TestKeyStorePassphrase(t *testing.T) { + dir, ks := tmpKeyStoreIface(t, true) + defer os.RemoveAll(dir) + + pass := "foo" + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + k2, err := ks.GetKey(k1.Address, account.URL, pass) + if err != nil { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.Address, k2.Address) { + t.Fatal(err) + } + if !reflect.DeepEqual(k1.PrivateKey, k2.PrivateKey) { + t.Fatal(err) + } +} + +func TestKeyStorePassphraseDecryptionFail(t *testing.T) { + dir, ks := tmpKeyStoreIface(t, true) + defer os.RemoveAll(dir) + + pass := "foo" + k1, account, err := storeNewKey(ks, rand.Reader, pass) + if err != nil { + t.Fatal(err) + } + if _, err = ks.GetKey(k1.Address, account.URL, "bar"); err != ErrDecrypt { + t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt) + } +} + +func TestImportPreSaleKey(t *testing.T) { + dir, ks := tmpKeyStoreIface(t, true) + defer os.RemoveAll(dir) + + // file content of a presale key file generated with: + // python pyethsaletool.py genwallet + // with password "foo" + fileContent := "{\"encseed\": \"26d87f5f2bf9835f9a47eefae571bc09f9107bb13d54ff12a4ec095d01f83897494cf34f7bed2ed34126ecba9db7b62de56c9d7cd136520a0427bfb11b8954ba7ac39b90d4650d3448e31185affcd74226a68f1e94b1108e6e0a4a91cdd83eba\", \"ethaddr\": \"d4584b5f6229b7be90727b0fc8c6b91bb427821f\", \"email\": \"gustav.simonsson@gmail.com\", \"btcaddr\": \"1EVknXyFC68kKNLkh6YnKzW41svSRoaAcx\"}" + pass := "foo" + account, _, err := importPreSaleKey(ks, []byte(fileContent), pass) + if err != nil { + t.Fatal(err) + } + if account.Address != common.HexToAddress("d4584b5f6229b7be90727b0fc8c6b91bb427821f") { + t.Errorf("imported account has wrong address %x", account.Address) + } + if !strings.HasPrefix(account.URL, dir) { + t.Errorf("imported account file not in keystore directory: %q", account.URL) + } +} + +// Test and utils for the key store tests in the Ethereum JSON tests; +// testdataKeyStoreTests/basic_tests.json +type KeyStoreTestV3 struct { + Json encryptedKeyJSONV3 + Password string + Priv string +} + +type KeyStoreTestV1 struct { + Json encryptedKeyJSONV1 + Password string + Priv string +} + +func TestV3_PBKDF2_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t) +} + +func TestV3_PBKDF2_2(t *testing.T) { + t.Parallel() + 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) + 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) + testDecryptV3(tests["evilnonce"], t) +} + +func TestV3_Scrypt_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["wikipage_test_vector_scrypt"], t) +} + +func TestV3_Scrypt_2(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("../../tests/files/KeyStoreTests/basic_tests.json", t) + testDecryptV3(tests["test2"], t) +} + +func TestV1_1(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t) + testDecryptV1(tests["test1"], t) +} + +func TestV1_2(t *testing.T) { + t.Parallel() + ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP} + addr := common.HexToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") + file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" + k, err := ks.GetKey(addr, file, "g") + if err != nil { + t.Fatal(err) + } + privHex := hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)) + expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" + if privHex != expectedHex { + t.Fatal(fmt.Errorf("Unexpected privkey: %v, expected %v", privHex, expectedHex)) + } +} + +func testDecryptV3(test KeyStoreTestV3, t *testing.T) { + privBytes, _, err := decryptKeyV3(&test.Json, test.Password) + if err != nil { + t.Fatal(err) + } + privHex := hex.EncodeToString(privBytes) + if test.Priv != privHex { + t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) + } +} + +func testDecryptV1(test KeyStoreTestV1, t *testing.T) { + privBytes, _, err := decryptKeyV1(&test.Json, test.Password) + if err != nil { + t.Fatal(err) + } + privHex := hex.EncodeToString(privBytes) + if test.Priv != privHex { + t.Fatal(fmt.Errorf("Decrypted bytes not equal to test, expected %v have %v", test.Priv, privHex)) + } +} + +func loadKeyStoreTestV3(file string, t *testing.T) map[string]KeyStoreTestV3 { + tests := make(map[string]KeyStoreTestV3) + err := common.LoadJSON(file, &tests) + if err != nil { + t.Fatal(err) + } + return tests +} + +func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 { + tests := make(map[string]KeyStoreTestV1) + err := common.LoadJSON(file, &tests) + if err != nil { + t.Fatal(err) + } + return tests +} + +func TestKeyForDirectICAP(t *testing.T) { + t.Parallel() + key := NewKeyForDirectICAP(rand.Reader) + if !strings.HasPrefix(key.Address.Hex(), "0x00") { + t.Errorf("Expected first address byte to be zero, have: %s", key.Address.Hex()) + } +} + +func TestV3_31_Byte_Key(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["31_byte_key"], t) +} + +func TestV3_30_Byte_Key(t *testing.T) { + t.Parallel() + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["30_byte_key"], t) +} diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go new file mode 100644 index 000000000..af2140c31 --- /dev/null +++ b/accounts/keystore/keystore_test.go @@ -0,0 +1,225 @@ +// Copyright 2015 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "io/ioutil" + "os" + "runtime" + "strings" + "testing" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" +) + +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, dir) { + t.Errorf("account file %s doesn't have dir prefix", a.URL) + } + stat, err := os.Stat(a.URL) + 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) { + 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 != ErrNeedPasswordOrUnlock { + t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock 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 != ErrNeedPasswordOrUnlock { + t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock 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 != ErrNeedPasswordOrUnlock { + t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) + } +} + +// This test should fail under -race if signing races the expiration goroutine. +func TestSignRace(t *testing.T) { + dir, 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 == ErrNeedPasswordOrUnlock { + 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 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/presale.go b/accounts/keystore/presale.go new file mode 100644 index 000000000..948320e0a --- /dev/null +++ b/accounts/keystore/presale.go @@ -0,0 +1,137 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "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) (accounts.Account, *Key, error) { + key, err := decryptPreSaleKey(keyJSON, password) + if err != nil { + return accounts.Account{}, nil, err + } + key.Id = uuid.NewRandom() + a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))} + err = keyStore.StoreKey(a.URL, key, password) + return a, key, err +} + +func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) { + preSaleKeyStruct := struct { + EncSeed string + EthAddr string + Email string + BtcAddr string + }{} + err = json.Unmarshal(fileContent, &preSaleKeyStruct) + if err != nil { + return nil, err + } + encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed) + if err != nil { + return nil, errors.New("invalid hex in encSeed") + } + iv := encSeedBytes[:16] + cipherText := encSeedBytes[16:] + /* + See https://github.com/ethereum/pyethsaletool + + pyethsaletool generates the encryption key from password by + 2000 rounds of PBKDF2 with HMAC-SHA-256 using password as salt (:(). + 16 byte key length within PBKDF2 and resulting key is used as AES key + */ + passBytes := []byte(password) + derivedKey := pbkdf2.Key(passBytes, passBytes, 2000, 16, sha256.New) + plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv) + if err != nil { + return nil, err + } + ethPriv := crypto.Keccak256(plainText) + ecKey := crypto.ToECDSA(ethPriv) + key = &Key{ + Id: nil, + Address: crypto.PubkeyToAddress(ecKey.PublicKey), + PrivateKey: ecKey, + } + derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x" + expectedAddr := preSaleKeyStruct.EthAddr + if derivedAddr != expectedAddr { + err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr) + } + return key, err +} + +func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { + // AES-128 is selected due to size of encryptKey. + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(aesBlock, iv) + outText := make([]byte, len(inText)) + stream.XORKeyStream(outText, inText) + return outText, err +} + +func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { + aesBlock, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + decrypter := cipher.NewCBCDecrypter(aesBlock, iv) + paddedPlaintext := make([]byte, len(cipherText)) + decrypter.CryptBlocks(paddedPlaintext, cipherText) + plaintext := pkcs7Unpad(paddedPlaintext) + if plaintext == nil { + return nil, ErrDecrypt + } + return plaintext, err +} + +// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes +func pkcs7Unpad(in []byte) []byte { + if len(in) == 0 { + return nil + } + + padding := in[len(in)-1] + if int(padding) > len(in) || padding > aes.BlockSize { + return nil + } else if padding == 0 { + return nil + } + + for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { + if in[i] != padding { + return nil + } + } + return in[:len(in)-int(padding)] +} diff --git a/accounts/keystore/testdata/dupes/1 b/accounts/keystore/testdata/dupes/1 new file mode 100644 index 000000000..a3868ec6d --- /dev/null +++ b/accounts/keystore/testdata/dupes/1 @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/dupes/2 b/accounts/keystore/testdata/dupes/2 new file mode 100644 index 000000000..a3868ec6d --- /dev/null +++ b/accounts/keystore/testdata/dupes/2 @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/dupes/foo b/accounts/keystore/testdata/dupes/foo new file mode 100644 index 000000000..c57060aea --- /dev/null +++ b/accounts/keystore/testdata/dupes/foo @@ -0,0 +1 @@ +{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/.hiddenfile b/accounts/keystore/testdata/keystore/.hiddenfile new file mode 100644 index 000000000..d91faccde --- /dev/null +++ b/accounts/keystore/testdata/keystore/.hiddenfile @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} diff --git a/accounts/keystore/testdata/keystore/README b/accounts/keystore/testdata/keystore/README new file mode 100644 index 000000000..a5a86f964 --- /dev/null +++ b/accounts/keystore/testdata/keystore/README @@ -0,0 +1,21 @@ +This directory contains accounts for testing. +The passphrase that unlocks them is "foobar". + +The "good" key files which are supposed to be loadable are: + +- File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 + Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +- File: aaa + Address: 0xf466859ead1932d743d622cb74fc058882e8648a +- File: zzz + Address: 0x289d485d9771714cce91d3393d764e1311907acc + +The other files (including this README) are broken in various ways +and should not be picked up by package accounts: + +- File: no-address (missing address field, otherwise same as "aaa") +- File: garbage (file with random data) +- File: empty (file with no content) +- File: swapfile~ (should be skipped) +- File: .hiddenfile (should be skipped) +- File: foo/... (should be skipped because it is a directory) diff --git a/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 new file mode 100644 index 000000000..c57060aea --- /dev/null +++ b/accounts/keystore/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 @@ -0,0 +1 @@ +{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/aaa b/accounts/keystore/testdata/keystore/aaa new file mode 100644 index 000000000..a3868ec6d --- /dev/null +++ b/accounts/keystore/testdata/keystore/aaa @@ -0,0 +1 @@ +{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/empty b/accounts/keystore/testdata/keystore/empty new file mode 100644 index 000000000..e69de29bb diff --git a/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e new file mode 100644 index 000000000..309841e52 --- /dev/null +++ b/accounts/keystore/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e @@ -0,0 +1 @@ +{"address":"fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e","crypto":{"cipher":"aes-128-ctr","ciphertext":"8124d5134aa4a927c79fd852989e4b5419397566f04b0936a1eb1d168c7c68a5","cipherparams":{"iv":"e2febe17176414dd2cda28287947eb2f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"44b415ede89f3bdd6830390a21b78965f571b347a589d1d943029f016c5e8bd5"},"mac":"5e149ff25bfd9dd45746a84bb2bcd2f015f2cbca2b6d25c5de8c29617f71fe5b"},"id":"d6ac5452-2b2c-4d3c-ad80-4bf0327d971c","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/garbage b/accounts/keystore/testdata/keystore/garbage new file mode 100644 index 000000000..ff45091e7 Binary files /dev/null and b/accounts/keystore/testdata/keystore/garbage differ diff --git a/accounts/keystore/testdata/keystore/no-address b/accounts/keystore/testdata/keystore/no-address new file mode 100644 index 000000000..ad51269ea --- /dev/null +++ b/accounts/keystore/testdata/keystore/no-address @@ -0,0 +1 @@ +{"crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/zero b/accounts/keystore/testdata/keystore/zero new file mode 100644 index 000000000..b52617f8a --- /dev/null +++ b/accounts/keystore/testdata/keystore/zero @@ -0,0 +1 @@ +{"address":"0000000000000000000000000000000000000000","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/keystore/zzz b/accounts/keystore/testdata/keystore/zzz new file mode 100644 index 000000000..cfd8a4701 --- /dev/null +++ b/accounts/keystore/testdata/keystore/zzz @@ -0,0 +1 @@ +{"address":"289d485d9771714cce91d3393d764e1311907acc","crypto":{"cipher":"aes-128-ctr","ciphertext":"faf32ca89d286b107f5e6d842802e05263c49b78d46eac74e6109e9a963378ab","cipherparams":{"iv":"558833eec4a665a8c55608d7d503407d"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"d571fff447ffb24314f9513f5160246f09997b857ac71348b73e785aab40dc04"},"mac":"21edb85ff7d0dab1767b9bf498f2c3cb7be7609490756bd32300bb213b59effe"},"id":"3279afcf-55ba-43ff-8997-02dcc46a6525","version":3} \ No newline at end of file diff --git a/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e new file mode 100644 index 000000000..498d8131e --- /dev/null +++ b/accounts/keystore/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e @@ -0,0 +1 @@ +{"address":"cb61d5a9c4896fb9658090b597ef0e7be6f7b67e","Crypto":{"cipher":"aes-128-cbc","ciphertext":"6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0","cipherparams":{"iv":"35337770fc2117994ecdcad026bccff4"},"kdf":"scrypt","kdfparams":{"n":262144,"r":8,"p":1,"dklen":32,"salt":"9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f"},"mac":"3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644","version":"1"},"id":"e25f7c1f-d318-4f29-b62c-687190d4d299","version":"1"} \ No newline at end of file diff --git a/accounts/keystore/testdata/v1_test_vector.json b/accounts/keystore/testdata/v1_test_vector.json new file mode 100644 index 000000000..3d09b55b5 --- /dev/null +++ b/accounts/keystore/testdata/v1_test_vector.json @@ -0,0 +1,28 @@ +{ + "test1": { + "json": { + "Crypto": { + "cipher": "aes-128-cbc", + "cipherparams": { + "iv": "35337770fc2117994ecdcad026bccff4" + }, + "ciphertext": "6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 262144, + "p": 1, + "r": 8, + "salt": "9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f" + }, + "mac": "3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644", + "version": "1" + }, + "address": "cb61d5a9c4896fb9658090b597ef0e7be6f7b67e", + "id": "e25f7c1f-d318-4f29-b62c-687190d4d299", + "version": "1" + }, + "password": "g", + "priv": "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" + } +} diff --git a/accounts/keystore/testdata/v3_test_vector.json b/accounts/keystore/testdata/v3_test_vector.json new file mode 100644 index 000000000..1e7f790c0 --- /dev/null +++ b/accounts/keystore/testdata/v3_test_vector.json @@ -0,0 +1,97 @@ +{ + "wikipage_test_vector_scrypt": { + "json": { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "83dbcc02d8ccb40e466191a123791e0e" + }, + "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 262144, + "r" : 1, + "p" : 8, + "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" + }, + "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + }, + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" + }, + "wikipage_test_vector_pbkdf2": { + "json": { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" + }, + "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", + "kdf" : "pbkdf2", + "kdfparams" : { + "c" : 262144, + "dklen" : 32, + "prf" : "hmac-sha256", + "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" + }, + "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" + }, + "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", + "version" : 3 + }, + "password": "testpassword", + "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" + }, + "31_byte_key": { + "json": { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "e0c41130a323adc1446fc82f724bca2f" + }, + "ciphertext" : "9517cd5bdbe69076f9bf5057248c6c050141e970efa36ce53692d5d59a3984", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 2, + "r" : 8, + "p" : 1, + "salt" : "711f816911c92d649fb4c84b047915679933555030b3552c1212609b38208c63" + }, + "mac" : "d5e116151c6aa71470e67a7d42c9620c75c4d23229847dcc127794f0732b0db5" + }, + "id" : "fecfc4ce-e956-48fd-953b-30f8b52ed66c", + "version" : 3 + }, + "password": "foo", + "priv": "fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35" + }, + "30_byte_key": { + "json": { + "crypto" : { + "cipher" : "aes-128-ctr", + "cipherparams" : { + "iv" : "3ca92af36ad7c2cd92454c59cea5ef00" + }, + "ciphertext" : "108b7d34f3442fc26ab1ab90ca91476ba6bfa8c00975a49ef9051dc675aa", + "kdf" : "scrypt", + "kdfparams" : { + "dklen" : 32, + "n" : 2, + "r" : 8, + "p" : 1, + "salt" : "d0769e608fb86cda848065642a9c6fa046845c928175662b8e356c77f914cd3b" + }, + "mac" : "75d0e6759f7b3cefa319c3be41680ab6beea7d8328653474bd06706d4cc67420" + }, + "id" : "a37e1559-5955-450d-8075-7b8931b392b2", + "version" : 3 + }, + "password": "foo", + "priv": "81c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018" + } +} diff --git a/accounts/keystore/testdata/very-light-scrypt.json b/accounts/keystore/testdata/very-light-scrypt.json new file mode 100644 index 000000000..d23b9b2b9 --- /dev/null +++ b/accounts/keystore/testdata/very-light-scrypt.json @@ -0,0 +1 @@ +{"address":"45dea0fb0bba44f4fcf290bba71fd57d7117cbb8","crypto":{"cipher":"aes-128-ctr","ciphertext":"b87781948a1befd247bff51ef4063f716cf6c2d3481163e9a8f42e1f9bb74145","cipherparams":{"iv":"dc4926b48a105133d2f16b96833abf1e"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":2,"p":1,"r":8,"salt":"004244bbdc51cadda545b1cfa43cff9ed2ae88e08c61f1479dbb45410722f8f0"},"mac":"39990c1684557447940d4c69e06b1b82b2aceacb43f284df65c956daf3046b85"},"id":"ce541d8d-c79b-40f8-9f8c-20f59616faba","version":3} diff --git a/accounts/keystore/watch.go b/accounts/keystore/watch.go new file mode 100644 index 000000000..04a87b12e --- /dev/null +++ b/accounts/keystore/watch.go @@ -0,0 +1,113 @@ +// Copyright 2016 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 . + +// +build darwin,!ios freebsd linux,!arm64 netbsd solaris + +package keystore + +import ( + "time" + + "github.com/ethereum/go-ethereum/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/rjeczalik/notify" +) + +type watcher struct { + ac *addressCache + starting bool + running bool + ev chan notify.EventInfo + quit chan struct{} +} + +func newWatcher(ac *addressCache) *watcher { + return &watcher{ + ac: ac, + ev: make(chan notify.EventInfo, 10), + quit: make(chan struct{}), + } +} + +// starts the watcher loop in the background. +// Start a watcher in the background if that's not already in progress. +// The caller must hold w.ac.mu. +func (w *watcher) start() { + if w.starting || w.running { + return + } + w.starting = true + go w.loop() +} + +func (w *watcher) close() { + close(w.quit) +} + +func (w *watcher) loop() { + defer func() { + w.ac.mu.Lock() + w.running = false + w.starting = false + w.ac.mu.Unlock() + }() + + err := notify.Watch(w.ac.keydir, w.ev, notify.All) + if err != nil { + glog.V(logger.Detail).Infof("can't watch %s: %v", w.ac.keydir, err) + return + } + defer notify.Stop(w.ev) + glog.V(logger.Detail).Infof("now watching %s", w.ac.keydir) + defer glog.V(logger.Detail).Infof("no longer watching %s", w.ac.keydir) + + w.ac.mu.Lock() + w.running = true + w.ac.mu.Unlock() + + // Wait for file system events and reload. + // When an event occurs, the reload call is delayed a bit so that + // multiple events arriving quickly only cause a single reload. + var ( + debounce = time.NewTimer(0) + debounceDuration = 500 * time.Millisecond + inCycle, hadEvent bool + ) + defer debounce.Stop() + for { + select { + case <-w.quit: + return + case <-w.ev: + if !inCycle { + debounce.Reset(debounceDuration) + inCycle = true + } else { + hadEvent = true + } + case <-debounce.C: + w.ac.mu.Lock() + w.ac.reload() + w.ac.mu.Unlock() + if hadEvent { + debounce.Reset(debounceDuration) + inCycle, hadEvent = true, false + } else { + inCycle, hadEvent = false, false + } + } + } +} diff --git a/accounts/keystore/watch_fallback.go b/accounts/keystore/watch_fallback.go new file mode 100644 index 000000000..6412f3b33 --- /dev/null +++ b/accounts/keystore/watch_fallback.go @@ -0,0 +1,28 @@ +// Copyright 2016 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 . + +// +build ios linux,arm64 windows !darwin,!freebsd,!linux,!netbsd,!solaris + +// This is the fallback implementation of directory watching. +// It is used on unsupported platforms. + +package keystore + +type watcher struct{ running bool } + +func newWatcher(*addressCache) *watcher { return new(watcher) } +func (*watcher) start() {} +func (*watcher) close() {} diff --git a/accounts/presale.go b/accounts/presale.go deleted file mode 100644 index f00b4f502..000000000 --- a/accounts/presale.go +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "crypto/aes" - "crypto/cipher" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - - "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) { - key, err := decryptPreSaleKey(keyJSON, password) - if err != nil { - return 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) - return a, key, err -} - -func decryptPreSaleKey(fileContent []byte, password string) (key *Key, err error) { - preSaleKeyStruct := struct { - EncSeed string - EthAddr string - Email string - BtcAddr string - }{} - err = json.Unmarshal(fileContent, &preSaleKeyStruct) - if err != nil { - return nil, err - } - encSeedBytes, err := hex.DecodeString(preSaleKeyStruct.EncSeed) - if err != nil { - return nil, errors.New("invalid hex in encSeed") - } - iv := encSeedBytes[:16] - cipherText := encSeedBytes[16:] - /* - See https://github.com/ethereum/pyethsaletool - - pyethsaletool generates the encryption key from password by - 2000 rounds of PBKDF2 with HMAC-SHA-256 using password as salt (:(). - 16 byte key length within PBKDF2 and resulting key is used as AES key - */ - passBytes := []byte(password) - derivedKey := pbkdf2.Key(passBytes, passBytes, 2000, 16, sha256.New) - plainText, err := aesCBCDecrypt(derivedKey, cipherText, iv) - if err != nil { - return nil, err - } - ethPriv := crypto.Keccak256(plainText) - ecKey := crypto.ToECDSA(ethPriv) - key = &Key{ - Id: nil, - Address: crypto.PubkeyToAddress(ecKey.PublicKey), - PrivateKey: ecKey, - } - derivedAddr := hex.EncodeToString(key.Address.Bytes()) // needed because .Hex() gives leading "0x" - expectedAddr := preSaleKeyStruct.EthAddr - if derivedAddr != expectedAddr { - err = fmt.Errorf("decrypted addr '%s' not equal to expected addr '%s'", derivedAddr, expectedAddr) - } - return key, err -} - -func aesCTRXOR(key, inText, iv []byte) ([]byte, error) { - // AES-128 is selected due to size of encryptKey. - aesBlock, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - stream := cipher.NewCTR(aesBlock, iv) - outText := make([]byte, len(inText)) - stream.XORKeyStream(outText, inText) - return outText, err -} - -func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { - aesBlock, err := aes.NewCipher(key) - if err != nil { - return nil, err - } - decrypter := cipher.NewCBCDecrypter(aesBlock, iv) - paddedPlaintext := make([]byte, len(cipherText)) - decrypter.CryptBlocks(paddedPlaintext, cipherText) - plaintext := pkcs7Unpad(paddedPlaintext) - if plaintext == nil { - return nil, ErrDecrypt - } - return plaintext, err -} - -// From https://leanpub.com/gocrypto/read#leanpub-auto-block-cipher-modes -func pkcs7Unpad(in []byte) []byte { - if len(in) == 0 { - return nil - } - - padding := in[len(in)-1] - if int(padding) > len(in) || padding > aes.BlockSize { - return nil - } else if padding == 0 { - return nil - } - - for i := len(in) - 1; i > len(in)-int(padding)-1; i-- { - if in[i] != padding { - return nil - } - } - return in[:len(in)-int(padding)] -} diff --git a/accounts/testdata/dupes/1 b/accounts/testdata/dupes/1 deleted file mode 100644 index a3868ec6d..000000000 --- a/accounts/testdata/dupes/1 +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/dupes/2 b/accounts/testdata/dupes/2 deleted file mode 100644 index a3868ec6d..000000000 --- a/accounts/testdata/dupes/2 +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/dupes/foo b/accounts/testdata/dupes/foo deleted file mode 100644 index c57060aea..000000000 --- a/accounts/testdata/dupes/foo +++ /dev/null @@ -1 +0,0 @@ -{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/.hiddenfile b/accounts/testdata/keystore/.hiddenfile deleted file mode 100644 index d91faccde..000000000 --- a/accounts/testdata/keystore/.hiddenfile +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} diff --git a/accounts/testdata/keystore/README b/accounts/testdata/keystore/README deleted file mode 100644 index a5a86f964..000000000 --- a/accounts/testdata/keystore/README +++ /dev/null @@ -1,21 +0,0 @@ -This directory contains accounts for testing. -The passphrase that unlocks them is "foobar". - -The "good" key files which are supposed to be loadable are: - -- File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 - Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8 -- File: aaa - Address: 0xf466859ead1932d743d622cb74fc058882e8648a -- File: zzz - Address: 0x289d485d9771714cce91d3393d764e1311907acc - -The other files (including this README) are broken in various ways -and should not be picked up by package accounts: - -- File: no-address (missing address field, otherwise same as "aaa") -- File: garbage (file with random data) -- File: empty (file with no content) -- File: swapfile~ (should be skipped) -- File: .hiddenfile (should be skipped) -- File: foo/... (should be skipped because it is a directory) diff --git a/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 deleted file mode 100644 index c57060aea..000000000 --- a/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 +++ /dev/null @@ -1 +0,0 @@ -{"address":"7ef5a6135f1fd6a02593eedc869c6d41d934aef8","crypto":{"cipher":"aes-128-ctr","ciphertext":"1d0839166e7a15b9c1333fc865d69858b22df26815ccf601b28219b6192974e1","cipherparams":{"iv":"8df6caa7ff1b00c4e871f002cb7921ed"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"e5e6ef3f4ea695f496b643ebd3f75c0aa58ef4070e90c80c5d3fb0241bf1595c"},"mac":"6d16dfde774845e4585357f24bce530528bc69f4f84e1e22880d34fa45c273e5"},"id":"950077c7-71e3-4c44-a4a1-143919141ed4","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/aaa b/accounts/testdata/keystore/aaa deleted file mode 100644 index a3868ec6d..000000000 --- a/accounts/testdata/keystore/aaa +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/empty b/accounts/testdata/keystore/empty deleted file mode 100644 index e69de29bb..000000000 diff --git a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e deleted file mode 100644 index 309841e52..000000000 --- a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e +++ /dev/null @@ -1 +0,0 @@ -{"address":"fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e","crypto":{"cipher":"aes-128-ctr","ciphertext":"8124d5134aa4a927c79fd852989e4b5419397566f04b0936a1eb1d168c7c68a5","cipherparams":{"iv":"e2febe17176414dd2cda28287947eb2f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"44b415ede89f3bdd6830390a21b78965f571b347a589d1d943029f016c5e8bd5"},"mac":"5e149ff25bfd9dd45746a84bb2bcd2f015f2cbca2b6d25c5de8c29617f71fe5b"},"id":"d6ac5452-2b2c-4d3c-ad80-4bf0327d971c","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/garbage b/accounts/testdata/keystore/garbage deleted file mode 100644 index ff45091e7..000000000 Binary files a/accounts/testdata/keystore/garbage and /dev/null differ diff --git a/accounts/testdata/keystore/no-address b/accounts/testdata/keystore/no-address deleted file mode 100644 index ad51269ea..000000000 --- a/accounts/testdata/keystore/no-address +++ /dev/null @@ -1 +0,0 @@ -{"crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/zero b/accounts/testdata/keystore/zero deleted file mode 100644 index b52617f8a..000000000 --- a/accounts/testdata/keystore/zero +++ /dev/null @@ -1 +0,0 @@ -{"address":"0000000000000000000000000000000000000000","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/zzz b/accounts/testdata/keystore/zzz deleted file mode 100644 index cfd8a4701..000000000 --- a/accounts/testdata/keystore/zzz +++ /dev/null @@ -1 +0,0 @@ -{"address":"289d485d9771714cce91d3393d764e1311907acc","crypto":{"cipher":"aes-128-ctr","ciphertext":"faf32ca89d286b107f5e6d842802e05263c49b78d46eac74e6109e9a963378ab","cipherparams":{"iv":"558833eec4a665a8c55608d7d503407d"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"d571fff447ffb24314f9513f5160246f09997b857ac71348b73e785aab40dc04"},"mac":"21edb85ff7d0dab1767b9bf498f2c3cb7be7609490756bd32300bb213b59effe"},"id":"3279afcf-55ba-43ff-8997-02dcc46a6525","version":3} \ No newline at end of file diff --git a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e b/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e deleted file mode 100644 index 498d8131e..000000000 --- a/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e +++ /dev/null @@ -1 +0,0 @@ -{"address":"cb61d5a9c4896fb9658090b597ef0e7be6f7b67e","Crypto":{"cipher":"aes-128-cbc","ciphertext":"6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0","cipherparams":{"iv":"35337770fc2117994ecdcad026bccff4"},"kdf":"scrypt","kdfparams":{"n":262144,"r":8,"p":1,"dklen":32,"salt":"9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f"},"mac":"3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644","version":"1"},"id":"e25f7c1f-d318-4f29-b62c-687190d4d299","version":"1"} \ No newline at end of file diff --git a/accounts/testdata/v1_test_vector.json b/accounts/testdata/v1_test_vector.json deleted file mode 100644 index 3d09b55b5..000000000 --- a/accounts/testdata/v1_test_vector.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "test1": { - "json": { - "Crypto": { - "cipher": "aes-128-cbc", - "cipherparams": { - "iv": "35337770fc2117994ecdcad026bccff4" - }, - "ciphertext": "6143d3192db8b66eabd693d9c4e414dcfaee52abda451af79ccf474dafb35f1bfc7ea013aa9d2ee35969a1a2e8d752d0", - "kdf": "scrypt", - "kdfparams": { - "dklen": 32, - "n": 262144, - "p": 1, - "r": 8, - "salt": "9afcddebca541253a2f4053391c673ff9fe23097cd8555d149d929e4ccf1257f" - }, - "mac": "3f3d5af884b17a100b0b3232c0636c230a54dc2ac8d986227219b0dd89197644", - "version": "1" - }, - "address": "cb61d5a9c4896fb9658090b597ef0e7be6f7b67e", - "id": "e25f7c1f-d318-4f29-b62c-687190d4d299", - "version": "1" - }, - "password": "g", - "priv": "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" - } -} diff --git a/accounts/testdata/v3_test_vector.json b/accounts/testdata/v3_test_vector.json deleted file mode 100644 index 1e7f790c0..000000000 --- a/accounts/testdata/v3_test_vector.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "wikipage_test_vector_scrypt": { - "json": { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "83dbcc02d8ccb40e466191a123791e0e" - }, - "ciphertext" : "d172bf743a674da9cdad04534d56926ef8358534d458fffccd4e6ad2fbde479c", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 262144, - "r" : 1, - "p" : 8, - "salt" : "ab0c7876052600dd703518d6fc3fe8984592145b591fc8fb5c6d43190334ba19" - }, - "mac" : "2103ac29920d71da29f15d75b4a16dbe95cfd7ff8faea1056c33131d846e3097" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - }, - "password": "testpassword", - "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" - }, - "wikipage_test_vector_pbkdf2": { - "json": { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "6087dab2f9fdbbfaddc31a909735c1e6" - }, - "ciphertext" : "5318b4d5bcd28de64ee5559e671353e16f075ecae9f99c7a79a38af5f869aa46", - "kdf" : "pbkdf2", - "kdfparams" : { - "c" : 262144, - "dklen" : 32, - "prf" : "hmac-sha256", - "salt" : "ae3cd4e7013836a3df6bd7241b12db061dbe2c6785853cce422d148a624ce0bd" - }, - "mac" : "517ead924a9d0dc3124507e3393d175ce3ff7c1e96529c6c555ce9e51205e9b2" - }, - "id" : "3198bc9c-6672-5ab3-d995-4942343ae5b6", - "version" : 3 - }, - "password": "testpassword", - "priv": "7a28b5ba57c53603b0b07b56bba752f7784bf506fa95edc395f5cf6c7514fe9d" - }, - "31_byte_key": { - "json": { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "e0c41130a323adc1446fc82f724bca2f" - }, - "ciphertext" : "9517cd5bdbe69076f9bf5057248c6c050141e970efa36ce53692d5d59a3984", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 2, - "r" : 8, - "p" : 1, - "salt" : "711f816911c92d649fb4c84b047915679933555030b3552c1212609b38208c63" - }, - "mac" : "d5e116151c6aa71470e67a7d42c9620c75c4d23229847dcc127794f0732b0db5" - }, - "id" : "fecfc4ce-e956-48fd-953b-30f8b52ed66c", - "version" : 3 - }, - "password": "foo", - "priv": "fa7b3db73dc7dfdf8c5fbdb796d741e4488628c41fc4febd9160a866ba0f35" - }, - "30_byte_key": { - "json": { - "crypto" : { - "cipher" : "aes-128-ctr", - "cipherparams" : { - "iv" : "3ca92af36ad7c2cd92454c59cea5ef00" - }, - "ciphertext" : "108b7d34f3442fc26ab1ab90ca91476ba6bfa8c00975a49ef9051dc675aa", - "kdf" : "scrypt", - "kdfparams" : { - "dklen" : 32, - "n" : 2, - "r" : 8, - "p" : 1, - "salt" : "d0769e608fb86cda848065642a9c6fa046845c928175662b8e356c77f914cd3b" - }, - "mac" : "75d0e6759f7b3cefa319c3be41680ab6beea7d8328653474bd06706d4cc67420" - }, - "id" : "a37e1559-5955-450d-8075-7b8931b392b2", - "version" : 3 - }, - "password": "foo", - "priv": "81c29e8142bb6a81bef5a92bda7a8328a5c85bb2f9542e76f9b0f94fc018" - } -} diff --git a/accounts/testdata/very-light-scrypt.json b/accounts/testdata/very-light-scrypt.json deleted file mode 100644 index d23b9b2b9..000000000 --- a/accounts/testdata/very-light-scrypt.json +++ /dev/null @@ -1 +0,0 @@ -{"address":"45dea0fb0bba44f4fcf290bba71fd57d7117cbb8","crypto":{"cipher":"aes-128-ctr","ciphertext":"b87781948a1befd247bff51ef4063f716cf6c2d3481163e9a8f42e1f9bb74145","cipherparams":{"iv":"dc4926b48a105133d2f16b96833abf1e"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":2,"p":1,"r":8,"salt":"004244bbdc51cadda545b1cfa43cff9ed2ae88e08c61f1479dbb45410722f8f0"},"mac":"39990c1684557447940d4c69e06b1b82b2aceacb43f284df65c956daf3046b85"},"id":"ce541d8d-c79b-40f8-9f8c-20f59616faba","version":3} diff --git a/accounts/watch.go b/accounts/watch.go deleted file mode 100644 index 472be2df7..000000000 --- a/accounts/watch.go +++ /dev/null @@ -1,113 +0,0 @@ -// Copyright 2016 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 . - -// +build darwin,!ios freebsd linux,!arm64 netbsd solaris - -package accounts - -import ( - "time" - - "github.com/ethereum/go-ethereum/logger" - "github.com/ethereum/go-ethereum/logger/glog" - "github.com/rjeczalik/notify" -) - -type watcher struct { - ac *addrCache - starting bool - running bool - ev chan notify.EventInfo - quit chan struct{} -} - -func newWatcher(ac *addrCache) *watcher { - return &watcher{ - ac: ac, - ev: make(chan notify.EventInfo, 10), - quit: make(chan struct{}), - } -} - -// starts the watcher loop in the background. -// Start a watcher in the background if that's not already in progress. -// The caller must hold w.ac.mu. -func (w *watcher) start() { - if w.starting || w.running { - return - } - w.starting = true - go w.loop() -} - -func (w *watcher) close() { - close(w.quit) -} - -func (w *watcher) loop() { - defer func() { - w.ac.mu.Lock() - w.running = false - w.starting = false - w.ac.mu.Unlock() - }() - - err := notify.Watch(w.ac.keydir, w.ev, notify.All) - if err != nil { - glog.V(logger.Detail).Infof("can't watch %s: %v", w.ac.keydir, err) - return - } - defer notify.Stop(w.ev) - glog.V(logger.Detail).Infof("now watching %s", w.ac.keydir) - defer glog.V(logger.Detail).Infof("no longer watching %s", w.ac.keydir) - - w.ac.mu.Lock() - w.running = true - w.ac.mu.Unlock() - - // Wait for file system events and reload. - // When an event occurs, the reload call is delayed a bit so that - // multiple events arriving quickly only cause a single reload. - var ( - debounce = time.NewTimer(0) - debounceDuration = 500 * time.Millisecond - inCycle, hadEvent bool - ) - defer debounce.Stop() - for { - select { - case <-w.quit: - return - case <-w.ev: - if !inCycle { - debounce.Reset(debounceDuration) - inCycle = true - } else { - hadEvent = true - } - case <-debounce.C: - w.ac.mu.Lock() - w.ac.reload() - w.ac.mu.Unlock() - if hadEvent { - debounce.Reset(debounceDuration) - inCycle, hadEvent = true, false - } else { - inCycle, hadEvent = false, false - } - } - } -} diff --git a/accounts/watch_fallback.go b/accounts/watch_fallback.go deleted file mode 100644 index bf971cb1b..000000000 --- a/accounts/watch_fallback.go +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2016 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 . - -// +build ios linux,arm64 windows !darwin,!freebsd,!linux,!netbsd,!solaris - -// This is the fallback implementation of directory watching. -// It is used on unsupported platforms. - -package accounts - -type watcher struct{ running bool } - -func newWatcher(*addrCache) *watcher { return new(watcher) } -func (*watcher) start() {} -func (*watcher) close() {} -- cgit v1.2.3 From ac2a0e615bd45eab81cd793cde5a4eef2a8581c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 23 Jan 2017 09:58:05 +0200 Subject: accounts/usbwallet: initial support for Ledger wallets --- accounts/usbwallet/ledger.go | 648 ++++++++++++++++++++++++++++++++++++++ accounts/usbwallet/ledger_test.go | 75 +++++ accounts/usbwallet/usb.go | 27 ++ 3 files changed, 750 insertions(+) create mode 100644 accounts/usbwallet/ledger.go create mode 100644 accounts/usbwallet/ledger_test.go create mode 100644 accounts/usbwallet/usb.go (limited to 'accounts') diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go new file mode 100644 index 000000000..55b313df9 --- /dev/null +++ b/accounts/usbwallet/ledger.go @@ -0,0 +1,648 @@ +// 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 . + +// 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 + +package usbwallet + +import ( + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io" + "math/big" + "strconv" + "strings" + "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/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rlp" + "github.com/karalabe/gousb/usb" +) + +// 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 +} + +// ledgerDerivationPath is the key derivation parameters used by the wallet. +var ledgerDerivationPath = [4]uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} + +// 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 +) + +// ledgerWallet represents a live USB Ledger hardware wallet. +type ledgerWallet struct { + 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 + + address common.Address // Current address of the wallet (may be zero if Ethereum app offline) + url string // Textual URL uniquely identifying this wallet + version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) +} + +// LedgerHub is a USB hardware wallet interface that can find and handle Ledger +// wallets. +type LedgerHub struct { + ctx *usb.Context // Context interfacing with a libusb instance + + wallets map[uint16]*ledgerWallet // Apparent Ledger wallets (some may be inactive) + accounts []accounts.Account // List of active Ledger accounts + index map[common.Address]uint16 // Set of addresses with active wallets + + 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, + wallets: make(map[uint16]*ledgerWallet), + index: make(map[common.Address]uint16), + quit: make(chan chan error), + } + go hub.watch() + return hub, nil +} + +// Accounts retrieves the live of accounts currently known by the Ledger hub. +func (hub *LedgerHub) Accounts() []accounts.Account { + hub.lock.RLock() + defer hub.lock.RUnlock() + + cpy := make([]accounts.Account, len(hub.accounts)) + copy(cpy, hub.accounts) + return cpy +} + +// HasAddress reports whether an account with the given address is present. +func (hub *LedgerHub) HasAddress(addr common.Address) bool { + hub.lock.RLock() + defer hub.lock.RUnlock() + + _, known := hub.index[addr] + return known +} + +// SignHash is not supported for Ledger wallets, so this method will always +// return an error. +func (hub *LedgerHub) SignHash(acc accounts.Account, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTx 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 (hub *LedgerHub) SignTx(acc accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + hub.lock.RLock() + defer hub.lock.RUnlock() + + // If the account contains the device URL, flatten it to make sure + var id uint16 + if acc.URL != "" { + if parts := strings.Split(acc.URL, "."); len(parts) == 2 { + bus, busErr := strconv.Atoi(parts[0]) + addr, addrErr := strconv.Atoi(parts[1]) + + if busErr == nil && addrErr == nil { + id = uint16(bus)<<8 + uint16(addr) + } + } + } + // If the id is still zero, URL is either missing or bad, resolve + if id == 0 { + var ok bool + if id, ok = hub.index[acc.Address]; !ok { + return nil, accounts.ErrUnknownAccount + } + } + // Retrieve the wallet associated with the URL + wallet, ok := hub.wallets[id] + if !ok { + return nil, accounts.ErrUnknownAccount + } + // Ensure the wallet is capable of signing the given transaction + if chainID != nil && wallet.version[0] <= 1 && wallet.version[1] <= 0 && wallet.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", + wallet.version[0], wallet.version[1], wallet.version[2]) + } + return wallet.sign(tx, chainID) +} + +// SignHashWithPassphrase is not supported for Ledger wallets, so this method +// will always return an error. +func (hub *LedgerHub) SignHashWithPassphrase(acc accounts.Account, passphrase string, hash []byte) ([]byte, error) { + return nil, accounts.ErrNotSupported +} + +// SignTxWithPassphrase requests the backend to sign the given transaction, with the +// given passphrase as extra authentication information. Since the Ledger does not +// support this feature, it will just silently ignore the passphrase. +func (hub *LedgerHub) SignTxWithPassphrase(acc accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return hub.SignTx(acc, tx, chainID) +} + +// Close terminates the usb watching for Ledger wallets and returns when it +// successfully terminated. +func (hub *LedgerHub) Close() error { + // Terminate the USB scanner + errc := make(chan error) + hub.quit <- errc + err := <-errc + + // Release the USB interface and return + hub.ctx.Close() + return err +} + +// watch starts watching the local machine's USB ports for the connection or +// disconnection of Ledger devices. +func (hub *LedgerHub) watch() { + for { + // Rescan the USB ports for devices newly added or removed + hub.rescan() + + // Sleep for a certain amount of time or until terminated + select { + case errc := <-hub.quit: + errc <- nil + return + case <-time.After(time.Second): + } + } +} + +// rescan searches the USB ports for attached Ledger hardware wallets. +func (hub *LedgerHub) rescan() { + hub.lock.Lock() + defer hub.lock.Unlock() + + // Iterate over all attached devices and fetch those seemingly Ledger + present := make(map[uint16]bool) + devices, _ := hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { + // Discard all devices not advertizing as Ledger + ledger := false + for _, id := range ledgerDeviceIDs { + if desc.Vendor == id.Vendor && desc.Product == id.Product { + ledger = true + } + } + if !ledger { + return false + } + // If we have a Ledger, mark as still present, or open as new + id := uint16(desc.Bus)<<8 + uint16(desc.Address) + if _, known := hub.wallets[id]; known { + // Track it's presence, but don't open again + present[id] = true + return false + } + // New Ledger device, open it for communication + return true + }) + // Drop any tracker wallet which disconnected + for id, wallet := range hub.wallets { + if !present[id] { + if wallet.address == (common.Address{}) { + glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] disconnected", wallet.device.Bus, wallet.device.Address) + } else { + // A live account disconnected, remove it from the tracked accounts + for i, account := range hub.accounts { + if account.Address == wallet.address && account.URL == wallet.url { + hub.accounts = append(hub.accounts[:i], hub.accounts[i+1:]...) + break + } + } + delete(hub.index, wallet.address) + + glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] v%d.%d.%d disconnected: %s", wallet.device.Bus, wallet.device.Address, + wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) + } + delete(hub.wallets, id) + wallet.device.Close() + } + } + // Start tracking all wallets which newly appeared + var err error + for _, device := range devices { + // Make sure the alleged device has the correct IO endpoints + wallet := &ledgerWallet{ + device: device, + url: fmt.Sprintf("%03d.%03d", device.Bus, device.Address), + } + 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 != "" { + glog.V(logger.Debug).Infof("ledger wallet [%s] deemed invalid: %s", wallet.url, invalid) + device.Close() + continue + } + // Open the input and output endpoints to the device + wallet.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 { + glog.V(logger.Debug).Infof("ledger wallet [%s] input open failed: %v", wallet.url, err) + device.Close() + continue + } + wallet.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 { + glog.V(logger.Debug).Infof("ledger wallet [%s] output open failed: %v", wallet.url, err) + device.Close() + continue + } + // Start tracking the device as a probably Ledger wallet + id := uint16(device.Bus)<<8 + uint16(device.Address) + hub.wallets[id] = wallet + + if wallet.resolveVersion() != nil || wallet.resolveAddress() != nil { + glog.V(logger.Info).Infof("ledger wallet [%s] connected, Ethereum app not started", wallet.url) + } else { + hub.accounts = append(hub.accounts, accounts.Account{ + Address: wallet.address, + URL: wallet.url, + }) + hub.index[wallet.address] = id + + glog.V(logger.Info).Infof("ledger wallet [%s] v%d.%d.%d connected: %s", wallet.url, + wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) + } + } +} + +// resolveVersion retrieves the current version of the Ethereum wallet app running +// on the Ledger wallet and caches it for future reference. +// +// 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 (wallet *ledgerWallet) resolveVersion() error { + // Send the request and wait for the response + reply, err := wallet.exchange(ledgerOpGetConfiguration, 0, 0, nil) + if err != nil { + return err + } + if len(reply) != 4 { + return errors.New("reply not of correct size") + } + // Cache the version for future reference + copy(wallet.version[:], reply[1:]) + return nil +} + +// resolveAddress retrieves the currently active Ethereum address from a Ledger +// wallet and caches it for future reference. +// +// 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 (wallet *ledgerWallet) resolveAddress() error { + // Flatten the derivation path into the Ledger request + path := make([]byte, 1+4*len(ledgerDerivationPath)) + path[0] = byte(len(ledgerDerivationPath)) + for i, component := range ledgerDerivationPath { + binary.BigEndian.PutUint32(path[1+4*i:], component) + } + // Send the request and wait for the response + reply, err := wallet.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) + if err != nil { + return err + } + // Discard the public key, we don't need that for now + if len(reply) < 1 || len(reply) < 1+int(reply[0]) { + return 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 errors.New("reply lacks address entry") + } + hexstr := reply[1 : 1+int(reply[0])] + + // Decode the hex sting into an Ethereum address and return + hex.Decode(wallet.address[:], hexstr) + return nil +} + +// sign 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 (wallet *ledgerWallet) sign(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) { wallet.device.ReadTimeout = old }(wallet.device.ReadTimeout) + wallet.device.ReadTimeout = time.Minute + + // Flatten the derivation path into the Ledger request + path := make([]byte, 1+4*len(ledgerDerivationPath)) + path[0] = byte(len(ledgerDerivationPath)) + for i, component := range ledgerDerivationPath { + 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 = wallet.exchange(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]-34)/2 - byte(chainID.Uint64()) + } + // 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 != wallet.address { + glog.V(logger.Error).Infof("Ledger signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) + return nil, fmt.Errorf("signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) + } + return signed, nil +} + +// exchange 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 (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { + // Construct the message payload, possibly split into multiple chunks + var chunks [][]byte + for left := data; len(left) > 0 || len(chunks) == 0; { + // Create the chunk header + var chunk []byte + + if len(chunks) == 0 { + // The first chunk encodes the length and all the opcodes + chunk = []byte{0x00, 0x00, 0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))} + binary.BigEndian.PutUint16(chunk, uint16(5+len(data))) + } + // Append the data blob to the end of the chunk + space := 64 - len(chunk) - 5 // 5 == header size + if len(left) > space { + chunks, left = append(chunks, append(chunk, left[:space]...)), left[space:] + continue + } + chunks, left = append(chunks, append(chunk, left...)), nil + } + // Stream all the chunks to the device + for i, chunk := range chunks { + // Construct the new message to stream + header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended + binary.BigEndian.PutUint16(header[3:], uint16(i)) + + msg := append(header, chunk...) + + // Send over to the device + if glog.V(logger.Core) { + glog.Infof("-> %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, msg) + } + if _, err := wallet.input.Write(msg); err != nil { + return nil, err + } + } + // Stream the reply back from the wallet in 64 byte chunks + var reply []byte + for { + // Read the next chunk from the Ledger wallet + chunk := make([]byte, 64) + if _, err := io.ReadFull(wallet.output, chunk); err != nil { + return nil, err + } + if glog.V(logger.Core) { + glog.Infof("<- %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, chunk) + } + // Make sure the transport header matches + if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { + return nil, fmt.Errorf("invalid reply header: %x", chunk[:3]) + } + // If it's the first chunk, retrieve the total message length + if chunk[3] == 0x00 && chunk[4] == 0x00 { + reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7]))) + chunk = chunk[7:] + } else { + chunk = chunk[5:] + } + // Append to the reply and stop when filled up + if left := cap(reply) - len(reply); left > len(chunk) { + reply = append(reply, chunk...) + } else { + reply = append(reply, chunk[:left]...) + break + } + } + return reply[:len(reply)-2], nil +} diff --git a/accounts/usbwallet/ledger_test.go b/accounts/usbwallet/ledger_test.go new file mode 100644 index 000000000..aeab2f0f0 --- /dev/null +++ b/accounts/usbwallet/ledger_test.go @@ -0,0 +1,75 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package usbwallet + +/* +func TestLedgerHub(t *testing.T) { + glog.SetV(6) + glog.SetToStderr(true) + + // Create a USB hub watching for Ledger devices + hub, err := NewLedgerHub() + if err != nil { + t.Fatalf("Failed to create Ledger hub: %v", err) + } + defer hub.Close() + + // Wait for events :P + time.Sleep(time.Minute) +} +*/ +/* +func TestLedger(t *testing.T) { + // Create a USB context to access devices through + ctx, err := usb.NewContext() + defer ctx.Close() + ctx.Debug(6) + + // List all of the Ledger wallets + wallets, err := findLedgerWallets(ctx) + if err != nil { + t.Fatalf("Failed to list Ledger wallets: %v", err) + } + // Retrieve the address from every one of them + for _, wallet := range wallets { + // Retrieve the version of the wallet app + ver, err := wallet.Version() + if err != nil { + t.Fatalf("Failed to retrieve wallet version: %v", err) + } + fmt.Printf("Ledger version: %s\n", ver) + + // Retrieve the address of the wallet + addr, err := wallet.Address() + if err != nil { + t.Fatalf("Failed to retrieve wallet address: %v", err) + } + fmt.Printf("Ledger address: %x\n", addr) + + // Try to sign a transaction with the wallet + unsigned := types.NewTransaction(1, common.HexToAddress("0xbabababababababababababababababababababa"), common.Ether, big.NewInt(20000), common.Shannon, nil) + signed, err := wallet.Sign(unsigned) + if err != nil { + t.Fatalf("Failed to sign transactions: %v", err) + } + signer, err := types.Sender(types.NewEIP155Signer(big.NewInt(1)), signed) + if err != nil { + t.Fatalf("Failed to recover signer: %v", err) + } + fmt.Printf("Ledger signature by: %x\n", signer) + } +}*/ diff --git a/accounts/usbwallet/usb.go b/accounts/usbwallet/usb.go new file mode 100644 index 000000000..550f05ba6 --- /dev/null +++ b/accounts/usbwallet/usb.go @@ -0,0 +1,27 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +// Package 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 +} -- cgit v1.2.3 From 1ecf99bd0f332247df3a8976ae94e1b63b9d8a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 27 Jan 2017 11:38:35 +0200 Subject: accounts/usbwallet: skip support on iOS altogether --- accounts/usbwallet/ledger.go | 2 ++ accounts/usbwallet/ledger_test.go | 2 ++ accounts/usbwallet/usb.go | 27 -------------------------- accounts/usbwallet/usbwallet.go | 29 ++++++++++++++++++++++++++++ accounts/usbwallet/usbwallet_ios.go | 38 +++++++++++++++++++++++++++++++++++++ 5 files changed, 71 insertions(+), 27 deletions(-) delete mode 100644 accounts/usbwallet/usb.go create mode 100644 accounts/usbwallet/usbwallet.go create mode 100644 accounts/usbwallet/usbwallet_ios.go (limited to 'accounts') diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go index 55b313df9..8f502c58e 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger.go @@ -18,6 +18,8 @@ // 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 ( diff --git a/accounts/usbwallet/ledger_test.go b/accounts/usbwallet/ledger_test.go index aeab2f0f0..16a1e0b3f 100644 --- a/accounts/usbwallet/ledger_test.go +++ b/accounts/usbwallet/ledger_test.go @@ -14,6 +14,8 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . +// +build !ios + package usbwallet /* diff --git a/accounts/usbwallet/usb.go b/accounts/usbwallet/usb.go deleted file mode 100644 index 550f05ba6..000000000 --- a/accounts/usbwallet/usb.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -// Package 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.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 . + +// +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 . + +// 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 +} -- cgit v1.2.3 From 470b79385bcde3b4c999daf6a8debb00072ad967 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 27 Jan 2017 13:23:24 +0200 Subject: accounts/usbwallet: support Ledger app version <1.0.2 --- accounts/usbwallet/ledger.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'accounts') diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go index 8f502c58e..cc64ccb42 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger.go @@ -330,9 +330,13 @@ func (hub *LedgerHub) rescan() { id := uint16(device.Bus)<<8 + uint16(device.Address) hub.wallets[id] = wallet - if wallet.resolveVersion() != nil || wallet.resolveAddress() != nil { + if wallet.resolveAddress() != nil { glog.V(logger.Info).Infof("ledger wallet [%s] connected, Ethereum app not started", wallet.url) } else { + // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 + if wallet.resolveVersion() != nil { + wallet.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 + } hub.accounts = append(hub.accounts, accounts.Account{ Address: wallet.address, URL: wallet.url, -- cgit v1.2.3 From b3c0e9d3ccb0bb326646aea47dda391a9552b122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Mon, 30 Jan 2017 12:21:39 +0200 Subject: accounts/usbwallet: two phase Ledger refreshes to avoid Windows bug --- accounts/usbwallet/ledger.go | 49 ++++++++++++++++++++++---------------------- 1 file changed, 24 insertions(+), 25 deletions(-) (limited to 'accounts') diff --git a/accounts/usbwallet/ledger.go b/accounts/usbwallet/ledger.go index cc64ccb42..9917d0d70 100644 --- a/accounts/usbwallet/ledger.go +++ b/accounts/usbwallet/ledger.go @@ -234,32 +234,11 @@ func (hub *LedgerHub) rescan() { hub.lock.Lock() defer hub.lock.Unlock() - // Iterate over all attached devices and fetch those seemingly Ledger - present := make(map[uint16]bool) - devices, _ := hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { - // Discard all devices not advertizing as Ledger - ledger := false - for _, id := range ledgerDeviceIDs { - if desc.Vendor == id.Vendor && desc.Product == id.Product { - ledger = true - } - } - if !ledger { - return false - } - // If we have a Ledger, mark as still present, or open as new - id := uint16(desc.Bus)<<8 + uint16(desc.Address) - if _, known := hub.wallets[id]; known { - // Track it's presence, but don't open again - present[id] = true - return false - } - // New Ledger device, open it for communication - return true - }) - // Drop any tracker wallet which disconnected + // Iterate over all connected Ledger devices and do a heartbeat test for id, wallet := range hub.wallets { - if !present[id] { + // If the device doesn't respond (io error on Windows, no device on Linux), drop + if err := wallet.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { + // Wallet disconnected or at least in a useless state if wallet.address == (common.Address{}) { glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] disconnected", wallet.device.Bus, wallet.device.Address) } else { @@ -279,6 +258,26 @@ func (hub *LedgerHub) rescan() { wallet.device.Close() } } + // Iterate over all attached devices and fetch those seemingly Ledger + devices, _ := hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { + // Discard all devices not advertizing as Ledger + ledger := false + for _, id := range ledgerDeviceIDs { + if desc.Vendor == id.Vendor && desc.Product == id.Product { + ledger = true + } + } + if !ledger { + return false + } + // If we have an already known Ledger, skip opening it + id := uint16(desc.Bus)<<8 + uint16(desc.Address) + if _, known := hub.wallets[id]; known { + return false + } + // New Ledger device, open it for communication + return true + }) // Start tracking all wallets which newly appeared var err error for _, device := range devices { -- cgit v1.2.3 From fad5eb0a87abfc12812647344a26de8a43830182 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Tue, 7 Feb 2017 12:47:34 +0200 Subject: accounts, cmd, eth, internal, miner, node: wallets and HD APIs --- accounts/accounts.go | 256 ++++++------ accounts/backend.go | 88 ----- accounts/errors.go | 29 +- accounts/keystore/account_cache.go | 292 ++++++++++++++ accounts/keystore/account_cache_test.go | 311 +++++++++++++++ accounts/keystore/address_cache.go | 271 ------------- accounts/keystore/address_cache_test.go | 284 -------------- accounts/keystore/keystore.go | 178 +++++++-- accounts/keystore/keystore_test.go | 156 +++++++- accounts/keystore/keystore_wallet.go | 133 +++++++ accounts/keystore/watch.go | 4 +- accounts/keystore/watch_fallback.go | 2 +- accounts/manager.go | 194 +++++++++ accounts/usbwallet/ledger.go | 653 ------------------------------- accounts/usbwallet/ledger_hub.go | 205 ++++++++++ accounts/usbwallet/ledger_wallet.go | 670 ++++++++++++++++++++++++++++++++ 16 files changed, 2250 insertions(+), 1476 deletions(-) delete mode 100644 accounts/backend.go create mode 100644 accounts/keystore/account_cache.go create mode 100644 accounts/keystore/account_cache_test.go delete mode 100644 accounts/keystore/address_cache.go delete mode 100644 accounts/keystore/address_cache_test.go create mode 100644 accounts/keystore/keystore_wallet.go create mode 100644 accounts/manager.go delete mode 100644 accounts/usbwallet/ledger.go create mode 100644 accounts/usbwallet/ledger_hub.go create mode 100644 accounts/usbwallet/ledger_wallet.go (limited to 'accounts') diff --git a/accounts/accounts.go b/accounts/accounts.go index 234b6e456..b367ee2f4 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -18,162 +18,128 @@ package accounts import ( - "encoding/json" - "errors" "math/big" - "reflect" - "sync" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/event" ) -// ErrUnknownAccount is returned for any requested operation for which no backend -// provides the specified account. -var ErrUnknownAccount = errors.New("unknown account") - -// ErrNotSupported is returned when an operation is requested from an account -// backend that it does not support. -var ErrNotSupported = errors.New("not supported") - -// Account represents a stored key. -// When used as an argument, it selects a unique key to act on. +// Account represents an Ethereum account located at a specific location defined +// by the optional URL field. type Account struct { - Address common.Address // Ethereum account address derived from the key - URL string // Optional resource locator within a backend - backend Backend // Backend where this account originates from -} - -func (acc *Account) MarshalJSON() ([]byte, error) { - return []byte(`"` + acc.Address.Hex() + `"`), nil -} - -func (acc *Account) UnmarshalJSON(raw []byte) error { - return json.Unmarshal(raw, &acc.Address) -} - -// Manager is an overarching account manager that can communicate with various -// backends for signing transactions. -type Manager struct { - backends []Backend // List of currently registered backends (ordered by registration) - index map[reflect.Type]Backend // Set of currently registered backends - lock sync.RWMutex -} - -// NewManager creates a generic account manager to sign transaction via various -// supported backends. -func NewManager(backends ...Backend) *Manager { - am := &Manager{ - backends: backends, - index: make(map[reflect.Type]Backend), - } - for _, backend := range backends { - am.index[reflect.TypeOf(backend)] = backend - } - return am -} - -// Backend retrieves the backend with the given type from the account manager. -func (am *Manager) Backend(backend reflect.Type) Backend { - return am.index[backend] + Address common.Address `json:"address"` // Ethereum account address derived from the key + URL string `json:"url"` // Optional resource locator within a backend } -// Accounts returns all signer accounts registered under this account manager. -func (am *Manager) Accounts() []Account { - am.lock.RLock() - defer am.lock.RUnlock() - - var all []Account - for _, backend := range am.backends { // TODO(karalabe): cache these after subscriptions are in - accounts := backend.Accounts() - for i := 0; i < len(accounts); i++ { - accounts[i].backend = backend - } - all = append(all, accounts...) - } - return all +// Wallet represents a software or hardware wallet that might contain one or more +// accounts (derived from the same seed). +type Wallet interface { + // Type retrieves a textual representation of the type of the wallet. + Type() string + + // 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() string + + // 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 string, pin bool) (Account, error) + + // 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) } -// HasAddress reports whether a key with the given address is present. -func (am *Manager) HasAddress(addr common.Address) bool { - am.lock.RLock() - defer am.lock.RUnlock() - - for _, backend := range am.backends { - if backend.HasAddress(addr) { - return true - } - } - return false +// 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 } -// SignHash requests the account manager to get the hash signed with an arbitrary -// signing backend holding the authorization for the specified account. -func (am *Manager) SignHash(acc Account, hash []byte) ([]byte, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignHash(acc, hash) -} - -// SignTx requests the account manager to get the transaction signed with an -// arbitrary signing backend holding the authorization for the specified account. -func (am *Manager) SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignTx(acc, tx, chainID) -} - -// SignHashWithPassphrase requests the account manager to get the hash signed with -// an arbitrary signing backend holding the authorization for the specified account. -func (am *Manager) SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignHashWithPassphrase(acc, passphrase, hash) -} - -// SignTxWithPassphrase requests the account manager to get the transaction signed -// with an arbitrary signing backend holding the authorization for the specified -// account. -func (am *Manager) SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - am.lock.RLock() - defer am.lock.RUnlock() - - if err := am.ensureBackend(&acc); err != nil { - return nil, err - } - return acc.backend.SignTxWithPassphrase(acc, passphrase, tx, chainID) -} - -// ensureBackend ensures that the account has a correctly set backend and that -// it is still alive. -// -// Please note, this method assumes the manager lock is held! -func (am *Manager) ensureBackend(acc *Account) error { - // If we have a backend, make sure it's still live - if acc.backend != nil { - if _, exists := am.index[reflect.TypeOf(acc.backend)]; !exists { - return ErrUnknownAccount - } - return nil - } - // If we don't have a known backend, look up one that can service it - for _, backend := range am.backends { - if backend.HasAddress(acc.Address) { // TODO(karalabe): this assumes unique addresses per backend - acc.backend = backend - return nil - } - } - return ErrUnknownAccount +// 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/backend.go b/accounts/backend.go deleted file mode 100644 index 5f7ac0717..000000000 --- a/accounts/backend.go +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2017 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package accounts - -import ( - "math/big" - - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/core/types" -) - -// Backend is an "account provider" that can specify a batch of accounts it can -// sign transactions with and upon request, do so. -type Backend interface { - // Accounts retrieves the list of signing accounts the backend is currently aware of. - Accounts() []Account - - // HasAddress reports whether an account with the given address is present. - HasAddress(addr common.Address) bool - - // SignHash requests the backend to sign the given hash. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the backend requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code o verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignHashWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). - SignHash(acc Account, hash []byte) ([]byte, error) - - // SignTx requests the backend to sign the given transaction. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - // - // If the backend requires additional authentication to sign the request (e.g. - // a password to decrypt the account, or a PIN code o verify the transaction), - // an AuthNeededError instance will be returned, containing infos for the user - // about which fields or actions are needed. The user may retry by providing - // the needed details via SignTxWithPassphrase, or by other means (e.g. unlock - // the account in a keystore). - SignTx(acc Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) - - // SignHashWithPassphrase requests the backend to sign the given transaction with - // the given passphrase as extra authentication information. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - SignHashWithPassphrase(acc Account, passphrase string, hash []byte) ([]byte, error) - - // SignTxWithPassphrase requests the backend to sign the given transaction, with the - // given passphrase as extra authentication information. - // - // It looks up the account specified either solely via its address contained within, - // or optionally with the aid of any location metadata from the embedded URL field. - SignTxWithPassphrase(acc Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) - - // TODO(karalabe,fjl): watching and caching needs the Go subscription system - // Watch requests the backend to send a notification to the specified channel whenever - // an new account appears or an existing one disappears. - //Watch(chan AccountEvent) error - - // Unwatch requests the backend stop sending notifications to the given channel. - //Unwatch(chan AccountEvent) error -} - -// TODO(karalabe,fjl): watching and caching needs the Go subscription system -// type AccountEvent struct { -// Account Account -// Added bool -// } diff --git a/accounts/errors.go b/accounts/errors.go index d6f578b52..9ecc1eafd 100644 --- a/accounts/errors.go +++ b/accounts/errors.go @@ -16,7 +16,34 @@ package accounts -import "fmt" +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. diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go new file mode 100644 index 000000000..cc8626afc --- /dev/null +++ b/accounts/keystore/account_cache.go @@ -0,0 +1,292 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "bufio" + "encoding/json" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" + "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" +) + +// Minimum amount of time between cache reloads. This limit applies if the platform does +// not support change notifications. It also applies if the keystore directory does not +// exist yet, the code will attempt to create a watcher at most this often. +const minReloadInterval = 2 * time.Second + +type accountsByURL []accounts.Account + +func (s accountsByURL) Len() int { return len(s) } +func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL } +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 []accounts.Account +} + +func (err *AmbiguousAddrError) Error() string { + files := "" + for i, a := range err.Matches { + files += a.URL + if i < len(err.Matches)-1 { + files += ", " + } + } + return fmt.Sprintf("multiple keys match address (%s)", files) +} + +// accountCache is a live index of all accounts in the keystore. +type accountCache struct { + keydir string + watcher *watcher + mu sync.Mutex + all accountsByURL + byAddr map[common.Address][]accounts.Account + throttle *time.Timer + notify chan struct{} +} + +func newAccountCache(keydir string) (*accountCache, chan struct{}) { + ac := &accountCache{ + keydir: keydir, + byAddr: make(map[common.Address][]accounts.Account), + notify: make(chan struct{}, 1), + } + ac.watcher = newWatcher(ac) + return ac, ac.notify +} + +func (ac *accountCache) accounts() []accounts.Account { + ac.maybeReload() + ac.mu.Lock() + defer ac.mu.Unlock() + cpy := make([]accounts.Account, len(ac.all)) + copy(cpy, ac.all) + return cpy +} + +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 *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].URL >= newAccount.URL }) + if i < len(ac.all) && ac.all[i] == newAccount { + return + } + // newAccount is not in the cache. + 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) + + select { + case ac.notify <- struct{}{}: + default: + } +} + +// note: removed needs to be unique here (i.e. both File and Address must be set). +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) + } else { + ac.byAddr[removed.Address] = ba + } + select { + case ac.notify <- struct{}{}: + default: + } +} + +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:]...) + } + } + return slice +} + +// find returns the cached account for address if there is a unique match. +// The exact matching rules are explained by the documentation of accounts.Account. +// Callers must hold ac.mu. +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.URL != "" { + // If only the basename is specified, complete the path. + if !strings.ContainsRune(a.URL, filepath.Separator) { + a.URL = filepath.Join(ac.keydir, a.URL) + } + for i := range matches { + if matches[i].URL == a.URL { + return matches[i], nil + } + } + if (a.Address == common.Address{}) { + return accounts.Account{}, ErrNoMatch + } + } + switch len(matches) { + case 1: + return matches[0], nil + case 0: + return accounts.Account{}, ErrNoMatch + default: + err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} + copy(err.Matches, matches) + return accounts.Account{}, err + } +} + +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. + } + if ac.throttle == nil { + ac.throttle = time.NewTimer(0) + } else { + select { + case <-ac.throttle.C: + default: + return // The cache was reloaded recently. + } + } + ac.watcher.start() + ac.reload() + ac.throttle.Reset(minReloadInterval) +} + +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 *accountCache) reload() { + accounts, err := ac.scan() + if err != nil && glog.V(logger.Debug) { + glog.Errorf("can't load keys: %v", err) + } + ac.all = accounts + sort.Sort(ac.all) + for k := range ac.byAddr { + delete(ac.byAddr, k) + } + 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 *accountCache) scan() ([]accounts.Account, error) { + files, err := ioutil.ReadDir(ac.keydir) + if err != nil { + return nil, err + } + + var ( + buf = new(bufio.Reader) + addrs []accounts.Account + keyJSON struct { + Address string `json:"address"` + } + ) + for _, fi := range files { + path := filepath.Join(ac.keydir, fi.Name()) + if skipKeyFile(fi) { + glog.V(logger.Detail).Infof("ignoring file %s", path) + continue + } + fd, err := os.Open(path) + if err != nil { + glog.V(logger.Detail).Infoln(err) + continue + } + buf.Reset(fd) + // Parse the address. + keyJSON.Address = "" + err = json.NewDecoder(buf).Decode(&keyJSON) + addr := common.HexToAddress(keyJSON.Address) + switch { + case err != nil: + glog.V(logger.Debug).Infof("can't decode key %s: %v", path, err) + case (addr == common.Address{}): + glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) + default: + addrs = append(addrs, accounts.Account{Address: addr, URL: path}) + } + fd.Close() + } + return addrs, err +} + +func skipKeyFile(fi os.FileInfo) bool { + // Skip editor backups and UNIX-style hidden files. + if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { + return true + } + // Skip misc special files, directories (yes, symlinks too). + if fi.IsDir() || fi.Mode()&os.ModeType != 0 { + return true + } + return false +} diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go new file mode 100644 index 000000000..ea6f7d011 --- /dev/null +++ b/accounts/keystore/account_cache_test.go @@ -0,0 +1,311 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "fmt" + "math/rand" + "os" + "path/filepath" + "reflect" + "sort" + "testing" + "time" + + "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 = []accounts.Account{ + { + Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + }, + { + Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: filepath.Join(cachetestDir, "aaa"), + }, + { + Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), + URL: filepath.Join(cachetestDir, "zzz"), + }, + } +) + +func TestWatchNewFile(t *testing.T) { + t.Parallel() + + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + + // Ensure the watcher is started before adding any files. + ks.Accounts() + time.Sleep(200 * time.Millisecond) + + // Move in the files. + wantAccounts := make([]accounts.Account, len(cachetestAccounts)) + for i := range cachetestAccounts { + a := cachetestAccounts[i] + a.URL = filepath.Join(dir, filepath.Base(a.URL)) + wantAccounts[i] = a + if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { + t.Fatal(err) + } + } + + // ks should see the accounts. + var list []accounts.Account + for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { + 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) + } + t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts)) +} + +func TestWatchNoDir(t *testing.T) { + t.Parallel() + + // 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())) + ks := NewKeyStore(dir, LightScryptN, LightScryptP) + + list := ks.Accounts() + if len(list) > 0 { + t.Error("initial account list not empty:", list) + } + time.Sleep(100 * time.Millisecond) + + // Create the directory and copy a key file into it. + os.MkdirAll(dir, 0700) + defer os.RemoveAll(dir) + file := filepath.Join(dir, "aaa") + if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { + t.Fatal(err) + } + + // ks should see the account. + wantAccounts := []accounts.Account{cachetestAccounts[0]} + wantAccounts[0].URL = file + for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { + 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) + } + t.Errorf("\ngot %v\nwant %v", list, wantAccounts) +} + +func TestCacheInitialReload(t *testing.T) { + cache, _ := newAccountCache(cachetestDir) + accounts := cache.accounts() + if !reflect.DeepEqual(accounts, cachetestAccounts) { + t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) + } +} + +func TestCacheAddDeleteOrder(t *testing.T) { + cache, notify := newAccountCache("testdata/no-such-dir") + cache.watcher.running = true // prevent unexpected reloads + + accs := []accounts.Account{ + { + Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), + URL: "-309830980", + }, + { + Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), + URL: "ggg", + }, + { + Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), + URL: "zzzzzz-the-very-last-one.keyXXX", + }, + { + Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: "SOMETHING.key", + }, + { + Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), + URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", + }, + { + Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: "aaa", + }, + { + Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), + URL: "zzz", + }, + } + for _, a := range accs { + cache.add(a) + } + select { + case <-notify: + default: + t.Fatalf("notifications didn't fire for adding new accounts") + } + // Add some of them twice to check that they don't get reinserted. + cache.add(accs[0]) + cache.add(accs[2]) + + select { + case <-notify: + t.Fatalf("notifications fired for adding existing accounts") + default: + } + // Check that the account list is sorted by filename. + 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(accs), spew.Sdump(wantAccounts)) + } + for _, a := range accs { + if !cache.hasAddress(a.Address) { + t.Errorf("expected hasAccount(%x) to return true", a.Address) + } + } + if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { + t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) + } + + // Delete a few keys from the cache. + for i := 0; i < len(accs); i += 2 { + cache.delete(wantAccounts[i]) + } + cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) + + select { + case <-notify: + default: + t.Fatalf("notifications didn't fire for deleting accounts") + } + // Check content again after deletion. + wantAccountsAfterDelete := []accounts.Account{ + wantAccounts[1], + wantAccounts[3], + wantAccounts[5], + } + list = cache.accounts() + if !reflect.DeepEqual(list, wantAccountsAfterDelete) { + t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) + } + for _, a := range wantAccountsAfterDelete { + if !cache.hasAddress(a.Address) { + t.Errorf("expected hasAccount(%x) to return true", a.Address) + } + } + if cache.hasAddress(wantAccounts[0].Address) { + t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) + } +} + +func TestCacheFind(t *testing.T) { + dir := filepath.Join("testdata", "dir") + cache, _ := newAccountCache(dir) + cache.watcher.running = true // prevent unexpected reloads + + accs := []accounts.Account{ + { + Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), + URL: filepath.Join(dir, "a.key"), + }, + { + Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), + URL: filepath.Join(dir, "b.key"), + }, + { + Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: filepath.Join(dir, "c.key"), + }, + { + Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), + URL: filepath.Join(dir, "c2.key"), + }, + } + for _, a := range accs { + cache.add(a) + } + + nomatchAccount := accounts.Account{ + Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), + URL: filepath.Join(dir, "something"), + } + tests := []struct { + Query accounts.Account + WantResult accounts.Account + WantError error + }{ + // by address + {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, + // by file + {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, + // by basename + {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, + // by file and address + {Query: accs[0], WantResult: accs[0]}, + // ambiguous address, tie resolved by file + {Query: accs[2], WantResult: accs[2]}, + // ambiguous address error + { + Query: accounts.Account{Address: accs[2].Address}, + WantError: &AmbiguousAddrError{ + Addr: accs[2].Address, + Matches: []accounts.Account{accs[2], accs[3]}, + }, + }, + // no match error + {Query: nomatchAccount, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, + {Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch}, + {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, + } + for i, test := range tests { + a, err := cache.find(test.Query) + if !reflect.DeepEqual(err, test.WantError) { + t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) + continue + } + if a != test.WantResult { + t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) + continue + } + } +} diff --git a/accounts/keystore/address_cache.go b/accounts/keystore/address_cache.go deleted file mode 100644 index eb3e3263b..000000000 --- a/accounts/keystore/address_cache.go +++ /dev/null @@ -1,271 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package keystore - -import ( - "bufio" - "encoding/json" - "fmt" - "io/ioutil" - "os" - "path/filepath" - "sort" - "strings" - "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" -) - -// Minimum amount of time between cache reloads. This limit applies if the platform does -// not support change notifications. It also applies if the keystore directory does not -// exist yet, the code will attempt to create a watcher at most this often. -const minReloadInterval = 2 * time.Second - -type accountsByFile []accounts.Account - -func (s accountsByFile) Len() int { return len(s) } -func (s accountsByFile) Less(i, j int) bool { return s[i].URL < s[j].URL } -func (s accountsByFile) Swap(i, j int) { s[i], s[j] = s[j], s[i] } - -// AmbiguousAddrError is returned when attempting to unlock -// an address for which more than one file exists. -type AmbiguousAddrError struct { - Addr common.Address - Matches []accounts.Account -} - -func (err *AmbiguousAddrError) Error() string { - files := "" - for i, a := range err.Matches { - files += a.URL - if i < len(err.Matches)-1 { - files += ", " - } - } - return fmt.Sprintf("multiple keys match address (%s)", files) -} - -// addressCache is a live index of all accounts in the keystore. -type addressCache struct { - keydir string - watcher *watcher - mu sync.Mutex - all accountsByFile - byAddr map[common.Address][]accounts.Account - throttle *time.Timer -} - -func newAddrCache(keydir string) *addressCache { - ac := &addressCache{ - keydir: keydir, - byAddr: make(map[common.Address][]accounts.Account), - } - ac.watcher = newWatcher(ac) - return ac -} - -func (ac *addressCache) accounts() []accounts.Account { - ac.maybeReload() - ac.mu.Lock() - defer ac.mu.Unlock() - cpy := make([]accounts.Account, len(ac.all)) - copy(cpy, ac.all) - return cpy -} - -func (ac *addressCache) hasAddress(addr common.Address) bool { - ac.maybeReload() - ac.mu.Lock() - defer ac.mu.Unlock() - return len(ac.byAddr[addr]) > 0 -} - -func (ac *addressCache) add(newAccount accounts.Account) { - ac.mu.Lock() - defer ac.mu.Unlock() - - i := sort.Search(len(ac.all), func(i int) bool { return ac.all[i].URL >= newAccount.URL }) - if i < len(ac.all) && ac.all[i] == newAccount { - return - } - // newAccount is not in the cache. - 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 *addressCache) 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) - } else { - ac.byAddr[removed.Address] = ba - } -} - -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:]...) - } - } - return slice -} - -// find returns the cached account for address if there is a unique match. -// The exact matching rules are explained by the documentation of accounts.Account. -// Callers must hold ac.mu. -func (ac *addressCache) find(a accounts.Account) (accounts.Account, error) { - // Limit search to address candidates if possible. - matches := ac.all - if (a.Address != common.Address{}) { - matches = ac.byAddr[a.Address] - } - if a.URL != "" { - // If only the basename is specified, complete the path. - if !strings.ContainsRune(a.URL, filepath.Separator) { - a.URL = filepath.Join(ac.keydir, a.URL) - } - for i := range matches { - if matches[i].URL == a.URL { - return matches[i], nil - } - } - if (a.Address == common.Address{}) { - return accounts.Account{}, ErrNoMatch - } - } - switch len(matches) { - case 1: - return matches[0], nil - case 0: - return accounts.Account{}, ErrNoMatch - default: - err := &AmbiguousAddrError{Addr: a.Address, Matches: make([]accounts.Account, len(matches))} - copy(err.Matches, matches) - return accounts.Account{}, err - } -} - -func (ac *addressCache) 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. - } - if ac.throttle == nil { - ac.throttle = time.NewTimer(0) - } else { - select { - case <-ac.throttle.C: - default: - return // The cache was reloaded recently. - } - } - ac.watcher.start() - ac.reload() - ac.throttle.Reset(minReloadInterval) -} - -func (ac *addressCache) close() { - ac.mu.Lock() - ac.watcher.close() - if ac.throttle != nil { - ac.throttle.Stop() - } - ac.mu.Unlock() -} - -// reload caches addresses of existing accounts. -// Callers must hold ac.mu. -func (ac *addressCache) reload() { - accounts, err := ac.scan() - if err != nil && glog.V(logger.Debug) { - glog.Errorf("can't load keys: %v", err) - } - ac.all = accounts - sort.Sort(ac.all) - for k := range ac.byAddr { - delete(ac.byAddr, k) - } - for _, a := range accounts { - ac.byAddr[a.Address] = append(ac.byAddr[a.Address], a) - } - glog.V(logger.Debug).Infof("reloaded keys, cache has %d accounts", len(ac.all)) -} - -func (ac *addressCache) scan() ([]accounts.Account, error) { - files, err := ioutil.ReadDir(ac.keydir) - if err != nil { - return nil, err - } - - var ( - buf = new(bufio.Reader) - addrs []accounts.Account - keyJSON struct { - Address string `json:"address"` - } - ) - for _, fi := range files { - path := filepath.Join(ac.keydir, fi.Name()) - if skipKeyFile(fi) { - glog.V(logger.Detail).Infof("ignoring file %s", path) - continue - } - fd, err := os.Open(path) - if err != nil { - glog.V(logger.Detail).Infoln(err) - continue - } - buf.Reset(fd) - // Parse the address. - keyJSON.Address = "" - err = json.NewDecoder(buf).Decode(&keyJSON) - addr := common.HexToAddress(keyJSON.Address) - switch { - case err != nil: - glog.V(logger.Debug).Infof("can't decode key %s: %v", path, err) - case (addr == common.Address{}): - glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path) - default: - addrs = append(addrs, accounts.Account{Address: addr, URL: path}) - } - fd.Close() - } - return addrs, err -} - -func skipKeyFile(fi os.FileInfo) bool { - // Skip editor backups and UNIX-style hidden files. - if strings.HasSuffix(fi.Name(), "~") || strings.HasPrefix(fi.Name(), ".") { - return true - } - // Skip misc special files, directories (yes, symlinks too). - if fi.IsDir() || fi.Mode()&os.ModeType != 0 { - return true - } - return false -} diff --git a/accounts/keystore/address_cache_test.go b/accounts/keystore/address_cache_test.go deleted file mode 100644 index 68af74338..000000000 --- a/accounts/keystore/address_cache_test.go +++ /dev/null @@ -1,284 +0,0 @@ -// Copyright 2016 The go-ethereum Authors -// This file is part of the go-ethereum library. -// -// The go-ethereum library is free software: you can redistribute it and/or modify -// it under the terms of the GNU Lesser General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// The go-ethereum library is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Lesser General Public License for more details. -// -// You should have received a copy of the GNU Lesser General Public License -// along with the go-ethereum library. If not, see . - -package keystore - -import ( - "fmt" - "math/rand" - "os" - "path/filepath" - "reflect" - "sort" - "testing" - "time" - - "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 = []accounts.Account{ - { - Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: filepath.Join(cachetestDir, "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - }, - { - Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(cachetestDir, "aaa"), - }, - { - Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: filepath.Join(cachetestDir, "zzz"), - }, - } -) - -func TestWatchNewFile(t *testing.T) { - t.Parallel() - - dir, am := tmpKeyStore(t, false) - defer os.RemoveAll(dir) - - // Ensure the watcher is started before adding any files. - am.Accounts() - time.Sleep(200 * time.Millisecond) - - // Move in the files. - wantAccounts := make([]accounts.Account, len(cachetestAccounts)) - for i := range cachetestAccounts { - a := cachetestAccounts[i] - a.URL = filepath.Join(dir, filepath.Base(a.URL)) - wantAccounts[i] = a - if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); err != nil { - t.Fatal(err) - } - } - - // am should see the accounts. - var list []accounts.Account - for d := 200 * time.Millisecond; d < 5*time.Second; d *= 2 { - list = am.Accounts() - if reflect.DeepEqual(list, wantAccounts) { - return - } - time.Sleep(d) - } - t.Errorf("got %s, want %s", spew.Sdump(list), spew.Sdump(wantAccounts)) -} - -func TestWatchNoDir(t *testing.T) { - t.Parallel() - - // Create am but not the directory that it watches. - rand.Seed(time.Now().UnixNano()) - dir := filepath.Join(os.TempDir(), fmt.Sprintf("eth-keystore-watch-test-%d-%d", os.Getpid(), rand.Int())) - am := NewKeyStore(dir, LightScryptN, LightScryptP) - - list := am.Accounts() - if len(list) > 0 { - t.Error("initial account list not empty:", list) - } - time.Sleep(100 * time.Millisecond) - - // Create the directory and copy a key file into it. - os.MkdirAll(dir, 0700) - defer os.RemoveAll(dir) - file := filepath.Join(dir, "aaa") - if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { - t.Fatal(err) - } - - // am should see the account. - wantAccounts := []accounts.Account{cachetestAccounts[0]} - wantAccounts[0].URL = file - for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { - list = am.Accounts() - if reflect.DeepEqual(list, wantAccounts) { - return - } - time.Sleep(d) - } - t.Errorf("\ngot %v\nwant %v", list, wantAccounts) -} - -func TestCacheInitialReload(t *testing.T) { - cache := newAddrCache(cachetestDir) - accounts := cache.accounts() - if !reflect.DeepEqual(accounts, cachetestAccounts) { - t.Fatalf("got initial accounts: %swant %s", spew.Sdump(accounts), spew.Sdump(cachetestAccounts)) - } -} - -func TestCacheAddDeleteOrder(t *testing.T) { - cache := newAddrCache("testdata/no-such-dir") - cache.watcher.running = true // prevent unexpected reloads - - accs := []accounts.Account{ - { - Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: "-309830980", - }, - { - Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: "ggg", - }, - { - Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - URL: "zzzzzz-the-very-last-one.keyXXX", - }, - { - Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: "SOMETHING.key", - }, - { - Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: "UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8", - }, - { - Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: "aaa", - }, - { - Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: "zzz", - }, - } - for _, a := range accs { - cache.add(a) - } - // Add some of them twice to check that they don't get reinserted. - cache.add(accs[0]) - cache.add(accs[2]) - - // Check that the account list is sorted by filename. - wantAccounts := make([]accounts.Account, len(accs)) - copy(wantAccounts, accs) - sort.Sort(accountsByFile(wantAccounts)) - list := cache.accounts() - if !reflect.DeepEqual(list, wantAccounts) { - t.Fatalf("got accounts: %s\nwant %s", spew.Sdump(accs), spew.Sdump(wantAccounts)) - } - for _, a := range accs { - if !cache.hasAddress(a.Address) { - t.Errorf("expected hasAccount(%x) to return true", a.Address) - } - } - if cache.hasAddress(common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) { - t.Errorf("expected hasAccount(%x) to return false", common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e")) - } - - // Delete a few keys from the cache. - for i := 0; i < len(accs); i += 2 { - cache.delete(wantAccounts[i]) - } - cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) - - // Check content again after deletion. - wantAccountsAfterDelete := []accounts.Account{ - wantAccounts[1], - wantAccounts[3], - wantAccounts[5], - } - list = cache.accounts() - if !reflect.DeepEqual(list, wantAccountsAfterDelete) { - t.Fatalf("got accounts after delete: %s\nwant %s", spew.Sdump(list), spew.Sdump(wantAccountsAfterDelete)) - } - for _, a := range wantAccountsAfterDelete { - if !cache.hasAddress(a.Address) { - t.Errorf("expected hasAccount(%x) to return true", a.Address) - } - } - if cache.hasAddress(wantAccounts[0].Address) { - t.Errorf("expected hasAccount(%x) to return false", wantAccounts[0].Address) - } -} - -func TestCacheFind(t *testing.T) { - dir := filepath.Join("testdata", "dir") - cache := newAddrCache(dir) - cache.watcher.running = true // prevent unexpected reloads - - accs := []accounts.Account{ - { - Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: filepath.Join(dir, "a.key"), - }, - { - Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: filepath.Join(dir, "b.key"), - }, - { - Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c.key"), - }, - { - Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c2.key"), - }, - } - for _, a := range accs { - cache.add(a) - } - - nomatchAccount := accounts.Account{ - Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(dir, "something"), - } - tests := []struct { - Query accounts.Account - WantResult accounts.Account - WantError error - }{ - // by address - {Query: accounts.Account{Address: accs[0].Address}, WantResult: accs[0]}, - // by file - {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, - // by basename - {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, - // by file and address - {Query: accs[0], WantResult: accs[0]}, - // ambiguous address, tie resolved by file - {Query: accs[2], WantResult: accs[2]}, - // ambiguous address error - { - Query: accounts.Account{Address: accs[2].Address}, - WantError: &AmbiguousAddrError{ - Addr: accs[2].Address, - Matches: []accounts.Account{accs[2], accs[3]}, - }, - }, - // no match error - {Query: nomatchAccount, WantError: ErrNoMatch}, - {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, - {Query: accounts.Account{URL: filepath.Base(nomatchAccount.URL)}, WantError: ErrNoMatch}, - {Query: accounts.Account{Address: nomatchAccount.Address}, WantError: ErrNoMatch}, - } - for i, test := range tests { - a, err := cache.find(test.Query) - if !reflect.DeepEqual(err, test.WantError) { - t.Errorf("test %d: error mismatch for query %v\ngot %q\nwant %q", i, test.Query, err, test.WantError) - continue - } - if a != test.WantResult { - t.Errorf("test %d: result mismatch for query %v\ngot %v\nwant %v", i, test.Query, a, test.WantResult) - continue - } - } -} diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go index d125f7d62..ce4e87ce9 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -37,23 +37,34 @@ import ( "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 ( - ErrNeedPasswordOrUnlock = accounts.NewAuthNeededError("password or unlock") - ErrNoMatch = errors.New("no key for given address or file") - ErrDecrypt = errors.New("could not decrypt key with given passphrase") + 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") ) -// BackendType can be used to query the account manager for encrypted keystores. -var BackendType = reflect.TypeOf(new(KeyStore)) +// KeyStoreType is the reflect type of a keystore backend. +var KeyStoreType = reflect.TypeOf(&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 { - cache *addressCache - keyStore keyStore - mu sync.RWMutex - unlocked map[common.Address]*unlocked + 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 { @@ -64,7 +75,7 @@ type unlocked struct { // NewKeyStore creates a keystore for the given directory. func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { keydir, _ = filepath.Abs(keydir) - ks := &KeyStore{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} + ks := &KeyStore{storage: &keyStorePassphrase{keydir, scryptN, scryptP}} ks.init(keydir) return ks } @@ -73,20 +84,136 @@ func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { // Deprecated: Use NewKeyStore. func NewPlaintextKeyStore(keydir string) *KeyStore { keydir, _ = filepath.Abs(keydir) - ks := &KeyStore{keyStore: &keyStorePlain{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 = newAddrCache(keydir) + 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 + accs := ks.cache.accounts() + + // Transform the current list of wallets into the new one + ks.mu.Lock() + + 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() < account.URL { + 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() > account.URL { + 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. @@ -118,6 +245,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { err = os.Remove(a.URL) if err == nil { ks.cache.delete(a) + ks.refreshWallets() } return err } @@ -131,7 +259,7 @@ func (ks *KeyStore) SignHash(a accounts.Account, hash []byte) ([]byte, error) { unlockedKey, found := ks.unlocked[a.Address] if !found { - return nil, ErrNeedPasswordOrUnlock + return nil, ErrLocked } // Sign the hash using plain ECDSA operations return crypto.Sign(hash, unlockedKey.PrivateKey) @@ -145,7 +273,7 @@ func (ks *KeyStore) SignTx(a accounts.Account, tx *types.Transaction, chainID *b unlockedKey, found := ks.unlocked[a.Address] if !found { - return nil, ErrNeedPasswordOrUnlock + return nil, ErrLocked } // Depending on the presence of the chain ID, sign with EIP155 or homestead if chainID != nil { @@ -221,10 +349,9 @@ func (ks *KeyStore) TimedUnlock(a accounts.Account, passphrase string, timeout t // it with a timeout would be confusing. zeroKey(key.PrivateKey) return nil - } else { - // Terminate the expire goroutine and replace it below. - close(u.abort) } + // Terminate the expire goroutine and replace it below. + close(u.abort) } if timeout > 0 { u = &unlocked{Key: key, abort: make(chan struct{})} @@ -250,7 +377,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A if err != nil { return a, nil, err } - key, err := ks.keyStore.GetKey(a.Address, a.URL, auth) + key, err := ks.storage.GetKey(a.Address, a.URL, auth) return a, key, err } @@ -277,13 +404,14 @@ func (ks *KeyStore) expire(addr common.Address, u *unlocked, timeout time.Durati // NewAccount generates a new key and stores it into the key directory, // encrypting it with the passphrase. func (ks *KeyStore) NewAccount(passphrase string) (accounts.Account, error) { - _, account, err := storeNewKey(ks.keyStore, crand.Reader, passphrase) + _, 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 } @@ -294,7 +422,7 @@ func (ks *KeyStore) Export(a accounts.Account, passphrase, newPassphrase string) return nil, err } var N, P int - if store, ok := ks.keyStore.(*keyStorePassphrase); ok { + if store, ok := ks.storage.(*keyStorePassphrase); ok { N, P = store.scryptN, store.scryptP } else { N, P = StandardScryptN, StandardScryptP @@ -325,11 +453,12 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, URL: ks.keyStore.JoinPath(keyFileName(key.Address))} - if err := ks.keyStore.StoreKey(a.URL, key, passphrase); err != nil { + a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))} + if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil { return accounts.Account{}, err } ks.cache.add(a) + ks.refreshWallets() return a, nil } @@ -339,17 +468,18 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) if err != nil { return err } - return ks.keyStore.StoreKey(a.URL, key, newPassphrase) + return ks.storage.StoreKey(a.URL, key, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores // a key file in the key directory. The key file is encrypted with the same passphrase. func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { - a, _, err := importPreSaleKey(ks.keyStore, keyJSON, passphrase) + a, _, err := importPreSaleKey(ks.storage, keyJSON, passphrase) if err != nil { return a, err } ks.cache.add(a) + ks.refreshWallets() return a, nil } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index af2140c31..6b7170a2f 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -18,14 +18,17 @@ 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) @@ -122,8 +125,8 @@ func TestTimedUnlock(t *testing.T) { // Signing without passphrase fails because account is locked _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrNeedPasswordOrUnlock { - t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock before unlocking, got ", err) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked before unlocking, got ", err) } // Signing with passphrase works @@ -140,8 +143,8 @@ func TestTimedUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrNeedPasswordOrUnlock { - t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } } @@ -180,8 +183,8 @@ func TestOverrideUnlock(t *testing.T) { // Signing fails again after automatic locking time.Sleep(250 * time.Millisecond) _, err = ks.SignHash(accounts.Account{Address: a1.Address}, testSigData) - if err != ErrNeedPasswordOrUnlock { - t.Fatal("Signing should've failed with ErrNeedPasswordOrUnlock timeout expired, got ", err) + if err != ErrLocked { + t.Fatal("Signing should've failed with ErrLocked timeout expired, got ", err) } } @@ -201,7 +204,7 @@ func TestSignRace(t *testing.T) { } end := time.Now().Add(500 * time.Millisecond) for time.Now().Before(end) { - if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrNeedPasswordOrUnlock { + if _, err := ks.SignHash(accounts.Account{Address: a1.Address}, testSigData); err == ErrLocked { return } else if err != nil { t.Errorf("Sign error: %v", err) @@ -212,6 +215,145 @@ func TestSignRace(t *testing.T) { 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]) + } + } + } + // Sleep a bit to avoid same-timestamp keyfiles + time.Sleep(10 * time.Millisecond) + } +} + func tmpKeyStore(t *testing.T, encrypted bool) (string, *KeyStore) { d, err := ioutil.TempDir("", "eth-keystore-test") if err != nil { diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go new file mode 100644 index 000000000..d92926478 --- /dev/null +++ b/accounts/keystore/keystore_wallet.go @@ -0,0 +1,133 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "math/big" + + "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 +} + +// Type implements accounts.Wallet, returning the textual type of the wallet. +func (w *keystoreWallet) Type() string { + return "secret-storage" +} + +// URL implements accounts.Wallet, returning the URL of the account within. +func (w *keystoreWallet) URL() string { + 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 { + return "Open" +} + +// 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 == "" || 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 string, pin bool) (accounts.Account, error) { + return accounts.Account{}, accounts.ErrNotSupported +} + +// 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 != "" && 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 != "" && 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 != "" && 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 != "" && 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/keystore/watch.go b/accounts/keystore/watch.go index 04a87b12e..0b4401255 100644 --- a/accounts/keystore/watch.go +++ b/accounts/keystore/watch.go @@ -27,14 +27,14 @@ import ( ) type watcher struct { - ac *addressCache + ac *accountCache starting bool running bool ev chan notify.EventInfo quit chan struct{} } -func newWatcher(ac *addressCache) *watcher { +func newWatcher(ac *accountCache) *watcher { return &watcher{ ac: ac, ev: make(chan notify.EventInfo, 10), diff --git a/accounts/keystore/watch_fallback.go b/accounts/keystore/watch_fallback.go index 6412f3b33..7c5e9cb2e 100644 --- a/accounts/keystore/watch_fallback.go +++ b/accounts/keystore/watch_fallback.go @@ -23,6 +23,6 @@ package keystore type watcher struct{ running bool } -func newWatcher(*addressCache) *watcher { return new(watcher) } +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..0822500eb --- /dev/null +++ b/accounts/manager.go @@ -0,0 +1,194 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "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() + + for _, wallet := range am.Wallets() { + if wallet.URL() == url { + 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() >= wallet.URL() }) + 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() >= wallet.URL() }) + 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/usbwallet/ledger.go b/accounts/usbwallet/ledger.go deleted file mode 100644 index 9917d0d70..000000000 --- a/accounts/usbwallet/ledger.go +++ /dev/null @@ -1,653 +0,0 @@ -// 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 . - -// 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" - "strconv" - "strings" - "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/logger" - "github.com/ethereum/go-ethereum/logger/glog" - "github.com/ethereum/go-ethereum/rlp" - "github.com/karalabe/gousb/usb" -) - -// 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 -} - -// ledgerDerivationPath is the key derivation parameters used by the wallet. -var ledgerDerivationPath = [4]uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} - -// 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 -) - -// ledgerWallet represents a live USB Ledger hardware wallet. -type ledgerWallet struct { - 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 - - address common.Address // Current address of the wallet (may be zero if Ethereum app offline) - url string // Textual URL uniquely identifying this wallet - version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) -} - -// LedgerHub is a USB hardware wallet interface that can find and handle Ledger -// wallets. -type LedgerHub struct { - ctx *usb.Context // Context interfacing with a libusb instance - - wallets map[uint16]*ledgerWallet // Apparent Ledger wallets (some may be inactive) - accounts []accounts.Account // List of active Ledger accounts - index map[common.Address]uint16 // Set of addresses with active wallets - - 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, - wallets: make(map[uint16]*ledgerWallet), - index: make(map[common.Address]uint16), - quit: make(chan chan error), - } - go hub.watch() - return hub, nil -} - -// Accounts retrieves the live of accounts currently known by the Ledger hub. -func (hub *LedgerHub) Accounts() []accounts.Account { - hub.lock.RLock() - defer hub.lock.RUnlock() - - cpy := make([]accounts.Account, len(hub.accounts)) - copy(cpy, hub.accounts) - return cpy -} - -// HasAddress reports whether an account with the given address is present. -func (hub *LedgerHub) HasAddress(addr common.Address) bool { - hub.lock.RLock() - defer hub.lock.RUnlock() - - _, known := hub.index[addr] - return known -} - -// SignHash is not supported for Ledger wallets, so this method will always -// return an error. -func (hub *LedgerHub) SignHash(acc accounts.Account, hash []byte) ([]byte, error) { - return nil, accounts.ErrNotSupported -} - -// SignTx 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 (hub *LedgerHub) SignTx(acc accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - hub.lock.RLock() - defer hub.lock.RUnlock() - - // If the account contains the device URL, flatten it to make sure - var id uint16 - if acc.URL != "" { - if parts := strings.Split(acc.URL, "."); len(parts) == 2 { - bus, busErr := strconv.Atoi(parts[0]) - addr, addrErr := strconv.Atoi(parts[1]) - - if busErr == nil && addrErr == nil { - id = uint16(bus)<<8 + uint16(addr) - } - } - } - // If the id is still zero, URL is either missing or bad, resolve - if id == 0 { - var ok bool - if id, ok = hub.index[acc.Address]; !ok { - return nil, accounts.ErrUnknownAccount - } - } - // Retrieve the wallet associated with the URL - wallet, ok := hub.wallets[id] - if !ok { - return nil, accounts.ErrUnknownAccount - } - // Ensure the wallet is capable of signing the given transaction - if chainID != nil && wallet.version[0] <= 1 && wallet.version[1] <= 0 && wallet.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", - wallet.version[0], wallet.version[1], wallet.version[2]) - } - return wallet.sign(tx, chainID) -} - -// SignHashWithPassphrase is not supported for Ledger wallets, so this method -// will always return an error. -func (hub *LedgerHub) SignHashWithPassphrase(acc accounts.Account, passphrase string, hash []byte) ([]byte, error) { - return nil, accounts.ErrNotSupported -} - -// SignTxWithPassphrase requests the backend to sign the given transaction, with the -// given passphrase as extra authentication information. Since the Ledger does not -// support this feature, it will just silently ignore the passphrase. -func (hub *LedgerHub) SignTxWithPassphrase(acc accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { - return hub.SignTx(acc, tx, chainID) -} - -// Close terminates the usb watching for Ledger wallets and returns when it -// successfully terminated. -func (hub *LedgerHub) Close() error { - // Terminate the USB scanner - errc := make(chan error) - hub.quit <- errc - err := <-errc - - // Release the USB interface and return - hub.ctx.Close() - return err -} - -// watch starts watching the local machine's USB ports for the connection or -// disconnection of Ledger devices. -func (hub *LedgerHub) watch() { - for { - // Rescan the USB ports for devices newly added or removed - hub.rescan() - - // Sleep for a certain amount of time or until terminated - select { - case errc := <-hub.quit: - errc <- nil - return - case <-time.After(time.Second): - } - } -} - -// rescan searches the USB ports for attached Ledger hardware wallets. -func (hub *LedgerHub) rescan() { - hub.lock.Lock() - defer hub.lock.Unlock() - - // Iterate over all connected Ledger devices and do a heartbeat test - for id, wallet := range hub.wallets { - // If the device doesn't respond (io error on Windows, no device on Linux), drop - if err := wallet.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { - // Wallet disconnected or at least in a useless state - if wallet.address == (common.Address{}) { - glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] disconnected", wallet.device.Bus, wallet.device.Address) - } else { - // A live account disconnected, remove it from the tracked accounts - for i, account := range hub.accounts { - if account.Address == wallet.address && account.URL == wallet.url { - hub.accounts = append(hub.accounts[:i], hub.accounts[i+1:]...) - break - } - } - delete(hub.index, wallet.address) - - glog.V(logger.Info).Infof("ledger wallet [%03d.%03d] v%d.%d.%d disconnected: %s", wallet.device.Bus, wallet.device.Address, - wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) - } - delete(hub.wallets, id) - wallet.device.Close() - } - } - // Iterate over all attached devices and fetch those seemingly Ledger - devices, _ := hub.ctx.ListDevices(func(desc *usb.Descriptor) bool { - // Discard all devices not advertizing as Ledger - ledger := false - for _, id := range ledgerDeviceIDs { - if desc.Vendor == id.Vendor && desc.Product == id.Product { - ledger = true - } - } - if !ledger { - return false - } - // If we have an already known Ledger, skip opening it - id := uint16(desc.Bus)<<8 + uint16(desc.Address) - if _, known := hub.wallets[id]; known { - return false - } - // New Ledger device, open it for communication - return true - }) - // Start tracking all wallets which newly appeared - var err error - for _, device := range devices { - // Make sure the alleged device has the correct IO endpoints - wallet := &ledgerWallet{ - device: device, - url: fmt.Sprintf("%03d.%03d", device.Bus, device.Address), - } - 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 != "" { - glog.V(logger.Debug).Infof("ledger wallet [%s] deemed invalid: %s", wallet.url, invalid) - device.Close() - continue - } - // Open the input and output endpoints to the device - wallet.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 { - glog.V(logger.Debug).Infof("ledger wallet [%s] input open failed: %v", wallet.url, err) - device.Close() - continue - } - wallet.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 { - glog.V(logger.Debug).Infof("ledger wallet [%s] output open failed: %v", wallet.url, err) - device.Close() - continue - } - // Start tracking the device as a probably Ledger wallet - id := uint16(device.Bus)<<8 + uint16(device.Address) - hub.wallets[id] = wallet - - if wallet.resolveAddress() != nil { - glog.V(logger.Info).Infof("ledger wallet [%s] connected, Ethereum app not started", wallet.url) - } else { - // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 - if wallet.resolveVersion() != nil { - wallet.version = [3]byte{1, 0, 0} // Assume worst case, can't verify if v1.0.0 or v1.0.1 - } - hub.accounts = append(hub.accounts, accounts.Account{ - Address: wallet.address, - URL: wallet.url, - }) - hub.index[wallet.address] = id - - glog.V(logger.Info).Infof("ledger wallet [%s] v%d.%d.%d connected: %s", wallet.url, - wallet.version[0], wallet.version[1], wallet.version[2], wallet.address.Hex()) - } - } -} - -// resolveVersion retrieves the current version of the Ethereum wallet app running -// on the Ledger wallet and caches it for future reference. -// -// 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 (wallet *ledgerWallet) resolveVersion() error { - // Send the request and wait for the response - reply, err := wallet.exchange(ledgerOpGetConfiguration, 0, 0, nil) - if err != nil { - return err - } - if len(reply) != 4 { - return errors.New("reply not of correct size") - } - // Cache the version for future reference - copy(wallet.version[:], reply[1:]) - return nil -} - -// resolveAddress retrieves the currently active Ethereum address from a Ledger -// wallet and caches it for future reference. -// -// 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 (wallet *ledgerWallet) resolveAddress() error { - // Flatten the derivation path into the Ledger request - path := make([]byte, 1+4*len(ledgerDerivationPath)) - path[0] = byte(len(ledgerDerivationPath)) - for i, component := range ledgerDerivationPath { - binary.BigEndian.PutUint32(path[1+4*i:], component) - } - // Send the request and wait for the response - reply, err := wallet.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) - if err != nil { - return err - } - // Discard the public key, we don't need that for now - if len(reply) < 1 || len(reply) < 1+int(reply[0]) { - return 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 errors.New("reply lacks address entry") - } - hexstr := reply[1 : 1+int(reply[0])] - - // Decode the hex sting into an Ethereum address and return - hex.Decode(wallet.address[:], hexstr) - return nil -} - -// sign 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 (wallet *ledgerWallet) sign(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) { wallet.device.ReadTimeout = old }(wallet.device.ReadTimeout) - wallet.device.ReadTimeout = time.Minute - - // Flatten the derivation path into the Ledger request - path := make([]byte, 1+4*len(ledgerDerivationPath)) - path[0] = byte(len(ledgerDerivationPath)) - for i, component := range ledgerDerivationPath { - 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 = wallet.exchange(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]-34)/2 - byte(chainID.Uint64()) - } - // 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 != wallet.address { - glog.V(logger.Error).Infof("Ledger signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) - return nil, fmt.Errorf("signer mismatch: expected %s, got %s", wallet.address.Hex(), sender.Hex()) - } - return signed, nil -} - -// exchange 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 (wallet *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { - // Construct the message payload, possibly split into multiple chunks - var chunks [][]byte - for left := data; len(left) > 0 || len(chunks) == 0; { - // Create the chunk header - var chunk []byte - - if len(chunks) == 0 { - // The first chunk encodes the length and all the opcodes - chunk = []byte{0x00, 0x00, 0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))} - binary.BigEndian.PutUint16(chunk, uint16(5+len(data))) - } - // Append the data blob to the end of the chunk - space := 64 - len(chunk) - 5 // 5 == header size - if len(left) > space { - chunks, left = append(chunks, append(chunk, left[:space]...)), left[space:] - continue - } - chunks, left = append(chunks, append(chunk, left...)), nil - } - // Stream all the chunks to the device - for i, chunk := range chunks { - // Construct the new message to stream - header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended - binary.BigEndian.PutUint16(header[3:], uint16(i)) - - msg := append(header, chunk...) - - // Send over to the device - if glog.V(logger.Core) { - glog.Infof("-> %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, msg) - } - if _, err := wallet.input.Write(msg); err != nil { - return nil, err - } - } - // Stream the reply back from the wallet in 64 byte chunks - var reply []byte - for { - // Read the next chunk from the Ledger wallet - chunk := make([]byte, 64) - if _, err := io.ReadFull(wallet.output, chunk); err != nil { - return nil, err - } - if glog.V(logger.Core) { - glog.Infof("<- %03d.%03d: %x", wallet.device.Bus, wallet.device.Address, chunk) - } - // Make sure the transport header matches - if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { - return nil, fmt.Errorf("invalid reply header: %x", chunk[:3]) - } - // If it's the first chunk, retrieve the total message length - if chunk[3] == 0x00 && chunk[4] == 0x00 { - reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7]))) - chunk = chunk[7:] - } else { - chunk = chunk[5:] - } - // Append to the reply and stop when filled up - if left := cap(reply) - len(reply); left > len(chunk) { - reply = append(reply, chunk...) - } else { - reply = append(reply, chunk[:left]...) - break - } - } - return reply[:len(reply)-2], nil -} diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go new file mode 100644 index 000000000..ebd6fddb4 --- /dev/null +++ b/accounts/usbwallet/ledger_hub.go @@ -0,0 +1,205 @@ +// 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 . + +// 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" +) + +// 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 := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff) + + // Drop wallets while they were in front of the next account + for len(hub.wallets) > 0 && hub.wallets[0].URL() < url { + events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) + hub.wallets = hub.wallets[1:] + } + // If there are no more wallets or the account is before the next, wrap new wallet + if len(hub.wallets) == 0 || hub.wallets[0].URL() > url { + 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 account is the same as the first wallet, keep it + if hub.wallets[0].URL() == url { + 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..35e671c46 --- /dev/null +++ b/accounts/usbwallet/ledger_wallet.go @@ -0,0 +1,670 @@ +// 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 . + +// 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" + "strconv" + "strings" + "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/logger" + "github.com/ethereum/go-ethereum/logger/glog" + "github.com/ethereum/go-ethereum/rlp" + "github.com/karalabe/gousb/usb" +) + +// ledgerDerivationPath is the base derivation parameters used by the wallet. +var ledgerDerivationPath = []uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} + +// 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 +) + +// 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 string // 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) + accounts []accounts.Account // List of derive accounts pinned on the Ledger + paths map[common.Address][]uint32 // Known derivation paths for signing operations + + quit chan chan error + lock sync.RWMutex +} + +// Type implements accounts.Wallet, returning the textual type of the wallet. +func (w *ledgerWallet) Type() string { + return "ledger" +} + +// URL implements accounts.Wallet, returning the URL of the Ledger device. +func (w *ledgerWallet) URL() string { + return w.url +} + +// 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.lock.RLock() + defer w.lock.RUnlock() + + if w.failure != nil { + return fmt.Sprintf("Failed: %v", w.failure) + } + if w.device == nil { + return "Closed" + } + if w.version == [3]byte{0, 0, 0} { + return "Ethereum app not started" + } + return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2]) +} + +// 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 +// is silently discarded. +func (w *ledgerWallet) Open(passphrase string) error { + w.lock.Lock() + defer w.lock.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 + } + // 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.paths = make(map[common.Address][]uint32) + w.quit = make(chan chan error) + defer func() { + go w.heartbeat() + }() + + if _, err := w.deriveAddress(ledgerDerivationPath); err != nil { + // Ethereum app is not running, nothing more to do, return + return nil + } + // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 + if w.resolveVersion() != 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() { + // Execute heartbeat checks until termination or error + var ( + errc chan error + fail error + ) + for errc == nil && fail == nil { + // Wait until termination is requested or the heartbeat cycle arrives + select { + case errc = <-w.quit: + // Termination requested + continue + case <-time.After(time.Second): + // Heartbeat time + } + // Execute a tiny data exchange to see responsiveness + w.lock.Lock() + if err := w.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { + w.failure = err + fail = err + } + w.lock.Unlock() + } + // In case of error, wait for termination + if fail != nil { + errc = <-w.quit + } + errc <- fail +} + +// Close implements accounts.Wallet, closing the USB connection to the Ledger. +func (w *ledgerWallet) Close() error { + // Terminate the health checks + errc := make(chan error) + w.quit <- errc + herr := <-errc // Save for later, we *must* close the USB + + // Terminate the device connection + w.lock.Lock() + defer w.lock.Unlock() + + if err := w.device.Close(); err != nil { + return err + } + w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil + + return herr // If all went well, return any health-check errors +} + +// Accounts implements accounts.Wallet, returning the list of accounts pinned to +// the Ledger hardware wallet. +func (w *ledgerWallet) Accounts() []accounts.Account { + w.lock.RLock() + defer w.lock.RUnlock() + + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy +} + +// 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.lock.RLock() + defer w.lock.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 string, pin bool) (accounts.Account, error) { + w.lock.Lock() + defer w.lock.Unlock() + + // If the wallet is closed, or the Ethereum app doesn't run, abort + if w.device == nil || w.version == [3]byte{0, 0, 0} { + return accounts.Account{}, accounts.ErrWalletClosed + } + // All seems fine, convert the user derivation path to Ledger representation + path = strings.TrimPrefix(path, "/") + + parts := strings.Split(path, "/") + lpath := make([]uint32, len(parts)) + for i, part := range parts { + // Handle hardened paths + if strings.HasSuffix(part, "'") { + lpath[i] = 0x80000000 + part = strings.TrimSuffix(part, "'") + } + // Handle the non hardened component + val, err := strconv.Atoi(part) + if err != nil { + return accounts.Account{}, fmt.Errorf("path element %d: %v", i, err) + } + lpath[i] += uint32(val) + } + // Try to derive the actual account and update it's URL if succeeful + address, err := w.deriveAddress(lpath) + if err != nil { + return accounts.Account{}, err + } + account := accounts.Account{ + Address: address, + URL: fmt.Sprintf("%s/%s", w.url, path), + } + // If pinning was requested, track the account + if pin { + if _, ok := w.paths[address]; !ok { + w.accounts = append(w.accounts, account) + w.paths[address] = lpath + } + } + return account, nil +} + +// 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.lock.Lock() + defer w.lock.Unlock() + + // 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]) + } + return w.sign(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) +} + +// resolveVersion retrieves the current version of the Ethereum wallet app running +// on the Ledger wallet and caches it for future reference. +// +// 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 (wallet *ledgerWallet) resolveVersion() error { + // Send the request and wait for the response + reply, err := wallet.exchange(ledgerOpGetConfiguration, 0, 0, nil) + if err != nil { + return err + } + if len(reply) != 4 { + return errors.New("reply not of correct size") + } + // Cache the version for future reference + copy(wallet.version[:], reply[1:]) + return nil +} + +// deriveAddress 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) deriveAddress(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.exchange(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 +} + +// sign 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) sign(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.Minute + + // 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.exchange(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]-34)/2 - byte(chainID.Uint64()) + } + // 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 +} + +// exchange 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) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { + // Construct the message payload, possibly split into multiple chunks + var chunks [][]byte + for left := data; len(left) > 0 || len(chunks) == 0; { + // Create the chunk header + var chunk []byte + + if len(chunks) == 0 { + // The first chunk encodes the length and all the opcodes + chunk = []byte{0x00, 0x00, 0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))} + binary.BigEndian.PutUint16(chunk, uint16(5+len(data))) + } + // Append the data blob to the end of the chunk + space := 64 - len(chunk) - 5 // 5 == header size + if len(left) > space { + chunks, left = append(chunks, append(chunk, left[:space]...)), left[space:] + continue + } + chunks, left = append(chunks, append(chunk, left...)), nil + } + // Stream all the chunks to the device + for i, chunk := range chunks { + // Construct the new message to stream + header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended + binary.BigEndian.PutUint16(header[3:], uint16(i)) + + msg := append(header, chunk...) + + // Send over to the device + if glog.V(logger.Core) { + glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, msg) + } + if _, err := w.input.Write(msg); err != nil { + return nil, err + } + } + // Stream the reply back from the wallet in 64 byte chunks + var reply []byte + for { + // Read the next chunk from the Ledger wallet + chunk := make([]byte, 64) + if _, err := io.ReadFull(w.output, chunk); err != nil { + return nil, err + } + if glog.V(logger.Core) { + 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, fmt.Errorf("invalid reply header: %x", chunk[:3]) + } + // If it's the first chunk, retrieve the total message length + if chunk[3] == 0x00 && chunk[4] == 0x00 { + reply = make([]byte, 0, int(binary.BigEndian.Uint16(chunk[5:7]))) + chunk = chunk[7:] + } else { + chunk = chunk[5:] + } + // Append to the reply and stop when filled up + if left := cap(reply) - len(reply); left > len(chunk) { + reply = append(reply, chunk...) + } else { + reply = append(reply, chunk[:left]...) + break + } + } + return reply[:len(reply)-2], nil +} -- cgit v1.2.3 From c5215fdd48231622dd56aba63a5187c6e42828d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Wed, 8 Feb 2017 15:53:02 +0200 Subject: accounts, cmd, internal, mobile, node: canonical account URLs --- accounts/accounts.go | 7 +-- accounts/keystore/account_cache.go | 14 +++--- accounts/keystore/account_cache_test.go | 49 ++++++++++---------- accounts/keystore/key.go | 4 +- accounts/keystore/keystore.go | 20 ++++---- accounts/keystore/keystore_plain_test.go | 8 ++-- accounts/keystore/keystore_test.go | 10 ++-- accounts/keystore/keystore_wallet.go | 25 +++++----- accounts/keystore/presale.go | 4 +- accounts/manager.go | 10 ++-- accounts/url.go | 79 ++++++++++++++++++++++++++++++++ accounts/usbwallet/ledger_hub.go | 14 ++++-- accounts/usbwallet/ledger_wallet.go | 23 ++++------ 13 files changed, 174 insertions(+), 93 deletions(-) create mode 100644 accounts/url.go (limited to 'accounts') diff --git a/accounts/accounts.go b/accounts/accounts.go index b367ee2f4..6c9e0bbce 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -29,19 +29,16 @@ import ( // by the optional URL field. type Account struct { Address common.Address `json:"address"` // Ethereum account address derived from the key - URL string `json:"url"` // Optional resource locator within a backend + 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 { - // Type retrieves a textual representation of the type of the wallet. - Type() string - // 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() string + URL() URL // Status returns a textual status to aid the user in the current state of the // wallet. diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index cc8626afc..e2f826250 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -42,7 +42,7 @@ const minReloadInterval = 2 * time.Second type accountsByURL []accounts.Account func (s accountsByURL) Len() int { return len(s) } -func (s accountsByURL) Less(i, j int) bool { return s[i].URL < s[j].URL } +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 @@ -55,7 +55,7 @@ type AmbiguousAddrError struct { func (err *AmbiguousAddrError) Error() string { files := "" for i, a := range err.Matches { - files += a.URL + files += a.URL.Path if i < len(err.Matches)-1 { files += ", " } @@ -104,7 +104,7 @@ 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].URL >= newAccount.URL }) + 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 } @@ -155,10 +155,10 @@ func (ac *accountCache) find(a accounts.Account) (accounts.Account, error) { if (a.Address != common.Address{}) { matches = ac.byAddr[a.Address] } - if a.URL != "" { + if a.URL.Path != "" { // If only the basename is specified, complete the path. - if !strings.ContainsRune(a.URL, filepath.Separator) { - a.URL = filepath.Join(ac.keydir, a.URL) + 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].URL == a.URL { @@ -272,7 +272,7 @@ func (ac *accountCache) scan() ([]accounts.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, accounts.Account{Address: addr, URL: path}) + addrs = append(addrs, accounts.Account{Address: addr, URL: accounts.URL{Scheme: KeyStoreScheme, Path: path}}) } fd.Close() } diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index ea6f7d011..3e68351ea 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -37,15 +37,15 @@ var ( cachetestAccounts = []accounts.Account{ { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: 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"), - URL: filepath.Join(cachetestDir, "aaa"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "aaa")}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: filepath.Join(cachetestDir, "zzz"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(cachetestDir, "zzz")}, }, } ) @@ -63,10 +63,11 @@ func TestWatchNewFile(t *testing.T) { // Move in the files. wantAccounts := make([]accounts.Account, len(cachetestAccounts)) for i := range cachetestAccounts { - a := cachetestAccounts[i] - a.URL = filepath.Join(dir, filepath.Base(a.URL)) - wantAccounts[i] = a - if err := cp.CopyFile(a.URL, cachetestAccounts[i].URL); 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) } } @@ -107,13 +108,13 @@ func TestWatchNoDir(t *testing.T) { os.MkdirAll(dir, 0700) defer os.RemoveAll(dir) file := filepath.Join(dir, "aaa") - if err := cp.CopyFile(file, cachetestAccounts[0].URL); err != nil { + if err := cp.CopyFile(file, cachetestAccounts[0].URL.Path); err != nil { t.Fatal(err) } // ks should see the account. wantAccounts := []accounts.Account{cachetestAccounts[0]} - wantAccounts[0].URL = file + wantAccounts[0].URL = accounts.URL{Scheme: KeyStoreScheme, Path: file} for d := 200 * time.Millisecond; d < 8*time.Second; d *= 2 { list = ks.Accounts() if reflect.DeepEqual(list, wantAccounts) { @@ -145,31 +146,31 @@ func TestCacheAddDeleteOrder(t *testing.T) { accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: "-309830980", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "-309830980"}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: "ggg", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "ggg"}, }, { Address: common.HexToAddress("8bda78331c916a08481428e4b07c96d3e916d165"), - URL: "zzzzzz-the-very-last-one.keyXXX", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzzzzz-the-very-last-one.keyXXX"}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: "SOMETHING.key", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "SOMETHING.key"}, }, { Address: common.HexToAddress("7ef5a6135f1fd6a02593eedc869c6d41d934aef8"), - URL: "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"), - URL: "aaa", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "aaa"}, }, { Address: common.HexToAddress("289d485d9771714cce91d3393d764e1311907acc"), - URL: "zzz", + URL: accounts.URL{Scheme: KeyStoreScheme, Path: "zzz"}, }, } for _, a := range accs { @@ -210,7 +211,7 @@ func TestCacheAddDeleteOrder(t *testing.T) { for i := 0; i < len(accs); i += 2 { cache.delete(wantAccounts[i]) } - cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: "something"}) + cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) select { case <-notify: @@ -245,19 +246,19 @@ func TestCacheFind(t *testing.T) { accs := []accounts.Account{ { Address: common.HexToAddress("095e7baea6a6c7c4c2dfeb977efac326af552d87"), - URL: filepath.Join(dir, "a.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "a.key")}, }, { Address: common.HexToAddress("2cac1adea150210703ba75ed097ddfe24e14f213"), - URL: filepath.Join(dir, "b.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "b.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c.key")}, }, { Address: common.HexToAddress("d49ff4eeb0b2686ed89c0fc0f2b6ea533ddbbd5e"), - URL: filepath.Join(dir, "c2.key"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "c2.key")}, }, } for _, a := range accs { @@ -266,7 +267,7 @@ func TestCacheFind(t *testing.T) { nomatchAccount := accounts.Account{ Address: common.HexToAddress("f466859ead1932d743d622cb74fc058882e8648a"), - URL: filepath.Join(dir, "something"), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Join(dir, "something")}, } tests := []struct { Query accounts.Account @@ -278,7 +279,7 @@ func TestCacheFind(t *testing.T) { // by file {Query: accounts.Account{URL: accs[0].URL}, WantResult: accs[0]}, // by basename - {Query: accounts.Account{URL: filepath.Base(accs[0].URL)}, WantResult: accs[0]}, + {Query: accounts.Account{URL: accounts.URL{Scheme: KeyStoreScheme, Path: filepath.Base(accs[0].URL.Path)}}, WantResult: accs[0]}, // by file and address {Query: accs[0], WantResult: accs[0]}, // ambiguous address, tie resolved by file @@ -294,7 +295,7 @@ func TestCacheFind(t *testing.T) { // no match error {Query: nomatchAccount, WantError: ErrNoMatch}, {Query: accounts.Account{URL: nomatchAccount.URL}, WantError: ErrNoMatch}, - {Query: accounts.Account{URL: filepath.Base(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 { diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 1cf5f9366..e2bdf09fe 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -181,8 +181,8 @@ func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, accounts.Accou if err != nil { return nil, accounts.Account{}, err } - a := accounts.Account{Address: key.Address, URL: ks.JoinPath(keyFileName(key.Address))} - if err := ks.StoreKey(a.URL, 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 index ce4e87ce9..a01ff17e3 100644 --- a/accounts/keystore/keystore.go +++ b/accounts/keystore/keystore.go @@ -49,6 +49,9 @@ var ( // 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 @@ -130,22 +133,21 @@ func (ks *KeyStore) Wallets() []accounts.Wallet { // 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 - ks.mu.Lock() - 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() < account.URL { + 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() > account.URL { + 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}) @@ -242,7 +244,7 @@ func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error { // 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) + err = os.Remove(a.URL.Path) if err == nil { ks.cache.delete(a) ks.refreshWallets() @@ -377,7 +379,7 @@ func (ks *KeyStore) getDecryptedKey(a accounts.Account, auth string) (accounts.A if err != nil { return a, nil, err } - key, err := ks.storage.GetKey(a.Address, a.URL, auth) + key, err := ks.storage.GetKey(a.Address, a.URL.Path, auth) return a, key, err } @@ -453,8 +455,8 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco } func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) { - a := accounts.Account{Address: key.Address, URL: ks.storage.JoinPath(keyFileName(key.Address))} - if err := ks.storage.StoreKey(a.URL, key, passphrase); err != nil { + 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) @@ -468,7 +470,7 @@ func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) if err != nil { return err } - return ks.storage.StoreKey(a.URL, key, newPassphrase) + return ks.storage.StoreKey(a.URL.Path, key, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores diff --git a/accounts/keystore/keystore_plain_test.go b/accounts/keystore/keystore_plain_test.go index dcb2ab901..8c0eb52ea 100644 --- a/accounts/keystore/keystore_plain_test.go +++ b/accounts/keystore/keystore_plain_test.go @@ -52,7 +52,7 @@ func TestKeyStorePlain(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.URL, pass) + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } @@ -73,7 +73,7 @@ func TestKeyStorePassphrase(t *testing.T) { if err != nil { t.Fatal(err) } - k2, err := ks.GetKey(k1.Address, account.URL, pass) + k2, err := ks.GetKey(k1.Address, account.URL.Path, pass) if err != nil { t.Fatal(err) } @@ -94,7 +94,7 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = ks.GetKey(k1.Address, account.URL, "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) } } @@ -115,7 +115,7 @@ 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.URL, dir) { + if !strings.HasPrefix(account.URL.Path, dir) { t.Errorf("imported account file not in keystore directory: %q", account.URL) } } diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 6b7170a2f..1f1935be6 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -41,10 +41,10 @@ func TestKeyStore(t *testing.T) { if err != nil { t.Fatal(err) } - if !strings.HasPrefix(a.URL, dir) { + 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) + stat, err := os.Stat(a.URL.Path) if err != nil { t.Fatalf("account file %s doesn't exist (%v)", a.URL, err) } @@ -60,7 +60,7 @@ func TestKeyStore(t *testing.T) { if err := ks.Delete(a, "bar"); err != nil { t.Errorf("Delete error: %v", err) } - if common.FileExist(a.URL) { + if common.FileExist(a.URL.Path) { t.Errorf("account file %s should be gone after Delete", a.URL) } if ks.HasAddress(a.Address) { @@ -286,7 +286,7 @@ func TestWalletNotifications(t *testing.T) { // 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++ { + for i := 0; i < 256; 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 @@ -349,8 +349,6 @@ func TestWalletNotifications(t *testing.T) { } } } - // Sleep a bit to avoid same-timestamp keyfiles - time.Sleep(10 * time.Millisecond) } } diff --git a/accounts/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go index d92926478..7d5507a4f 100644 --- a/accounts/keystore/keystore_wallet.go +++ b/accounts/keystore/keystore_wallet.go @@ -30,20 +30,21 @@ type keystoreWallet struct { keystore *KeyStore // Keystore where the account originates from } -// Type implements accounts.Wallet, returning the textual type of the wallet. -func (w *keystoreWallet) Type() string { - return "secret-storage" -} - // URL implements accounts.Wallet, returning the URL of the account within. -func (w *keystoreWallet) URL() string { +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 { - return "Open" + 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 @@ -63,7 +64,7 @@ func (w *keystoreWallet) Accounts() []accounts.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 == "" || account.URL == w.account.URL) + 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 @@ -81,7 +82,7 @@ func (w *keystoreWallet) SignHash(account accounts.Account, hash []byte) ([]byte if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -97,7 +98,7 @@ func (w *keystoreWallet) SignTx(account accounts.Account, tx *types.Transaction, if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -111,7 +112,7 @@ func (w *keystoreWallet) SignHashWithPassphrase(account accounts.Account, passph if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign @@ -125,7 +126,7 @@ func (w *keystoreWallet) SignTxWithPassphrase(account accounts.Account, passphra if account.Address != w.account.Address { return nil, accounts.ErrUnknownAccount } - if account.URL != "" && account.URL != w.account.URL { + if account.URL != (accounts.URL{}) && account.URL != w.account.URL { return nil, accounts.ErrUnknownAccount } // Account seems valid, request the keystore to sign diff --git a/accounts/keystore/presale.go b/accounts/keystore/presale.go index 948320e0a..5b883c45f 100644 --- a/accounts/keystore/presale.go +++ b/accounts/keystore/presale.go @@ -38,8 +38,8 @@ func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (accou return accounts.Account{}, nil, err } key.Id = uuid.NewRandom() - a := accounts.Account{Address: key.Address, URL: keyStore.JoinPath(keyFileName(key.Address))} - err = keyStore.StoreKey(a.URL, 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/manager.go b/accounts/manager.go index 0822500eb..12a5bfcd9 100644 --- a/accounts/manager.go +++ b/accounts/manager.go @@ -134,8 +134,12 @@ 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() == url { + if wallet.URL() == parsed { return wallet, nil } } @@ -169,7 +173,7 @@ func (am *Manager) Subscribe(sink chan<- WalletEvent) event.Subscription { // 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() >= wallet.URL() }) + 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 @@ -183,7 +187,7 @@ func merge(slice []Wallet, wallets ...Wallet) []Wallet { // 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() >= wallet.URL() }) + 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 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 . + +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 index ebd6fddb4..bd397249f 100644 --- a/accounts/usbwallet/ledger_hub.go +++ b/accounts/usbwallet/ledger_hub.go @@ -32,6 +32,9 @@ import ( "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 @@ -124,23 +127,24 @@ func (hub *LedgerHub) refreshWallets() { for i := 0; i < len(devIDs); i++ { devID, busID := devIDs[i], busIDs[i] - url := fmt.Sprintf("ledger://%03d:%03d", busID>>8, busID&0xff) + + url := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)} // Drop wallets while they were in front of the next account - for len(hub.wallets) > 0 && hub.wallets[0].URL() < url { + for len(hub.wallets) > 0 && hub.wallets[0].URL().Cmp(url) < 0 { events = append(events, accounts.WalletEvent{Wallet: hub.wallets[0], Arrive: false}) hub.wallets = hub.wallets[1:] } // If there are no more wallets or the account is before the next, wrap new wallet - if len(hub.wallets) == 0 || hub.wallets[0].URL() > url { - wallet := &ledgerWallet{context: hub.ctx, hardwareID: devID, locationID: busID, url: url} + 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 account is the same as the first wallet, keep it - if hub.wallets[0].URL() == url { + if hub.wallets[0].URL().Cmp(url) == 0 { wallets = append(wallets, hub.wallets[0]) hub.wallets = hub.wallets[1:] continue diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index 35e671c46..f712a503f 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -72,10 +72,10 @@ const ( // 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 string // Textual URL uniquely identifying this wallet + 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 @@ -90,14 +90,9 @@ type ledgerWallet struct { lock sync.RWMutex } -// Type implements accounts.Wallet, returning the textual type of the wallet. -func (w *ledgerWallet) Type() string { - return "ledger" -} - // URL implements accounts.Wallet, returning the URL of the Ledger device. -func (w *ledgerWallet) URL() string { - return w.url +func (w *ledgerWallet) URL() accounts.URL { + return *w.url } // Status implements accounts.Wallet, always whether the Ledger is opened, closed @@ -113,9 +108,9 @@ func (w *ledgerWallet) Status() string { return "Closed" } if w.version == [3]byte{0, 0, 0} { - return "Ethereum app not started" + return "Ethereum app offline" } - return fmt.Sprintf("Ethereum app v%d.%d.%d", w.version[0], w.version[1], w.version[2]) + return fmt.Sprintf("Ethereum app v%d.%d.%d online", w.version[0], w.version[1], w.version[2]) } // Open implements accounts.Wallet, attempting to open a USB connection to the @@ -309,7 +304,7 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) { } account := accounts.Account{ Address: address, - URL: fmt.Sprintf("%s/%s", w.url, path), + URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, } // If pinning was requested, track the account if pin { -- cgit v1.2.3 From 205ea9580215cca4093dff22ec61222bc3a6ff96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Wed, 8 Feb 2017 20:25:52 +0200 Subject: accounts, cmd, internal, node: implement HD wallet self-derivation --- accounts/accounts.go | 15 +++- accounts/hd.go | 130 +++++++++++++++++++++++++++++ accounts/hd_test.go | 79 ++++++++++++++++++ accounts/keystore/keystore_wallet.go | 7 +- accounts/usbwallet/ledger_test.go | 77 ------------------ accounts/usbwallet/ledger_wallet.go | 154 +++++++++++++++++++++++++++-------- 6 files changed, 347 insertions(+), 115 deletions(-) create mode 100644 accounts/hd.go create mode 100644 accounts/hd_test.go delete mode 100644 accounts/usbwallet/ledger_test.go (limited to 'accounts') diff --git a/accounts/accounts.go b/accounts/accounts.go index 6c9e0bbce..640de5220 100644 --- a/accounts/accounts.go +++ b/accounts/accounts.go @@ -20,6 +20,7 @@ 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" @@ -71,7 +72,19 @@ type Wallet interface { // 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 string, pin bool) (Account, error) + 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. // 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 . + +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 . + +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/keystore/keystore_wallet.go b/accounts/keystore/keystore_wallet.go index 7d5507a4f..7165d2821 100644 --- a/accounts/keystore/keystore_wallet.go +++ b/accounts/keystore/keystore_wallet.go @@ -19,6 +19,7 @@ package keystore import ( "math/big" + ethereum "github.com/ethereum/go-ethereum" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/core/types" ) @@ -69,10 +70,14 @@ func (w *keystoreWallet) Contains(account accounts.Account) bool { // 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 string, pin bool) (accounts.Account, error) { +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 diff --git a/accounts/usbwallet/ledger_test.go b/accounts/usbwallet/ledger_test.go deleted file mode 100644 index 16a1e0b3f..000000000 --- a/accounts/usbwallet/ledger_test.go +++ /dev/null @@ -1,77 +0,0 @@ -// 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 . - -// +build !ios - -package usbwallet - -/* -func TestLedgerHub(t *testing.T) { - glog.SetV(6) - glog.SetToStderr(true) - - // Create a USB hub watching for Ledger devices - hub, err := NewLedgerHub() - if err != nil { - t.Fatalf("Failed to create Ledger hub: %v", err) - } - defer hub.Close() - - // Wait for events :P - time.Sleep(time.Minute) -} -*/ -/* -func TestLedger(t *testing.T) { - // Create a USB context to access devices through - ctx, err := usb.NewContext() - defer ctx.Close() - ctx.Debug(6) - - // List all of the Ledger wallets - wallets, err := findLedgerWallets(ctx) - if err != nil { - t.Fatalf("Failed to list Ledger wallets: %v", err) - } - // Retrieve the address from every one of them - for _, wallet := range wallets { - // Retrieve the version of the wallet app - ver, err := wallet.Version() - if err != nil { - t.Fatalf("Failed to retrieve wallet version: %v", err) - } - fmt.Printf("Ledger version: %s\n", ver) - - // Retrieve the address of the wallet - addr, err := wallet.Address() - if err != nil { - t.Fatalf("Failed to retrieve wallet address: %v", err) - } - fmt.Printf("Ledger address: %x\n", addr) - - // Try to sign a transaction with the wallet - unsigned := types.NewTransaction(1, common.HexToAddress("0xbabababababababababababababababababababa"), common.Ether, big.NewInt(20000), common.Shannon, nil) - signed, err := wallet.Sign(unsigned) - if err != nil { - t.Fatalf("Failed to sign transactions: %v", err) - } - signer, err := types.Sender(types.NewEIP155Signer(big.NewInt(1)), signed) - if err != nil { - t.Fatalf("Failed to recover signer: %v", err) - } - fmt.Printf("Ledger signature by: %x\n", signer) - } -}*/ diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index f712a503f..0481a8990 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -29,11 +29,10 @@ import ( "fmt" "io" "math/big" - "strconv" - "strings" "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" @@ -41,10 +40,15 @@ import ( "github.com/ethereum/go-ethereum/logger/glog" "github.com/ethereum/go-ethereum/rlp" "github.com/karalabe/gousb/usb" + "golang.org/x/net/context" ) -// ledgerDerivationPath is the base derivation parameters used by the wallet. -var ledgerDerivationPath = []uint32{0x80000000 + 44, 0x80000000 + 60, 0x80000000 + 0, 0} +// 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 @@ -82,9 +86,15 @@ type ledgerWallet struct { 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) - accounts []accounts.Account // List of derive accounts pinned on the Ledger - paths map[common.Address][]uint32 // Known derivation paths for signing operations + version [3]byte // Current version of the Ledger Ethereum app (zero if app is offline) + accounts []accounts.Account // List of derive accounts pinned on the Ledger + paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations + + selfDeriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery + selfDeriveNextAddr common.Address // Next derived account address for auto-discovery + selfDerivePrevZero common.Address // Last zero-address where auto-discovery stopped + selfDeriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with + selfDeriveTime time.Time // Timestamp of the last self-derivation to avoid thrashing quit chan chan error lock sync.RWMutex @@ -107,12 +117,17 @@ func (w *ledgerWallet) Status() string { if w.device == nil { return "Closed" } - if w.version == [3]byte{0, 0, 0} { + 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. +func (w *ledgerWallet) offline() bool { + return w.version == [3]byte{0, 0, 0} +} + // 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 // is silently discarded. @@ -176,13 +191,13 @@ func (w *ledgerWallet) Open(passphrase string) error { // Wallet seems to be successfully opened, guess if the Ethereum app is running w.device, w.input, w.output = device, input, output - w.paths = make(map[common.Address][]uint32) + w.paths = make(map[common.Address]accounts.DerivationPath) w.quit = make(chan chan error) defer func() { go w.heartbeat() }() - if _, err := w.deriveAddress(ledgerDerivationPath); err != nil { + if _, err := w.deriveAddress(accounts.DefaultBaseDerivationPath); err != nil { // Ethereum app is not running, nothing more to do, return return nil } @@ -209,7 +224,7 @@ func (w *ledgerWallet) heartbeat() { case errc = <-w.quit: // Termination requested continue - case <-time.After(time.Second): + case <-time.After(ledgerHeartbeatCycle): // Heartbeat time } // Execute a tiny data exchange to see responsiveness @@ -242,16 +257,86 @@ func (w *ledgerWallet) Close() error { return err } w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil + w.version = [3]byte{} return herr // If all went well, return any health-check errors } // Accounts implements accounts.Wallet, returning the list of accounts pinned to -// the Ledger hardware wallet. +// 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 { - w.lock.RLock() - defer w.lock.RUnlock() + w.lock.Lock() + defer w.lock.Unlock() + + // If the wallet is offline, there are no accounts to return + if w.offline() { + return nil + } + // If no self derivation is done (or throttled), return the current accounts + if w.selfDeriveChain == nil || time.Since(w.selfDeriveTime) < ledgerSelfDeriveThrottling { + cpy := make([]accounts.Account, len(w.accounts)) + copy(cpy, w.accounts) + return cpy + } + // Self derivation requested, try to expand our account list + ctx := context.Background() + for empty := false; !empty; { + // Retrieve the next derived Ethereum account + var err error + if w.selfDeriveNextAddr == (common.Address{}) { + w.selfDeriveNextAddr, err = w.deriveAddress(w.selfDeriveNextPath) + if err != nil { + // Derivation failed, disable auto discovery + glog.V(logger.Warn).Infof("self-derivation failed: %v", err) + w.selfDeriveChain = nil + break + } + } + // Check the account's status against the current chain state + balance, err := w.selfDeriveChain.BalanceAt(ctx, w.selfDeriveNextAddr, nil) + if err != nil { + glog.V(logger.Warn).Infof("self-derivation balance retrieval failed: %v", err) + w.selfDeriveChain = nil + break + } + nonce, err := w.selfDeriveChain.NonceAt(ctx, w.selfDeriveNextAddr, nil) + if err != nil { + glog.V(logger.Warn).Infof("self-derivation nonce retrieval failed: %v", err) + w.selfDeriveChain = nil + break + } + // If the next account is empty, stop self-derivation, but add it nonetheless + if balance.BitLen() == 0 && nonce == 0 { + w.selfDerivePrevZero = w.selfDeriveNextAddr + empty = true + } + // We've just self-derived a new non-zero account, start tracking it + path := make(accounts.DerivationPath, len(w.selfDeriveNextPath)) + copy(path[:], w.selfDeriveNextPath[:]) + account := accounts.Account{ + Address: w.selfDeriveNextAddr, + URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, + } + _, known := w.paths[w.selfDeriveNextAddr] + if !known || (!empty && w.selfDeriveNextAddr == w.selfDerivePrevZero) { + // Either fully new account, or previous zero. Report discovery either way + glog.V(logger.Info).Infof("%s discovered %s (balance %d, nonce %d) at %s", w.url.String(), w.selfDeriveNextAddr.Hex(), balance, nonce, path) + } + if !known { + w.accounts = append(w.accounts, account) + w.paths[w.selfDeriveNextAddr] = path + } + // Fetch the next potential account + if !empty { + w.selfDeriveNextAddr = common.Address{} + w.selfDeriveNextPath[len(w.selfDeriveNextPath)-1]++ + } + } + w.selfDeriveTime = time.Now() + + // Return whatever account list we ended up with cpy := make([]accounts.Account, len(w.accounts)) copy(cpy, w.accounts) return cpy @@ -271,34 +356,16 @@ func (w *ledgerWallet) Contains(account accounts.Account) bool { // 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 string, pin bool) (accounts.Account, error) { +func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { w.lock.Lock() defer w.lock.Unlock() // If the wallet is closed, or the Ethereum app doesn't run, abort - if w.device == nil || w.version == [3]byte{0, 0, 0} { + if w.device == nil || w.offline() { return accounts.Account{}, accounts.ErrWalletClosed } - // All seems fine, convert the user derivation path to Ledger representation - path = strings.TrimPrefix(path, "/") - - parts := strings.Split(path, "/") - lpath := make([]uint32, len(parts)) - for i, part := range parts { - // Handle hardened paths - if strings.HasSuffix(part, "'") { - lpath[i] = 0x80000000 - part = strings.TrimSuffix(part, "'") - } - // Handle the non hardened component - val, err := strconv.Atoi(part) - if err != nil { - return accounts.Account{}, fmt.Errorf("path element %d: %v", i, err) - } - lpath[i] += uint32(val) - } // Try to derive the actual account and update it's URL if succeeful - address, err := w.deriveAddress(lpath) + address, err := w.deriveAddress(path) if err != nil { return accounts.Account{}, err } @@ -310,12 +377,27 @@ func (w *ledgerWallet) Derive(path string, pin bool) (accounts.Account, error) { if pin { if _, ok := w.paths[address]; !ok { w.accounts = append(w.accounts, account) - w.paths[address] = lpath + 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.lock.Lock() + defer w.lock.Unlock() + + w.selfDeriveNextPath = make(accounts.DerivationPath, len(base)) + copy(w.selfDeriveNextPath[:], base[:]) + + w.selfDeriveNextAddr = common.Address{} + w.selfDeriveChain = 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) { -- cgit v1.2.3 From fb1984685562479bfe2b041ae98f901525d1d9d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 9 Feb 2017 03:28:22 +0200 Subject: accounts/usbwallet: Ledger teardown on health-check failure --- accounts/usbwallet/ledger_hub.go | 8 ++++---- accounts/usbwallet/ledger_wallet.go | 33 +++++++++++++++++++++++++++++---- 2 files changed, 33 insertions(+), 8 deletions(-) (limited to 'accounts') diff --git a/accounts/usbwallet/ledger_hub.go b/accounts/usbwallet/ledger_hub.go index bd397249f..ad5940cd4 100644 --- a/accounts/usbwallet/ledger_hub.go +++ b/accounts/usbwallet/ledger_hub.go @@ -130,12 +130,12 @@ func (hub *LedgerHub) refreshWallets() { url := accounts.URL{Scheme: LedgerScheme, Path: fmt.Sprintf("%03d:%03d", busID>>8, busID&0xff)} - // Drop wallets while they were in front of the next account - for len(hub.wallets) > 0 && hub.wallets[0].URL().Cmp(url) < 0 { + // 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 account is before the next, wrap new wallet + // 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} @@ -143,7 +143,7 @@ func (hub *LedgerHub) refreshWallets() { wallets = append(wallets, wallet) continue } - // If the account is the same as the first wallet, keep it + // 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:] diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index 0481a8990..4f848aebd 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -128,6 +128,15 @@ 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. +func (w *ledgerWallet) failed() bool { + w.lock.RLock() + defer w.lock.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 // is silently discarded. @@ -231,6 +240,8 @@ func (w *ledgerWallet) heartbeat() { w.lock.Lock() if err := w.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { w.failure = err + w.close() + fail = err } w.lock.Unlock() @@ -253,15 +264,29 @@ func (w *ledgerWallet) Close() error { w.lock.Lock() defer w.lock.Unlock() - if err := w.device.Close(); err != nil { + w.quit = nil + if err := w.close(); err != nil { return err } - w.device, w.input, w.output, w.paths, w.quit = nil, nil, nil, nil, nil - w.version = [3]byte{} - return herr // If all went well, return any health-check errors } +// close is the internal wallet closer that terminates the USB connection and +// resets all the fields to their defaults. It assumes the 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.version, w.paths = [3]byte{}, 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. -- cgit v1.2.3 From 26cd41f0c7a1d1e379f15117d3a62235f6c2d739 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Thu, 9 Feb 2017 14:39:26 +0200 Subject: accounts/usbwallet: make wallet responsive while Ledger is busy --- accounts/usbwallet/ledger_wallet.go | 460 ++++++++++++++++++++++++------------ 1 file changed, 309 insertions(+), 151 deletions(-) (limited to 'accounts') diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index 4f848aebd..b57a6f741 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -90,26 +90,47 @@ type ledgerWallet struct { accounts []accounts.Account // List of derive accounts pinned on the Ledger paths map[common.Address]accounts.DerivationPath // Known derivation paths for signing operations - selfDeriveNextPath accounts.DerivationPath // Next derivation path for account auto-discovery - selfDeriveNextAddr common.Address // Next derived account address for auto-discovery - selfDerivePrevZero common.Address // Last zero-address where auto-discovery stopped - selfDeriveChain ethereum.ChainStateReader // Blockchain state reader to discover used account with - selfDeriveTime time.Time // Timestamp of the last self-derivation to avoid thrashing - - quit chan chan error - lock sync.RWMutex + 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 + 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.lock.RLock() - defer w.lock.RUnlock() + 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) @@ -124,25 +145,29 @@ func (w *ledgerWallet) Status() string { } // 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.lock.RLock() - defer w.lock.RUnlock() + 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 -// is silently discarded. +// 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.lock.Lock() - defer w.lock.Unlock() + 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 { @@ -199,19 +224,26 @@ func (w *ledgerWallet) Open(passphrase string) error { } // 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.quit = make(chan chan error) + + 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.deriveAddress(accounts.DefaultBaseDerivationPath); err != nil { + if _, err = w.ledgerDerive(accounts.DefaultBaseDerivationPath); err != nil { // Ethereum app is not running, nothing more to do, return return nil } // Try to resolve the Ethereum app's version, will fail prior to v1.0.2 - if w.resolveVersion() != nil { + 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 @@ -222,57 +254,94 @@ func (w *ledgerWallet) Open(passphrase string) error { // - 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 - fail error + err error ) - for errc == nil && fail == nil { + for errc == nil && err == nil { // Wait until termination is requested or the heartbeat cycle arrives select { - case errc = <-w.quit: + case errc = <-w.healthQuit: // Termination requested continue case <-time.After(ledgerHeartbeatCycle): // Heartbeat time } // Execute a tiny data exchange to see responsiveness - w.lock.Lock() - if err := w.resolveVersion(); err == usb.ERROR_IO || err == usb.ERROR_NO_DEVICE { + 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() - - fail = err + w.stateLock.Unlock() } - w.lock.Unlock() + // Ignore uninteresting errors + err = nil } // In case of error, wait for termination - if fail != nil { - errc = <-w.quit + if err != nil { + glog.V(logger.Debug).Infof("%s health-check failed: %v", w.url.String(), err) + errc = <-w.healthQuit } - errc <- fail + errc <- err } // Close implements accounts.Wallet, closing the USB connection to the Ledger. func (w *ledgerWallet) Close() error { - // Terminate the health checks - errc := make(chan error) - w.quit <- errc - herr := <-errc // Save for later, we *must* close the USB + // 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.lock.Lock() - defer w.lock.Unlock() + w.stateLock.Lock() + defer w.stateLock.Unlock() + + w.healthQuit = nil + w.deriveQuit = nil + w.deriveReq = nil - w.quit = nil if err := w.close(); err != nil { return err } - return herr // If all went well, return any health-check errors + 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. It assumes the lock is held. +// 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 { @@ -282,97 +351,169 @@ func (w *ledgerWallet) close() error { err := w.device.Close() w.device, w.input, w.output = nil, nil, nil - w.version, w.paths = [3]byte{}, nil + w.version, w.accounts, w.paths = [3]byte{}, 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 +// 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 { - w.lock.Lock() - defer w.lock.Unlock() - - // If the wallet is offline, there are no accounts to return - if w.offline() { - return nil + // 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 } - // If no self derivation is done (or throttled), return the current accounts - if w.selfDeriveChain == nil || time.Since(w.selfDeriveTime) < ledgerSelfDeriveThrottling { - cpy := make([]accounts.Account, len(w.accounts)) - copy(cpy, w.accounts) - return cpy - } - // Self derivation requested, try to expand our account list - ctx := context.Background() - for empty := false; !empty; { - // Retrieve the next derived Ethereum account - var err error - if w.selfDeriveNextAddr == (common.Address{}) { - w.selfDeriveNextAddr, err = w.deriveAddress(w.selfDeriveNextPath) - if err != nil { - // Derivation failed, disable auto discovery - glog.V(logger.Warn).Infof("self-derivation failed: %v", err) - w.selfDeriveChain = nil - break - } - } - // Check the account's status against the current chain state - balance, err := w.selfDeriveChain.BalanceAt(ctx, w.selfDeriveNextAddr, nil) - if err != nil { - glog.V(logger.Warn).Infof("self-derivation balance retrieval failed: %v", err) - w.selfDeriveChain = nil - break + // 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 } - nonce, err := w.selfDeriveChain.NonceAt(ctx, w.selfDeriveNextAddr, nil) - if err != nil { - glog.V(logger.Warn).Infof("self-derivation nonce retrieval failed: %v", err) - w.selfDeriveChain = nil - break + // 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 } - // If the next account is empty, stop self-derivation, but add it nonetheless - if balance.BitLen() == 0 && nonce == 0 { - w.selfDerivePrevZero = w.selfDeriveNextAddr - empty = true + select { + case <-w.commsLock: + default: + w.stateLock.RUnlock() + reqc <- struct{}{} + continue } - // We've just self-derived a new non-zero account, start tracking it - path := make(accounts.DerivationPath, len(w.selfDeriveNextPath)) - copy(path[:], w.selfDeriveNextPath[:]) + // 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) - account := accounts.Account{ - Address: w.selfDeriveNextAddr, - URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, - } - _, known := w.paths[w.selfDeriveNextAddr] - if !known || (!empty && w.selfDeriveNextAddr == w.selfDerivePrevZero) { - // Either fully new account, or previous zero. Report discovery either way - glog.V(logger.Info).Infof("%s discovered %s (balance %d, nonce %d) at %s", w.url.String(), w.selfDeriveNextAddr.Hex(), balance, nonce, path) + // 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 %d, nonce %d) at %s", w.url.String(), nextAddr.Hex(), balance, nonce, path) + } + // Fetch the next potential account + if !empty { + nextAddr = common.Address{} + nextPath[len(nextPath)-1]++ + } } - if !known { - w.accounts = append(w.accounts, account) - w.paths[w.selfDeriveNextAddr] = path + // 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] + } } - // Fetch the next potential account - if !empty { - w.selfDeriveNextAddr = common.Address{} - w.selfDeriveNextPath[len(w.selfDeriveNextPath)-1]++ + // 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 + } } } - w.selfDeriveTime = time.Now() - - // Return whatever account list we ended up with - cpy := make([]accounts.Account, len(w.accounts)) - copy(cpy, w.accounts) - return cpy + // 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.lock.RLock() - defer w.lock.RUnlock() + w.stateLock.RLock() + defer w.stateLock.RUnlock() _, exists := w.paths[account.Address] return exists @@ -382,15 +523,20 @@ func (w *ledgerWallet) Contains(account accounts.Account) bool { // 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) { - w.lock.Lock() - defer w.lock.Unlock() + // Try to derive the actual account and update its URL if successful + w.stateLock.RLock() // Avoid device disappearing during derivation - // If the wallet is closed, or the Ethereum app doesn't run, abort if w.device == nil || w.offline() { + w.stateLock.RUnlock() return accounts.Account{}, accounts.ErrWalletClosed } - // Try to derive the actual account and update it's URL if succeeful - address, err := w.deriveAddress(path) + <-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 } @@ -398,12 +544,16 @@ func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts. Address: address, URL: accounts.URL{Scheme: w.url.Scheme, Path: fmt.Sprintf("%s/%s", w.url.Path, path)}, } - // If pinning was requested, track the account - if pin { - if _, ok := w.paths[address]; !ok { - w.accounts = append(w.accounts, account) - w.paths[address] = 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 } @@ -413,14 +563,14 @@ func (w *ledgerWallet) Derive(path accounts.DerivationPath, pin bool) (accounts. // 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.lock.Lock() - defer w.lock.Unlock() + w.stateLock.Lock() + defer w.stateLock.Unlock() - w.selfDeriveNextPath = make(accounts.DerivationPath, len(base)) - copy(w.selfDeriveNextPath[:], base[:]) + w.deriveNextPath = make(accounts.DerivationPath, len(base)) + copy(w.deriveNextPath[:], base[:]) - w.selfDeriveNextAddr = common.Address{} - w.selfDeriveChain = chain + w.deriveNextAddr = common.Address{} + w.deriveChain = chain } // SignHash implements accounts.Wallet, however signing arbitrary data is not @@ -437,9 +587,13 @@ func (w *ledgerWallet) SignHash(acc accounts.Account, hash []byte) ([]byte, erro // 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.lock.Lock() - defer w.lock.Unlock() + 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 { @@ -447,10 +601,13 @@ func (w *ledgerWallet) SignTx(account accounts.Account, tx *types.Transaction, c } // 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]) + 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]) } - return w.sign(path, account.Address, tx, chainID) + // 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 @@ -467,8 +624,8 @@ func (w *ledgerWallet) SignTxWithPassphrase(account accounts.Account, passphrase return w.SignTx(account, tx, chainID) } -// resolveVersion retrieves the current version of the Ethereum wallet app running -// on the Ledger wallet and caches it for future reference. +// ledgerVersion retrieves the current version of the Ethereum wallet app running +// on the Ledger wallet. // // The version retrieval protocol is defined as follows: // @@ -484,21 +641,22 @@ func (w *ledgerWallet) SignTxWithPassphrase(account accounts.Account, passphrase // Application major version | 1 byte // Application minor version | 1 byte // Application patch version | 1 byte -func (wallet *ledgerWallet) resolveVersion() error { +func (w *ledgerWallet) ledgerVersion() ([3]byte, error) { // Send the request and wait for the response - reply, err := wallet.exchange(ledgerOpGetConfiguration, 0, 0, nil) + reply, err := w.ledgerExchange(ledgerOpGetConfiguration, 0, 0, nil) if err != nil { - return err + return [3]byte{}, err } if len(reply) != 4 { - return errors.New("reply not of correct size") + return [3]byte{}, errors.New("reply not of correct size") } // Cache the version for future reference - copy(wallet.version[:], reply[1:]) - return nil + var version [3]byte + copy(version[:], reply[1:]) + return version, nil } -// deriveAddress retrieves the currently active Ethereum address from a Ledger +// ledgerDerive retrieves the currently active Ethereum address from a Ledger // wallet at the specified derivation path. // // The address derivation protocol is defined as follows: @@ -529,7 +687,7 @@ func (wallet *ledgerWallet) resolveVersion() error { // Ethereum address length | 1 byte // Ethereum address | 40 bytes hex ascii // Chain code if requested | 32 bytes -func (w *ledgerWallet) deriveAddress(derivationPath []uint32) (common.Address, error) { +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)) @@ -537,7 +695,7 @@ func (w *ledgerWallet) deriveAddress(derivationPath []uint32) (common.Address, e binary.BigEndian.PutUint32(path[1+4*i:], component) } // Send the request and wait for the response - reply, err := w.exchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) + reply, err := w.ledgerExchange(ledgerOpRetrieveAddress, ledgerP1DirectlyFetchAddress, ledgerP2DiscardAddressChainCode, path) if err != nil { return common.Address{}, err } @@ -559,8 +717,8 @@ func (w *ledgerWallet) deriveAddress(derivationPath []uint32) (common.Address, e return address, nil } -// sign sends the transaction to the Ledger wallet, and waits for the user to -// confirm or deny the transaction. +// 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: // @@ -593,7 +751,7 @@ func (w *ledgerWallet) deriveAddress(derivationPath []uint32) (common.Address, e // signature V | 1 byte // signature R | 32 bytes // signature S | 32 bytes -func (w *ledgerWallet) sign(derivationPath []uint32, address common.Address, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { +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.Minute @@ -632,7 +790,7 @@ func (w *ledgerWallet) sign(derivationPath []uint32, address common.Address, tx chunk = len(payload) } // Send the chunk over, ensuring it's processed correctly - reply, err = w.exchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) + reply, err = w.ledgerExchange(ledgerOpSignTransaction, op, 0, payload[:chunk]) if err != nil { return nil, err } @@ -669,8 +827,8 @@ func (w *ledgerWallet) sign(derivationPath []uint32, address common.Address, tx return signed, nil } -// exchange performs a data exchange with the Ledger wallet, sending it a message -// and retrieving the response. +// 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: // @@ -702,7 +860,7 @@ func (w *ledgerWallet) sign(derivationPath []uint32, address common.Address, tx // APDU P2 | 1 byte // APDU length | 1 byte // Optional APDU data | arbitrary -func (w *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { +func (w *ledgerWallet) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerParam2, data []byte) ([]byte, error) { // Construct the message payload, possibly split into multiple chunks var chunks [][]byte for left := data; len(left) > 0 || len(chunks) == 0; { @@ -731,7 +889,7 @@ func (w *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerP msg := append(header, chunk...) // Send over to the device - if glog.V(logger.Core) { + if glog.V(logger.Detail) { glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, msg) } if _, err := w.input.Write(msg); err != nil { @@ -746,7 +904,7 @@ func (w *ledgerWallet) exchange(opcode ledgerOpcode, p1 ledgerParam1, p2 ledgerP if _, err := io.ReadFull(w.output, chunk); err != nil { return nil, err } - if glog.V(logger.Core) { + if glog.V(logger.Detail) { glog.Infof("<- %03d.%03d: %x", w.device.Bus, w.device.Address, chunk) } // Make sure the transport header matches -- cgit v1.2.3 From c7022c1a0c2aa4c0326129ef483b27bcd6c1262d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Fri, 10 Feb 2017 14:10:56 +0200 Subject: accounts/usbwallet: detect and report in Ledger is in browser mode --- accounts/usbwallet/ledger_wallet.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) (limited to 'accounts') diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index b57a6f741..6044b9caf 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -74,6 +74,11 @@ const ( 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 @@ -87,6 +92,7 @@ type ledgerWallet struct { 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 @@ -138,6 +144,9 @@ func (w *ledgerWallet) Status() string { if w.device == nil { return "Closed" } + if w.browser { + return "Ethereum app in browser mode" + } if w.offline() { return "Ethereum app offline" } @@ -239,7 +248,10 @@ func (w *ledgerWallet) Open(passphrase string) error { }() if _, err = w.ledgerDerive(accounts.DefaultBaseDerivationPath); err != nil { - // Ethereum app is not running, nothing more to do, return + // 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 @@ -351,7 +363,8 @@ func (w *ledgerWallet) close() error { err := w.device.Close() w.device, w.input, w.output = nil, nil, nil - w.version, w.accounts, w.paths = [3]byte{}, nil, nil + w.browser, w.version = false, [3]byte{} + w.accounts, w.paths = nil, nil return err } @@ -463,7 +476,7 @@ func (w *ledgerWallet) selfDerive() { // 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 %d, nonce %d) at %s", w.url.String(), nextAddr.Hex(), balance, nonce, path) + 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 { @@ -909,7 +922,7 @@ func (w *ledgerWallet) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 l } // Make sure the transport header matches if chunk[0] != 0x01 || chunk[1] != 0x01 || chunk[2] != 0x05 { - return nil, fmt.Errorf("invalid reply header: %x", chunk[:3]) + return nil, errReplyInvalidHeader } // If it's the first chunk, retrieve the total message length if chunk[3] == 0x00 && chunk[4] == 0x00 { -- cgit v1.2.3 From e99c788155ddd754c73d2c81b6051dcbd42e6575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A9ter=20Szil=C3=A1gyi?= Date: Sat, 11 Feb 2017 17:02:00 +0200 Subject: accounts: ledger and HD review fixes - Handle a data race where a Ledger drops between list and open - Prolong Ledger tx confirmation window to 30 days from 1 minute - Simplify Ledger chainid-signature calculation and validation - Simplify Ledger USB APDU request chunking algorithm - Silence keystore account cache notifications for manual actions - Only enable self derivations if wallet open succeeds --- accounts/keystore/account_cache.go | 9 ----- accounts/keystore/account_cache_test.go | 17 +-------- accounts/keystore/keystore_test.go | 2 +- accounts/usbwallet/ledger_wallet.go | 68 +++++++++++++++++---------------- 4 files changed, 37 insertions(+), 59 deletions(-) (limited to 'accounts') diff --git a/accounts/keystore/account_cache.go b/accounts/keystore/account_cache.go index e2f826250..3fae3ef5b 100644 --- a/accounts/keystore/account_cache.go +++ b/accounts/keystore/account_cache.go @@ -113,11 +113,6 @@ func (ac *accountCache) add(newAccount accounts.Account) { copy(ac.all[i+1:], ac.all[i:]) ac.all[i] = newAccount ac.byAddr[newAccount.Address] = append(ac.byAddr[newAccount.Address], newAccount) - - select { - case ac.notify <- struct{}{}: - default: - } } // note: removed needs to be unique here (i.e. both File and Address must be set). @@ -131,10 +126,6 @@ func (ac *accountCache) delete(removed accounts.Account) { } else { ac.byAddr[removed.Address] = ba } - select { - case ac.notify <- struct{}{}: - default: - } } func removeAccount(slice []accounts.Account, elem accounts.Account) []accounts.Account { diff --git a/accounts/keystore/account_cache_test.go b/accounts/keystore/account_cache_test.go index 3e68351ea..554196321 100644 --- a/accounts/keystore/account_cache_test.go +++ b/accounts/keystore/account_cache_test.go @@ -140,7 +140,7 @@ func TestCacheInitialReload(t *testing.T) { } func TestCacheAddDeleteOrder(t *testing.T) { - cache, notify := newAccountCache("testdata/no-such-dir") + cache, _ := newAccountCache("testdata/no-such-dir") cache.watcher.running = true // prevent unexpected reloads accs := []accounts.Account{ @@ -176,20 +176,10 @@ func TestCacheAddDeleteOrder(t *testing.T) { for _, a := range accs { cache.add(a) } - select { - case <-notify: - default: - t.Fatalf("notifications didn't fire for adding new accounts") - } // Add some of them twice to check that they don't get reinserted. cache.add(accs[0]) cache.add(accs[2]) - select { - case <-notify: - t.Fatalf("notifications fired for adding existing accounts") - default: - } // Check that the account list is sorted by filename. wantAccounts := make([]accounts.Account, len(accs)) copy(wantAccounts, accs) @@ -213,11 +203,6 @@ func TestCacheAddDeleteOrder(t *testing.T) { } cache.delete(accounts.Account{Address: common.HexToAddress("fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e"), URL: accounts.URL{Scheme: KeyStoreScheme, Path: "something"}}) - select { - case <-notify: - default: - t.Fatalf("notifications didn't fire for deleting accounts") - } // Check content again after deletion. wantAccountsAfterDelete := []accounts.Account{ wantAccounts[1], diff --git a/accounts/keystore/keystore_test.go b/accounts/keystore/keystore_test.go index 1f1935be6..60f2606ee 100644 --- a/accounts/keystore/keystore_test.go +++ b/accounts/keystore/keystore_test.go @@ -286,7 +286,7 @@ func TestWalletNotifications(t *testing.T) { // 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 < 256; i++ { + 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 diff --git a/accounts/usbwallet/ledger_wallet.go b/accounts/usbwallet/ledger_wallet.go index 6044b9caf..a667f580a 100644 --- a/accounts/usbwallet/ledger_wallet.go +++ b/accounts/usbwallet/ledger_wallet.go @@ -192,6 +192,9 @@ func (w *ledgerWallet) Open(passphrase string) error { if err != nil { return err } + if len(devices) == 0 { + return accounts.ErrUnknownWallet + } // Device opened, attach to the input and output endpoints device := devices[0] @@ -767,7 +770,7 @@ func (w *ledgerWallet) ledgerDerive(derivationPath []uint32) (common.Address, er 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.Minute + 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)) @@ -823,7 +826,7 @@ func (w *ledgerWallet) ledgerSign(derivationPath []uint32, address common.Addres signer = new(types.HomesteadSigner) } else { signer = types.NewEIP155Signer(chainID) - signature[64] = (signature[64]-34)/2 - byte(chainID.Uint64()) + 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) @@ -875,45 +878,42 @@ func (w *ledgerWallet) ledgerSign(derivationPath []uint32, address common.Addres // 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 - var chunks [][]byte - for left := data; len(left) > 0 || len(chunks) == 0; { - // Create the chunk header - var chunk []byte - - if len(chunks) == 0 { - // The first chunk encodes the length and all the opcodes - chunk = []byte{0x00, 0x00, 0xe0, byte(opcode), byte(p1), byte(p2), byte(len(data))} - binary.BigEndian.PutUint16(chunk, uint16(5+len(data))) - } - // Append the data blob to the end of the chunk - space := 64 - len(chunk) - 5 // 5 == header size - if len(left) > space { - chunks, left = append(chunks, append(chunk, left[:space]...)), left[space:] - continue - } - chunks, left = append(chunks, append(chunk, left...)), nil - } + 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 - for i, chunk := range chunks { - // Construct the new message to stream - header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended - binary.BigEndian.PutUint16(header[3:], uint16(i)) + header := []byte{0x01, 0x01, 0x05, 0x00, 0x00} // Channel ID and command tag appended + chunk := make([]byte, 64) + space := len(chunk) - len(header) - msg := append(header, chunk...) + 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, msg) + glog.Infof("-> %03d.%03d: %x", w.device.Bus, w.device.Address, chunk) } - if _, err := w.input.Write(msg); err != nil { + 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 - chunk := make([]byte, 64) if _, err := io.ReadFull(w.output, chunk); err != nil { return nil, err } @@ -925,17 +925,19 @@ func (w *ledgerWallet) ledgerExchange(opcode ledgerOpcode, p1 ledgerParam1, p2 l 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]))) - chunk = chunk[7:] + payload = chunk[7:] } else { - chunk = chunk[5:] + payload = chunk[5:] } // Append to the reply and stop when filled up - if left := cap(reply) - len(reply); left > len(chunk) { - reply = append(reply, chunk...) + if left := cap(reply) - len(reply); left > len(payload) { + reply = append(reply, payload...) } else { - reply = append(reply, chunk[:left]...) + reply = append(reply, payload[:left]...) break } } -- cgit v1.2.3