aboutsummaryrefslogtreecommitdiffstats
path: root/accounts
diff options
context:
space:
mode:
Diffstat (limited to 'accounts')
-rw-r--r--accounts/abi/bind/auth.go17
-rw-r--r--accounts/abi/bind/bind_test.go34
-rw-r--r--accounts/account_manager.go303
-rw-r--r--accounts/accounts_test.go96
-rw-r--r--accounts/addrcache.go269
-rw-r--r--accounts/addrcache_test.go283
-rw-r--r--accounts/key.go229
-rw-r--r--accounts/key_store_passphrase.go296
-rw-r--r--accounts/key_store_passphrase_test.go60
-rw-r--r--accounts/key_store_plain.go62
-rw-r--r--accounts/key_store_test.go241
-rw-r--r--accounts/presale.go132
-rw-r--r--accounts/testdata/dupes/11
-rw-r--r--accounts/testdata/dupes/21
-rw-r--r--accounts/testdata/dupes/foo1
-rw-r--r--accounts/testdata/keystore/.hiddenfile1
-rw-r--r--accounts/testdata/keystore/README21
-rw-r--r--accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef81
-rw-r--r--accounts/testdata/keystore/aaa1
-rw-r--r--accounts/testdata/keystore/empty0
-rw-r--r--accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e1
-rw-r--r--accounts/testdata/keystore/garbagebin0 -> 300 bytes
-rw-r--r--accounts/testdata/keystore/no-address1
-rw-r--r--accounts/testdata/keystore/zero1
-rw-r--r--accounts/testdata/keystore/zzz1
-rw-r--r--accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e1
-rw-r--r--accounts/testdata/v1_test_vector.json28
-rw-r--r--accounts/testdata/v3_test_vector.json49
-rw-r--r--accounts/testdata/very-light-scrypt.json1
-rw-r--r--accounts/watch.go113
-rw-r--r--accounts/watch_fallback.go28
31 files changed, 2103 insertions, 170 deletions
diff --git a/accounts/abi/bind/auth.go b/accounts/abi/bind/auth.go
index 624f995b0..2cf22768c 100644
--- a/accounts/abi/bind/auth.go
+++ b/accounts/abi/bind/auth.go
@@ -17,10 +17,12 @@
package bind
import (
+ "crypto/ecdsa"
"errors"
"io"
"io/ioutil"
+ "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"
@@ -33,23 +35,24 @@ func NewTransactor(keyin io.Reader, passphrase string) (*TransactOpts, error) {
if err != nil {
return nil, err
}
- key, err := crypto.DecryptKey(json, passphrase)
+ key, err := accounts.DecryptKey(json, passphrase)
if err != nil {
return nil, err
}
- return NewKeyedTransactor(key), nil
+ return NewKeyedTransactor(key.PrivateKey), nil
}
// NewKeyedTransactor is a utility method to easily create a transaction signer
-// from a plain go-ethereum crypto key.
-func NewKeyedTransactor(key *crypto.Key) *TransactOpts {
+// from a single private key.
+func NewKeyedTransactor(key *ecdsa.PrivateKey) *TransactOpts {
+ keyAddr := crypto.PubkeyToAddress(key.PublicKey)
return &TransactOpts{
- From: key.Address,
+ From: keyAddr,
Signer: func(address common.Address, tx *types.Transaction) (*types.Transaction, error) {
- if address != key.Address {
+ if address != keyAddr {
return nil, errors.New("not authorized to sign this account")
}
- signature, err := crypto.Sign(tx.SigHash().Bytes(), key.PrivateKey)
+ signature, err := crypto.Sign(tx.SigHash().Bytes(), key)
if err != nil {
return nil, err
}
diff --git a/accounts/abi/bind/bind_test.go b/accounts/abi/bind/bind_test.go
index 3f02af017..5c36bc48f 100644
--- a/accounts/abi/bind/bind_test.go
+++ b/accounts/abi/bind/bind_test.go
@@ -167,11 +167,9 @@ var bindTests = []struct {
`[{"constant":true,"inputs":[],"name":"transactString","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":true,"inputs":[],"name":"deployString","outputs":[{"name":"","type":"string"}],"type":"function"},{"constant":false,"inputs":[{"name":"str","type":"string"}],"name":"transact","outputs":[],"type":"function"},{"inputs":[{"name":"str","type":"string"}],"type":"constructor"}]`,
`
// Generate a new random account and a funded simulator
- key := crypto.NewKey(rand.Reader)
- sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: key.Address, Balance: big.NewInt(10000000000)})
-
- // Convert the tester key to an authorized transactor for ease of use
+ key, _ := crypto.GenerateKey()
auth := bind.NewKeyedTransactor(key)
+ sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})
// Deploy an interaction tester contract and call a transaction on it
_, _, interactor, err := DeployInteractor(auth, sim, "Deploy string")
@@ -210,11 +208,9 @@ var bindTests = []struct {
`[{"constant":true,"inputs":[],"name":"tuple","outputs":[{"name":"a","type":"string"},{"name":"b","type":"int256"},{"name":"c","type":"bytes32"}],"type":"function"}]`,
`
// Generate a new random account and a funded simulator
- key := crypto.NewKey(rand.Reader)
- sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: key.Address, Balance: big.NewInt(10000000000)})
-
- // Convert the tester key to an authorized transactor for ease of use
+ key, _ := crypto.GenerateKey()
auth := bind.NewKeyedTransactor(key)
+ sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})
// Deploy a tuple tester contract and execute a structured call on it
_, _, tupler, err := DeployTupler(auth, sim)
@@ -252,11 +248,9 @@ var bindTests = []struct {
`[{"constant":true,"inputs":[{"name":"input","type":"address[]"}],"name":"echoAddresses","outputs":[{"name":"output","type":"address[]"}],"type":"function"},{"constant":true,"inputs":[{"name":"input","type":"uint24[23]"}],"name":"echoFancyInts","outputs":[{"name":"output","type":"uint24[23]"}],"type":"function"},{"constant":true,"inputs":[{"name":"input","type":"int256[]"}],"name":"echoInts","outputs":[{"name":"output","type":"int256[]"}],"type":"function"},{"constant":true,"inputs":[{"name":"input","type":"bool[]"}],"name":"echoBools","outputs":[{"name":"output","type":"bool[]"}],"type":"function"}]`,
`
// Generate a new random account and a funded simulator
- key := crypto.NewKey(rand.Reader)
- sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: key.Address, Balance: big.NewInt(10000000000)})
-
- // Convert the tester key to an authorized transactor for ease of use
+ key, _ := crypto.GenerateKey()
auth := bind.NewKeyedTransactor(key)
+ sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})
// Deploy a slice tester contract and execute a n array call on it
_, _, slicer, err := DeploySlicer(auth, sim)
@@ -265,10 +259,10 @@ var bindTests = []struct {
}
sim.Commit()
- if out, err := slicer.EchoAddresses(nil, []common.Address{key.Address, common.Address{}}); err != nil {
+ if out, err := slicer.EchoAddresses(nil, []common.Address{auth.From, common.Address{}}); err != nil {
t.Fatalf("Failed to call slice echoer: %v", err)
- } else if !reflect.DeepEqual(out, []common.Address{key.Address, common.Address{}}) {
- t.Fatalf("Slice return mismatch: have %v, want %v", out, []common.Address{key.Address, common.Address{}})
+ } else if !reflect.DeepEqual(out, []common.Address{auth.From, common.Address{}}) {
+ t.Fatalf("Slice return mismatch: have %v, want %v", out, []common.Address{auth.From, common.Address{}})
}
`,
},
@@ -288,11 +282,9 @@ var bindTests = []struct {
`[{"constant":true,"inputs":[],"name":"caller","outputs":[{"name":"","type":"address"}],"type":"function"}]`,
`
// Generate a new random account and a funded simulator
- key := crypto.NewKey(rand.Reader)
- sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: key.Address, Balance: big.NewInt(10000000000)})
-
- // Convert the tester key to an authorized transactor for ease of use
+ key, _ := crypto.GenerateKey()
auth := bind.NewKeyedTransactor(key)
+ sim := backends.NewSimulatedBackend(core.GenesisAccount{Address: auth.From, Balance: big.NewInt(10000000000)})
// Deploy a default method invoker contract and execute its default method
_, _, defaulter, err := DeployDefaulter(auth, sim)
@@ -306,8 +298,8 @@ var bindTests = []struct {
if caller, err := defaulter.Caller(nil); err != nil {
t.Fatalf("Failed to call address retriever: %v", err)
- } else if (caller != key.Address) {
- t.Fatalf("Address mismatch: have %v, want %v", caller, key.Address)
+ } else if (caller != auth.From) {
+ t.Fatalf("Address mismatch: have %v, want %v", caller, auth.From)
}
`,
},
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
}
diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go
index 55ddecdea..829cf3968 100644
--- a/accounts/accounts_test.go
+++ b/accounts/accounts_test.go
@@ -19,95 +19,132 @@ package accounts
import (
"io/ioutil"
"os"
+ "runtime"
+ "strings"
"testing"
"time"
- "github.com/ethereum/go-ethereum/crypto"
+ "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.DeleteAccount(a, "bar"); err != nil {
+ t.Errorf("DeleteAccount error: %v", err)
+ }
+ if common.FileExist(a.File) {
+ t.Errorf("account file %s should be gone after DeleteAccount", a.File)
+ }
+ if am.HasAddress(a.Address) {
+ t.Errorf("HasAccount(%x) should've returned true after DeleteAccount", a.Address)
+ }
+}
+
func TestSign(t *testing.T) {
- dir, ks := tmpKeyStore(t, crypto.NewKeyStorePlain)
+ dir, am := tmpManager(t, true)
defer os.RemoveAll(dir)
- am := NewManager(ks)
pass := "" // not used but required by API
a1, err := am.NewAccount(pass)
- am.Unlock(a1.Address, "")
-
- _, err = am.Sign(a1, testSigData)
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 TestTimedUnlock(t *testing.T) {
- dir, ks := tmpKeyStore(t, crypto.NewKeyStorePlain)
+ dir, am := tmpManager(t, true)
defer os.RemoveAll(dir)
- am := NewManager(ks)
pass := "foo"
a1, err := am.NewAccount(pass)
// Signing without passphrase fails because account is locked
- _, err = am.Sign(a1, testSigData)
+ _, 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.Address, pass, 100*time.Millisecond); err != nil {
+ 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, testSigData)
+ _, 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(350 * time.Millisecond)
- _, err = am.Sign(a1, testSigData)
+ 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, ks := tmpKeyStore(t, crypto.NewKeyStorePlain)
+ dir, am := tmpManager(t, false)
defer os.RemoveAll(dir)
- am := NewManager(ks)
pass := "foo"
a1, err := am.NewAccount(pass)
- // Unlock indefinitely
- if err = am.Unlock(a1.Address, pass); err != nil {
+ // 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, testSigData)
+ _, 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.Address, pass, 100*time.Millisecond); err != nil {
+ 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, testSigData)
+ _, 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(150 * time.Millisecond)
- _, err = am.Sign(a1, testSigData)
+ 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)
}
@@ -115,22 +152,21 @@ func TestOverrideUnlock(t *testing.T) {
// This test should fail under -race if signing races the expiration goroutine.
func TestSignRace(t *testing.T) {
- dir, ks := tmpKeyStore(t, crypto.NewKeyStorePlain)
+ dir, am := tmpManager(t, false)
defer os.RemoveAll(dir)
// Create a test account.
- am := NewManager(ks)
a1, err := am.NewAccount("")
if err != nil {
t.Fatal("could not create the test account", err)
}
- if err := am.TimedUnlock(a1.Address, "", 15*time.Millisecond); err != nil {
- t.Fatalf("could not unlock 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, testSigData); err == ErrLocked {
+ if _, err := am.Sign(a1.Address, testSigData); err == ErrLocked {
return
} else if err != nil {
t.Errorf("Sign error: %v", err)
@@ -141,10 +177,14 @@ func TestSignRace(t *testing.T) {
t.Errorf("Account did not lock within the timeout")
}
-func tmpKeyStore(t *testing.T, new func(string) crypto.KeyStore) (string, crypto.KeyStore) {
+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
new file mode 100644
index 000000000..0a904f788
--- /dev/null
+++ b/accounts/addrcache.go
@@ -0,0 +1,269 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+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 common.Address `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 = common.Address{}
+ err = json.NewDecoder(buf).Decode(&keyJSON)
+ switch {
+ case err != nil:
+ glog.V(logger.Debug).Infof("can't decode key %s: %v", path, err)
+ case (keyJSON.Address == common.Address{}):
+ glog.V(logger.Debug).Infof("can't decode key %s: missing or zero address", path)
+ default:
+ addrs = append(addrs, Account{Address: keyJSON.Address, 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
new file mode 100644
index 000000000..e5f08cffc
--- /dev/null
+++ b/accounts/addrcache_test.go
@@ -0,0 +1,283 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+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/key.go b/accounts/key.go
new file mode 100644
index 000000000..dbcb49dcf
--- /dev/null
+++ b/accounts/key.go
@@ -0,0 +1,229 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+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--<created_at UTC ISO8601>-<address hex>
+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
new file mode 100644
index 000000000..87c7cb98f
--- /dev/null
+++ b/accounts/key_store_passphrase.go
@@ -0,0 +1,296 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+/*
+
+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"
+
+ // n,r,p = 2^18, 8, 1 uses 256MB memory and approx 1s CPU time on a modern CPU.
+ StandardScryptN = 1 << 18
+ StandardScryptP = 1
+
+ // n,r,p = 2^12, 8, 6 uses 4MB memory and approx 100ms CPU time on a modern CPU.
+ LightScryptN = 1 << 12
+ 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]
+ keyBytes := crypto.FromECDSA(key.PrivateKey)
+
+ 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
new file mode 100644
index 000000000..217393fa5
--- /dev/null
+++ b/accounts/key_store_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 <http://www.gnu.org/licenses/>.
+
+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
new file mode 100644
index 000000000..ceb455281
--- /dev/null
+++ b/accounts/key_store_plain.go
@@ -0,0 +1,62 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+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
new file mode 100644
index 000000000..b0417c87e
--- /dev/null
+++ b/accounts/key_store_test.go
@@ -0,0 +1,241 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+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())
+ }
+}
diff --git a/accounts/presale.go b/accounts/presale.go
new file mode 100644
index 000000000..bb82821b9
--- /dev/null
+++ b/accounts/presale.go
@@ -0,0 +1,132 @@
+// 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 <http://www.gnu.org/licenses/>.
+
+package accounts
+
+import (
+ "crypto/aes"
+ "crypto/cipher"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "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)
+ 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
new file mode 100644
index 000000000..a3868ec6d
--- /dev/null
+++ b/accounts/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/testdata/dupes/2 b/accounts/testdata/dupes/2
new file mode 100644
index 000000000..a3868ec6d
--- /dev/null
+++ b/accounts/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/testdata/dupes/foo b/accounts/testdata/dupes/foo
new file mode 100644
index 000000000..c57060aea
--- /dev/null
+++ b/accounts/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/testdata/keystore/.hiddenfile b/accounts/testdata/keystore/.hiddenfile
new file mode 100644
index 000000000..d91faccde
--- /dev/null
+++ b/accounts/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/testdata/keystore/README b/accounts/testdata/keystore/README
new file mode 100644
index 000000000..a5a86f964
--- /dev/null
+++ b/accounts/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/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 b/accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8
new file mode 100644
index 000000000..c57060aea
--- /dev/null
+++ b/accounts/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/testdata/keystore/aaa b/accounts/testdata/keystore/aaa
new file mode 100644
index 000000000..a3868ec6d
--- /dev/null
+++ b/accounts/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/testdata/keystore/empty b/accounts/testdata/keystore/empty
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/accounts/testdata/keystore/empty
diff --git a/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e b/accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e
new file mode 100644
index 000000000..309841e52
--- /dev/null
+++ b/accounts/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/testdata/keystore/garbage b/accounts/testdata/keystore/garbage
new file mode 100644
index 000000000..ff45091e7
--- /dev/null
+++ b/accounts/testdata/keystore/garbage
Binary files differ
diff --git a/accounts/testdata/keystore/no-address b/accounts/testdata/keystore/no-address
new file mode 100644
index 000000000..ad51269ea
--- /dev/null
+++ b/accounts/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/testdata/keystore/zero b/accounts/testdata/keystore/zero
new file mode 100644
index 000000000..b52617f8a
--- /dev/null
+++ b/accounts/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/testdata/keystore/zzz b/accounts/testdata/keystore/zzz
new file mode 100644
index 000000000..cfd8a4701
--- /dev/null
+++ b/accounts/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/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e b/accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e
new file mode 100644
index 000000000..498d8131e
--- /dev/null
+++ b/accounts/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/testdata/v1_test_vector.json b/accounts/testdata/v1_test_vector.json
new file mode 100644
index 000000000..3d09b55b5
--- /dev/null
+++ b/accounts/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/testdata/v3_test_vector.json b/accounts/testdata/v3_test_vector.json
new file mode 100644
index 000000000..e9d7b62f0
--- /dev/null
+++ b/accounts/testdata/v3_test_vector.json
@@ -0,0 +1,49 @@
+{
+ "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"
+ }
+}
diff --git a/accounts/testdata/very-light-scrypt.json b/accounts/testdata/very-light-scrypt.json
new file mode 100644
index 000000000..d23b9b2b9
--- /dev/null
+++ b/accounts/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/watch.go b/accounts/watch.go
new file mode 100644
index 000000000..08337b608
--- /dev/null
+++ b/accounts/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 <http://www.gnu.org/licenses/>.
+
+// +build darwin freebsd linux netbsd solaris windows
+
+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
new file mode 100644
index 000000000..ff5a2daf1
--- /dev/null
+++ b/accounts/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 <http://www.gnu.org/licenses/>.
+
+// +build !darwin,!freebsd,!linux,!netbsd,!solaris,!windows
+
+// 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() {}