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 -- cmd/geth/accountcmd.go | 42 ++- cmd/geth/accountcmd_test.go | 6 +- cmd/geth/main.go | 6 +- cmd/gethrpctest/main.go | 8 +- cmd/swarm/main.go | 22 +- cmd/utils/flags.go | 20 +- eth/backend.go | 14 +- internal/ethapi/api.go | 75 +++-- internal/guide/guide_test.go | 35 +- mobile/accounts.go | 99 +++--- mobile/android_test.go | 34 +- mobile/types.go | 5 + node/config.go | 13 +- 75 files changed, 3015 insertions(+), 2635 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 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() {} diff --git a/cmd/geth/accountcmd.go b/cmd/geth/accountcmd.go index 237af99eb..3a0eb13e8 100644 --- a/cmd/geth/accountcmd.go +++ b/cmd/geth/accountcmd.go @@ -21,6 +21,7 @@ import ( "io/ioutil" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/console" "github.com/ethereum/go-ethereum/crypto" @@ -181,30 +182,30 @@ nodes. func accountList(ctx *cli.Context) error { stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) for i, acct := range stack.AccountManager().Accounts() { - fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.File) + fmt.Printf("Account #%d: {%x} %s\n", i, acct.Address, acct.URL) } return nil } // tries unlocking the specified account a few times. -func unlockAccount(ctx *cli.Context, accman *accounts.Manager, address string, i int, passwords []string) (accounts.Account, string) { - account, err := utils.MakeAddress(accman, address) +func unlockAccount(ctx *cli.Context, ks *keystore.KeyStore, address string, i int, passwords []string) (accounts.Account, string) { + account, err := utils.MakeAddress(ks, address) if err != nil { utils.Fatalf("Could not list accounts: %v", err) } for trials := 0; trials < 3; trials++ { prompt := fmt.Sprintf("Unlocking account %s | Attempt %d/%d", address, trials+1, 3) password := getPassPhrase(prompt, false, i, passwords) - err = accman.Unlock(account, password) + err = ks.Unlock(account, password) if err == nil { glog.V(logger.Info).Infof("Unlocked account %x", account.Address) return account, password } - if err, ok := err.(*accounts.AmbiguousAddrError); ok { + if err, ok := err.(*keystore.AmbiguousAddrError); ok { glog.V(logger.Info).Infof("Unlocked account %x", account.Address) - return ambiguousAddrRecovery(accman, err, password), password + return ambiguousAddrRecovery(ks, err, password), password } - if err != accounts.ErrDecrypt { + if err != keystore.ErrDecrypt { // No need to prompt again if the error is not decryption-related. break } @@ -244,15 +245,15 @@ func getPassPhrase(prompt string, confirmation bool, i int, passwords []string) return password } -func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrError, auth string) accounts.Account { +func ambiguousAddrRecovery(ks *keystore.KeyStore, err *keystore.AmbiguousAddrError, auth string) accounts.Account { fmt.Printf("Multiple key files exist for address %x:\n", err.Addr) for _, a := range err.Matches { - fmt.Println(" ", a.File) + fmt.Println(" ", a.URL) } fmt.Println("Testing your passphrase against all of them...") var match *accounts.Account for _, a := range err.Matches { - if err := am.Unlock(a, auth); err == nil { + if err := ks.Unlock(a, auth); err == nil { match = &a break } @@ -260,11 +261,11 @@ func ambiguousAddrRecovery(am *accounts.Manager, err *accounts.AmbiguousAddrErro if match == nil { utils.Fatalf("None of the listed files could be unlocked.") } - fmt.Printf("Your passphrase unlocked %s\n", match.File) + fmt.Printf("Your passphrase unlocked %s\n", match.URL) fmt.Println("In order to avoid this warning, you need to remove the following duplicate key files:") for _, a := range err.Matches { if a != *match { - fmt.Println(" ", a.File) + fmt.Println(" ", a.URL) } } return *match @@ -275,7 +276,8 @@ func accountCreate(ctx *cli.Context) error { stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) password := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - account, err := stack.AccountManager().NewAccount(password) + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + account, err := ks.NewAccount(password) if err != nil { utils.Fatalf("Failed to create account: %v", err) } @@ -290,9 +292,11 @@ func accountUpdate(ctx *cli.Context) error { utils.Fatalf("No accounts specified to update") } stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) - account, oldPassword := unlockAccount(ctx, stack.AccountManager(), ctx.Args().First(), 0, nil) + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + + account, oldPassword := unlockAccount(ctx, ks, ctx.Args().First(), 0, nil) newPassword := getPassPhrase("Please give a new password. Do not forget this password.", true, 0, nil) - if err := stack.AccountManager().Update(account, oldPassword, newPassword); err != nil { + if err := ks.Update(account, oldPassword, newPassword); err != nil { utils.Fatalf("Could not update the account: %v", err) } return nil @@ -310,7 +314,9 @@ func importWallet(ctx *cli.Context) error { stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) passphrase := getPassPhrase("", false, 0, utils.MakePasswordList(ctx)) - acct, err := stack.AccountManager().ImportPreSaleKey(keyJson, passphrase) + + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + acct, err := ks.ImportPreSaleKey(keyJson, passphrase) if err != nil { utils.Fatalf("%v", err) } @@ -329,7 +335,9 @@ func accountImport(ctx *cli.Context) error { } stack := utils.MakeNode(ctx, clientIdentifier, gitCommit) passphrase := getPassPhrase("Your new account is locked with a password. Please give a password. Do not forget this password.", true, 0, utils.MakePasswordList(ctx)) - acct, err := stack.AccountManager().ImportECDSA(key, passphrase) + + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + acct, err := ks.ImportECDSA(key, passphrase) if err != nil { utils.Fatalf("Could not create the account: %v", err) } diff --git a/cmd/geth/accountcmd_test.go b/cmd/geth/accountcmd_test.go index 113df983e..7e03d7548 100644 --- a/cmd/geth/accountcmd_test.go +++ b/cmd/geth/accountcmd_test.go @@ -35,7 +35,7 @@ import ( func tmpDatadirWithKeystore(t *testing.T) string { datadir := tmpdir(t) keystore := filepath.Join(datadir, "keystore") - source := filepath.Join("..", "..", "accounts", "testdata", "keystore") + source := filepath.Join("..", "..", "accounts", "keystore", "testdata", "keystore") if err := cp.CopyAll(keystore, source); err != nil { t.Fatal(err) } @@ -230,7 +230,7 @@ Fatal: Failed to unlock account 0 (could not decrypt key with given passphrase) } func TestUnlockFlagAmbiguous(t *testing.T) { - store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes") geth := runGeth(t, "--keystore", store, "--nat", "none", "--nodiscover", "--dev", "--unlock", "f466859ead1932d743d622cb74fc058882e8648a", @@ -267,7 +267,7 @@ In order to avoid this warning, you need to remove the following duplicate key f } func TestUnlockFlagAmbiguousWrongPassword(t *testing.T) { - store := filepath.Join("..", "..", "accounts", "testdata", "dupes") + store := filepath.Join("..", "..", "accounts", "keystore", "testdata", "dupes") geth := runGeth(t, "--keystore", store, "--nat", "none", "--nodiscover", "--dev", "--unlock", "f466859ead1932d743d622cb74fc058882e8648a") diff --git a/cmd/geth/main.go b/cmd/geth/main.go index d7e4cc7b5..2c4963cac 100644 --- a/cmd/geth/main.go +++ b/cmd/geth/main.go @@ -25,6 +25,7 @@ import ( "strings" "time" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console" @@ -245,12 +246,13 @@ func startNode(ctx *cli.Context, stack *node.Node) { utils.StartNode(stack) // Unlock any account specifically requested - accman := stack.AccountManager() + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + passwords := utils.MakePasswordList(ctx) accounts := strings.Split(ctx.GlobalString(utils.UnlockedAccountFlag.Name), ",") for i, account := range accounts { if trimmed := strings.TrimSpace(account); trimmed != "" { - unlockAccount(ctx, accman, trimmed, i, passwords) + unlockAccount(ctx, ks, trimmed, i, passwords) } } // Start auxiliary services if enabled diff --git a/cmd/gethrpctest/main.go b/cmd/gethrpctest/main.go index 850bf8eb2..348eeebce 100644 --- a/cmd/gethrpctest/main.go +++ b/cmd/gethrpctest/main.go @@ -23,6 +23,7 @@ import ( "os" "os/signal" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/eth" "github.com/ethereum/go-ethereum/ethdb" @@ -99,17 +100,18 @@ func MakeSystemNode(privkey string, test *tests.BlockTest) (*node.Node, error) { return nil, err } // Create the keystore and inject an unlocked account if requested - accman := stack.AccountManager() + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) + if len(privkey) > 0 { key, err := crypto.HexToECDSA(privkey) if err != nil { return nil, err } - a, err := accman.ImportECDSA(key, "") + a, err := ks.ImportECDSA(key, "") if err != nil { return nil, err } - if err := accman.Unlock(a, ""); err != nil { + if err := ks.Unlock(a, ""); err != nil { return nil, err } } diff --git a/cmd/swarm/main.go b/cmd/swarm/main.go index 546c646f1..466b17f30 100644 --- a/cmd/swarm/main.go +++ b/cmd/swarm/main.go @@ -26,6 +26,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/cmd/utils" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/console" @@ -327,29 +328,36 @@ func getAccount(ctx *cli.Context, stack *node.Node) *ecdsa.PrivateKey { return key } // Otherwise try getting it from the keystore. - return decryptStoreAccount(stack.AccountManager(), keyid) + am := stack.AccountManager() + ks := am.Backend(keystore.BackendType).(*keystore.KeyStore) + + return decryptStoreAccount(ks, keyid) } -func decryptStoreAccount(accman *accounts.Manager, account string) *ecdsa.PrivateKey { +func decryptStoreAccount(ks *keystore.KeyStore, account string) *ecdsa.PrivateKey { var a accounts.Account var err error if common.IsHexAddress(account) { - a, err = accman.Find(accounts.Account{Address: common.HexToAddress(account)}) - } else if ix, ixerr := strconv.Atoi(account); ixerr == nil { - a, err = accman.AccountByIndex(ix) + a, err = ks.Find(accounts.Account{Address: common.HexToAddress(account)}) + } else if ix, ixerr := strconv.Atoi(account); ixerr == nil && ix > 0 { + if accounts := ks.Accounts(); len(accounts) > ix { + a = accounts[ix] + } else { + err = fmt.Errorf("index %d higher than number of accounts %d", ix, len(accounts)) + } } else { utils.Fatalf("Can't find swarm account key %s", account) } if err != nil { utils.Fatalf("Can't find swarm account key: %v", err) } - keyjson, err := ioutil.ReadFile(a.File) + keyjson, err := ioutil.ReadFile(a.URL) if err != nil { utils.Fatalf("Can't load swarm account key: %v", err) } for i := 1; i <= 3; i++ { passphrase := promptPassphrase(fmt.Sprintf("Unlocking swarm account %s [%d/3]", a.Address.Hex(), i)) - key, err := accounts.DecryptKey(keyjson, passphrase) + key, err := keystore.DecryptKey(keyjson, passphrase) if err == nil { return key.PrivateKey } diff --git a/cmd/utils/flags.go b/cmd/utils/flags.go index 9ba33df80..1196a64b5 100644 --- a/cmd/utils/flags.go +++ b/cmd/utils/flags.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/state" @@ -587,23 +588,27 @@ func MakeDatabaseHandles() int { // MakeAddress converts an account specified directly as a hex encoded string or // a key index in the key store to an internal account representation. -func MakeAddress(accman *accounts.Manager, account string) (accounts.Account, error) { +func MakeAddress(ks *keystore.KeyStore, account string) (accounts.Account, error) { // If the specified account is a valid address, return it if common.IsHexAddress(account) { return accounts.Account{Address: common.HexToAddress(account)}, nil } // Otherwise try to interpret the account as a keystore index index, err := strconv.Atoi(account) - if err != nil { + if err != nil || index < 0 { return accounts.Account{}, fmt.Errorf("invalid account address or index %q", account) } - return accman.AccountByIndex(index) + accs := ks.Accounts() + if len(accs) <= index { + return accounts.Account{}, fmt.Errorf("index %d higher than number of accounts %d", index, len(accs)) + } + return accs[index], nil } // MakeEtherbase retrieves the etherbase either from the directly specified // command line flags or from the keystore if CLI indexed. -func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { - accounts := accman.Accounts() +func MakeEtherbase(ks *keystore.KeyStore, ctx *cli.Context) common.Address { + accounts := ks.Accounts() if !ctx.GlobalIsSet(EtherbaseFlag.Name) && len(accounts) == 0 { glog.V(logger.Error).Infoln("WARNING: No etherbase set and no accounts found as default") return common.Address{} @@ -613,7 +618,7 @@ func MakeEtherbase(accman *accounts.Manager, ctx *cli.Context) common.Address { return common.Address{} } // If the specified etherbase is a valid address, return it - account, err := MakeAddress(accman, etherbase) + account, err := MakeAddress(ks, etherbase) if err != nil { Fatalf("Option %q: %v", EtherbaseFlag.Name, err) } @@ -721,9 +726,10 @@ func RegisterEthService(ctx *cli.Context, stack *node.Node, extra []byte) { if networks > 1 { Fatalf("The %v flags are mutually exclusive", netFlags) } + ks := stack.AccountManager().Backend(keystore.BackendType).(*keystore.KeyStore) ethConf := ð.Config{ - Etherbase: MakeEtherbase(stack.AccountManager(), ctx), + Etherbase: MakeEtherbase(ks, ctx), ChainConfig: MakeChainConfig(ctx, stack), FastSync: ctx.GlobalBool(FastSyncFlag.Name), LightMode: ctx.GlobalBool(LightModeFlag.Name), diff --git a/eth/backend.go b/eth/backend.go index af120cbad..004be7e26 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -361,15 +361,13 @@ func (s *Ethereum) ResetWithGenesisBlock(gb *types.Block) { } func (s *Ethereum) Etherbase() (eb common.Address, err error) { - eb = s.etherbase - if (eb == common.Address{}) { - firstAccount, err := s.AccountManager().AccountByIndex(0) - eb = firstAccount.Address - if err != nil { - return eb, fmt.Errorf("etherbase address must be explicitly specified") - } + if s.etherbase != (common.Address{}) { + return s.etherbase, nil + } + if accounts := s.AccountManager().Accounts(); len(accounts) > 0 { + return accounts[0].Address, nil } - return eb, nil + return common.Address{}, fmt.Errorf("etherbase address must be explicitly specified") } // set in js console via admin interface or wrapper from cli flags diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index 4c8e784c5..7962f8b17 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -28,6 +28,7 @@ import ( "github.com/ethereum/ethash" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/core" @@ -219,13 +220,18 @@ func (s *PrivateAccountAPI) ListAccounts() []common.Address { // NewAccount will create a new account and returns the address for the new account. func (s *PrivateAccountAPI) NewAccount(password string) (common.Address, error) { - acc, err := s.am.NewAccount(password) + acc, err := fetchKeystore(s.am).NewAccount(password) if err == nil { return acc.Address, nil } return common.Address{}, err } +// fetchKeystore retrives the encrypted keystore from the account manager. +func fetchKeystore(am *accounts.Manager) *keystore.KeyStore { + return am.Backend(keystore.BackendType).(*keystore.KeyStore) +} + // ImportRawKey stores the given hex encoded ECDSA key into the key directory, // encrypting it with the passphrase. func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (common.Address, error) { @@ -234,7 +240,7 @@ func (s *PrivateAccountAPI) ImportRawKey(privkey string, password string) (commo return common.Address{}, err } - acc, err := s.am.ImportECDSA(crypto.ToECDSA(hexkey), password) + acc, err := fetchKeystore(s.am).ImportECDSA(crypto.ToECDSA(hexkey), password) return acc.Address, err } @@ -251,13 +257,13 @@ func (s *PrivateAccountAPI) UnlockAccount(addr common.Address, password string, } else { d = time.Duration(*duration) * time.Second } - err := s.am.TimedUnlock(accounts.Account{Address: addr}, password, d) + err := fetchKeystore(s.am).TimedUnlock(accounts.Account{Address: addr}, password, d) return err == nil, err } // LockAccount will lock the account associated with the given address when it's unlocked. func (s *PrivateAccountAPI) LockAccount(addr common.Address) bool { - return s.am.Lock(addr) == nil + return fetchKeystore(s.am).Lock(addr) == nil } // SendTransaction will create a transaction from the given arguments and @@ -268,13 +274,16 @@ func (s *PrivateAccountAPI) SendTransaction(ctx context.Context, args SendTxArgs return common.Hash{}, err } tx := args.toTransaction() - signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number()) - signature, err := s.am.SignWithPassphrase(accounts.Account{Address: args.From}, passwd, signer.Hash(tx).Bytes()) + + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := s.am.SignTxWithPassphrase(accounts.Account{Address: args.From}, passwd, tx, chainID) if err != nil { return common.Hash{}, err } - - return submitTransaction(ctx, s.b, tx, signature) + return submitTransaction(ctx, s.b, signed) } // signHash is a helper function that calculates a hash for the given message that can be @@ -299,7 +308,7 @@ func signHash(data []byte) []byte { // // https://github.com/ethereum/go-ethereum/wiki/Management-APIs#personal_sign func (s *PrivateAccountAPI) Sign(ctx context.Context, data hexutil.Bytes, addr common.Address, passwd string) (hexutil.Bytes, error) { - signature, err := s.b.AccountManager().SignWithPassphrase(accounts.Account{Address: addr}, passwd, signHash(data)) + signature, err := s.b.AccountManager().SignHashWithPassphrase(accounts.Account{Address: addr}, passwd, signHash(data)) if err != nil { return nil, err } @@ -1023,13 +1032,11 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(txHash common.Hash) (ma // sign is a helper function that signs a transaction with the private key of the given address. func (s *PublicTransactionPoolAPI) sign(addr common.Address, tx *types.Transaction) (*types.Transaction, error) { - signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number()) - - signature, err := s.b.AccountManager().Sign(addr, signer.Hash(tx).Bytes()) - if err != nil { - return nil, err + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId } - return tx.WithSignature(signer, signature) + return s.b.AccountManager().SignTx(accounts.Account{Address: addr}, tx, chainID) } // SendTxArgs represents the arguments to sumbit a new transaction into the transaction pool. @@ -1076,27 +1083,19 @@ func (args *SendTxArgs) toTransaction() *types.Transaction { } // submitTransaction is a helper function that submits tx to txPool and logs a message. -func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction, signature []byte) (common.Hash, error) { - signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) - - signedTx, err := tx.WithSignature(signer, signature) - if err != nil { +func submitTransaction(ctx context.Context, b Backend, tx *types.Transaction) (common.Hash, error) { + if err := b.SendTx(ctx, tx); err != nil { return common.Hash{}, err } - - if err := b.SendTx(ctx, signedTx); err != nil { - return common.Hash{}, err - } - - if signedTx.To() == nil { - from, _ := types.Sender(signer, signedTx) - addr := crypto.CreateAddress(from, signedTx.Nonce()) - glog.V(logger.Info).Infof("Tx(%s) created: %s\n", signedTx.Hash().Hex(), addr.Hex()) + if tx.To() == nil { + signer := types.MakeSigner(b.ChainConfig(), b.CurrentBlock().Number()) + from, _ := types.Sender(signer, tx) + addr := crypto.CreateAddress(from, tx.Nonce()) + glog.V(logger.Info).Infof("Tx(%s) created: %s\n", tx.Hash().Hex(), addr.Hex()) } else { - glog.V(logger.Info).Infof("Tx(%s) to: %s\n", signedTx.Hash().Hex(), tx.To().Hex()) + glog.V(logger.Info).Infof("Tx(%s) to: %s\n", tx.Hash().Hex(), tx.To().Hex()) } - - return signedTx.Hash(), nil + return tx.Hash(), nil } // SendTransaction creates a transaction for the given argument, sign it and submit it to the @@ -1106,12 +1105,16 @@ func (s *PublicTransactionPoolAPI) SendTransaction(ctx context.Context, args Sen return common.Hash{}, err } tx := args.toTransaction() - signer := types.MakeSigner(s.b.ChainConfig(), s.b.CurrentBlock().Number()) - signature, err := s.b.AccountManager().Sign(args.From, signer.Hash(tx).Bytes()) + + var chainID *big.Int + if config := s.b.ChainConfig(); config.IsEIP155(s.b.CurrentBlock().Number()) { + chainID = config.ChainId + } + signed, err := s.b.AccountManager().SignTx(accounts.Account{Address: args.From}, tx, chainID) if err != nil { return common.Hash{}, err } - return submitTransaction(ctx, s.b, tx, signature) + return submitTransaction(ctx, s.b, signed) } // SendRawTransaction will add the signed transaction to the transaction pool. @@ -1151,7 +1154,7 @@ func (s *PublicTransactionPoolAPI) SendRawTransaction(ctx context.Context, encod // // https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_sign func (s *PublicTransactionPoolAPI) Sign(addr common.Address, data hexutil.Bytes) (hexutil.Bytes, error) { - signature, err := s.b.AccountManager().Sign(addr, signHash(data)) + signature, err := s.b.AccountManager().SignHash(accounts.Account{Address: addr}, signHash(data)) if err == nil { signature[64] += 27 // Transform V from 0/1 to 27/28 according to the yellow paper } diff --git a/internal/guide/guide_test.go b/internal/guide/guide_test.go index 8f89037bd..9c7ad16d1 100644 --- a/internal/guide/guide_test.go +++ b/internal/guide/guide_test.go @@ -24,13 +24,14 @@ package guide import ( "io/ioutil" + "math/big" "os" "path/filepath" "testing" "time" - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/core/types" ) // Tests that the account management snippets work correctly. @@ -42,59 +43,59 @@ func TestAccountManagement(t *testing.T) { } defer os.RemoveAll(workdir) - // Create an encrypted keystore manager with standard crypto parameters - am := accounts.NewManager(filepath.Join(workdir, "keystore"), accounts.StandardScryptN, accounts.StandardScryptP) + // Create an encrypted keystore with standard crypto parameters + ks := keystore.NewKeyStore(filepath.Join(workdir, "keystore"), keystore.StandardScryptN, keystore.StandardScryptP) // Create a new account with the specified encryption passphrase - newAcc, err := am.NewAccount("Creation password") + newAcc, err := ks.NewAccount("Creation password") if err != nil { t.Fatalf("Failed to create new account: %v", err) } // Export the newly created account with a different passphrase. The returned // data from this method invocation is a JSON encoded, encrypted key-file - jsonAcc, err := am.Export(newAcc, "Creation password", "Export password") + jsonAcc, err := ks.Export(newAcc, "Creation password", "Export password") if err != nil { t.Fatalf("Failed to export account: %v", err) } // Update the passphrase on the account created above inside the local keystore - if err := am.Update(newAcc, "Creation password", "Update password"); err != nil { + if err := ks.Update(newAcc, "Creation password", "Update password"); err != nil { t.Fatalf("Failed to update account: %v", err) } // Delete the account updated above from the local keystore - if err := am.Delete(newAcc, "Update password"); err != nil { + if err := ks.Delete(newAcc, "Update password"); err != nil { t.Fatalf("Failed to delete account: %v", err) } // Import back the account we've exported (and then deleted) above with yet // again a fresh passphrase - if _, err := am.Import(jsonAcc, "Export password", "Import password"); err != nil { + if _, err := ks.Import(jsonAcc, "Export password", "Import password"); err != nil { t.Fatalf("Failed to import account: %v", err) } // Create a new account to sign transactions with - signer, err := am.NewAccount("Signer password") + signer, err := ks.NewAccount("Signer password") if err != nil { t.Fatalf("Failed to create signer account: %v", err) } - txHash := common.HexToHash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef") + tx, chain := new(types.Transaction), big.NewInt(1) // Sign a transaction with a single authorization - if _, err := am.SignWithPassphrase(signer, "Signer password", txHash.Bytes()); err != nil { + if _, err := ks.SignTxWithPassphrase(signer, "Signer password", tx, chain); err != nil { t.Fatalf("Failed to sign with passphrase: %v", err) } // Sign a transaction with multiple manually cancelled authorizations - if err := am.Unlock(signer, "Signer password"); err != nil { + if err := ks.Unlock(signer, "Signer password"); err != nil { t.Fatalf("Failed to unlock account: %v", err) } - if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil { + if _, err := ks.SignTx(signer, tx, chain); err != nil { t.Fatalf("Failed to sign with unlocked account: %v", err) } - if err := am.Lock(signer.Address); err != nil { + if err := ks.Lock(signer.Address); err != nil { t.Fatalf("Failed to lock account: %v", err) } // Sign a transaction with multiple automatically cancelled authorizations - if err := am.TimedUnlock(signer, "Signer password", time.Second); err != nil { + if err := ks.TimedUnlock(signer, "Signer password", time.Second); err != nil { t.Fatalf("Failed to time unlock account: %v", err) } - if _, err := am.Sign(signer.Address, txHash.Bytes()); err != nil { + if _, err := ks.SignTx(signer, tx, chain); err != nil { t.Fatalf("Failed to sign with time unlocked account: %v", err) } } diff --git a/mobile/accounts.go b/mobile/accounts.go index 621be4d7a..897549a6b 100644 --- a/mobile/accounts.go +++ b/mobile/accounts.go @@ -24,24 +24,25 @@ import ( "time" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" ) const ( // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptN = int(accounts.StandardScryptN) + StandardScryptN = int(keystore.StandardScryptN) // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptP = int(accounts.StandardScryptP) + StandardScryptP = int(keystore.StandardScryptP) // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptN = int(accounts.LightScryptN) + LightScryptN = int(keystore.LightScryptN) // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptP = int(accounts.LightScryptP) + LightScryptP = int(keystore.LightScryptP) ) // Account represents a stored key. @@ -79,57 +80,73 @@ func (a *Account) GetAddress() *Address { // GetFile retrieves the path of the file containing the account key. func (a *Account) GetFile() string { - return a.account.File + return a.account.URL } -// AccountManager manages a key storage directory on disk. -type AccountManager struct{ manager *accounts.Manager } +// KeyStore manages a key storage directory on disk. +type KeyStore struct{ keystore *keystore.KeyStore } -// NewAccountManager creates a manager for the given directory. -func NewAccountManager(keydir string, scryptN, scryptP int) *AccountManager { - return &AccountManager{manager: accounts.NewManager(keydir, scryptN, scryptP)} +// NewKeyStore creates a keystore for the given directory. +func NewKeyStore(keydir string, scryptN, scryptP int) *KeyStore { + return &KeyStore{keystore: keystore.NewKeyStore(keydir, scryptN, scryptP)} } // HasAddress reports whether a key with the given address is present. -func (am *AccountManager) HasAddress(address *Address) bool { - return am.manager.HasAddress(address.address) +func (ks *KeyStore) HasAddress(address *Address) bool { + return ks.keystore.HasAddress(address.address) } // GetAccounts returns all key files present in the directory. -func (am *AccountManager) GetAccounts() *Accounts { - return &Accounts{am.manager.Accounts()} +func (ks *KeyStore) GetAccounts() *Accounts { + return &Accounts{ks.keystore.Accounts()} } // DeleteAccount deletes the key matched by account if the passphrase is correct. // If a contains no filename, the address must match a unique key. -func (am *AccountManager) DeleteAccount(account *Account, passphrase string) error { - return am.manager.Delete(accounts.Account{ - Address: account.account.Address, - File: account.account.File, - }, passphrase) +func (ks *KeyStore) DeleteAccount(account *Account, passphrase string) error { + return ks.keystore.Delete(account.account, passphrase) } -// Sign calculates a ECDSA signature for the given hash. The produced signature +// SignHash calculates a ECDSA signature for the given hash. The produced signature // is in the [R || S || V] format where V is 0 or 1. -func (am *AccountManager) Sign(address *Address, hash []byte) (signature []byte, _ error) { - return am.manager.Sign(address.address, hash) +func (ks *KeyStore) SignHash(address *Address, hash []byte) (signature []byte, _ error) { + return ks.keystore.SignHash(accounts.Account{Address: address.address}, hash) } -// SignPassphrase signs hash if the private key matching the given address can +// SignTx signs the given transaction with the requested account. +func (ks *KeyStore) SignTx(account *Account, tx *Transaction, chainID *BigInt) (*Transaction, error) { + signed, err := ks.keystore.SignTx(account.account, tx.tx, chainID.bigint) + if err != nil { + return nil, err + } + return &Transaction{signed}, nil +} + +// SignHashPassphrase signs hash if the private key matching the given address can // be decrypted with the given passphrase. The produced signature is in the // [R || S || V] format where V is 0 or 1. -func (am *AccountManager) SignPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) { - return am.manager.SignWithPassphrase(account.account, passphrase, hash) +func (ks *KeyStore) SignHashPassphrase(account *Account, passphrase string, hash []byte) (signature []byte, _ error) { + return ks.keystore.SignHashWithPassphrase(account.account, passphrase, hash) +} + +// SignTxPassphrase signs the transaction if the private key matching the +// given address can be decrypted with the given passphrase. +func (ks *KeyStore) SignTxPassphrase(account *Account, passphrase string, tx *Transaction, chainID *BigInt) (*Transaction, error) { + signed, err := ks.keystore.SignTxWithPassphrase(account.account, passphrase, tx.tx, chainID.bigint) + if err != nil { + return nil, err + } + return &Transaction{signed}, nil } // Unlock unlocks the given account indefinitely. -func (am *AccountManager) Unlock(account *Account, passphrase string) error { - return am.manager.TimedUnlock(account.account, passphrase, 0) +func (ks *KeyStore) Unlock(account *Account, passphrase string) error { + return ks.keystore.TimedUnlock(account.account, passphrase, 0) } // Lock removes the private key with the given address from memory. -func (am *AccountManager) Lock(address *Address) error { - return am.manager.Lock(address.address) +func (ks *KeyStore) Lock(address *Address) error { + return ks.keystore.Lock(address.address) } // TimedUnlock unlocks the given account with the passphrase. The account stays @@ -139,14 +156,14 @@ func (am *AccountManager) Lock(address *Address) error { // If the account address is already unlocked for a duration, TimedUnlock extends or // shortens the active unlock timeout. If the address was previously unlocked // indefinitely the timeout is not altered. -func (am *AccountManager) TimedUnlock(account *Account, passphrase string, timeout int64) error { - return am.manager.TimedUnlock(account.account, passphrase, time.Duration(timeout)) +func (ks *KeyStore) TimedUnlock(account *Account, passphrase string, timeout int64) error { + return ks.keystore.TimedUnlock(account.account, passphrase, time.Duration(timeout)) } // NewAccount generates a new key and stores it into the key directory, // encrypting it with the passphrase. -func (am *AccountManager) NewAccount(passphrase string) (*Account, error) { - account, err := am.manager.NewAccount(passphrase) +func (ks *KeyStore) NewAccount(passphrase string) (*Account, error) { + account, err := ks.keystore.NewAccount(passphrase) if err != nil { return nil, err } @@ -154,13 +171,13 @@ func (am *AccountManager) NewAccount(passphrase string) (*Account, error) { } // ExportKey exports as a JSON key, encrypted with newPassphrase. -func (am *AccountManager) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) { - return am.manager.Export(account.account, passphrase, newPassphrase) +func (ks *KeyStore) ExportKey(account *Account, passphrase, newPassphrase string) (key []byte, _ error) { + return ks.keystore.Export(account.account, passphrase, newPassphrase) } // ImportKey stores the given encrypted JSON key into the key directory. -func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) { - acc, err := am.manager.Import(keyJSON, passphrase, newPassphrase) +func (ks *KeyStore) ImportKey(keyJSON []byte, passphrase, newPassphrase string) (account *Account, _ error) { + acc, err := ks.keystore.Import(keyJSON, passphrase, newPassphrase) if err != nil { return nil, err } @@ -168,14 +185,14 @@ func (am *AccountManager) ImportKey(keyJSON []byte, passphrase, newPassphrase st } // UpdateAccount changes the passphrase of an existing account. -func (am *AccountManager) UpdateAccount(account *Account, passphrase, newPassphrase string) error { - return am.manager.Update(account.account, passphrase, newPassphrase) +func (ks *KeyStore) UpdateAccount(account *Account, passphrase, newPassphrase string) error { + return ks.keystore.Update(account.account, passphrase, newPassphrase) } // ImportPreSaleKey decrypts the given Ethereum presale wallet and stores // a key file in the key directory. The key file is encrypted with the same passphrase. -func (am *AccountManager) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) { - account, err := am.manager.ImportPreSaleKey(keyJSON, passphrase) +func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (ccount *Account, _ error) { + account, err := ks.keystore.ImportPreSaleKey(keyJSON, passphrase) if err != nil { return nil, err } diff --git a/mobile/android_test.go b/mobile/android_test.go index 3776f8291..6f5076d64 100644 --- a/mobile/android_test.go +++ b/mobile/android_test.go @@ -43,42 +43,46 @@ public class AndroidTest extends InstrumentationTestCase { public AndroidTest() {} public void testAccountManagement() { - // Create an encrypted keystore manager with light crypto parameters. - AccountManager am = new AccountManager(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP); + // Create an encrypted keystore with light crypto parameters. + KeyStore ks = new KeyStore(getInstrumentation().getContext().getFilesDir() + "/keystore", Geth.LightScryptN, Geth.LightScryptP); try { // Create a new account with the specified encryption passphrase. - Account newAcc = am.newAccount("Creation password"); + Account newAcc = ks.newAccount("Creation password"); // Export the newly created account with a different passphrase. The returned // data from this method invocation is a JSON encoded, encrypted key-file. - byte[] jsonAcc = am.exportKey(newAcc, "Creation password", "Export password"); + byte[] jsonAcc = ks.exportKey(newAcc, "Creation password", "Export password"); // Update the passphrase on the account created above inside the local keystore. - am.updateAccount(newAcc, "Creation password", "Update password"); + ks.updateAccount(newAcc, "Creation password", "Update password"); // Delete the account updated above from the local keystore. - am.deleteAccount(newAcc, "Update password"); + ks.deleteAccount(newAcc, "Update password"); // Import back the account we've exported (and then deleted) above with yet // again a fresh passphrase. - Account impAcc = am.importKey(jsonAcc, "Export password", "Import password"); + Account impAcc = ks.importKey(jsonAcc, "Export password", "Import password"); // Create a new account to sign transactions with - Account signer = am.newAccount("Signer password"); - Hash txHash = new Hash("0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"); + Account signer = ks.newAccount("Signer password"); + + Transaction tx = new Transaction( + 1, new Address("0x0000000000000000000000000000000000000000"), + new BigInt(0), new BigInt(0), new BigInt(1), null); // Random empty transaction + BigInt chain = new BigInt(1); // Chain identifier of the main net // Sign a transaction with a single authorization - byte[] signature = am.signPassphrase(signer, "Signer password", txHash.getBytes()); + Transaction signed = ks.signTxPassphrase(signer, "Signer password", tx, chain); // Sign a transaction with multiple manually cancelled authorizations - am.unlock(signer, "Signer password"); - signature = am.sign(signer.getAddress(), txHash.getBytes()); - am.lock(signer.getAddress()); + ks.unlock(signer, "Signer password"); + signed = ks.signTx(signer, tx, chain); + ks.lock(signer.getAddress()); // Sign a transaction with multiple automatically cancelled authorizations - am.timedUnlock(signer, "Signer password", 1000000000); - signature = am.sign(signer.getAddress(), txHash.getBytes()); + ks.timedUnlock(signer, "Signer password", 1000000000); + signed = ks.signTx(signer, tx, chain); } catch (Exception e) { fail(e.toString()); } diff --git a/mobile/types.go b/mobile/types.go index 9ea70ea9b..a9c8cf68c 100644 --- a/mobile/types.go +++ b/mobile/types.go @@ -132,6 +132,11 @@ type Transaction struct { tx *types.Transaction } +// NewTransaction creates a new transaction with the given properties. +func NewTransaction(nonce int64, to *Address, amount, gasLimit, gasPrice *BigInt, data []byte) *Transaction { + return &Transaction{types.NewTransaction(uint64(nonce), to.address, amount.bigint, gasLimit.bigint, gasPrice.bigint, data)} +} + func (tx *Transaction) GetData() []byte { return tx.tx.Data() } func (tx *Transaction) GetGas() int64 { return tx.tx.Gas().Int64() } func (tx *Transaction) GetGasPrice() *BigInt { return &BigInt{tx.tx.GasPrice()} } diff --git a/node/config.go b/node/config.go index 8d75e441b..f3b4dcc73 100644 --- a/node/config.go +++ b/node/config.go @@ -27,6 +27,7 @@ import ( "strings" "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/logger" @@ -401,11 +402,11 @@ func (c *Config) parsePersistentNodes(path string) []*discover.Node { } func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore string, err error) { - scryptN := accounts.StandardScryptN - scryptP := accounts.StandardScryptP + scryptN := keystore.StandardScryptN + scryptP := keystore.StandardScryptP if conf.UseLightweightKDF { - scryptN = accounts.LightScryptN - scryptP = accounts.LightScryptP + scryptN = keystore.LightScryptN + scryptP = keystore.LightScryptP } var keydir string @@ -431,6 +432,6 @@ func makeAccountManager(conf *Config) (am *accounts.Manager, ephemeralKeystore s if err := os.MkdirAll(keydir, 0700); err != nil { return nil, "", err } - - return accounts.NewManager(keydir, scryptN, scryptP), ephemeralKeystore, nil + ks := keystore.NewKeyStore(keydir, scryptN, scryptP) + return accounts.NewManager(ks), ephemeralKeystore, nil } -- cgit v1.2.3