status-go/geth-patches/0000-accounts-hd-keys.patch

353 lines
11 KiB
Diff

diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go
index 211fa863d..65c83f3b0 100644
--- a/accounts/keystore/key.go
+++ b/accounts/keystore/key.go
@@ -33,6 +33,7 @@ import (
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid"
+ "github.com/status-im/status-go/extkeys"
)
const (
@@ -46,6 +47,10 @@ type Key struct {
// we only store privkey as pubkey/address can be derived from it
// privkey in this struct is always in plaintext
PrivateKey *ecdsa.PrivateKey
+ // extended key is the root node for new hardened children i.e. sub-accounts
+ ExtendedKey *extkeys.ExtendedKey
+ // next index to be used for sub-account child derivation
+ SubAccountIndex uint32
}
type keyStore interface {
@@ -65,10 +70,12 @@ type plainKeyJSON struct {
}
type encryptedKeyJSONV3 struct {
- Address string `json:"address"`
- Crypto cryptoJSON `json:"crypto"`
- Id string `json:"id"`
- Version int `json:"version"`
+ Address string `json:"address"`
+ Crypto cryptoJSON `json:"crypto"`
+ Id string `json:"id"`
+ Version int `json:"version"`
+ ExtendedKey cryptoJSON `json:"extendedkey"`
+ SubAccountIndex uint32 `json:"subaccountindex"`
}
type encryptedKeyJSONV1 struct {
@@ -137,6 +144,40 @@ func newKeyFromECDSA(privateKeyECDSA *ecdsa.PrivateKey) *Key {
return key
}
+func newKeyFromExtendedKey(extKey *extkeys.ExtendedKey) (*Key, error) {
+ var (
+ extChild1, extChild2 *extkeys.ExtendedKey
+ err error
+ )
+
+ if extKey.Depth == 0 { // we are dealing with master key
+ // CKD#1 - main account
+ extChild1, err = extKey.BIP44Child(extkeys.CoinTypeETH, 0)
+ if err != nil {
+ return &Key{}, err
+ }
+
+ // CKD#2 - sub-accounts root
+ extChild2, err = extKey.BIP44Child(extkeys.CoinTypeETH, 1)
+ if err != nil {
+ return &Key{}, err
+ }
+ } else { // we are dealing with non-master key, so it is safe to persist and extend from it
+ extChild1 = extKey
+ extChild2 = extKey
+ }
+
+ privateKeyECDSA := extChild1.ToECDSA()
+ id := uuid.NewRandom()
+ key := &Key{
+ Id: id,
+ Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
+ PrivateKey: privateKeyECDSA,
+ ExtendedKey: extChild2,
+ }
+ return key, nil
+}
+
// 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.
diff --git a/accounts/keystore/keystore.go b/accounts/keystore/keystore.go
index 80ccd3741..750608145 100644
--- a/accounts/keystore/keystore.go
+++ b/accounts/keystore/keystore.go
@@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event"
+ "github.com/status-im/status-go/extkeys"
)
var (
@@ -228,6 +229,11 @@ func (ks *KeyStore) Accounts() []accounts.Account {
return ks.cache.accounts()
}
+// AccountDecryptedKey returns decrypted key for account (provided that password is correct).
+func (ks *KeyStore) AccountDecryptedKey(a accounts.Account, auth string) (accounts.Account, *Key, error) {
+ return ks.getDecryptedKey(a, auth)
+}
+
// Delete deletes the key matched by account if the passphrase is correct.
// If the account contains no filename, the address must match a unique key.
func (ks *KeyStore) Delete(a accounts.Account, passphrase string) error {
@@ -453,6 +459,34 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
return ks.importKey(key, passphrase)
}
+// ImportExtendedKey stores ECDSA key (obtained from extended key) along with CKD#2 (root for sub-accounts)
+// If key file is not found, it is created. Key is encrypted with the given passphrase.
+func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
+ key, err := newKeyFromExtendedKey(extKey)
+ if err != nil {
+ zeroKey(key.PrivateKey)
+ return accounts.Account{}, err
+ }
+
+ // if account is already imported, return cached version
+ if ks.cache.hasAddress(key.Address) {
+ a := accounts.Account{
+ Address: key.Address,
+ }
+ ks.cache.maybeReload()
+ ks.cache.mu.Lock()
+ a, err := ks.cache.find(a)
+ ks.cache.mu.Unlock()
+ if err != nil {
+ zeroKey(key.PrivateKey)
+ return a, err
+ }
+ return a, nil
+ }
+
+ return ks.importKey(key, passphrase)
+}
+
func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, error) {
a := accounts.Account{Address: key.Address, URL: accounts.URL{Scheme: KeyStoreScheme, Path: ks.storage.JoinPath(keyFileName(key.Address))}}
if err := ks.storage.StoreKey(a.URL.Path, key, passphrase); err != nil {
@@ -463,6 +497,15 @@ func (ks *KeyStore) importKey(key *Key, passphrase string) (accounts.Account, er
return a, nil
}
+func (ks *KeyStore) IncSubAccountIndex(a accounts.Account, passphrase string) error {
+ a, key, err := ks.getDecryptedKey(a, passphrase)
+ if err != nil {
+ return err
+ }
+ key.SubAccountIndex++
+ return ks.storage.StoreKey(a.URL.Path, key, passphrase)
+}
+
// Update changes the passphrase of an existing account.
func (ks *KeyStore) Update(a accounts.Account, passphrase, newPassphrase string) error {
a, key, err := ks.getDecryptedKey(a, passphrase)
@@ -486,6 +529,9 @@ func (ks *KeyStore) ImportPreSaleKey(keyJSON []byte, passphrase string) (account
// zeroKey zeroes a private key in memory.
func zeroKey(k *ecdsa.PrivateKey) {
+ if k == nil {
+ return
+ }
b := k.D.Bits()
for i := range b {
b[i] = 0
diff --git a/accounts/keystore/keystore_passphrase.go b/accounts/keystore/keystore_passphrase.go
index eaec39f7d..902b213e2 100644
--- a/accounts/keystore/keystore_passphrase.go
+++ b/accounts/keystore/keystore_passphrase.go
@@ -41,6 +41,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/crypto/randentropy"
"github.com/pborman/uuid"
+ "github.com/status-im/status-go/extkeys"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/scrypt"
)
@@ -151,15 +152,62 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) {
KDFParams: scryptParamsJSON,
MAC: hex.EncodeToString(mac),
}
+ encryptedExtendedKey, err := EncryptExtendedKey(key.ExtendedKey, auth, scryptN, scryptP)
+ if err != nil {
+ return nil, err
+ }
encryptedKeyJSONV3 := encryptedKeyJSONV3{
hex.EncodeToString(key.Address[:]),
cryptoStruct,
key.Id.String(),
version,
+ encryptedExtendedKey,
+ key.SubAccountIndex,
}
return json.Marshal(encryptedKeyJSONV3)
}
+func EncryptExtendedKey(extKey *extkeys.ExtendedKey, auth string, scryptN, scryptP int) (cryptoJSON, error) {
+ if extKey == nil {
+ return cryptoJSON{}, nil
+ }
+ authArray := []byte(auth)
+ salt := randentropy.GetEntropyCSPRNG(32)
+ derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen)
+ if err != nil {
+ return cryptoJSON{}, err
+ }
+ encryptKey := derivedKey[:16]
+ keyBytes := []byte(extKey.String())
+
+ iv := randentropy.GetEntropyCSPRNG(aes.BlockSize) // 16
+ cipherText, err := aesCTRXOR(encryptKey, keyBytes, 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),
+ }
+
+ return cryptoJSON{
+ Cipher: "aes-128-ctr",
+ CipherText: hex.EncodeToString(cipherText),
+ CipherParams: cipherParamsJSON,
+ KDF: "scrypt",
+ KDFParams: scryptParamsJSON,
+ MAC: hex.EncodeToString(mac),
+ }, nil
+}
+
// 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
@@ -171,20 +219,43 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) {
var (
keyBytes, keyId []byte
err error
+ extKeyBytes []byte
+ extKey *extkeys.ExtendedKey
)
+
+ subAccountIndex, ok := m["subaccountindex"].(float64)
+ if !ok {
+ subAccountIndex = 0
+ }
+
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)
+ if err != nil {
+ return nil, err
+ }
+
+ extKey, err = extkeys.NewKeyFromString(extkeys.EmptyExtendedKeyString)
} else {
k := new(encryptedKeyJSONV3)
if err := json.Unmarshal(keyjson, k); err != nil {
return nil, err
}
keyBytes, keyId, err = decryptKeyV3(k, auth)
+ if err != nil {
+ return nil, err
+ }
+
+ extKeyBytes, err = decryptExtendedKey(k, auth)
+ if err != nil {
+ return nil, err
+ }
+ extKey, err = extkeys.NewKeyFromString(string(extKeyBytes))
}
+
// Handle any decryption errors and return the key
if err != nil {
return nil, err
@@ -192,9 +263,11 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) {
key := crypto.ToECDSAUnsafe(keyBytes)
return &Key{
- Id: uuid.UUID(keyId),
- Address: crypto.PubkeyToAddress(key.PublicKey),
- PrivateKey: key,
+ Id: uuid.UUID(keyId),
+ Address: crypto.PubkeyToAddress(key.PublicKey),
+ PrivateKey: key,
+ ExtendedKey: extKey,
+ SubAccountIndex: uint32(subAccountIndex),
}, nil
}
@@ -274,6 +347,51 @@ func decryptKeyV1(keyProtected *encryptedKeyJSONV1, auth string) (keyBytes []byt
return plainText, keyId, err
}
+func decryptExtendedKey(keyProtected *encryptedKeyJSONV3, auth string) (plainText []byte, err error) {
+ if len(keyProtected.ExtendedKey.CipherText) == 0 {
+ return []byte(extkeys.EmptyExtendedKeyString), nil
+ }
+
+ if keyProtected.Version != version {
+ return nil, fmt.Errorf("Version not supported: %v", keyProtected.Version)
+ }
+
+ if keyProtected.ExtendedKey.Cipher != "aes-128-ctr" {
+ return nil, fmt.Errorf("Cipher not supported: %v", keyProtected.ExtendedKey.Cipher)
+ }
+
+ mac, err := hex.DecodeString(keyProtected.ExtendedKey.MAC)
+ if err != nil {
+ return nil, err
+ }
+
+ iv, err := hex.DecodeString(keyProtected.ExtendedKey.CipherParams.IV)
+ if err != nil {
+ return nil, err
+ }
+
+ cipherText, err := hex.DecodeString(keyProtected.ExtendedKey.CipherText)
+ if err != nil {
+ return nil, err
+ }
+
+ derivedKey, err := getKDFKey(keyProtected.ExtendedKey, 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 getKDFKey(cryptoJSON cryptoJSON, auth string) ([]byte, error) {
authArray := []byte(auth)
salt, err := hex.DecodeString(cryptoJSON.KDFParams["salt"].(string))