Merge pull request #29 from farazdagi/feature/hd-child-derivation

HD Keys: sub-accounts support added
This commit is contained in:
Roman Volosovskyi 2016-09-08 10:44:00 +03:00 committed by GitHub
commit 4babe9101d
8 changed files with 545 additions and 137 deletions

View File

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

View File

@ -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 {

View File

@ -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")
}

View File

@ -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 {

View File

@ -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 (

View File

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

View File

@ -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.

View File

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