diff options
Diffstat (limited to 'accounts/account_manager.go')
-rw-r--r-- | accounts/account_manager.go | 303 |
1 files changed, 189 insertions, 114 deletions
diff --git a/accounts/account_manager.go b/accounts/account_manager.go index 34cf0fa53..56499672e 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -14,20 +14,21 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>. -// Package implements a private key management facility. +// Package accounts implements encrypted storage of secp256k1 private keys. // -// This abstracts part of a user's interaction with an account she controls. +// 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 -// Currently this is pretty much a passthrough to the KeyStore interface, -// and accounts persistence is derived from stored keys' addresses - import ( "crypto/ecdsa" crand "crypto/rand" + "encoding/json" "errors" "fmt" "os" + "path/filepath" + "runtime" "sync" "time" @@ -36,109 +37,182 @@ import ( ) var ( - ErrLocked = errors.New("account is locked") - ErrNoKeys = errors.New("no keys in store") + 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 + 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 { - keyStore crypto.KeyStore + cache *addrCache + keyStore keyStore + mu sync.RWMutex unlocked map[common.Address]*unlocked - mutex sync.RWMutex } type unlocked struct { - *crypto.Key + *Key abort chan struct{} } -func NewManager(keyStore crypto.KeyStore) *Manager { - return &Manager{ - keyStore: keyStore, - unlocked: make(map[common.Address]*unlocked), - } +// 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 } -func (am *Manager) HasAccount(addr common.Address) bool { - accounts, _ := am.Accounts() - for _, acct := range accounts { - if acct.Address == addr { - return true - } - } - return false +// 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) DeleteAccount(address common.Address, auth string) error { - return am.keyStore.DeleteKey(address, auth) +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() +} + +// 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 *Manager) DeleteAccount(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 } -func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) { - am.mutex.RLock() - defer am.mutex.RUnlock() - unlockedKey, found := am.unlocked[a.Address] +// Sign signs hash with an unlocked private key matching the given address. +func (am *Manager) Sign(addr common.Address, hash []byte) (signature []byte, err error) { + am.mu.RLock() + defer am.mu.RUnlock() + unlockedKey, found := am.unlocked[addr] if !found { return nil, ErrLocked } - signature, err = crypto.Sign(toSign, unlockedKey.PrivateKey) - return signature, err + return crypto.Sign(hash, unlockedKey.PrivateKey) } // Unlock unlocks the given account indefinitely. -func (am *Manager) Unlock(addr common.Address, keyAuth string) error { - return am.TimedUnlock(addr, keyAuth, 0) +func (am *Manager) Unlock(a Account, keyAuth string) error { + return am.TimedUnlock(a, keyAuth, 0) } +// Lock removes the private key with the given address from memory. func (am *Manager) Lock(addr common.Address) error { - am.mutex.Lock() + am.mu.Lock() if unl, found := am.unlocked[addr]; found { - am.mutex.Unlock() + am.mu.Unlock() am.expire(addr, unl, time.Duration(0)*time.Nanosecond) } else { - am.mutex.Unlock() + am.mu.Unlock() } return nil } -// TimedUnlock unlocks the account with the given address. The account +// 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. +// until the program exits. The account must match a unique key file. // -// If the accout is already unlocked, TimedUnlock extends or shortens -// the active unlock timeout. -func (am *Manager) TimedUnlock(addr common.Address, keyAuth string, timeout time.Duration) error { - key, err := am.keyStore.GetKey(addr, keyAuth) +// 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 } - var u *unlocked - am.mutex.Lock() - defer am.mutex.Unlock() - var found bool - u, found = am.unlocked[addr] + + am.mu.Lock() + defer am.mu.Unlock() + u, found := am.unlocked[a.Address] if found { - // terminate dropLater for this key to avoid unexpected drops. - if u.abort != nil { + 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(addr, u, timeout) + go am.expire(a.Address, u, timeout) } else { u = &unlocked{Key: key} } - am.unlocked[addr] = u + am.unlocked[a.Address] = u return nil } +func (am *Manager) getDecryptedKey(a Account, auth string) (Account, *Key, error) { + am.cache.maybeReload() + am.cache.mu.Lock() + a, err := am.cache.find(a) + am.cache.mu.Unlock() + 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() @@ -146,7 +220,7 @@ func (am *Manager) expire(addr common.Address, u *unlocked, timeout time.Duratio case <-u.abort: // just quit case <-t.C: - am.mutex.Lock() + 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 @@ -155,96 +229,97 @@ func (am *Manager) expire(addr common.Address, u *unlocked, timeout time.Duratio zeroKey(u.PrivateKey) delete(am.unlocked, addr) } - am.mutex.Unlock() + am.mu.Unlock() } } -func (am *Manager) NewAccount(auth string) (Account, error) { - key, err := am.keyStore.GenerateNewKey(crand.Reader, auth) +// 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 } - return Account{Address: key.Address}, nil + // 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 } -func (am *Manager) AddressByIndex(index int) (addr string, err error) { - var addrs []common.Address - addrs, err = am.keyStore.GetKeyAddresses() +// 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 + return nil, err } - if index < 0 || index >= len(addrs) { - err = fmt.Errorf("index out of range: %d (should be 0-%d)", index, len(addrs)-1) + var N, P int + if store, ok := am.keyStore.(*keyStorePassphrase); ok { + N, P = store.scryptN, store.scryptP } else { - addr = addrs[index].Hex() + N, P = StandardScryptN, StandardScryptP } - return + return EncryptKey(key, newPassphrase, N, P) } -func (am *Manager) Accounts() ([]Account, error) { - addresses, err := am.keyStore.GetKeyAddresses() - if os.IsNotExist(err) { - return nil, ErrNoKeys - } else if err != nil { - return nil, err +// 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) } - accounts := make([]Account, len(addresses)) - for i, addr := range addresses { - accounts[i] = Account{ - Address: addr, - } + if err != nil { + return Account{}, err } - return accounts, err + return am.importKey(key, newPassphrase) } -// zeroKey zeroes a private key in memory. -func zeroKey(k *ecdsa.PrivateKey) { - b := k.D.Bits() - for i := range b { - b[i] = 0 - } +// 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) { + return am.importKey(newKeyFromECDSA(priv), passphrase) } -// USE WITH CAUTION = this will save an unencrypted private key on disk -// no cli or js interface -func (am *Manager) Export(path string, addr common.Address, keyAuth string) error { - key, err := am.keyStore.GetKey(addr, keyAuth) - if err != nil { - return err +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 } - return crypto.SaveECDSA(path, key.PrivateKey) + am.cache.add(a) + return a, nil } -func (am *Manager) Import(path string, keyAuth string) (Account, error) { - privateKeyECDSA, err := crypto.LoadECDSA(path) +// 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 Account{}, err - } - key := crypto.NewKeyFromECDSA(privateKeyECDSA) - if err = am.keyStore.StoreKey(key, keyAuth); err != nil { - return Account{}, err + return err } - return Account{Address: key.Address}, nil + return am.keyStore.StoreKey(a.File, key, newPassphrase) } -func (am *Manager) Update(addr common.Address, authFrom, authTo string) (err error) { - var key *crypto.Key - key, err = am.keyStore.GetKey(addr, authFrom) - - if err == nil { - err = am.keyStore.StoreKey(key, authTo) - if err == nil { - am.keyStore.Cleanup(addr) - } +// 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 } - return + am.cache.add(a) + return a, nil } -func (am *Manager) ImportPreSaleKey(keyJSON []byte, password string) (acc Account, err error) { - var key *crypto.Key - key, err = crypto.ImportPreSaleKey(am.keyStore, keyJSON, password) - if err != nil { - return +// zeroKey zeroes a private key in memory. +func zeroKey(k *ecdsa.PrivateKey) { + b := k.D.Bits() + for i := range b { + b[i] = 0 } - return Account{Address: key.Address}, nil } |