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))