diff --git a/src/extkeys/hdkey.go b/src/extkeys/hdkey.go index 9d35611f6..21edf2deb 100644 --- a/src/extkeys/hdkey.go +++ b/src/extkeys/hdkey.go @@ -16,7 +16,11 @@ import ( "github.com/btcsuite/btcutil/base58" ) -// Implementation of BIP32 https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki +// Implementation of the following BIPs: +// - BIP32 (https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki) +// - BIP39 (https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) +// - BIP44 (https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki) +// // Referencing // https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki // https://bitcoin.org/en/developer-guide#hardened-keys @@ -58,6 +62,8 @@ const ( CoinTypeTestNet = 1 // 0x80000001 CoinTypeETH = 60 // 0x8000003c CoinTypeETC = 60 // 0x80000000 + + EmptyExtendedKeyString = "Zeroed extended key" ) var ( @@ -193,7 +199,7 @@ func (parent *ExtendedKey) Child(i uint32) (*ExtendedKey, error) { return child, nil } -// Child1 returns Status CKD#1 (used for ETH and SHH). +// BIP44Child returns Status CKD#i (where i is child index). // BIP44 format is used: m / purpose' / coin_type' / account' / change / address_index func (master *ExtendedKey) BIP44Child(coinType, i uint32) (*ExtendedKey, error) { if !master.IsPrivate { @@ -259,8 +265,8 @@ func (k *ExtendedKey) Neuter() (*ExtendedKey, error) { // String returns the extended key as a human-readable base58-encoded string. func (k *ExtendedKey) String() string { - if len(k.KeyData) == 0 { - return "zeroed extended key" + if k == nil || len(k.KeyData) == 0 { + return EmptyExtendedKeyString } var childNumBytes [4]byte @@ -316,6 +322,10 @@ func (k *ExtendedKey) ToECDSA() *ecdsa.PrivateKey { // NewKeyFromString returns a new extended key instance from a base58-encoded // extended key. func NewKeyFromString(key string) (*ExtendedKey, error) { + if key == EmptyExtendedKeyString || len(key) == 0 { + return &ExtendedKey{}, nil + } + // The base58-decoded extended key must consist of a serialized payload // plus an additional 4 bytes for the checksum. decoded := base58.Decode(key) diff --git a/src/gethdep.go b/src/gethdep.go index bbd7eeae7..f200eaf17 100644 --- a/src/gethdep.go +++ b/src/gethdep.go @@ -27,94 +27,130 @@ var ( ErrInvalidAccountManager = errors.New("could not retrieve account manager") ErrAddressToAccountMappingFailure = errors.New("cannot retreive a valid account for a given address") ErrAccountToKeyMappingFailure = errors.New("cannot retreive a valid key for a given account") - ErrUnlockCalled = errors.New("no need to unlock accounts, use Login() instead") + ErrUnlockCalled = errors.New("no need to unlock accounts, login instead") ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper") ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities") + ErrWhisperNoIdentityFound = errors.New("failed to locate identity previously injected into Whisper") + ErrNoAccountSelected = errors.New("no account has been selected, please login") ) // createAccount creates an internal geth account -func createAccount(password string) (string, string, string, error) { - - if currentNode != nil { - - w := true - - if accountManager != nil { - // generate mnemonic phrase - m := extkeys.NewMnemonic() - mnemonic, err := m.MnemonicPhrase(128, extkeys.EnglishLanguage) - if err != nil { - return "", "", "", errextra.Wrap(err, "Can not create mnemonic seed") - } - - // generate extended master key (see BIP32) - extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) - if err != nil { - return "", "", "", errextra.Wrap(err, "Can not create master extended key") - } - - // derive hardened child (see BIP44) - extChild1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0) - if err != nil { - return "", "", "", errextra.Wrap(err, "Can not derive hardened child key (#1)") - } - - // generate the account - account, err := accountManager.NewAccountUsingExtendedKey(extChild1, password, w) - if err != nil { - return "", "", "", errextra.Wrap(err, "Account manager could not create the account") - } - address := fmt.Sprintf("%x", account.Address) - - // recover the public key to return - account, key, err := accountManager.AccountDecryptedKey(account, password) - if err != nil { - return address, "", "", errextra.Wrap(err, "Could not recover the key") - } - pubKey := common.ToHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey)) - - return address, pubKey, mnemonic, nil - } - - return "", "", "", errors.New("Could not retrieve account manager") +// BIP44-compatible keys are generated: CKD#1 is stored as account key, CKD#2 stored as sub-account root +// Public key of CKD#1 is returned, with CKD#2 securely encoded into account key file (to be used for +// sub-account derivations) +func createAccount(password string) (address, pubKey, mnemonic string, err error) { + if currentNode == nil { + return "", "", "", ErrInvalidGethNode } - return "", "", "", errors.New("No running node detected for account creation") -} - -func recoverAccount(password, mnemonic string) (string, string, error) { - - if currentNode != nil { - - if accountManager != nil { - m := extkeys.NewMnemonic() - // re-create extended key (see BIP32) - extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) - if err != nil { - return "", "", errextra.Wrap(err, "Can not create master extended key") - } - - // derive hardened child (see BIP44) - extChild1, err := extKey.BIP44Child(extkeys.CoinTypeETH, 0) - if err != nil { - return "", "", errextra.Wrap(err, "Can not derive hardened child key (#1)") - } - - privateKeyECDSA := extChild1.ToECDSA() - address := fmt.Sprintf("%x", crypto.PubkeyToAddress(privateKeyECDSA.PublicKey)) - pubKey := common.ToHex(crypto.FromECDSAPub(&privateKeyECDSA.PublicKey)) - - accountManager.ImportECDSA(privateKeyECDSA, password) // try caching key, ignore errors - - return address, pubKey, nil - } - - return "", "", errors.New("Could not retrieve account manager") + if accountManager == nil { + return "", "", "", ErrInvalidAccountManager } - return "", "", errors.New("No running node detected for account unlock") + // generate mnemonic phrase + m := extkeys.NewMnemonic() + mnemonic, err = m.MnemonicPhrase(128, extkeys.EnglishLanguage) + if err != nil { + return "", "", "", errextra.Wrap(err, "Can not create mnemonic seed") + } + + // generate extended master key (see BIP32) + extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) + if err != nil { + return "", "", "", errextra.Wrap(err, "Can not create master extended key") + } + + // import created key into account keystore + address, pubKey, err = importExtendedKey(extKey, password) + if err != nil { + return "", "", "", err + } + + return address, pubKey, mnemonic, nil } +// createChildAccount creates sub-account for an account identified by parent address. +// CKD#2 is used as root for master accounts (when parentAddress is ""). +// Otherwise (when parentAddress != ""), child is derived directly from parent. +func createChildAccount(parentAddress, password string) (address, pubKey string, err error) { + if currentNode == nil { + return "", "", ErrInvalidGethNode + } + + if accountManager == nil { + return "", "", ErrInvalidAccountManager + } + + if parentAddress == "" { // by default derive from currently selected account + parentAddress = selectedAddress + } + + if parentAddress == "" { + return "", "", ErrNoAccountSelected + } + + // make sure that given password can decrypt key associated with a given parent address + account, err := utils.MakeAddress(accountManager, parentAddress) + if err != nil { + return "", "", ErrAddressToAccountMappingFailure + } + + account, accountKey, err := accountManager.AccountDecryptedKey(account, password) + if err != nil { + return "", "", fmt.Errorf("%s: %v", ErrAccountToKeyMappingFailure.Error(), err) + } + + parentKey, err := extkeys.NewKeyFromString(accountKey.ExtendedKey.String()) + if err != nil { + return "", "", err + } + + // derive child key + childKey, err := parentKey.Child(accountKey.SubAccountIndex) + if err != nil { + return "", "", err + } + accountManager.IncSubAccountIndex(account, password) + + // import derived key into account keystore + address, pubKey, err = importExtendedKey(childKey, password) + if err != nil { + return + } + + return address, pubKey, nil +} + +// recoverAccount re-creates master key using given details. +// Once master key is re-generated, it is inserted into keystore (if not already there). +func recoverAccount(password, mnemonic string) (address, pubKey string, err error) { + if currentNode == nil { + return "", "", ErrInvalidGethNode + } + + if accountManager == nil { + return "", "", ErrInvalidAccountManager + } + + // re-create extended key (see BIP32) + m := extkeys.NewMnemonic() + extKey, err := extkeys.NewMaster(m.MnemonicSeed(mnemonic, password), []byte(extkeys.Salt)) + if err != nil { + return "", "", errextra.Wrap(err, "Can not create master extended key") + } + + // import re-created key into account keystore + address, pubKey, err = importExtendedKey(extKey, password) + if err != nil { + return + } + + return address, pubKey, nil +} + +// selectAccount selects current account, by verifying that address has corresponding account which can be decrypted +// using provided password. Once verification is done, decrypted key is injected into Whisper (as a single identity, +// all previous identities are removed). func selectAccount(address, password string) error { if currentNode == nil { return ErrInvalidGethNode @@ -140,6 +176,9 @@ func selectAccount(address, password string) error { return ErrWhisperIdentityInjectionFailure } + // persist address for easier recovery of currently selected key (from Whisper) + selectedAddress = address + return nil } @@ -157,6 +196,8 @@ func logout() error { return fmt.Errorf("%s: %v", ErrWhisperClearIdentitiesFailure, err) } + selectedAddress = "" + return nil } @@ -171,6 +212,26 @@ func unlockAccount(address, password string, seconds int) error { return ErrUnlockCalled } +// importExtendedKey processes incoming extended key, extracts required info and creates corresponding account key. +// Once account key is formed, that key is put (if not already) into keystore i.e. key is *encoded* into key file. +func importExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) { + // imports extended key, create key file (if necessary) + account, err := accountManager.ImportExtendedKey(extKey, password) + if err != nil { + return "", "", errextra.Wrap(err, "Account manager could not create the account") + } + address = fmt.Sprintf("%x", account.Address) + + // obtain public key to return + account, key, err := accountManager.AccountDecryptedKey(account, password) + if err != nil { + return address, "", errextra.Wrap(err, "Could not recover the key") + } + pubKey = common.ToHex(crypto.FromECDSAPub(&key.PrivateKey.PublicKey)) + + return +} + // createAndStartNode creates a node entity and starts the // node running locally func createAndStartNode(inputDir string) error { diff --git a/src/gethdep_test.go b/src/gethdep_test.go index e8c2a9206..eefe8ea4f 100644 --- a/src/gethdep_test.go +++ b/src/gethdep_test.go @@ -35,6 +35,87 @@ const ( whisperMessage5 = "test message 5 (K2 -> K1)" ) +func TestCreateChildAccount(t *testing.T) { + err := prepareTestNode() + if err != nil { + t.Error(err) + return + } + + // create an account + address, pubKey, mnemonic, err := createAccount(newAccountPassword) + if err != nil { + t.Errorf("could not create account: %v", err) + return + } + glog.V(logger.Info).Infof("Account created: {address: %s, key: %s, mnemonic:%s}", address, pubKey, mnemonic) + + account, err := utils.MakeAddress(accountManager, address) + if err != nil { + t.Errorf("can not get account from address: %v", err) + return + } + + // obtain decrypted key, and make sure that extended key (which will be used as root for sub-accounts) is present + account, key, err := accountManager.AccountDecryptedKey(account, newAccountPassword) + if err != nil { + t.Errorf("can not obtain decrypted account key: %v", err) + return + } + + if key.ExtendedKey == nil { + t.Error("CKD#2 has not been generated for new account") + return + } + + // try creating sub-account, w/o selecting main account i.e. w/o login to main account + _, _, err = createChildAccount("", newAccountPassword) + if !reflect.DeepEqual(err, ErrNoAccountSelected) { + t.Errorf("expected error is not returned (tried to create sub-account w/o login): %v", err) + return + } + + err = selectAccount(address, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + + // try to create sub-account with wrong password + _, _, err = createChildAccount("", "wrong password") + if !reflect.DeepEqual(err, errors.New("cannot retreive a valid key for a given account: could not decrypt key with given passphrase")) { + t.Errorf("expected error is not returned (tried to create sub-account with wrong password): %v", err) + return + } + + // create sub-account (from implicit parent) + subAccount1, subPubKey1, err := createChildAccount("", newAccountPassword) + if err != nil { + t.Errorf("cannot create sub-account: %v", err) + return + } + + // make sure that sub-account index automatically progresses + subAccount2, subPubKey2, err := createChildAccount("", newAccountPassword) + if err != nil { + t.Errorf("cannot create sub-account: %v", err) + } + if subAccount1 == subAccount2 || subPubKey1 == subPubKey2 { + t.Error("sub-account index auto-increament failed") + return + } + + // create sub-account (from explicit parent) + subAccount3, subPubKey3, err := createChildAccount(subAccount2, newAccountPassword) + if err != nil { + t.Errorf("cannot create sub-account: %v", err) + } + if subAccount1 == subAccount3 || subPubKey1 == subPubKey3 || subAccount2 == subAccount3 || subPubKey2 == subPubKey3 { + t.Error("sub-account index auto-increament failed") + return + } +} + func TestRecoverAccount(t *testing.T) { err := prepareTestNode() if err != nil { @@ -57,7 +138,72 @@ func TestRecoverAccount(t *testing.T) { return } if address != addressCheck || pubKey != pubKeyCheck { - t.Error("Test failed: recover account details failed to pull the correct details") + t.Error("recover account details failed to pull the correct details") + } + + // now test recovering, but make sure that account/key file is removed i.e. simulate recovering on a new device + account, err := utils.MakeAddress(accountManager, address) + if err != nil { + t.Errorf("can not get account from address: %v", err) + } + + account, key, err := accountManager.AccountDecryptedKey(account, newAccountPassword) + if err != nil { + t.Errorf("can not obtain decrypted account key: %v", err) + return + } + extChild2String := key.ExtendedKey.String() + + if err := accountManager.DeleteAccount(account, newAccountPassword); err != nil { + t.Errorf("cannot remove account: %v", err) + } + + addressCheck, pubKeyCheck, err = recoverAccount(newAccountPassword, mnemonic) + if err != nil { + t.Errorf("recover account failed (for non-cached account): %v", err) + return + } + if address != addressCheck || pubKey != pubKeyCheck { + t.Error("recover account details failed to pull the correct details (for non-cached account)") + } + + // make sure that extended key exists and is imported ok too + account, key, err = accountManager.AccountDecryptedKey(account, newAccountPassword) + if err != nil { + t.Errorf("can not obtain decrypted account key: %v", err) + return + } + if extChild2String != key.ExtendedKey.String() { + t.Errorf("CKD#2 key mismatch, expected: %s, got: %s", extChild2String, key.ExtendedKey.String()) + } + + // make sure that calling import several times, just returns from cache (no error is expected) + addressCheck, pubKeyCheck, err = recoverAccount(newAccountPassword, mnemonic) + if err != nil { + t.Errorf("recover account failed (for non-cached account): %v", err) + return + } + if address != addressCheck || pubKey != pubKeyCheck { + t.Error("recover account details failed to pull the correct details (for non-cached account)") + } + + // time to login with recovered data + var whisperInstance *whisper.Whisper + if err := currentNode.Service(&whisperInstance); err != nil { + t.Errorf("whisper service not running: %v", err) + } + + // make sure that identity is not (yet injected) + if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKeyCheck))) { + t.Errorf("identity already present in whisper") + } + err = selectAccount(addressCheck, newAccountPassword) + if err != nil { + t.Errorf("Test failed: could not select account: %v", err) + return + } + if !whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKeyCheck))) { + t.Errorf("identity not injected into whisper: %v", err) } } @@ -75,11 +221,10 @@ func TestAccountSelect(t *testing.T) { t.Errorf("whisper service not running: %v", err) } - // create an accounts + // create an account address1, pubKey1, _, err := createAccount(newAccountPassword) if err != nil { - fmt.Println(err.Error()) - t.Error("Test failed: could not create account") + t.Errorf("could not create account: %v", err) return } glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address1, pubKey1) @@ -92,7 +237,7 @@ func TestAccountSelect(t *testing.T) { } glog.V(logger.Info).Infof("Account created: {address: %s, key: %s}", address2, pubKey2) - // inject key of newly created account into Whisper, as identity + // make sure that identity is not (yet injected) if whisperInstance.HasIdentity(crypto.ToECDSAPub(common.FromHex(pubKey1))) { t.Errorf("identity already present in whisper") } diff --git a/src/library.go b/src/library.go index aa4cdc6aa..b5739fcad 100644 --- a/src/library.go +++ b/src/library.go @@ -34,6 +34,27 @@ func CreateAccount(password *C.char) *C.char { return C.CString(string(outBytes)) } +//export CreateChildAccount +func CreateChildAccount(parentAddress, password *C.char) *C.char { + + address, pubKey, err := createChildAccount(C.GoString(parentAddress), C.GoString(password)) + + errString := emptyError + if err != nil { + fmt.Fprintln(os.Stderr, err) + errString = err.Error() + } + + out := AccountInfo{ + Address: address, + PubKey: pubKey, + Error: errString, + } + outBytes, _ := json.Marshal(&out) + + return C.CString(string(outBytes)) +} + //export RecoverAccount func RecoverAccount(password, mnemonic *C.char) *C.char { diff --git a/src/main.go b/src/main.go index 3c07be9ba..76d8b7eb6 100644 --- a/src/main.go +++ b/src/main.go @@ -35,19 +35,20 @@ const ( ) var ( - vString string // Combined textual representation of the version - rConfig release.Config // Structured version information and release oracle config - currentNode *node.Node // currently running geth node - c *cli.Context // the CLI context used to start the geth node - accountSync *[]node.Service // the object used to sync accounts between geth services - lightEthereum *les.LightEthereum // LES service - accountManager *accounts.Manager // the account manager attached to the currentNode - whisperService *whisper.Whisper // whisper service - datadir string // data directory for geth - rpcport int = 8545 // RPC port (replaced in unit tests) - client rpc.Client - gitCommit = "rely on linker: -ldflags -X main.GitCommit" - buildStamp = "rely on linker: -ldflags -X main.buildStamp" + vString string // Combined textual representation of the version + rConfig release.Config // Structured version information and release oracle config + currentNode *node.Node // currently running geth node + c *cli.Context // the CLI context used to start the geth node + accountSync *[]node.Service // the object used to sync accounts between geth services + lightEthereum *les.LightEthereum // LES service + accountManager *accounts.Manager // the account manager attached to the currentNode + selectedAddress string // address of the account that was processed during the last call to SelectAccount() + whisperService *whisper.Whisper // whisper service + datadir string // data directory for geth + rpcport int = 8545 // RPC port (replaced in unit tests) + client rpc.Client + gitCommit = "rely on linker: -ldflags -X main.GitCommit" + buildStamp = "rely on linker: -ldflags -X main.buildStamp" ) var ( diff --git a/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go b/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go index 57bf5d6d2..af7485224 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go +++ b/src/vendor/github.com/ethereum/go-ethereum/accounts/account_manager.go @@ -38,7 +38,6 @@ import ( "github.com/ethereum/go-ethereum/p2p" "github.com/ethereum/go-ethereum/rpc" "github.com/ethereum/go-ethereum/whisper" - "github.com/pborman/uuid" "github.com/status-im/status-go/src/extkeys" ) @@ -306,34 +305,6 @@ func (am *Manager) NewAccount(passphrase string, w bool) (Account, error) { return account, nil } -// NewAccount stores into key directory the provided extended key (see BIP32). -// Provided key is encrypting with the passphrase. -func (am *Manager) NewAccountUsingExtendedKey(k *extkeys.ExtendedKey, passphrase string, w bool) (Account, error) { - if !k.IsPrivate { - return Account{}, fmt.Errorf("failed creating account using public key") - } - - privateKeyECDSA := k.ToECDSA() - key := &Key{ - Id: uuid.NewRandom(), - Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), - PrivateKey: privateKeyECDSA, - } - - key.WhisperEnabled = w - account := Account{Address: key.Address, File: am.keyStore.JoinPath(keyFileName(key.Address))} - if err := am.keyStore.StoreKey(account.File, key, passphrase); err != nil { - zeroKey(key.PrivateKey) - return Account{}, err - } - - // 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 -} - // AccountByIndex returns the ith account. func (am *Manager) AccountByIndex(i int) (Account, error) { accounts := am.Accounts() @@ -380,6 +351,34 @@ func (am *Manager) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (Accou return am.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 (am *Manager) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (Account, error) { + key, err := newKeyFromExtendedKey(extKey) + if err != nil { + zeroKey(key.PrivateKey) + return Account{}, err + } + + // if account is already imported, return cached version + if am.cache.hasAddress(key.Address) { + a := Account{ + Address: key.Address, + } + am.cache.maybeReload() + am.cache.mu.Lock() + a, err := am.cache.find(a) + am.cache.mu.Unlock() + if err != nil { + zeroKey(key.PrivateKey) + return a, err + } + return a, nil + } + + return am.importKey(key, passphrase) +} + 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, passphrase); err != nil { @@ -398,6 +397,15 @@ func (am *Manager) Update(a Account, passphrase, newPassphrase string) error { return am.keyStore.StoreKey(a.File, key, newPassphrase) } +func (am *Manager) IncSubAccountIndex(a Account, passphrase string) error { + a, key, err := am.getDecryptedKey(a, passphrase) + if err != nil { + return err + } + key.SubAccountIndex++ + return am.keyStore.StoreKey(a.File, key, passphrase) +} + // 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) { @@ -411,6 +419,9 @@ func (am *Manager) 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/src/vendor/github.com/ethereum/go-ethereum/accounts/key.go b/src/vendor/github.com/ethereum/go-ethereum/accounts/key.go index c73e6c240..f56d32eac 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/accounts/key.go +++ b/src/vendor/github.com/ethereum/go-ethereum/accounts/key.go @@ -33,6 +33,7 @@ import ( "github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto/secp256k1" "github.com/pborman/uuid" + "github.com/status-im/status-go/src/extkeys" ) const ( @@ -49,6 +50,10 @@ type Key struct { // if whisper is enabled here, the address will be used as a whisper // identity upon creation of the account or unlocking of the account WhisperEnabled bool + // 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 { @@ -68,11 +73,13 @@ type plainKeyJSON struct { } type encryptedKeyJSONV3 struct { - Address string `json:"address"` - Crypto cryptoJSON `json:"crypto"` - Id string `json:"id"` - Version int `json:"version"` - WhisperEnabled bool `json:"whisperenabled"` + Address string `json:"address"` + Crypto cryptoJSON `json:"crypto"` + Id string `json:"id"` + Version int `json:"version"` + WhisperEnabled bool `json:"whisperenabled"` + ExtendedKey cryptoJSON `json:"extendedkey"` + SubAccountIndex uint32 `json:"subaccountindex"` } type encryptedKeyJSONV1 struct { @@ -150,6 +157,41 @@ 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, + WhisperEnabled: true, + 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/src/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go b/src/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go index 3f4fd3518..a054f0006 100644 --- a/src/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go +++ b/src/vendor/github.com/ethereum/go-ethereum/accounts/key_store_passphrase.go @@ -39,6 +39,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/src/extkeys" "golang.org/x/crypto/pbkdf2" "golang.org/x/crypto/scrypt" ) @@ -135,16 +136,63 @@ 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, key.WhisperEnabled, + 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 @@ -156,19 +204,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 { @@ -176,10 +246,12 @@ func DecryptKey(keyjson []byte, auth string) (*Key, error) { } key := crypto.ToECDSA(keyBytes) return &Key{ - Id: uuid.UUID(keyId), - Address: crypto.PubkeyToAddress(key.PublicKey), - PrivateKey: key, - WhisperEnabled: m["whisperenabled"].(bool), + Id: uuid.UUID(keyId), + Address: crypto.PubkeyToAddress(key.PublicKey), + PrivateKey: key, + WhisperEnabled: m["whisperenabled"].(bool), + ExtendedKey: extKey, + SubAccountIndex: uint32(subAccountIndex), }, nil } @@ -259,6 +331,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))