From 85e6c40c0081bd0db80448640db648887804010c Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Wed, 2 Mar 2016 13:57:15 +0100 Subject: accounts, crypto: move keystore to package accounts The account management API was originally implemented as a thin layer around crypto.KeyStore, on the grounds that several kinds of key stores would be implemented later on. It turns out that this won't happen so KeyStore is a superflous abstraction. In this commit crypto.KeyStore and everything related to it moves to package accounts and is unexported. --- accounts/abi/bind/auth.go | 17 +- accounts/abi/bind/bind_test.go | 34 +-- accounts/account_manager.go | 34 ++- accounts/accounts_test.go | 20 +- accounts/key.go | 179 ++++++++++++ accounts/key_store_passphrase.go | 316 +++++++++++++++++++++ accounts/key_store_passphrase_test.go | 51 ++++ accounts/key_store_plain.go | 199 +++++++++++++ accounts/key_store_test.go | 234 +++++++++++++++ accounts/presale.go | 132 +++++++++ .../cb61d5a9c4896fb9658090b597ef0e7be6f7b67e | 1 + accounts/testdata/v1_test_vector.json | 28 ++ accounts/testdata/v3_test_vector.json | 49 ++++ 13 files changed, 1242 insertions(+), 52 deletions(-) create mode 100644 accounts/key.go create mode 100644 accounts/key_store_passphrase.go create mode 100644 accounts/key_store_passphrase_test.go create mode 100644 accounts/key_store_plain.go create mode 100644 accounts/key_store_test.go create mode 100644 accounts/presale.go create mode 100644 accounts/testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e create mode 100644 accounts/testdata/v1_test_vector.json create mode 100644 accounts/testdata/v3_test_vector.json (limited to 'accounts') 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..c85304066 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -19,9 +19,6 @@ // This abstracts part of a user's interaction with an account she controls. 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" @@ -49,19 +46,26 @@ func (acc *Account) MarshalJSON() ([]byte, error) { } type Manager struct { - keyStore crypto.KeyStore + keyStore keyStore unlocked map[common.Address]*unlocked mutex sync.RWMutex } type unlocked struct { - *crypto.Key + *Key abort chan struct{} } -func NewManager(keyStore crypto.KeyStore) *Manager { +func NewManager(keydir string, scryptN, scryptP int) *Manager { + return &Manager{ + keyStore: newKeyStorePassphrase(keydir, scryptN, scryptP), + unlocked: make(map[common.Address]*unlocked), + } +} + +func NewPlaintextManager(keydir string) *Manager { return &Manager{ - keyStore: keyStore, + keyStore: newKeyStorePlain(keydir), unlocked: make(map[common.Address]*unlocked), } } @@ -216,19 +220,23 @@ func (am *Manager) Export(path string, addr common.Address, keyAuth string) erro } func (am *Manager) Import(path string, keyAuth string) (Account, error) { - privateKeyECDSA, err := crypto.LoadECDSA(path) + priv, err := crypto.LoadECDSA(path) if err != nil { return Account{}, err } - key := crypto.NewKeyFromECDSA(privateKeyECDSA) - if err = am.keyStore.StoreKey(key, keyAuth); err != nil { + return am.ImportECDSA(priv, keyAuth) +} + +func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, keyAuth string) (Account, error) { + key := newKeyFromECDSA(priv) + if err := am.keyStore.StoreKey(key, keyAuth); err != nil { return Account{}, err } return Account{Address: key.Address}, nil } func (am *Manager) Update(addr common.Address, authFrom, authTo string) (err error) { - var key *crypto.Key + var key *Key key, err = am.keyStore.GetKey(addr, authFrom) if err == nil { @@ -241,8 +249,8 @@ func (am *Manager) Update(addr common.Address, authFrom, authTo string) (err err } func (am *Manager) ImportPreSaleKey(keyJSON []byte, password string) (acc Account, err error) { - var key *crypto.Key - key, err = crypto.ImportPreSaleKey(am.keyStore, keyJSON, password) + var key *Key + key, err = importPreSaleKey(am.keyStore, keyJSON, password) if err != nil { return } diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 55ddecdea..02dd74c8a 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -21,17 +21,14 @@ import ( "os" "testing" "time" - - "github.com/ethereum/go-ethereum/crypto" ) var testSigData = make([]byte, 32) func TestSign(t *testing.T) { - dir, ks := tmpKeyStore(t, crypto.NewKeyStorePlain) + dir, am := tmpManager(t, false) defer os.RemoveAll(dir) - am := NewManager(ks) pass := "" // not used but required by API a1, err := am.NewAccount(pass) am.Unlock(a1.Address, "") @@ -43,10 +40,9 @@ func TestSign(t *testing.T) { } func TestTimedUnlock(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) @@ -76,10 +72,9 @@ func TestTimedUnlock(t *testing.T) { } 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) @@ -115,11 +110,10 @@ 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) @@ -141,10 +135,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, LightScryptN, LightScryptP) } + } return d, new(d) } diff --git a/accounts/key.go b/accounts/key.go new file mode 100644 index 000000000..34fefa27c --- /dev/null +++ b/accounts/key.go @@ -0,0 +1,179 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "bytes" + "crypto/ecdsa" + "encoding/hex" + "encoding/json" + "io" + "strings" + + "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 { + // create new key using io.Reader entropy source and optionally using auth string + GenerateNewKey(io.Reader, string) (*Key, error) + GetKey(common.Address, string) (*Key, error) // get key from addr and auth string + GetKeyAddresses() ([]common.Address, error) // get all addresses + StoreKey(*Key, string) error // store key optionally using auth string + DeleteKey(common.Address, string) error // delete key by addr and auth string + Cleanup(keyAddr common.Address) (err error) +} + +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 +} + +func NewKey(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()) + } + + return newKeyFromECDSA(privateKeyECDSA) +} + +// generate 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 +} diff --git a/accounts/key_store_passphrase.go b/accounts/key_store_passphrase.go new file mode 100644 index 000000000..cb00b90af --- /dev/null +++ b/accounts/key_store_passphrase.go @@ -0,0 +1,316 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +/* + +This key store behaves as KeyStorePlain with the difference that +the private key is encrypted and on disk uses another JSON encoding. + +The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition + +*/ + +package accounts + +import ( + "bytes" + "crypto/aes" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + + "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 newKeyStorePassphrase(path string, scryptN int, scryptP int) keyStore { + return &keyStorePassphrase{path, scryptN, scryptP} +} + +func (ks keyStorePassphrase) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { + return generateNewKeyDefault(ks, rand, auth) +} + +func (ks keyStorePassphrase) GetKey(keyAddr common.Address, auth string) (key *Key, err error) { + return decryptKeyFromFile(ks.keysDirPath, keyAddr, auth) +} + +func (ks keyStorePassphrase) Cleanup(keyAddr common.Address) (err error) { + return cleanup(ks.keysDirPath, keyAddr) +} + +func (ks keyStorePassphrase) GetKeyAddresses() (addresses []common.Address, err error) { + return getKeyAddresses(ks.keysDirPath) +} + +func (ks keyStorePassphrase) StoreKey(key *Key, auth string) error { + keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) + if err != nil { + return err + } + return writeKeyFile(key.Address, ks.keysDirPath, keyjson) +} + +// 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) +} + +func (ks keyStorePassphrase) DeleteKey(keyAddr common.Address, auth string) error { + // only delete if correct passphrase is given + if _, err := decryptKeyFromFile(ks.keysDirPath, keyAddr, auth); err != nil { + return err + } + return deleteKey(ks.keysDirPath, keyAddr) +} + +// 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 decryptKeyFromFile(keysDirPath string, keyAddr common.Address, auth string) (*Key, error) { + // Load the key from the keystore and decrypt its contents + keyjson, err := getKeyFile(keysDirPath, keyAddr) + 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 keyAddr != key.Address { + return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, keyAddr) + } + return 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, errors.New("Decryption failed: MAC mismatch") + } + + 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, errors.New("Decryption failed: MAC mismatch") + } + + 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: ", prf) + } + key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) + return key, nil + } + + return nil, fmt.Errorf("Unsupported KDF: ", 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..afa751d44 --- /dev/null +++ b/accounts/key_store_passphrase_test.go @@ -0,0 +1,51 @@ +// Copyright 2016 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "testing" + + "github.com/ethereum/go-ethereum/common" +) + +// Tests that a json key file can be decrypted and encrypted in multiple rounds. +func TestKeyEncryptDecrypt(t *testing.T) { + address := common.HexToAddress("f626acac23772cbe04dd578bee681b06bdefb9fa") + keyjson := []byte("{\"address\":\"f626acac23772cbe04dd578bee681b06bdefb9fa\",\"crypto\":{\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"1bcf0ab9b14459795ce59f63e63255ffd84dc38d31614a5a78e37144d7e4a17f\",\"cipherparams\":{\"iv\":\"df4c7e225ee2d81adef522013e3fbe24\"},\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"2909a99dd2bfa7079a4b40991773b1083f8512c0c55b9b63402ab0e3dc8db8b3\"},\"mac\":\"4ecf6a4ad92ae2c016cb7c44abade74799480c3303eb024661270dfefdbc7510\"},\"id\":\"b4718210-9a30-4883-b8a6-dbdd08bd0ceb\",\"version\":3}") + password := "" + + // 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.Error("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, LightScryptN, LightScryptP); err != nil { + t.Errorf("test %d: failed to recrypt key %v", err) + } + } +} diff --git a/accounts/key_store_plain.go b/accounts/key_store_plain.go new file mode 100644 index 000000000..ca1d89757 --- /dev/null +++ b/accounts/key_store_plain.go @@ -0,0 +1,199 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "encoding/hex" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "time" + + "github.com/ethereum/go-ethereum/common" +) + +type keyStorePlain struct { + keysDirPath string +} + +func newKeyStorePlain(path string) keyStore { + return &keyStorePlain{path} +} + +func (ks keyStorePlain) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { + return generateNewKeyDefault(ks, rand, auth) +} + +func generateNewKeyDefault(ks keyStore, rand io.Reader, auth string) (key *Key, err error) { + defer func() { + if r := recover(); r != nil { + err = fmt.Errorf("GenerateNewKey error: %v", r) + } + }() + key = NewKey(rand) + err = ks.StoreKey(key, auth) + return key, err +} + +func (ks keyStorePlain) GetKey(keyAddr common.Address, auth string) (*Key, error) { + keyjson, err := getKeyFile(ks.keysDirPath, keyAddr) + if err != nil { + return nil, err + } + key := new(Key) + if err := json.Unmarshal(keyjson, key); err != nil { + return nil, err + } + return key, nil +} + +func (ks keyStorePlain) GetKeyAddresses() (addresses []common.Address, err error) { + return getKeyAddresses(ks.keysDirPath) +} + +func (ks keyStorePlain) Cleanup(keyAddr common.Address) (err error) { + return cleanup(ks.keysDirPath, keyAddr) +} + +func (ks keyStorePlain) StoreKey(key *Key, auth string) (err error) { + keyJSON, err := json.Marshal(key) + if err != nil { + return + } + err = writeKeyFile(key.Address, ks.keysDirPath, keyJSON) + return +} + +func (ks keyStorePlain) DeleteKey(keyAddr common.Address, auth string) (err error) { + return deleteKey(ks.keysDirPath, keyAddr) +} + +func deleteKey(keysDirPath string, keyAddr common.Address) (err error) { + var path string + path, err = getKeyFilePath(keysDirPath, keyAddr) + if err == nil { + addrHex := hex.EncodeToString(keyAddr[:]) + if path == filepath.Join(keysDirPath, addrHex, addrHex) { + path = filepath.Join(keysDirPath, addrHex) + } + err = os.RemoveAll(path) + } + return +} + +func getKeyFilePath(keysDirPath string, keyAddr common.Address) (keyFilePath string, err error) { + addrHex := hex.EncodeToString(keyAddr[:]) + matches, err := filepath.Glob(filepath.Join(keysDirPath, fmt.Sprintf("*--%s", addrHex))) + if len(matches) > 0 { + if err == nil { + keyFilePath = matches[len(matches)-1] + } + return + } + keyFilePath = filepath.Join(keysDirPath, addrHex, addrHex) + _, err = os.Stat(keyFilePath) + return +} + +func cleanup(keysDirPath string, keyAddr common.Address) (err error) { + fileInfos, err := ioutil.ReadDir(keysDirPath) + if err != nil { + return + } + var paths []string + account := hex.EncodeToString(keyAddr[:]) + for _, fileInfo := range fileInfos { + path := filepath.Join(keysDirPath, fileInfo.Name()) + if len(path) >= 40 { + addr := path[len(path)-40 : len(path)] + if addr == account { + if path == filepath.Join(keysDirPath, addr, addr) { + path = filepath.Join(keysDirPath, addr) + } + paths = append(paths, path) + } + } + } + if len(paths) > 1 { + for i := 0; err == nil && i < len(paths)-1; i++ { + err = os.RemoveAll(paths[i]) + if err != nil { + break + } + } + } + return +} + +func getKeyFile(keysDirPath string, keyAddr common.Address) (fileContent []byte, err error) { + var keyFilePath string + keyFilePath, err = getKeyFilePath(keysDirPath, keyAddr) + if err == nil { + fileContent, err = ioutil.ReadFile(keyFilePath) + } + return +} + +func writeKeyFile(addr common.Address, keysDirPath string, content []byte) (err error) { + filename := keyFileName(addr) + // read, write and dir search for user + err = os.MkdirAll(keysDirPath, 0700) + if err != nil { + return err + } + // read, write for user + return ioutil.WriteFile(filepath.Join(keysDirPath, filename), content, 0600) +} + +// keyFilePath implements the naming convention for keyfiles: +// UTC---
+func keyFileName(keyAddr common.Address) string { + ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) +} + +func toISO8601(t time.Time) string { + var tz string + name, offset := t.Zone() + if name == "UTC" { + tz = "Z" + } else { + tz = fmt.Sprintf("%03d00", offset/3600) + } + return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) +} + +func getKeyAddresses(keysDirPath string) (addresses []common.Address, err error) { + fileInfos, err := ioutil.ReadDir(keysDirPath) + if err != nil { + return nil, err + } + for _, fileInfo := range fileInfos { + filename := fileInfo.Name() + if len(filename) >= 40 { + addr := filename[len(filename)-40 : len(filename)] + address, err := hex.DecodeString(addr) + if err == nil { + addresses = append(addresses, common.BytesToAddress(address)) + } + } + } + return addresses, err +} diff --git a/accounts/key_store_test.go b/accounts/key_store_test.go new file mode 100644 index 000000000..62ace3720 --- /dev/null +++ b/accounts/key_store_test.go @@ -0,0 +1,234 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package accounts + +import ( + "encoding/hex" + "fmt" + "reflect" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/crypto/randentropy" +) + +func TestKeyStorePlain(t *testing.T) { + ks := newKeyStorePlain(common.DefaultDataDir()) + pass := "" // not used but required by API + k1, err := ks.GenerateNewKey(randentropy.Reader, pass) + if err != nil { + t.Fatal(err) + } + + k2 := new(Key) + k2, err = ks.GetKey(k1.Address, 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) + } + + err = ks.DeleteKey(k2.Address, pass) + if err != nil { + t.Fatal(err) + } +} + +func TestKeyStorePassphrase(t *testing.T) { + ks := newKeyStorePassphrase(common.DefaultDataDir(), LightScryptN, LightScryptP) + pass := "foo" + k1, err := ks.GenerateNewKey(randentropy.Reader, pass) + if err != nil { + t.Fatal(err) + } + k2 := new(Key) + k2, err = ks.GetKey(k1.Address, 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) + } + + err = ks.DeleteKey(k2.Address, pass) // also to clean up created files + if err != nil { + t.Fatal(err) + } +} + +func TestKeyStorePassphraseDecryptionFail(t *testing.T) { + ks := newKeyStorePassphrase(common.DefaultDataDir(), LightScryptN, LightScryptP) + pass := "foo" + k1, err := ks.GenerateNewKey(randentropy.Reader, pass) + if err != nil { + t.Fatal(err) + } + + _, err = ks.GetKey(k1.Address, "bar") // wrong passphrase + if err == nil { + t.Fatal(err) + } + + err = ks.DeleteKey(k1.Address, "bar") // wrong passphrase + if err == nil { + t.Fatal(err) + } + + err = ks.DeleteKey(k1.Address, pass) // to clean up + if err != nil { + t.Fatal(err) + } +} + +func TestImportPreSaleKey(t *testing.T) { + // 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\"}" + ks := newKeyStorePassphrase(common.DefaultDataDir(), LightScryptN, LightScryptP) + pass := "foo" + _, err := importPreSaleKey(ks, []byte(fileContent), pass) + if err != nil { + t.Fatal(err) + } +} + +// 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) { + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["wikipage_test_vector_pbkdf2"], t) +} + +func TestV3_PBKDF2_2(t *testing.T) { + tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + testDecryptV3(tests["test1"], t) +} + +func TestV3_PBKDF2_3(t *testing.T) { + tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + testDecryptV3(tests["python_generated_test_with_odd_iv"], t) +} + +func TestV3_PBKDF2_4(t *testing.T) { + tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + testDecryptV3(tests["evilnonce"], t) +} + +func TestV3_Scrypt_1(t *testing.T) { + tests := loadKeyStoreTestV3("testdata/v3_test_vector.json", t) + testDecryptV3(tests["wikipage_test_vector_scrypt"], t) +} + +func TestV3_Scrypt_2(t *testing.T) { + tests := loadKeyStoreTestV3("../tests/files/KeyStoreTests/basic_tests.json", t) + testDecryptV3(tests["test2"], t) +} + +func TestV1_1(t *testing.T) { + tests := loadKeyStoreTestV1("testdata/v1_test_vector.json", t) + testDecryptV1(tests["test1"], t) +} + +func TestV1_2(t *testing.T) { + ks := newKeyStorePassphrase("testdata/v1", LightScryptN, LightScryptP) + addr := common.HexToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") + k, err := ks.GetKey(addr, "g") + if err != nil { + t.Fatal(err) + } + if k.Address != addr { + t.Fatal(fmt.Errorf("Unexpected address: %v, expected %v", k.Address, addr)) + } + + 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) { + key := NewKeyForDirectICAP(randentropy.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..8faa98558 --- /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 . + +package accounts + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + + "github.com/ethereum/go-ethereum/crypto" + "github.com/pborman/uuid" + "golang.org/x/crypto/pbkdf2" +) + +// creates a Key and stores that in the given KeyStore by decrypting a presale key JSON +func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (*Key, error) { + key, err := decryptPreSaleKey(keyJSON, password) + if err != nil { + return nil, err + } + key.Id = uuid.NewRandom() + err = keyStore.StoreKey(key, password) + return 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 { + err = errors.New("Decryption failed: PKCS7Unpad failed after AES decryption") + } + 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/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" + } +} -- cgit v1.2.3 From 4e6d8b348d864c8af74e0aca114bf730e42a160a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 3 Mar 2016 00:46:56 +0100 Subject: accounts: fix go vet warnings --- accounts/accounts_test.go | 2 +- accounts/key_store_passphrase.go | 4 ++-- accounts/key_store_passphrase_test.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) (limited to 'accounts') diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 02dd74c8a..4a1d1b848 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -120,7 +120,7 @@ func TestSignRace(t *testing.T) { } if err := am.TimedUnlock(a1.Address, "", 15*time.Millisecond); err != nil { - t.Fatalf("could not unlock the test account", err) + t.Fatal("could not unlock the test account", err) } end := time.Now().Add(500 * time.Millisecond) for time.Now().Before(end) { diff --git a/accounts/key_store_passphrase.go b/accounts/key_store_passphrase.go index cb00b90af..3ee86588e 100644 --- a/accounts/key_store_passphrase.go +++ b/accounts/key_store_passphrase.go @@ -295,13 +295,13 @@ func getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) { c := ensureInt(cryptoJSON.KDFParams["c"]) prf := cryptoJSON.KDFParams["prf"].(string) if prf != "hmac-sha256" { - return nil, fmt.Errorf("Unsupported PBKDF2 PRF: ", prf) + 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: ", cryptoJSON.KDF) + return nil, fmt.Errorf("Unsupported KDF: %s", cryptoJSON.KDF) } // TODO: can we do without this when unmarshalling dynamic JSON? diff --git a/accounts/key_store_passphrase_test.go b/accounts/key_store_passphrase_test.go index afa751d44..6ff3ae422 100644 --- a/accounts/key_store_passphrase_test.go +++ b/accounts/key_store_passphrase_test.go @@ -32,7 +32,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { for i := 0; i < 3; i++ { // Try a bad password first if _, err := DecryptKey(keyjson, password+"bad"); err == nil { - t.Error("test %d: json key decrypted with bad password", i) + t.Errorf("test %d: json key decrypted with bad password", i) } // Decrypt with the correct password key, err := DecryptKey(keyjson, password) @@ -45,7 +45,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { // Recrypt with a new password and start over password += "new data appended" if keyjson, err = EncryptKey(key, password, LightScryptN, LightScryptP); err != nil { - t.Errorf("test %d: failed to recrypt key %v", err) + t.Errorf("test %d: failed to recrypt key %v", i, err) } } } -- cgit v1.2.3 From 46e8940b19fee9bc21767a1341c382fd9c9d572a Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 3 Mar 2016 01:09:16 +0100 Subject: accounts: streamline API - Manager.Accounts no longer returns an error. - Manager methods take Account instead of common.Address. - All uses of Account with unkeyed fields are converted. --- accounts/account_manager.go | 57 +++++++++++++++++++-------------------------- accounts/accounts_test.go | 10 ++++---- 2 files changed, 29 insertions(+), 38 deletions(-) (limited to 'accounts') diff --git a/accounts/account_manager.go b/accounts/account_manager.go index c85304066..acf4d8e21 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -24,7 +24,6 @@ import ( crand "crypto/rand" "errors" "fmt" - "os" "sync" "time" @@ -70,8 +69,8 @@ func NewPlaintextManager(keydir string) *Manager { } } -func (am *Manager) HasAccount(addr common.Address) bool { - accounts, _ := am.Accounts() +func (am *Manager) HasAddress(addr common.Address) bool { + accounts := am.Accounts() for _, acct := range accounts { if acct.Address == addr { return true @@ -80,8 +79,8 @@ func (am *Manager) HasAccount(addr common.Address) bool { return false } -func (am *Manager) DeleteAccount(address common.Address, auth string) error { - return am.keyStore.DeleteKey(address, auth) +func (am *Manager) DeleteAccount(a Account, auth string) error { + return am.keyStore.DeleteKey(a.Address, auth) } func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) { @@ -96,8 +95,8 @@ func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) } // 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) } func (am *Manager) Lock(addr common.Address) error { @@ -117,8 +116,8 @@ func (am *Manager) Lock(addr common.Address) error { // // 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) +func (am *Manager) TimedUnlock(a Account, keyAuth string, timeout time.Duration) error { + key, err := am.keyStore.GetKey(a.Address, keyAuth) if err != nil { return err } @@ -126,7 +125,7 @@ func (am *Manager) TimedUnlock(addr common.Address, keyAuth string, timeout time am.mutex.Lock() defer am.mutex.Unlock() var found bool - u, found = am.unlocked[addr] + u, found = am.unlocked[a.Address] if found { // terminate dropLater for this key to avoid unexpected drops. if u.abort != nil { @@ -135,11 +134,11 @@ func (am *Manager) TimedUnlock(addr common.Address, keyAuth string, timeout time } 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 } @@ -171,34 +170,26 @@ func (am *Manager) NewAccount(auth string) (Account, error) { return Account{Address: key.Address}, nil } -func (am *Manager) AddressByIndex(index int) (addr string, err error) { - var addrs []common.Address - addrs, err = am.keyStore.GetKeyAddresses() +func (am *Manager) AccountByIndex(index int) (Account, error) { + addrs, err := am.keyStore.GetKeyAddresses() if err != nil { - return + return Account{}, err } if index < 0 || index >= len(addrs) { - err = fmt.Errorf("index out of range: %d (should be 0-%d)", index, len(addrs)-1) - } else { - addr = addrs[index].Hex() + return Account{}, fmt.Errorf("account index %d not in range [0, %d]", index, len(addrs)-1) } - return + return Account{Address: addrs[index]}, nil } -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 - } +func (am *Manager) Accounts() []Account { + addresses, _ := am.keyStore.GetKeyAddresses() accounts := make([]Account, len(addresses)) for i, addr := range addresses { accounts[i] = Account{ Address: addr, } } - return accounts, err + return accounts } // zeroKey zeroes a private key in memory. @@ -211,8 +202,8 @@ func zeroKey(k *ecdsa.PrivateKey) { // 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) +func (am *Manager) Export(path string, a Account, keyAuth string) error { + key, err := am.keyStore.GetKey(a.Address, keyAuth) if err != nil { return err } @@ -235,14 +226,14 @@ func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, keyAuth string) (Account, return Account{Address: key.Address}, nil } -func (am *Manager) Update(addr common.Address, authFrom, authTo string) (err error) { +func (am *Manager) Update(a Account, authFrom, authTo string) (err error) { var key *Key - key, err = am.keyStore.GetKey(addr, authFrom) + key, err = am.keyStore.GetKey(a.Address, authFrom) if err == nil { err = am.keyStore.StoreKey(key, authTo) if err == nil { - am.keyStore.Cleanup(addr) + am.keyStore.Cleanup(a.Address) } } return diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 4a1d1b848..0cb87a8f1 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -31,7 +31,7 @@ func TestSign(t *testing.T) { pass := "" // not used but required by API a1, err := am.NewAccount(pass) - am.Unlock(a1.Address, "") + am.Unlock(a1, "") _, err = am.Sign(a1, testSigData) if err != nil { @@ -53,7 +53,7 @@ func TestTimedUnlock(t *testing.T) { } // 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) } @@ -79,7 +79,7 @@ func TestOverrideUnlock(t *testing.T) { a1, err := am.NewAccount(pass) // Unlock indefinitely - if err = am.Unlock(a1.Address, pass); err != nil { + if err = am.Unlock(a1, pass); err != nil { t.Fatal(err) } @@ -90,7 +90,7 @@ func TestOverrideUnlock(t *testing.T) { } // 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) } @@ -119,7 +119,7 @@ func TestSignRace(t *testing.T) { t.Fatal("could not create the test account", err) } - if err := am.TimedUnlock(a1.Address, "", 15*time.Millisecond); err != nil { + 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) -- cgit v1.2.3 From ee1682ffe6728618cc4458f3923a4c46fff64d98 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 18 Mar 2016 01:35:03 +0100 Subject: cmd/geth: add tests for account commands --- accounts/testdata/keystore/.hiddenfile | 1 + accounts/testdata/keystore/README | 21 +++++++++++++++++++++ ...1759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 | 1 + ...0983Z--f466859ead1932d743d622cb74fc058882e8648a | 1 + ...8523Z--289d485d9771714cce91d3393d764e1311907acc | 1 + accounts/testdata/keystore/empty | 0 .../foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e | 1 + accounts/testdata/keystore/garbage | Bin 0 -> 300 bytes accounts/testdata/keystore/no-address | 1 + accounts/testdata/keystore/zero | 1 + 10 files changed, 28 insertions(+) create mode 100644 accounts/testdata/keystore/.hiddenfile create mode 100644 accounts/testdata/keystore/README create mode 100644 accounts/testdata/keystore/UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 create mode 100644 accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a create mode 100644 accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc create mode 100644 accounts/testdata/keystore/empty create mode 100644 accounts/testdata/keystore/foo/fd9bd350f08ee3c0c19b85a8e16114a11a60aa4e create mode 100644 accounts/testdata/keystore/garbage create mode 100644 accounts/testdata/keystore/no-address create mode 100644 accounts/testdata/keystore/zero (limited to 'accounts') 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..5d52f55e0 --- /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: UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a + Address: 0xf466859ead1932d743d622cb74fc058882e8648a +- File: UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc + 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/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a b/accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a new file mode 100644 index 000000000..a3868ec6d --- /dev/null +++ b/accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a @@ -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/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc b/accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc new file mode 100644 index 000000000..cfd8a4701 --- /dev/null +++ b/accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc @@ -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/keystore/empty b/accounts/testdata/keystore/empty new file mode 100644 index 000000000..e69de29bb 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 Binary files /dev/null and b/accounts/testdata/keystore/garbage 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 -- cgit v1.2.3 From a9f26dcd0d14c0cb9f309ebccf81e8f741fc4636 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 3 Mar 2016 01:15:42 +0100 Subject: accounts: cache key addresses In order to avoid disk thrashing for Accounts and HasAccount, address->key file mappings are now cached in memory. This makes it no longer necessary to keep the key address in the file name. The address of each key is derived from file content instead. There are minor user-visible changes: - "geth account list" now reports key file paths alongside the address. - If multiple keys are present for an address, unlocking by address is not possible. Users are directed to remove the duplicate files instead. Unlocking by index is still possible. - Key files are overwritten written in place when updating the password. --- accounts/account_manager.go | 189 ++++++++------ accounts/accounts_test.go | 54 +++- accounts/addrcache.go | 269 ++++++++++++++++++++ accounts/addrcache_test.go | 283 +++++++++++++++++++++ accounts/key.go | 94 +++++-- accounts/key_store_passphrase.go | 73 ++---- accounts/key_store_passphrase_test.go | 15 +- accounts/key_store_plain.go | 165 +----------- accounts/key_store_test.go | 103 ++++---- accounts/presale.go | 9 +- accounts/testdata/keystore/README | 4 +- ...0983Z--f466859ead1932d743d622cb74fc058882e8648a | 1 - ...8523Z--289d485d9771714cce91d3393d764e1311907acc | 1 - accounts/testdata/keystore/aaa | 1 + accounts/testdata/keystore/zzz | 1 + accounts/testdata/very-light-scrypt.json | 1 + accounts/watch.go | 113 ++++++++ accounts/watch_fallback.go | 28 ++ 18 files changed, 1042 insertions(+), 362 deletions(-) create mode 100644 accounts/addrcache.go create mode 100644 accounts/addrcache_test.go delete mode 100644 accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a delete mode 100644 accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc create mode 100644 accounts/testdata/keystore/aaa create mode 100644 accounts/testdata/keystore/zzz create mode 100644 accounts/testdata/very-light-scrypt.json create mode 100644 accounts/watch.go create mode 100644 accounts/watch_fallback.go (limited to 'accounts') diff --git a/accounts/account_manager.go b/accounts/account_manager.go index acf4d8e21..dc9f40048 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -22,8 +22,12 @@ package accounts import ( "crypto/ecdsa" crand "crypto/rand" + "encoding/json" "errors" "fmt" + "os" + "path/filepath" + "runtime" "sync" "time" @@ -32,22 +36,28 @@ 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") ) type Account struct { Address common.Address + 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) +} + type Manager struct { + cache *addrCache keyStore keyStore + mu sync.RWMutex unlocked map[common.Address]*unlocked - mutex sync.RWMutex } type unlocked struct { @@ -56,36 +66,62 @@ type unlocked struct { } func NewManager(keydir string, scryptN, scryptP int) *Manager { - return &Manager{ - keyStore: newKeyStorePassphrase(keydir, scryptN, scryptP), - unlocked: make(map[common.Address]*unlocked), - } + keydir, _ = filepath.Abs(keydir) + am := &Manager{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} + am.init(keydir) + return am } func NewPlaintextManager(keydir string) *Manager { - return &Manager{ - keyStore: newKeyStorePlain(keydir), - unlocked: make(map[common.Address]*unlocked), - } + keydir, _ = filepath.Abs(keydir) + am := &Manager{keyStore: &keyStorePlain{keydir}} + am.init(keydir) + return am +} + +func (am *Manager) init(keydir string) { + am.unlocked = make(map[common.Address]*unlocked) + am.cache = newAddrCache(keydir) + // TODO: In order for this finalizer to work, there must be no references + // to am. addrCache doesn't keep a reference but unlocked keys do, + // so the finalizer will not trigger until all timed unlocks have expired. + runtime.SetFinalizer(am, func(m *Manager) { + m.cache.close() + }) } func (am *Manager) HasAddress(addr common.Address) bool { - accounts := am.Accounts() - for _, acct := range accounts { - if acct.Address == addr { - return true - } - } - return false + return am.cache.hasAddress(addr) +} + +func (am *Manager) Accounts() []Account { + return am.cache.accounts() } func (am *Manager) DeleteAccount(a Account, auth string) error { - return am.keyStore.DeleteKey(a.Address, auth) + // 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, auth) + 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() + am.mu.RLock() + defer am.mu.RUnlock() unlockedKey, found := am.unlocked[a.Address] if !found { return nil, ErrLocked @@ -100,12 +136,12 @@ func (am *Manager) Unlock(a Account, keyAuth string) error { } 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 } @@ -117,15 +153,14 @@ func (am *Manager) Lock(addr common.Address) error { // If the accout is already unlocked, TimedUnlock extends or shortens // the active unlock timeout. func (am *Manager) TimedUnlock(a Account, keyAuth string, timeout time.Duration) error { - key, err := am.keyStore.GetKey(a.Address, keyAuth) + _, key, err := am.getDecryptedKey(a, keyAuth) if err != nil { return err } - var u *unlocked - am.mutex.Lock() - defer am.mutex.Unlock() - var found bool - u, found = am.unlocked[a.Address] + + 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 { @@ -142,6 +177,18 @@ func (am *Manager) TimedUnlock(a Account, keyAuth string, timeout time.Duration) 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() @@ -149,7 +196,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 @@ -158,52 +205,33 @@ 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) + _, account, err := storeNewKey(am.keyStore, crand.Reader, auth) 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) AccountByIndex(index int) (Account, error) { - addrs, err := am.keyStore.GetKeyAddresses() - if err != nil { - return Account{}, err - } - if index < 0 || index >= len(addrs) { - return Account{}, fmt.Errorf("account index %d not in range [0, %d]", index, len(addrs)-1) - } - return Account{Address: addrs[index]}, nil -} - -func (am *Manager) Accounts() []Account { - addresses, _ := am.keyStore.GetKeyAddresses() - accounts := make([]Account, len(addresses)) - for i, addr := range addresses { - accounts[i] = Account{ - Address: addr, - } - } - return accounts -} - -// zeroKey zeroes a private key in memory. -func zeroKey(k *ecdsa.PrivateKey) { - b := k.D.Bits() - for i := range b { - b[i] = 0 + accounts := am.Accounts() + if index < 0 || index >= len(accounts) { + return Account{}, fmt.Errorf("account index %d out of range [0, %d]", index, len(accounts)-1) } + return accounts[index], nil } // USE WITH CAUTION = this will save an unencrypted private key on disk // no cli or js interface func (am *Manager) Export(path string, a Account, keyAuth string) error { - key, err := am.keyStore.GetKey(a.Address, keyAuth) + _, key, err := am.getDecryptedKey(a, keyAuth) if err != nil { return err } @@ -220,30 +248,35 @@ func (am *Manager) Import(path string, keyAuth string) (Account, error) { func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, keyAuth string) (Account, error) { key := newKeyFromECDSA(priv) - if err := am.keyStore.StoreKey(key, keyAuth); err != nil { + a := Account{Address: key.Address, File: am.keyStore.JoinPath(keyFileName(key.Address))} + if err := am.keyStore.StoreKey(a.File, key, keyAuth); err != nil { return Account{}, err } - return Account{Address: key.Address}, nil + am.cache.add(a) + return a, nil } -func (am *Manager) Update(a Account, authFrom, authTo string) (err error) { - var key *Key - key, err = am.keyStore.GetKey(a.Address, authFrom) - - if err == nil { - err = am.keyStore.StoreKey(key, authTo) - if err == nil { - am.keyStore.Cleanup(a.Address) - } +func (am *Manager) Update(a Account, authFrom, authTo string) error { + a, key, err := am.getDecryptedKey(a, authFrom) + if err != nil { + return err } - return + return am.keyStore.StoreKey(a.File, key, authTo) } -func (am *Manager) ImportPreSaleKey(keyJSON []byte, password string) (acc Account, err error) { - var key *Key - key, err = importPreSaleKey(am.keyStore, keyJSON, password) +func (am *Manager) ImportPreSaleKey(keyJSON []byte, password string) (Account, error) { + a, _, err := importPreSaleKey(am.keyStore, keyJSON, password) if err != nil { - return + return a, err + } + am.cache.add(a) + return a, nil +} + +// zeroKey zeroes a private key in memory. +func zeroKey(k *ecdsa.PrivateKey) { + b := k.D.Bits() + for i := range b { + b[i] = 0 } - return Account{Address: key.Address}, nil } diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 0cb87a8f1..95945acd5 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -19,28 +19,70 @@ package accounts import ( "io/ioutil" "os" + "runtime" + "strings" "testing" "time" + + "github.com/ethereum/go-ethereum/common" ) var testSigData = make([]byte, 32) +func TestManager(t *testing.T) { + dir, am := tmpManager(t, true) + defer os.RemoveAll(dir) + + a, err := am.NewAccount("foo") + if err != nil { + t.Fatal(err) + } + if !strings.HasPrefix(a.File, dir) { + t.Errorf("account file %s doesn't have dir prefix", a.File) + } + stat, err := os.Stat(a.File) + if err != nil { + t.Fatalf("account file %s doesn't exist (%v)", a.File, err) + } + if runtime.GOOS != "windows" && stat.Mode() != 0600 { + t.Fatalf("account file has wrong mode: got %o, want %o", stat.Mode(), 0600) + } + if !am.HasAddress(a.Address) { + t.Errorf("HasAccount(%x) should've returned true", a.Address) + } + if err := am.Update(a, "foo", "bar"); err != nil { + t.Errorf("Update error: %v", err) + } + if err := am.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, am := tmpManager(t, false) + dir, am := tmpManager(t, true) defer os.RemoveAll(dir) pass := "" // not used but required by API a1, err := am.NewAccount(pass) - am.Unlock(a1, "") - - _, 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, testSigData); err != nil { + t.Fatal(err) + } } func TestTimedUnlock(t *testing.T) { - dir, am := tmpManager(t, false) + dir, am := tmpManager(t, true) defer os.RemoveAll(dir) pass := "foo" @@ -142,7 +184,7 @@ func tmpManager(t *testing.T, encrypted bool) (string, *Manager) { } new := NewPlaintextManager if encrypted { - new = func(kd string) *Manager { return NewManager(kd, LightScryptN, LightScryptP) } + 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 . + +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 . + +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 index 34fefa27c..668fa86a0 100644 --- a/accounts/key.go +++ b/accounts/key.go @@ -21,8 +21,13 @@ import ( "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" @@ -44,13 +49,12 @@ type Key struct { } type keyStore interface { - // create new key using io.Reader entropy source and optionally using auth string - GenerateNewKey(io.Reader, string) (*Key, error) - GetKey(common.Address, string) (*Key, error) // get key from addr and auth string - GetKeyAddresses() ([]common.Address, error) // get all addresses - StoreKey(*Key, string) error // store key optionally using auth string - DeleteKey(common.Address, string) error // delete key by addr and auth string - Cleanup(keyAddr common.Address) (err error) + // 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 { @@ -142,21 +146,6 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { return key } -func NewKey(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()) - } - - return newKeyFromECDSA(privateKeyECDSA) -} - // generate 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. @@ -177,3 +166,64 @@ func NewKeyForDirectICAP(rand io.Reader) *Key { } return key } + +func newKey(rand io.Reader) (*Key, error) { + privateKeyECDSA, err := ecdsa.GenerateKey(secp256k1.S256(), rand) + if err != nil { + return nil, err + } + return newKeyFromECDSA(privateKeyECDSA), nil +} + +func storeNewKey(ks keyStore, rand io.Reader, auth string) (*Key, Account, error) { + key, err := newKey(rand) + if err != nil { + return nil, Account{}, err + } + a := Account{Address: key.Address, File: ks.JoinPath(keyFileName(key.Address))} + if err := ks.StoreKey(a.File, key, auth); err != nil { + zeroKey(key.PrivateKey) + return nil, a, err + } + return key, a, err +} + +func writeKeyFile(file string, content []byte) error { + // Create the keystore directory with appropriate permissions + // in case it is not present yet. + const dirPerm = 0700 + if err := os.MkdirAll(filepath.Dir(file), dirPerm); err != nil { + return err + } + // Atomic write: create a temporary hidden file first + // then move it into place. TempFile assigns mode 0600. + f, err := ioutil.TempFile(filepath.Dir(file), "."+filepath.Base(file)+".tmp") + if err != nil { + return err + } + if _, err := f.Write(content); err != nil { + f.Close() + os.Remove(f.Name()) + return err + } + f.Close() + return os.Rename(f.Name(), file) +} + +// keyFileName implements the naming convention for keyfiles: +// UTC---
+func keyFileName(keyAddr common.Address) string { + ts := time.Now().UTC() + return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) +} + +func toISO8601(t time.Time) string { + var tz string + name, offset := t.Zone() + if name == "UTC" { + tz = "Z" + } else { + tz = fmt.Sprintf("%03d00", offset/3600) + } + return fmt.Sprintf("%04d-%02d-%02dT%02d-%02d-%02d.%09d%s", t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), t.Second(), t.Nanosecond(), tz) +} diff --git a/accounts/key_store_passphrase.go b/accounts/key_store_passphrase.go index 3ee86588e..0cc598bbc 100644 --- a/accounts/key_store_passphrase.go +++ b/accounts/key_store_passphrase.go @@ -33,7 +33,8 @@ import ( "encoding/json" "errors" "fmt" - "io" + "io/ioutil" + "path/filepath" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" @@ -64,32 +65,37 @@ type keyStorePassphrase struct { scryptP int } -func newKeyStorePassphrase(path string, scryptN int, scryptP int) keyStore { - return &keyStorePassphrase{path, scryptN, scryptP} -} - -func (ks keyStorePassphrase) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { - return generateNewKeyDefault(ks, rand, auth) -} - -func (ks keyStorePassphrase) GetKey(keyAddr common.Address, auth string) (key *Key, err error) { - return decryptKeyFromFile(ks.keysDirPath, keyAddr, auth) -} - -func (ks keyStorePassphrase) Cleanup(keyAddr common.Address) (err error) { - return cleanup(ks.keysDirPath, keyAddr) -} - -func (ks keyStorePassphrase) GetKeyAddresses() (addresses []common.Address, err error) { - return getKeyAddresses(ks.keysDirPath) +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(key *Key, auth string) error { +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(key.Address, ks.keysDirPath, keyjson) + 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 @@ -139,14 +145,6 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) { return json.Marshal(encryptedKeyJSONV3) } -func (ks keyStorePassphrase) DeleteKey(keyAddr common.Address, auth string) error { - // only delete if correct passphrase is given - if _, err := decryptKeyFromFile(ks.keysDirPath, keyAddr, auth); err != nil { - return err - } - return deleteKey(ks.keysDirPath, keyAddr) -} - // 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 @@ -184,23 +182,6 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { }, nil } -func decryptKeyFromFile(keysDirPath string, keyAddr common.Address, auth string) (*Key, error) { - // Load the key from the keystore and decrypt its contents - keyjson, err := getKeyFile(keysDirPath, keyAddr) - 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 keyAddr != key.Address { - return nil, fmt.Errorf("key content mismatch: have account %x, want %x", key.Address, keyAddr) - } - return 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) diff --git a/accounts/key_store_passphrase_test.go b/accounts/key_store_passphrase_test.go index 6ff3ae422..217393fa5 100644 --- a/accounts/key_store_passphrase_test.go +++ b/accounts/key_store_passphrase_test.go @@ -17,16 +17,25 @@ 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) { - address := common.HexToAddress("f626acac23772cbe04dd578bee681b06bdefb9fa") - keyjson := []byte("{\"address\":\"f626acac23772cbe04dd578bee681b06bdefb9fa\",\"crypto\":{\"cipher\":\"aes-128-ctr\",\"ciphertext\":\"1bcf0ab9b14459795ce59f63e63255ffd84dc38d31614a5a78e37144d7e4a17f\",\"cipherparams\":{\"iv\":\"df4c7e225ee2d81adef522013e3fbe24\"},\"kdf\":\"scrypt\",\"kdfparams\":{\"dklen\":32,\"n\":262144,\"p\":1,\"r\":8,\"salt\":\"2909a99dd2bfa7079a4b40991773b1083f8512c0c55b9b63402ab0e3dc8db8b3\"},\"mac\":\"4ecf6a4ad92ae2c016cb7c44abade74799480c3303eb024661270dfefdbc7510\"},\"id\":\"b4718210-9a30-4883-b8a6-dbdd08bd0ceb\",\"version\":3}") + 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++ { @@ -44,7 +53,7 @@ func TestKeyEncryptDecrypt(t *testing.T) { } // Recrypt with a new password and start over password += "new data appended" - if keyjson, err = EncryptKey(key, password, LightScryptN, LightScryptP); err != nil { + 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 index ca1d89757..ceb455281 100644 --- a/accounts/key_store_plain.go +++ b/accounts/key_store_plain.go @@ -17,14 +17,10 @@ package accounts import ( - "encoding/hex" "encoding/json" "fmt" - "io" - "io/ioutil" "os" "path/filepath" - "time" "github.com/ethereum/go-ethereum/common" ) @@ -33,167 +29,34 @@ type keyStorePlain struct { keysDirPath string } -func newKeyStorePlain(path string) keyStore { - return &keyStorePlain{path} -} - -func (ks keyStorePlain) GenerateNewKey(rand io.Reader, auth string) (key *Key, err error) { - return generateNewKeyDefault(ks, rand, auth) -} - -func generateNewKeyDefault(ks keyStore, rand io.Reader, auth string) (key *Key, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("GenerateNewKey error: %v", r) - } - }() - key = NewKey(rand) - err = ks.StoreKey(key, auth) - return key, err -} - -func (ks keyStorePlain) GetKey(keyAddr common.Address, auth string) (*Key, error) { - keyjson, err := getKeyFile(ks.keysDirPath, keyAddr) +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.Unmarshal(keyjson, key); err != nil { + if err := json.NewDecoder(fd).Decode(key); err != nil { return nil, err } - return key, nil -} - -func (ks keyStorePlain) GetKeyAddresses() (addresses []common.Address, err error) { - return getKeyAddresses(ks.keysDirPath) -} - -func (ks keyStorePlain) Cleanup(keyAddr common.Address) (err error) { - return cleanup(ks.keysDirPath, keyAddr) -} - -func (ks keyStorePlain) StoreKey(key *Key, auth string) (err error) { - keyJSON, err := json.Marshal(key) - if err != nil { - return - } - err = writeKeyFile(key.Address, ks.keysDirPath, keyJSON) - return -} - -func (ks keyStorePlain) DeleteKey(keyAddr common.Address, auth string) (err error) { - return deleteKey(ks.keysDirPath, keyAddr) -} - -func deleteKey(keysDirPath string, keyAddr common.Address) (err error) { - var path string - path, err = getKeyFilePath(keysDirPath, keyAddr) - if err == nil { - addrHex := hex.EncodeToString(keyAddr[:]) - if path == filepath.Join(keysDirPath, addrHex, addrHex) { - path = filepath.Join(keysDirPath, addrHex) - } - err = os.RemoveAll(path) - } - return -} - -func getKeyFilePath(keysDirPath string, keyAddr common.Address) (keyFilePath string, err error) { - addrHex := hex.EncodeToString(keyAddr[:]) - matches, err := filepath.Glob(filepath.Join(keysDirPath, fmt.Sprintf("*--%s", addrHex))) - if len(matches) > 0 { - if err == nil { - keyFilePath = matches[len(matches)-1] - } - return - } - keyFilePath = filepath.Join(keysDirPath, addrHex, addrHex) - _, err = os.Stat(keyFilePath) - return -} - -func cleanup(keysDirPath string, keyAddr common.Address) (err error) { - fileInfos, err := ioutil.ReadDir(keysDirPath) - if err != nil { - return - } - var paths []string - account := hex.EncodeToString(keyAddr[:]) - for _, fileInfo := range fileInfos { - path := filepath.Join(keysDirPath, fileInfo.Name()) - if len(path) >= 40 { - addr := path[len(path)-40 : len(path)] - if addr == account { - if path == filepath.Join(keysDirPath, addr, addr) { - path = filepath.Join(keysDirPath, addr) - } - paths = append(paths, path) - } - } - } - if len(paths) > 1 { - for i := 0; err == nil && i < len(paths)-1; i++ { - err = os.RemoveAll(paths[i]) - if err != nil { - break - } - } - } - return -} - -func getKeyFile(keysDirPath string, keyAddr common.Address) (fileContent []byte, err error) { - var keyFilePath string - keyFilePath, err = getKeyFilePath(keysDirPath, keyAddr) - if err == nil { - fileContent, err = ioutil.ReadFile(keyFilePath) + if key.Address != addr { + return nil, fmt.Errorf("key content mismatch: have address %x, want %x", key.Address, addr) } - return + return key, nil } -func writeKeyFile(addr common.Address, keysDirPath string, content []byte) (err error) { - filename := keyFileName(addr) - // read, write and dir search for user - err = os.MkdirAll(keysDirPath, 0700) +func (ks keyStorePlain) StoreKey(filename string, key *Key, auth string) error { + content, err := json.Marshal(key) if err != nil { return err } - // read, write for user - return ioutil.WriteFile(filepath.Join(keysDirPath, filename), content, 0600) + return writeKeyFile(filename, content) } -// keyFilePath implements the naming convention for keyfiles: -// UTC---
-func keyFileName(keyAddr common.Address) string { - ts := time.Now().UTC() - return fmt.Sprintf("UTC--%s--%s", toISO8601(ts), hex.EncodeToString(keyAddr[:])) -} - -func toISO8601(t time.Time) string { - var tz string - name, offset := t.Zone() - if name == "UTC" { - tz = "Z" +func (ks keyStorePlain) JoinPath(filename string) string { + if filepath.IsAbs(filename) { + return filename } 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) -} - -func getKeyAddresses(keysDirPath string) (addresses []common.Address, err error) { - fileInfos, err := ioutil.ReadDir(keysDirPath) - if err != nil { - return nil, err - } - for _, fileInfo := range fileInfos { - filename := fileInfo.Name() - if len(filename) >= 40 { - addr := filename[len(filename)-40 : len(filename)] - address, err := hex.DecodeString(addr) - if err == nil { - addresses = append(addresses, common.BytesToAddress(address)) - } - } + return filepath.Join(ks.keysDirPath, filename) } - return addresses, err } diff --git a/accounts/key_store_test.go b/accounts/key_store_test.go index 62ace3720..01bf1b50a 100644 --- a/accounts/key_store_test.go +++ b/accounts/key_store_test.go @@ -17,106 +17,107 @@ 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" - "github.com/ethereum/go-ethereum/crypto/randentropy" ) +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) { - ks := newKeyStorePlain(common.DefaultDataDir()) + dir, ks := tmpKeyStore(t, false) + defer os.RemoveAll(dir) + pass := "" // not used but required by API - k1, err := ks.GenerateNewKey(randentropy.Reader, pass) + k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } - - k2 := new(Key) - k2, err = ks.GetKey(k1.Address, pass) + 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) } - - err = ks.DeleteKey(k2.Address, pass) - if err != nil { - t.Fatal(err) - } } func TestKeyStorePassphrase(t *testing.T) { - ks := newKeyStorePassphrase(common.DefaultDataDir(), LightScryptN, LightScryptP) + dir, ks := tmpKeyStore(t, true) + defer os.RemoveAll(dir) + pass := "foo" - k1, err := ks.GenerateNewKey(randentropy.Reader, pass) + k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } - k2 := new(Key) - k2, err = ks.GetKey(k1.Address, pass) + 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) } - - err = ks.DeleteKey(k2.Address, pass) // also to clean up created files - if err != nil { - t.Fatal(err) - } } func TestKeyStorePassphraseDecryptionFail(t *testing.T) { - ks := newKeyStorePassphrase(common.DefaultDataDir(), LightScryptN, LightScryptP) + dir, ks := tmpKeyStore(t, true) + defer os.RemoveAll(dir) + pass := "foo" - k1, err := ks.GenerateNewKey(randentropy.Reader, pass) + k1, account, err := storeNewKey(ks, rand.Reader, pass) if err != nil { t.Fatal(err) } - - _, err = ks.GetKey(k1.Address, "bar") // wrong passphrase - if err == nil { - t.Fatal(err) - } - - err = ks.DeleteKey(k1.Address, "bar") // wrong passphrase - if err == nil { - t.Fatal(err) - } - - err = ks.DeleteKey(k1.Address, pass) // to clean up - if err != nil { - t.Fatal(err) + if _, err = ks.GetKey(k1.Address, account.File, "bar"); err == nil { + t.Fatal("no error for invalid passphrase") } } 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\"}" - ks := newKeyStorePassphrase(common.DefaultDataDir(), LightScryptN, LightScryptP) pass := "foo" - _, err := importPreSaleKey(ks, []byte(fileContent), pass) + 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; @@ -134,51 +135,56 @@ type KeyStoreTestV1 struct { } 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) { - ks := newKeyStorePassphrase("testdata/v1", LightScryptN, LightScryptP) + t.Parallel() + ks := &keyStorePassphrase{"testdata/v1", LightScryptN, LightScryptP} addr := common.HexToAddress("cb61d5a9c4896fb9658090b597ef0e7be6f7b67e") - k, err := ks.GetKey(addr, "g") + file := "testdata/v1/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e/cb61d5a9c4896fb9658090b597ef0e7be6f7b67e" + k, err := ks.GetKey(addr, file, "g") if err != nil { t.Fatal(err) } - if k.Address != addr { - t.Fatal(fmt.Errorf("Unexpected address: %v, expected %v", k.Address, addr)) - } - privHex := hex.EncodeToString(crypto.FromECDSA(k.PrivateKey)) expectedHex := "d1b1178d3529626a1a93e073f65028370d14c7eb0936eb42abef05db6f37ad7d" if privHex != expectedHex { @@ -227,7 +233,8 @@ func loadKeyStoreTestV1(file string, t *testing.T) map[string]KeyStoreTestV1 { } func TestKeyForDirectICAP(t *testing.T) { - key := NewKeyForDirectICAP(randentropy.Reader) + 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 index 8faa98558..86bfc519c 100644 --- a/accounts/presale.go +++ b/accounts/presale.go @@ -31,14 +31,15 @@ import ( ) // creates a Key and stores that in the given KeyStore by decrypting a presale key JSON -func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (*Key, error) { +func importPreSaleKey(keyStore keyStore, keyJSON []byte, password string) (Account, *Key, error) { key, err := decryptPreSaleKey(keyJSON, password) if err != nil { - return nil, err + return Account{}, nil, err } key.Id = uuid.NewRandom() - err = keyStore.StoreKey(key, password) - return key, err + 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) { diff --git a/accounts/testdata/keystore/README b/accounts/testdata/keystore/README index 5d52f55e0..a5a86f964 100644 --- a/accounts/testdata/keystore/README +++ b/accounts/testdata/keystore/README @@ -5,9 +5,9 @@ The "good" key files which are supposed to be loadable are: - File: UTC--2016-03-22T12-57-55.920751759Z--7ef5a6135f1fd6a02593eedc869c6d41d934aef8 Address: 0x7ef5a6135f1fd6a02593eedc869c6d41d934aef8 -- File: UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a +- File: aaa Address: 0xf466859ead1932d743d622cb74fc058882e8648a -- File: UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc +- File: zzz Address: 0x289d485d9771714cce91d3393d764e1311907acc The other files (including this README) are broken in various ways diff --git a/accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a b/accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a deleted file mode 100644 index a3868ec6d..000000000 --- a/accounts/testdata/keystore/UTC--2016-03-23T09-30-22.528630983Z--f466859ead1932d743d622cb74fc058882e8648a +++ /dev/null @@ -1 +0,0 @@ -{"address":"f466859ead1932d743d622cb74fc058882e8648a","crypto":{"cipher":"aes-128-ctr","ciphertext":"cb664472deacb41a2e995fa7f96fe29ce744471deb8d146a0e43c7898c9ddd4d","cipherparams":{"iv":"dfd9ee70812add5f4b8f89d0811c9158"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"0d6769bf016d45c479213990d6a08d938469c4adad8a02ce507b4a4e7b7739f1"},"mac":"bac9af994b15a45dd39669fc66f9aa8a3b9dd8c22cb16e4d8d7ea089d0f1a1a9"},"id":"472e8b3d-afb6-45b5-8111-72c89895099a","version":3} \ No newline at end of file diff --git a/accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc b/accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc deleted file mode 100644 index cfd8a4701..000000000 --- a/accounts/testdata/keystore/UTC--2016-03-23T09-30-26.532308523Z--289d485d9771714cce91d3393d764e1311907acc +++ /dev/null @@ -1 +0,0 @@ -{"address":"289d485d9771714cce91d3393d764e1311907acc","crypto":{"cipher":"aes-128-ctr","ciphertext":"faf32ca89d286b107f5e6d842802e05263c49b78d46eac74e6109e9a963378ab","cipherparams":{"iv":"558833eec4a665a8c55608d7d503407d"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":8,"p":16,"r":8,"salt":"d571fff447ffb24314f9513f5160246f09997b857ac71348b73e785aab40dc04"},"mac":"21edb85ff7d0dab1767b9bf498f2c3cb7be7609490756bd32300bb213b59effe"},"id":"3279afcf-55ba-43ff-8997-02dcc46a6525","version":3} \ No newline at end of file diff --git a/accounts/testdata/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/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/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 . + +// +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 . + +// +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() {} -- cgit v1.2.3 From 6f1ca0bc910b65b517277f72ca52dadcdc713570 Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 1 Apr 2016 22:41:47 +0200 Subject: accounts: add ErrDecrypt --- accounts/account_manager.go | 1 + accounts/key_store_passphrase.go | 5 ++--- accounts/key_store_test.go | 4 ++-- accounts/presale.go | 3 +-- 4 files changed, 6 insertions(+), 7 deletions(-) (limited to 'accounts') diff --git a/accounts/account_manager.go b/accounts/account_manager.go index dc9f40048..2489d29a0 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -38,6 +38,7 @@ import ( var ( ErrLocked = errors.New("account is locked") ErrNoMatch = errors.New("no key for given address or file") + ErrDecrypt = errors.New("could not decrypt key with given passphrase") ) type Account struct { diff --git a/accounts/key_store_passphrase.go b/accounts/key_store_passphrase.go index 0cc598bbc..87c7cb98f 100644 --- a/accounts/key_store_passphrase.go +++ b/accounts/key_store_passphrase.go @@ -31,7 +31,6 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "io/ioutil" "path/filepath" @@ -214,7 +213,7 @@ func decryptKeyV3(keyProtected *encryptedKeyJSONV3, auth string) (keyBytes []byt calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, errors.New("Decryption failed: MAC mismatch") + return nil, nil, ErrDecrypt } plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) @@ -248,7 +247,7 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, errors.New("Decryption failed: MAC mismatch") + return nil, nil, ErrDecrypt } plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) diff --git a/accounts/key_store_test.go b/accounts/key_store_test.go index 01bf1b50a..b0417c87e 100644 --- a/accounts/key_store_test.go +++ b/accounts/key_store_test.go @@ -94,8 +94,8 @@ func TestKeyStorePassphraseDecryptionFail(t *testing.T) { if err != nil { t.Fatal(err) } - if _, err = ks.GetKey(k1.Address, account.File, "bar"); err == nil { - t.Fatal("no error for invalid passphrase") + if _, err = ks.GetKey(k1.Address, account.File, "bar"); err != ErrDecrypt { + t.Fatalf("wrong error for invalid passphrase\ngot %q\nwant %q", err, ErrDecrypt) } } diff --git a/accounts/presale.go b/accounts/presale.go index 86bfc519c..bb82821b9 100644 --- a/accounts/presale.go +++ b/accounts/presale.go @@ -22,7 +22,6 @@ import ( "crypto/sha256" "encoding/hex" "encoding/json" - "errors" "fmt" "github.com/ethereum/go-ethereum/crypto" @@ -106,7 +105,7 @@ func aesCBCDecrypt(key, cipherText, iv []byte) ([]byte, error) { decrypter.CryptBlocks(paddedPlaintext, cipherText) plaintext := pkcs7Unpad(paddedPlaintext) if plaintext == nil { - err = errors.New("Decryption failed: PKCS7Unpad failed after AES decryption") + return nil, ErrDecrypt } return plaintext, err } -- cgit v1.2.3 From 91aaddaeb38ff25118896fb436a938d14636760b Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Fri, 1 Apr 2016 18:10:58 +0200 Subject: cmd/geth: add recovery procedure for AmbiguousAddrError --- accounts/testdata/dupes/1 | 1 + accounts/testdata/dupes/2 | 1 + accounts/testdata/dupes/foo | 1 + 3 files changed, 3 insertions(+) create mode 100644 accounts/testdata/dupes/1 create mode 100644 accounts/testdata/dupes/2 create mode 100644 accounts/testdata/dupes/foo (limited to 'accounts') 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 -- cgit v1.2.3 From 46df50be181afca503aff4a545e3f322ad04448b Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 5 Apr 2016 01:08:50 +0200 Subject: accounts: improve API and add documentation - Sign takes common.Address, not Account - Import/Export methods work with encrypted JSON keys --- accounts/account_manager.go | 106 +++++++++++++++++++++++++++++--------------- accounts/accounts_test.go | 20 ++++----- accounts/key.go | 6 +-- 3 files changed, 84 insertions(+), 48 deletions(-) (limited to 'accounts') diff --git a/accounts/account_manager.go b/accounts/account_manager.go index 2489d29a0..c174eef1e 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -14,9 +14,10 @@ // You should have received a copy of the GNU Lesser General Public License // along with the go-ethereum library. If not, see . -// 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 import ( @@ -41,9 +42,16 @@ var ( 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 - File string + 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) { @@ -54,6 +62,7 @@ func (acc *Account) UnmarshalJSON(raw []byte) error { return json.Unmarshal(raw, &acc.Address) } +// Manager manages a key storage directory on disk. type Manager struct { cache *addrCache keyStore keyStore @@ -66,6 +75,7 @@ type unlocked struct { abort chan struct{} } +// NewManager creates a manager for the given directory. func NewManager(keydir string, scryptN, scryptP int) *Manager { keydir, _ = filepath.Abs(keydir) am := &Manager{keyStore: &keyStorePassphrase{keydir, scryptN, scryptP}} @@ -73,6 +83,8 @@ func NewManager(keydir string, scryptN, scryptP int) *Manager { return am } +// NewPlaintextManager creates a manager for the given directory. +// Deprecated: Use NewManager. func NewPlaintextManager(keydir string) *Manager { keydir, _ = filepath.Abs(keydir) am := &Manager{keyStore: &keyStorePlain{keydir}} @@ -91,19 +103,23 @@ func (am *Manager) init(keydir string) { }) } +// 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() } -func (am *Manager) DeleteAccount(a Account, auth string) error { +// 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, auth) + a, key, err := am.getDecryptedKey(a, passphrase) if key != nil { zeroKey(key.PrivateKey) } @@ -120,15 +136,15 @@ func (am *Manager) DeleteAccount(a Account, auth string) error { return err } -func (am *Manager) Sign(a Account, toSign []byte) (signature []byte, err error) { +// 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[a.Address] + 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. @@ -136,6 +152,7 @@ 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.mu.Lock() if unl, found := am.unlocked[addr]; found { @@ -147,9 +164,9 @@ func (am *Manager) Lock(addr common.Address) error { return nil } -// TimedUnlock unlocks the account with the given address. The account +// TimedUnlock unlocks the given account with. 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. // // If the accout is already unlocked, TimedUnlock extends or shortens // the active unlock timeout. @@ -210,8 +227,10 @@ func (am *Manager) expire(addr common.Address, u *unlocked, timeout time.Duratio } } -func (am *Manager) NewAccount(auth string) (Account, error) { - _, account, err := storeNewKey(am.keyStore, 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 } @@ -221,52 +240,69 @@ func (am *Manager) NewAccount(auth string) (Account, error) { return account, nil } -func (am *Manager) AccountByIndex(index int) (Account, error) { +// AccountByIndex returns the ith account. +func (am *Manager) AccountByIndex(i int) (Account, error) { accounts := am.Accounts() - if index < 0 || index >= len(accounts) { - return Account{}, fmt.Errorf("account index %d out of range [0, %d]", index, len(accounts)-1) + if i < 0 || i >= len(accounts) { + return Account{}, fmt.Errorf("account index %d out of range [0, %d]", i, len(accounts)-1) } - return accounts[index], nil + return accounts[i], nil } -// USE WITH CAUTION = this will save an unencrypted private key on disk -// no cli or js interface -func (am *Manager) Export(path string, a Account, keyAuth string) error { - _, key, err := am.getDecryptedKey(a, keyAuth) +// 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 err + return nil, err + } + var N, P int + if store, ok := am.keyStore.(*keyStorePassphrase); ok { + N, P = store.scryptN, store.scryptP + } else { + N, P = StandardScryptN, StandardScryptP } - return crypto.SaveECDSA(path, key.PrivateKey) + return EncryptKey(key, newPassphrase, N, P) } -func (am *Manager) Import(path string, keyAuth string) (Account, error) { - priv, err := crypto.LoadECDSA(path) +// Import stores the given encrypted JSON key into the key directory. +func (am *Manager) Import(keyJSON []byte, passphrase, newPassphrase string) (Account, error) { + key, err := DecryptKey(keyJSON, passphrase) + if key != nil && key.PrivateKey != nil { + defer zeroKey(key.PrivateKey) + } if err != nil { return Account{}, err } - return am.ImportECDSA(priv, keyAuth) + return am.importKey(key, newPassphrase) +} + +// ImportECDSA stores the given key into the key directory, encrypting it with the passphrase. +func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Account, error) { + return am.importKey(newKeyFromECDSA(priv), passphrase) } -func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, keyAuth string) (Account, error) { - key := newKeyFromECDSA(priv) +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, keyAuth); err != nil { + if err := am.keyStore.StoreKey(a.File, key, passphrase); err != nil { return Account{}, err } am.cache.add(a) return a, nil } -func (am *Manager) Update(a Account, authFrom, authTo string) error { - a, key, err := am.getDecryptedKey(a, authFrom) +// Update changes the passphrase of an existing account. +func (am *Manager) Update(a Account, passphrase, newPassphrase string) error { + a, key, err := am.getDecryptedKey(a, passphrase) if err != nil { return err } - return am.keyStore.StoreKey(a.File, key, authTo) + return am.keyStore.StoreKey(a.File, key, newPassphrase) } -func (am *Manager) ImportPreSaleKey(keyJSON []byte, password string) (Account, error) { - a, _, err := importPreSaleKey(am.keyStore, keyJSON, password) +// 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 } diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 95945acd5..56d4040c3 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -76,7 +76,7 @@ func TestSign(t *testing.T) { if err := am.Unlock(a1, ""); err != nil { t.Fatal(err) } - if _, err := am.Sign(a1, testSigData); err != nil { + if _, err := am.Sign(a1.Address, testSigData); err != nil { t.Fatal(err) } } @@ -89,7 +89,7 @@ func TestTimedUnlock(t *testing.T) { 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) } @@ -100,14 +100,14 @@ func TestTimedUnlock(t *testing.T) { } // 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) } @@ -126,7 +126,7 @@ func TestOverrideUnlock(t *testing.T) { } // 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) } @@ -137,14 +137,14 @@ func TestOverrideUnlock(t *testing.T) { } // 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) } @@ -166,7 +166,7 @@ func TestSignRace(t *testing.T) { } 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) diff --git a/accounts/key.go b/accounts/key.go index 668fa86a0..dbcb49dcf 100644 --- a/accounts/key.go +++ b/accounts/key.go @@ -146,9 +146,9 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key { return key } -// generate 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. +// 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) -- cgit v1.2.3 From 6498df7b0290139df57629568d824dfa242900cc Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Thu, 7 Apr 2016 17:00:34 +0200 Subject: accounts: ensure TimedUnlock does not override indefinite unlock timeout --- accounts/account_manager.go | 22 ++++++++++++++-------- accounts/accounts_test.go | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) (limited to 'accounts') diff --git a/accounts/account_manager.go b/accounts/account_manager.go index c174eef1e..56499672e 100644 --- a/accounts/account_manager.go +++ b/accounts/account_manager.go @@ -164,14 +164,15 @@ func (am *Manager) Lock(addr common.Address) error { return nil } -// TimedUnlock unlocks the given account with. 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. The account must match a unique key. +// 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(a Account, keyAuth string, timeout time.Duration) error { - _, key, err := am.getDecryptedKey(a, 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 } @@ -180,8 +181,13 @@ func (am *Manager) TimedUnlock(a Account, keyAuth string, timeout time.Duration) 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) } } diff --git a/accounts/accounts_test.go b/accounts/accounts_test.go index 56d4040c3..829cf3968 100644 --- a/accounts/accounts_test.go +++ b/accounts/accounts_test.go @@ -120,8 +120,8 @@ func TestOverrideUnlock(t *testing.T) { pass := "foo" a1, err := am.NewAccount(pass) - // Unlock indefinitely - if err = am.Unlock(a1, pass); err != nil { + // Unlock indefinitely. + if err = am.TimedUnlock(a1, pass, 5*time.Minute); err != nil { t.Fatal(err) } -- cgit v1.2.3