diff --git a/accounts/keystore/db_keystore.go b/accounts/keystore/db_keystore.go index b2a1d6f4fb..a4a81efb91 100644 --- a/accounts/keystore/db_keystore.go +++ b/accounts/keystore/db_keystore.go @@ -1,9 +1,327 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + package keystore -import "reflect" +import ( + "crypto/ecdsa" + crand "crypto/rand" + "fmt" + "math/big" + "reflect" + "sync" + "time" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/ethereum/go-ethereum/event" + "github.com/pborman/uuid" +) // DBKeyStoreType is the reflect type of a keystore backend. var DBKeyStoreType = reflect.TypeOf(&keyStoreDB{}) type keyStoreDB struct { + storage *keyStorePassphraseDB // storage backend, might be mysql or postgres + unlocked map[common.Address]*unlocked // Currently unlocked account (decrypted private keys) + + mu sync.RWMutex +} + +// Wallets implements accounts.Backend, returning all single-key wallets from the KeyStore. +func (ks *keyStoreDB) Wallets() []accounts.Wallet { + accs := ks.storage.All() + wallets := make([]accounts.Wallet, len(accs)) + for idx, acc := range accs { + wallets[idx] = &keystoreWalletDB{account: acc, keystore: ks} + } + return wallets +} + +// Subscribe implements accounts.Backend, creating an async subscription to +// receive notifications on the addition or removal of KeyStore wallets. +func (ks *keyStoreDB) Subscribe(sink chan<- accounts.WalletEvent) event.Subscription { + // Since this is a database backend, we don't need a in-memory cache to hold all the wallets + // so notifications on actions of wallets can be ignored + return nil +} + +// HasAddress reports whether a key with the given address is present. +func (ks *keyStoreDB) HasAddress(addr common.Address) bool { + return ks.storage.Exists(addr) +} + +// Accounts returns all key files present in the KeyStore. +func (ks *keyStoreDB) Accounts() []accounts.Account { + return ks.storage.All() +} + +// Delete deletes the key matched by account if the passphrase is correct. +// If the account contains no filename, the address must match a unique key. +func (ks *keyStoreDB) Delete(a accounts.Account, passphrase string) error { + // Decrypting the key isn't really necessary, but we do + // it anyway to check the password and zero out the key + // immediately afterwards. + a, key, err := ks.getDecryptedKey(a, passphrase) + if key != nil { + zeroKey(key.PrivateKey) + } + if err != nil { + return err + } + ks.storage.db.Del(a.Address.Hex()) + return nil +} + +// SignHash calculates an ECDSA signature for the given hash. The produced +// signature is in the [R || S || V] format where V is 0 or 1. +func (ks *keyStoreDB) SignHash(a accounts.Account, hash []byte) ([]byte, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrLocked + } + // Sign the hash using plain ECDSA operations + return crypto.Sign(hash, unlockedKey.PrivateKey) +} + +// SignTx signs the given transaction with the requested account. +func (ks *keyStoreDB) SignTx(a accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Look up the key to sign with and abort if it cannot be found + ks.mu.RLock() + defer ks.mu.RUnlock() + + unlockedKey, found := ks.unlocked[a.Address] + if !found { + return nil, ErrLocked + } + // Depending on the presence of the chain ID, sign with EIP155 or homestead + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), unlockedKey.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, unlockedKey.PrivateKey) +} + +// SignHashWithPassphrase signs hash if the private key matching the given address +// can be decrypted with the given passphrase. The produced signature is in the +// [R || S || V] format where V is 0 or 1. +func (ks *keyStoreDB) SignHashWithPassphrase(a accounts.Account, passphrase string, hash []byte) (signature []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + defer zeroKey(key.PrivateKey) + return crypto.Sign(hash, key.PrivateKey) +} + +// SignTxWithPassphrase signs the transaction if the private key matching the +// given address can be decrypted with the given passphrase. +func (ks *keyStoreDB) SignTxWithPassphrase(a accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + defer zeroKey(key.PrivateKey) + + // Depending on the presence of the chain ID, sign with EIP155 or homestead + if chainID != nil { + return types.SignTx(tx, types.NewEIP155Signer(chainID), key.PrivateKey) + } + return types.SignTx(tx, types.HomesteadSigner{}, key.PrivateKey) +} + +// Unlock unlocks the given account indefinitely. +func (ks *keyStoreDB) Unlock(a accounts.Account, passphrase string) error { + return ks.TimedUnlock(a, passphrase, 0) +} + +// Lock removes the private key with the given address from memory. +func (ks *keyStoreDB) Lock(addr common.Address) error { + ks.mu.Lock() + if unl, found := ks.unlocked[addr]; found { + ks.mu.Unlock() + ks.expire(addr, unl, time.Duration(0)*time.Nanosecond) + } else { + ks.mu.Unlock() + } + return nil +} + +// TimedUnlock unlocks the given account with the passphrase. The account +// stays unlocked for the duration of timeout. A timeout of 0 unlocks the account +// until the program exits. The account must match a unique key file. +// +// If the account address is already unlocked for a duration, TimedUnlock extends or +// shortens the active unlock timeout. If the address was previously unlocked +// indefinitely the timeout is not altered. +func (ks *keyStoreDB) TimedUnlock(a accounts.Account, passphrase string, timeout time.Duration) error { + a, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + + ks.mu.Lock() + defer ks.mu.Unlock() + + u, found := ks.unlocked[a.Address] + if found { + if u.abort == nil { + // The address was unlocked indefinitely, so unlocking + // it with a timeout would be confusing. + zeroKey(key.PrivateKey) + return nil + } + // Terminate the expire goroutine and replace it below. + close(u.abort) + } + if timeout > 0 { + u = &unlocked{Key: key, abort: make(chan struct{})} + go ks.expire(a.Address, u, timeout) + } else { + u = &unlocked{Key: key} + } + ks.unlocked[a.Address] = u + return nil +} + +func (ks *keyStoreDB) expire(addr common.Address, u *unlocked, timeout time.Duration) { + t := time.NewTimer(timeout) + defer t.Stop() + select { + case <-u.abort: + // just quit + case <-t.C: + ks.mu.Lock() + // only drop if it's still the same key instance that dropLater + // was launched with. we can check that using pointer equality + // because the map stores a new pointer every time the key is + // unlocked. + if ks.unlocked[addr] == u { + zeroKey(u.PrivateKey) + delete(ks.unlocked, addr) + } + ks.mu.Unlock() + } +} + +// Find resolves the given account into a unique entry in the KeyStore. +func (ks *keyStoreDB) Find(a accounts.Account) (accounts.Account, error) { + return ks.storage.Find(a) +} + +// NewAccount generates a new key and stores it into the KeyStore, +// encrypting it with the passphrase. +func (ks *keyStoreDB) NewAccount(passphrase string) (accounts.Account, error) { + key, err := newKey(crand.Reader) + if err != nil { + return accounts.Account{}, err + } + a := accounts.Account{ + Address: key.Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(key.Address.Hex())}, + } + if err := ks.storage.StoreKey(key, passphrase); err != nil { + zeroKey(key.PrivateKey) + return accounts.Account{}, err + } + return a, err +} + +// Export exports as a JSON key, encrypted with newPassphrase. +func (ks *keyStoreDB) Export(a accounts.Account, passphrase, newPassphrase string) (keyJSON []byte, err error) { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return nil, err + } + N, P := ks.storage.scryptN, ks.storage.scryptP + return EncryptKey(key, newPassphrase, N, P) +} + +// Import stores the given encrypted JSON key into the KeyStore. +func (ks *keyStoreDB) Import(keyJSON []byte, passphrase, newPassphrase string) (accounts.Account, error) { + key, err := DecryptKey(keyJSON, passphrase) + if key != nil && key.PrivateKey != nil { + defer zeroKey(key.PrivateKey) + } + if err != nil { + return accounts.Account{}, err + } + if ks.storage.Exists(key.Address) { + return accounts.Account{}, fmt.Errorf("account already exists") + } + return ks.importKey(key, newPassphrase) +} + +// ImportECDSA stores the given key into the KeyStore, encrypting it with the passphrase. +func (ks *keyStoreDB) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (accounts.Account, error) { + key := newKeyFromECDSA(priv) + if ks.storage.Exists(key.Address) { + return accounts.Account{}, fmt.Errorf("account already exists") + } + return ks.importKey(key, passphrase) +} + +func (ks *keyStoreDB) importKey(key *Key, passphrase string) (accounts.Account, error) { + a := accounts.Account{ + Address: key.Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(key.Address.Hex())}, + } + if err := ks.storage.StoreKey(key, passphrase); err != nil { + return accounts.Account{}, err + } + return a, nil +} + +// Update changes the passphrase of an existing account. +func (ks *keyStoreDB) Update(a accounts.Account, passphrase, newPassphrase string) error { + _, key, err := ks.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + return ks.storage.StoreKey(key, newPassphrase) +} + +// ImportPreSaleKey decrypts the given Ethereum presale wallet and stores +// a key file in the KeyStore. The key file is encrypted with the same passphrase. +func (ks *keyStoreDB) ImportPreSaleKey(keyJSON []byte, passphrase string) (accounts.Account, error) { + key, err := decryptPreSaleKey(keyJSON, passphrase) + if err != nil { + return accounts.Account{}, nil + } + key.Id = uuid.NewRandom() + if err := ks.storage.StoreKey(key, passphrase); err != nil { + return accounts.Account{}, err + } + a := accounts.Account{ + Address: key.Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(key.Address.Hex())}, + } + return a, nil +} + +func (ks *keyStoreDB) getDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) { + a, err := ks.Find(a) + if err != nil { + return a, nil, err + } + key, err := ks.storage.GetKey(a.Address, auth) + return a, key, err } diff --git a/accounts/keystore/db_passphrase.go b/accounts/keystore/db_passphrase.go new file mode 100644 index 0000000000..6ed7b80bc1 --- /dev/null +++ b/accounts/keystore/db_passphrase.go @@ -0,0 +1,117 @@ +// Copyright 2014 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/cmd/clef/dbutil" + "github.com/ethereum/go-ethereum/common" +) + +type keyStorePassphraseDB struct { + db *dbutil.KVStore + scryptN int + scryptP int + + // skipKeyFileVerification disables the security-feature which does + // reads and decrypts any newly created keyfiles. This should be 'false' in all + // cases except tests -- setting this to 'true' is not recommended. + skipKeyFileVerification bool +} + +func (ks keyStorePassphraseDB) GetKey(addr common.Address, auth string) (*Key, error) { + keyjson, err := ks.db.Get(addr.Hex()) + if err != nil { + return nil, err + } + + key, err := DecryptKey([]byte(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 keyStorePassphraseDB) StoreKey(key *Key, auth string) error { + keyjson, err := EncryptKey(key, auth, ks.scryptN, ks.scryptP) + if err != nil { + return err + } + // Write into database + err = ks.db.Put(key.Address.Hex(), string(keyjson)) + if err != nil { + return err + } + if !ks.skipKeyFileVerification { + // Verify that we can decrypt the file with the given password. + _, err = ks.GetKey(key.Address, auth) + if err != nil { + msg := "An error was encountered when saving and verifying the keystore file. \n" + + "This indicates that the keystore is corrupted. \n" + + "The corrupted key is stored at \n%v\n" + + "Please file a ticket at:\n\n" + + "https://github.com/ethereum/go-ethereum/issues." + + "The error was : %s" + //lint:ignore ST1005 This is a message for the user + return fmt.Errorf(msg, ks.db.Conf.DataSourceName(), err) + } + } + return nil +} + +// JoinPath returns Path (custom database related) for creating accounts.Account +func (ks keyStorePassphraseDB) JoinPath(key string) string { + return ks.db.Conf.Adapter + "/" + ks.db.Table + "/" + key +} + +func (ks keyStorePassphraseDB) Exists(addr common.Address) bool { + return ks.db.Exists(addr.Hex()) +} + +func (ks keyStorePassphraseDB) All() []accounts.Account { + keys := ks.db.All() + accs := make([]accounts.Account, len(keys)) + for idx, key := range keys { + accs[idx] = accounts.Account{ + Address: common.HexToAddress(key), + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(key)}, + } + } + return accs +} + +// Find returns the account with the correct URL for database backed account +func (ks keyStorePassphraseDB) Find(a accounts.Account) (accounts.Account, error) { + found := ks.Exists(a.Address) + if found { + return accounts.Account{ + Address: a.Address, + URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.JoinPath(a.Address.Hex())}, + }, nil + } + return accounts.Account{}, ErrNoMatch +} + +func (ks keyStorePassphraseDB) Size() int { + return ks.db.Size() +} diff --git a/accounts/keystore/db_wallet.go b/accounts/keystore/db_wallet.go new file mode 100644 index 0000000000..89f25b4bcb --- /dev/null +++ b/accounts/keystore/db_wallet.go @@ -0,0 +1,145 @@ +// Copyright 2017 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package keystore + +import ( + "math/big" + + ethereum "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" +) + +// keystoreWalletDB implements the accounts.Wallet interface for the original +// keystore. +type keystoreWalletDB struct { + account accounts.Account // Single account contained in this wallet + keystore *keyStoreDB // Keystore where the account originates from +} + +// URL implements accounts.Wallet, returning the URL of the account within. +func (w *keystoreWalletDB) URL() accounts.URL { + return w.account.URL +} + +// Status implements accounts.Wallet, returning whether the account held by the +// keystore wallet is unlocked or not. +func (w *keystoreWalletDB) Status() (string, error) { + if _, ok := w.keystore.unlocked[w.account.Address]; ok { + return "Unlocked", nil + } + return "Locked", nil +} + +// Open implements accounts.Wallet, but is a noop for plain wallets since there +// is no connection or decryption step necessary to access the list of accounts. +func (w *keystoreWalletDB) Open(passphrase string) error { return nil } + +// Close implements accounts.Wallet, but is a noop for plain wallets since there +// is no meaningful open operation. +func (w *keystoreWalletDB) Close() error { return nil } + +// Accounts implements accounts.Wallet, returning an account list consisting of +// a single account that the plain kestore wallet contains. +func (w *keystoreWalletDB) Accounts() []accounts.Account { + return []accounts.Account{w.account} +} + +// Contains implements accounts.Wallet, returning whether a particular account is +// or is not wrapped by this wallet instance. +func (w *keystoreWalletDB) Contains(account accounts.Account) bool { + return account.Address == w.account.Address && (account.URL == (accounts.URL{}) || account.URL == w.account.URL) +} + +// Derive implements accounts.Wallet, but is a noop for plain wallets since there +// is no notion of hierarchical account derivation for plain keystore accounts. +func (w *keystoreWalletDB) Derive(path accounts.DerivationPath, pin bool) (accounts.Account, error) { + return accounts.Account{}, accounts.ErrNotSupported +} + +// SelfDerive implements accounts.Wallet, but is a noop for plain wallets since +// there is no notion of hierarchical account derivation for plain keystore accounts. +func (w *keystoreWalletDB) SelfDerive(bases []accounts.DerivationPath, chain ethereum.ChainStateReader) { +} + +// signHash attempts to sign the given hash with +// the given account. If the wallet does not wrap this particular account, an +// error is returned to avoid account leakage (even though in theory we may be +// able to sign via our shared keystore backend). +func (w *keystoreWalletDB) signHash(account accounts.Account, hash []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHash(account, hash) +} + +// SignData signs keccak256(data). The mimetype parameter describes the type of data being signed +func (w *keystoreWalletDB) SignData(account accounts.Account, mimeType string, data []byte) ([]byte, error) { + return w.signHash(account, crypto.Keccak256(data)) +} + +// SignDataWithPassphrase signs keccak256(data). The mimetype parameter describes the type of data being signed +func (w *keystoreWalletDB) SignDataWithPassphrase(account accounts.Account, passphrase, mimeType string, data []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHashWithPassphrase(account, passphrase, crypto.Keccak256(data)) +} + +func (w *keystoreWalletDB) SignText(account accounts.Account, text []byte) ([]byte, error) { + return w.signHash(account, accounts.TextHash(text)) +} + +// SignTextWithPassphrase implements accounts.Wallet, attempting to sign the +// given hash with the given account using passphrase as extra authentication. +func (w *keystoreWalletDB) SignTextWithPassphrase(account accounts.Account, passphrase string, text []byte) ([]byte, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignHashWithPassphrase(account, passphrase, accounts.TextHash(text)) +} + +// SignTx implements accounts.Wallet, attempting to sign the given transaction +// with the given account. If the wallet does not wrap this particular account, +// an error is returned to avoid account leakage (even though in theory we may +// be able to sign via our shared keystore backend). +func (w *keystoreWalletDB) SignTx(account accounts.Account, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignTx(account, tx, chainID) +} + +// SignTxWithPassphrase implements accounts.Wallet, attempting to sign the given +// transaction with the given account using passphrase as extra authentication. +func (w *keystoreWalletDB) SignTxWithPassphrase(account accounts.Account, passphrase string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + // Make sure the requested account is contained within + if !w.Contains(account) { + return nil, accounts.ErrUnknownAccount + } + // Account seems valid, request the keystore to sign + return w.keystore.SignTxWithPassphrase(account, passphrase, tx, chainID) +} diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go index 84d8df0c5a..8a002ac6ff 100644 --- a/accounts/keystore/key.go +++ b/accounts/keystore/key.go @@ -52,8 +52,10 @@ type keyStore interface { // Loads and decrypts the key from disk. GetKey(addr common.Address, filename string, auth string) (*Key, error) // Writes and encrypts the key. + // If it is a database backed keyStore, the input filename will be ignored StoreKey(filename string, k *Key, auth string) error - // Joins filename with the key directory unless it is already absolute. + // JoinPath forms the Account URL Path + // If it is a database backed keyStore, the input should be the key string JoinPath(filename string) string } diff --git a/accounts/keystore/passphrase.go b/accounts/keystore/passphrase.go index 89cdf0bfca..99ea613f8c 100644 --- a/accounts/keystore/passphrase.go +++ b/accounts/keystore/passphrase.go @@ -26,48 +26,14 @@ The crypto is documented at https://github.com/ethereum/wiki/wiki/Web3-Secret-St package keystore import ( - "bytes" - "crypto/aes" "crypto/rand" - "crypto/sha256" - "encoding/hex" - "encoding/json" "fmt" - "io" "io/ioutil" "os" "path/filepath" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/math" - "github.com/ethereum/go-ethereum/crypto" - "github.com/pborman/uuid" - "golang.org/x/crypto/pbkdf2" - "golang.org/x/crypto/scrypt" -) - -const ( - keyHeaderKDF = "scrypt" - - // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB - // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptN = 1 << 18 - - // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB - // memory and taking approximately 1s CPU time on a modern processor. - StandardScryptP = 1 - - // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB - // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptN = 1 << 12 - - // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB - // memory and taking approximately 100ms CPU time on a modern processor. - LightScryptP = 6 - - scryptR = 8 - scryptDKLen = 32 ) type keyStorePassphrase struct { @@ -136,222 +102,3 @@ func (ks keyStorePassphrase) JoinPath(filename string) string { } return filepath.Join(ks.keysDirPath, filename) } - -// Encryptdata encrypts the data given as 'data' with the password 'auth'. -func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { - - salt := make([]byte, 32) - if _, err := io.ReadFull(rand.Reader, salt); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) - } - derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) - if err != nil { - return CryptoJSON{}, err - } - encryptKey := derivedKey[:16] - - iv := make([]byte, aes.BlockSize) // 16 - if _, err := io.ReadFull(rand.Reader, iv); err != nil { - panic("reading from crypto/rand failed: " + err.Error()) - } - cipherText, err := aesCTRXOR(encryptKey, data, iv) - if err != nil { - return CryptoJSON{}, 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: keyHeaderKDF, - KDFParams: scryptParamsJSON, - MAC: hex.EncodeToString(mac), - } - return cryptoStruct, nil -} - -// 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) { - keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32) - cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) - if err != nil { - return nil, err - } - encryptedKeyJSONV3 := encryptedKeyJSONV3{ - hex.EncodeToString(key.Address[:]), - cryptoStruct, - key.Id.String(), - version, - } - return json.Marshal(encryptedKeyJSONV3) -} - -// DecryptKey decrypts a key from a json blob, returning the private key itself. -func DecryptKey(keyjson []byte, auth string) (*Key, error) { - // Parse the json into a simple map to fetch the key version - m := make(map[string]interface{}) - if err := json.Unmarshal(keyjson, &m); err != nil { - return nil, err - } - // Depending on the version try to parse one way or another - var ( - keyBytes, keyId []byte - err error - ) - if version, ok := m["version"].(string); ok && version == "1" { - k := new(encryptedKeyJSONV1) - if err := json.Unmarshal(keyjson, k); err != nil { - return nil, err - } - keyBytes, keyId, err = decryptKeyV1(k, auth) - } else { - k := new(encryptedKeyJSONV3) - if err := json.Unmarshal(keyjson, k); err != nil { - return nil, err - } - keyBytes, keyId, err = decryptKeyV3(k, auth) - } - // Handle any decryption errors and return the key - if err != nil { - return nil, err - } - key := crypto.ToECDSAUnsafe(keyBytes) - - return &Key{ - Id: uuid.UUID(keyId), - Address: crypto.PubkeyToAddress(key.PublicKey), - PrivateKey: key, - }, nil -} - -func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { - if cryptoJson.Cipher != "aes-128-ctr" { - return nil, fmt.Errorf("cipher not supported: %v", cryptoJson.Cipher) - } - mac, err := hex.DecodeString(cryptoJson.MAC) - if err != nil { - return nil, err - } - - iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) - if err != nil { - return nil, err - } - - cipherText, err := hex.DecodeString(cryptoJson.CipherText) - if err != nil { - return nil, err - } - - derivedKey, err := getKDFKey(cryptoJson, auth) - if err != nil { - return nil, err - } - - calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) - if !bytes.Equal(calculatedMAC, mac) { - return nil, ErrDecrypt - } - - plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) - if err != nil { - return nil, err - } - return plainText, err -} - -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) - } - keyId = uuid.Parse(keyProtected.Id) - plainText, err := DecryptDataV3(keyProtected.Crypto, auth) - if err != nil { - return nil, nil, err - } - return plainText, keyId, err -} - -func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { - keyId = uuid.Parse(keyProtected.Id) - mac, err := hex.DecodeString(keyProtected.Crypto.MAC) - if err != nil { - return nil, nil, err - } - - iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) - if err != nil { - return nil, nil, err - } - - cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) - if err != nil { - return nil, nil, err - } - - derivedKey, err := getKDFKey(keyProtected.Crypto, auth) - if err != nil { - return nil, nil, err - } - - calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) - if !bytes.Equal(calculatedMAC, mac) { - return nil, nil, ErrDecrypt - } - - plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) - if err != nil { - return nil, nil, err - } - return plainText, keyId, err -} - -func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { - authArray := []byte(auth) - salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) - if err != nil { - return nil, err - } - dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) - - if cryptoJSON.KDF == keyHeaderKDF { - n := ensureInt(cryptoJSON.KDFParams["n"]) - r := ensureInt(cryptoJSON.KDFParams["r"]) - p := ensureInt(cryptoJSON.KDFParams["p"]) - return scrypt.Key(authArray, salt, n, r, p, dkLen) - - } else if cryptoJSON.KDF == "pbkdf2" { - c := ensureInt(cryptoJSON.KDFParams["c"]) - prf := cryptoJSON.KDFParams["prf"].(string) - if prf != "hmac-sha256" { - return nil, fmt.Errorf("unsupported PBKDF2 PRF: %s", prf) - } - key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) - return key, nil - } - - return nil, fmt.Errorf("unsupported KDF: %s", cryptoJSON.KDF) -} - -// TODO: can we do without this when unmarshalling dynamic JSON? -// why do integers in KDF params end up as float64 and not int after -// unmarshal? -func ensureInt(x interface{}) int { - res, ok := x.(int) - if !ok { - res = int(x.(float64)) - } - return res -} diff --git a/accounts/keystore/passphrase_util.go b/accounts/keystore/passphrase_util.go new file mode 100644 index 0000000000..e920ddb0ea --- /dev/null +++ b/accounts/keystore/passphrase_util.go @@ -0,0 +1,260 @@ +package keystore + +import ( + "bytes" + "crypto/aes" + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + + "github.com/ethereum/go-ethereum/common/math" + "github.com/ethereum/go-ethereum/crypto" + "github.com/pborman/uuid" + "golang.org/x/crypto/pbkdf2" + "golang.org/x/crypto/scrypt" +) + +const ( + keyHeaderKDF = "scrypt" + + // StandardScryptN is the N parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptN = 1 << 18 + + // StandardScryptP is the P parameter of Scrypt encryption algorithm, using 256MB + // memory and taking approximately 1s CPU time on a modern processor. + StandardScryptP = 1 + + // LightScryptN is the N parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptN = 1 << 12 + + // LightScryptP is the P parameter of Scrypt encryption algorithm, using 4MB + // memory and taking approximately 100ms CPU time on a modern processor. + LightScryptP = 6 + + scryptR = 8 + scryptDKLen = 32 +) + +// Encryptdata encrypts the data given as 'data' with the password 'auth'. +func EncryptDataV3(data, auth []byte, scryptN, scryptP int) (CryptoJSON, error) { + + salt := make([]byte, 32) + if _, err := io.ReadFull(rand.Reader, salt); err != nil { + panic("reading from crypto/rand failed: " + err.Error()) + } + derivedKey, err := scrypt.Key(auth, salt, scryptN, scryptR, scryptP, scryptDKLen) + if err != nil { + return CryptoJSON{}, err + } + encryptKey := derivedKey[:16] + + iv := make([]byte, aes.BlockSize) // 16 + if _, err := io.ReadFull(rand.Reader, iv); err != nil { + panic("reading from crypto/rand failed: " + err.Error()) + } + cipherText, err := aesCTRXOR(encryptKey, data, iv) + if err != nil { + return CryptoJSON{}, 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: keyHeaderKDF, + KDFParams: scryptParamsJSON, + MAC: hex.EncodeToString(mac), + } + return cryptoStruct, nil +} + +// 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) { + keyBytes := math.PaddedBigBytes(key.PrivateKey.D, 32) + cryptoStruct, err := EncryptDataV3(keyBytes, []byte(auth), scryptN, scryptP) + if err != nil { + return nil, err + } + encryptedKeyJSONV3 := encryptedKeyJSONV3{ + hex.EncodeToString(key.Address[:]), + cryptoStruct, + key.Id.String(), + version, + } + return json.Marshal(encryptedKeyJSONV3) +} + +// DecryptKey decrypts a key from a json blob, returning the private key itself. +func DecryptKey(keyjson []byte, auth string) (*Key, error) { + // Parse the json into a simple map to fetch the key version + m := make(map[string]interface{}) + if err := json.Unmarshal(keyjson, &m); err != nil { + return nil, err + } + // Depending on the version try to parse one way or another + var ( + keyBytes, keyId []byte + err error + ) + if version, ok := m["version"].(string); ok && version == "1" { + k := new(encryptedKeyJSONV1) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV1(k, auth) + } else { + k := new(encryptedKeyJSONV3) + if err := json.Unmarshal(keyjson, k); err != nil { + return nil, err + } + keyBytes, keyId, err = decryptKeyV3(k, auth) + } + // Handle any decryption errors and return the key + if err != nil { + return nil, err + } + key := crypto.ToECDSAUnsafe(keyBytes) + + return &Key{ + Id: uuid.UUID(keyId), + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: key, + }, nil +} + +func DecryptDataV3(cryptoJson CryptoJSON, auth string) ([]byte, error) { + if cryptoJson.Cipher != "aes-128-ctr" { + return nil, fmt.Errorf("cipher not supported: %v", cryptoJson.Cipher) + } + mac, err := hex.DecodeString(cryptoJson.MAC) + if err != nil { + return nil, err + } + + iv, err := hex.DecodeString(cryptoJson.CipherParams.IV) + if err != nil { + return nil, err + } + + cipherText, err := hex.DecodeString(cryptoJson.CipherText) + if err != nil { + return nil, err + } + + derivedKey, err := getKDFKey(cryptoJson, auth) + if err != nil { + return nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, ErrDecrypt + } + + plainText, err := aesCTRXOR(derivedKey[:16], cipherText, iv) + if err != nil { + return nil, err + } + return plainText, err +} + +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) + } + keyId = uuid.Parse(keyProtected.Id) + plainText, err := DecryptDataV3(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byte, keyId []byte, err error) { + keyId = uuid.Parse(keyProtected.Id) + mac, err := hex.DecodeString(keyProtected.Crypto.MAC) + if err != nil { + return nil, nil, err + } + + iv, err := hex.DecodeString(keyProtected.Crypto.CipherParams.IV) + if err != nil { + return nil, nil, err + } + + cipherText, err := hex.DecodeString(keyProtected.Crypto.CipherText) + if err != nil { + return nil, nil, err + } + + derivedKey, err := getKDFKey(keyProtected.Crypto, auth) + if err != nil { + return nil, nil, err + } + + calculatedMAC := crypto.Keccak256(derivedKey[16:32], cipherText) + if !bytes.Equal(calculatedMAC, mac) { + return nil, nil, ErrDecrypt + } + + plainText, err := aesCBCDecrypt(crypto.Keccak256(derivedKey[:16])[:16], cipherText, iv) + if err != nil { + return nil, nil, err + } + return plainText, keyId, err +} + +func getKDFKey(cryptoJSON CryptoJSON, auth string) ([]byte, error) { + authArray := []byte(auth) + salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string)) + if err != nil { + return nil, err + } + dkLen := ensureInt(cryptoJSON.KDFParams["dklen"]) + + if cryptoJSON.KDF == keyHeaderKDF { + n := ensureInt(cryptoJSON.KDFParams["n"]) + r := ensureInt(cryptoJSON.KDFParams["r"]) + p := ensureInt(cryptoJSON.KDFParams["p"]) + return scrypt.Key(authArray, salt, n, r, p, dkLen) + + } else if cryptoJSON.KDF == "pbkdf2" { + c := ensureInt(cryptoJSON.KDFParams["c"]) + prf := cryptoJSON.KDFParams["prf"].(string) + if prf != "hmac-sha256" { + return nil, fmt.Errorf("unsupported PBKDF2 PRF: %s", prf) + } + key := pbkdf2.Key(authArray, salt, c, dkLen, sha256.New) + return key, nil + } + + return nil, fmt.Errorf("unsupported KDF: %s", cryptoJSON.KDF) +} + +// TODO: can we do without this when unmarshalling dynamic JSON? +// why do integers in KDF params end up as float64 and not int after +// unmarshal? +func ensureInt(x interface{}) int { + res, ok := x.(int) + if !ok { + res = int(x.(float64)) + } + return res +} diff --git a/cmd/clef/dbutil/dbutil.go b/cmd/clef/dbutil/dbutil.go index 4d5771afd1..b2f72edc7b 100644 --- a/cmd/clef/dbutil/dbutil.go +++ b/cmd/clef/dbutil/dbutil.go @@ -66,9 +66,9 @@ func NewKVStore(path, table string) (*KVStore, error) { } return &KVStore{ - conf: conf, + Conf: conf, db: db, - table: table, + Table: table, }, nil } @@ -124,8 +124,8 @@ CREATE TABLE IF NOT EXISTS %s ( // KVStore is used for abstracting a generic database as a simple key value storage type KVStore struct { - table string - conf *DBConf + Table string + Conf *DBConf db *sql.DB } @@ -145,8 +145,7 @@ func (kvstore *KVStore) Put(key, value string) error { if len(key) == 0 { return errors.New("0-length key") } - _, err := kvstore.Get(key) - if err != nil || err == sql.ErrNoRows { + if !kvstore.Exists(key) { return kvstore.insertRow(key, value) } else { return kvstore.updateRow(key, value) @@ -161,15 +160,8 @@ func (kvstore *KVStore) Del(key string) { // All returns all keys in the database func (kvstore *KVStore) All() []string { - var size int - sql := kvstore.adjustSQLPlaceholder(countSQL) - err := kvstore.db.QueryRow(sql).Scan(&size) - if err != nil { - log.Error("Error counting key numbers: ", err) - return nil - } - - sql = kvstore.adjustSQLPlaceholder(allSQL) + size := kvstore.Size() + sql := kvstore.adjustSQLPlaceholder(allSQL) rows, err := kvstore.db.Query(sql) if err != nil { log.Error("Error retrieving all keys: ", err) @@ -192,6 +184,24 @@ func (kvstore *KVStore) All() []string { return result } +// Exists returns a boolean indicates if the key exists or not +func (kvstore *KVStore) Exists(key string) bool { + v, err := kvstore.Get(key) + return err == nil && v != "" +} + +// Size returns number of entries that exists in the kvstore +func (kvstore *KVStore) Size() int { + var size int + sql := kvstore.adjustSQLPlaceholder(countSQL) + err := kvstore.db.QueryRow(sql).Scan(&size) + if err != nil { + log.Error("Error counting key numbers: ", err) + return 0 + } + return size +} + func (kvstore *KVStore) insertRow(key, value string) error { sql := kvstore.adjustSQLPlaceholder(insertSQL) return kvstore.exec(sql, key, value) @@ -203,7 +213,7 @@ func (kvstore *KVStore) updateRow(key, value string) error { } func (kvstore *KVStore) adjustSQLPlaceholder(sql string) string { - switch kvstore.conf.Adapter { + switch kvstore.Conf.Adapter { case "postgres": params := strings.Count(sql, "?") for i := 1; i <= params; i++ { @@ -214,7 +224,7 @@ func (kvstore *KVStore) adjustSQLPlaceholder(sql string) string { // since they're already using ? as placeholder, do nothing } - return strings.ReplaceAll(sql, "tableName", kvstore.table) + return strings.ReplaceAll(sql, "tableName", kvstore.Table) } func (kvstore *KVStore) queryRow(query string, args ...interface{}) (string, error) {