HD Keys: sub-accounts support added
This commit is contained in:
parent
7d78d11a14
commit
db39f49407
|
@ -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)
|
||||
|
|
215
src/gethdep.go
215
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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
27
src/main.go
27
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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
|
|
Loading…
Reference in New Issue