358 lines
11 KiB
Diff
358 lines
11 KiB
Diff
diff --git a/accounts/keystore/key.go b/accounts/keystore/key.go
|
|
index 84d8df0c5..551b386a5 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 2918047cc..333fbef6f 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,10 @@ 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/passphrase.go b/accounts/keystore/passphrase.go
|
|
index a0b6cf538..e2512c6e8 100644
|
|
--- a/accounts/keystore/passphrase.go
|
|
+++ b/accounts/keystore/passphrase.go
|
|
@@ -42,6 +42,7 @@ import (
|
|
"github.com/ethereum/go-ethereum/common/math"
|
|
"github.com/ethereum/go-ethereum/crypto"
|
|
"github.com/pborman/uuid"
|
|
+ "github.com/status-im/status-go/extkeys"
|
|
"golang.org/x/crypto/pbkdf2"
|
|
"golang.org/x/crypto/scrypt"
|
|
)
|
|
@@ -187,15 +188,68 @@ func EncryptKey(key *Key, auth string, scryptN, scryptP int) ([]byte, error) {
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
+ 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 := make([]byte, 32)
|
|
+ if _, err := io.ReadFull(rand.Reader, salt); err != nil {
|
|
+ panic("reading from crypto/rand failed: " + err.Error())
|
|
+ }
|
|
+ derivedKey, err := scrypt.Key(authArray, salt, scryptN, scryptR, scryptP, scryptDKLen)
|
|
+ if err != nil {
|
|
+ return CryptoJSON{}, err
|
|
+ }
|
|
+ encryptKey := derivedKey[:16]
|
|
+ keyBytes := []byte(extKey.String())
|
|
+
|
|
+ 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, 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
|
|
@@ -207,19 +261,41 @@ 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 {
|
|
@@ -228,9 +304,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
|
|
}
|
|
|
|
@@ -316,6 +394,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))
|