add multi-account support (#1500)

* add multiaccount support

add multi account ImportPrivateKey and StoreAccount

test derivation from normal keys

* add multiaccount to mobile pkg

* use multiaccount params structs from the mobile pkg

* move multiaccount tests together with the other lib tests

* fix codeclimate warning and temporarily increase methods threshold

* split library_test_utils.go to avoid linter warnings
This commit is contained in:
Andrea Franz 2019-07-24 20:59:15 +02:00 committed by GitHub
parent e93d994460
commit dcb0fa5262
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 1729 additions and 64 deletions

View file

@ -18,7 +18,7 @@ checks:
# Classes defined with a high number of functions or methods.
method-count:
config:
threshold: 20
threshold: 21
# Excessive lines of code within a single function or method
method-lines:
config:

View file

@ -12,11 +12,13 @@ import (
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
gethcommon "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/extkeys"
)
@ -42,7 +44,8 @@ type Manager struct {
mu sync.RWMutex
onboarding *Onboarding
accountsGenerator *generator.Generator
onboarding *Onboarding
selectedWalletAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
selectedChatAccount *SelectedExtKey // account that was processed during the last call to SelectAccount()
@ -50,9 +53,19 @@ type Manager struct {
// NewManager returns new node account manager.
func NewManager(geth GethServiceProvider) *Manager {
return &Manager{
manager := &Manager{
geth: geth,
}
accountsGenerator := generator.New(manager)
manager.accountsGenerator = accountsGenerator
return manager
}
// AccountsGenerator returns accountsGenerator.
func (m *Manager) AccountsGenerator() *generator.Generator {
return m.accountsGenerator
}
// CreateAccount creates an internal geth account
@ -237,6 +250,8 @@ func (m *Manager) SelectAccount(walletAddress, chatAddress, password string) err
m.mu.Lock()
defer m.mu.Unlock()
m.accountsGenerator.Reset()
selectedWalletAccount, err := m.unlockExtendedKey(walletAddress, password)
if err != nil {
return err
@ -299,10 +314,48 @@ func (m *Manager) Logout() {
m.mu.Lock()
defer m.mu.Unlock()
m.accountsGenerator.Reset()
m.selectedWalletAccount = nil
m.selectedChatAccount = nil
}
// ImportAccount imports the account specified with privateKey.
func (m *Manager) ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error) {
keyStore, err := m.geth.AccountKeyStore()
if err != nil {
return common.Address{}, err
}
account, err := keyStore.ImportECDSA(privateKey, password)
return account.Address, err
}
func (m *Manager) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
keyStore, err := m.geth.AccountKeyStore()
if err != nil {
return "", "", err
}
// imports extended key, create key file (if necessary)
account, err := keyStore.ImportSingleExtendedKey(extKey, password)
if err != nil {
return "", "", err
}
address = account.Address.Hex()
// obtain public key to return
account, key, err := keyStore.AccountDecryptedKey(account, password)
if err != nil {
return address, "", err
}
pubKey = hexutil.Encode(crypto.FromECDSAPub(&key.PrivateKey.PublicKey))
return
}
// 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 (m *Manager) importExtendedKey(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error) {
@ -491,7 +544,6 @@ func (m *Manager) AddressToDecryptedAccount(address, password string) (accounts.
}
var key *keystore.Key
account, key, err = keyStore.AccountDecryptedKey(account, password)
if err != nil {
err = fmt.Errorf("%s: %s", ErrAccountToKeyMappingFailure, err)

View file

@ -0,0 +1,71 @@
package generator
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/extkeys"
)
type account struct {
privateKey *ecdsa.PrivateKey
extendedKey *extkeys.ExtendedKey
}
func (a *account) toAccountInfo() AccountInfo {
publicKeyHex := hexutil.Encode(crypto.FromECDSAPub(&a.privateKey.PublicKey))
addressHex := crypto.PubkeyToAddress(a.privateKey.PublicKey).Hex()
return AccountInfo{
PublicKey: publicKeyHex,
Address: addressHex,
}
}
func (a *account) toIdentifiedAccountInfo(id string) IdentifiedAccountInfo {
info := a.toAccountInfo()
return IdentifiedAccountInfo{
AccountInfo: info,
ID: id,
}
}
func (a *account) toGeneratedAccountInfo(id string, mnemonic string) GeneratedAccountInfo {
idInfo := a.toIdentifiedAccountInfo(id)
return GeneratedAccountInfo{
IdentifiedAccountInfo: idInfo,
Mnemonic: mnemonic,
}
}
// AccountInfo contains a PublicKey and an Address of an account.
type AccountInfo struct {
PublicKey string `json:"publicKey"`
Address string `json:"address"`
}
// IdentifiedAccountInfo contains AccountInfo and the ID of an account.
type IdentifiedAccountInfo struct {
AccountInfo
ID string `json:"id"`
}
// GeneratedAccountInfo contains IdentifiedAccountInfo and the mnemonic of an account.
type GeneratedAccountInfo struct {
IdentifiedAccountInfo
Mnemonic string `json:"mnemonic"`
}
func (a GeneratedAccountInfo) toGeneratedAndDerived(derived map[string]AccountInfo) GeneratedAndDerivedAccountInfo {
return GeneratedAndDerivedAccountInfo{
GeneratedAccountInfo: a,
Derived: derived,
}
}
// GeneratedAndDerivedAccountInfo contains GeneratedAccountInfo and derived AccountInfo mapped by derivation path.
type GeneratedAndDerivedAccountInfo struct {
GeneratedAccountInfo
Derived map[string]AccountInfo `json:"derived"`
}

View file

@ -0,0 +1,311 @@
package generator
import (
"crypto/ecdsa"
"errors"
"fmt"
"strings"
"sync"
"github.com/ethereum/go-ethereum/accounts"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid"
"github.com/status-im/status-go/extkeys"
)
var (
// ErrAccountNotFoundByID is returned when the selected account doesn't exist in memory.
ErrAccountNotFoundByID = errors.New("account not found")
// ErrAccountCannotDeriveChildKeys is returned when trying to derive child accounts from a normal key.
ErrAccountCannotDeriveChildKeys = errors.New("selected account cannot derive child keys")
// ErrAccountManagerNotSet is returned when the account mananger instance is not set.
ErrAccountManagerNotSet = errors.New("account manager not set")
)
type AccountManager interface {
AddressToDecryptedAccount(address, password string) (accounts.Account, *keystore.Key, error)
ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, password string) (address, pubKey string, err error)
ImportAccount(privateKey *ecdsa.PrivateKey, password string) (common.Address, error)
}
type Generator struct {
am AccountManager
accounts map[string]*account
sync.Mutex
}
func New(am AccountManager) *Generator {
return &Generator{
am: am,
accounts: make(map[string]*account),
}
}
func (g *Generator) Generate(mnemonicPhraseLength int, n int, bip39Passphrase string) ([]GeneratedAccountInfo, error) {
entropyStrength, err := MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
if err != nil {
return nil, err
}
infos := make([]GeneratedAccountInfo, 0)
for i := 0; i < n; i++ {
mnemonic := extkeys.NewMnemonic()
mnemonicPhrase, err := mnemonic.MnemonicPhrase(entropyStrength, extkeys.EnglishLanguage)
if err != nil {
return nil, fmt.Errorf("can not create mnemonic seed: %v", err)
}
info, err := g.ImportMnemonic(mnemonicPhrase, bip39Passphrase)
if err != nil {
return nil, err
}
infos = append(infos, info)
}
return infos, err
}
func (g *Generator) ImportPrivateKey(privateKeyHex string) (IdentifiedAccountInfo, error) {
privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x")
privateKey, err := crypto.HexToECDSA(privateKeyHex)
if err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &account{
privateKey: privateKey,
}
id := g.addAccount(acc)
return acc.toIdentifiedAccountInfo(id), nil
}
func (g *Generator) ImportJSONKey(json string, password string) (IdentifiedAccountInfo, error) {
key, err := keystore.DecryptKey([]byte(json), password)
if err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &account{
privateKey: key.PrivateKey,
}
id := g.addAccount(acc)
return acc.toIdentifiedAccountInfo(id), nil
}
func (g *Generator) ImportMnemonic(mnemonicPhrase string, bip39Passphrase string) (GeneratedAccountInfo, error) {
mnemonic := extkeys.NewMnemonic()
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, bip39Passphrase))
if err != nil {
return GeneratedAccountInfo{}, fmt.Errorf("can not create master extended key: %v", err)
}
acc := &account{
privateKey: masterExtendedKey.ToECDSA(),
extendedKey: masterExtendedKey,
}
id := g.addAccount(acc)
return acc.toGeneratedAccountInfo(id, mnemonicPhrase), nil
}
func (g *Generator) GenerateAndDeriveAddresses(mnemonicPhraseLength int, n int, bip39Passphrase string, pathStrings []string) ([]GeneratedAndDerivedAccountInfo, error) {
masterAccounts, err := g.Generate(mnemonicPhraseLength, n, bip39Passphrase)
if err != nil {
return nil, err
}
accs := make([]GeneratedAndDerivedAccountInfo, n)
for i := 0; i < len(masterAccounts); i++ {
acc := masterAccounts[i]
derived, err := g.DeriveAddresses(acc.ID, pathStrings)
if err != nil {
return nil, err
}
accs[i] = acc.toGeneratedAndDerived(derived)
}
return accs, nil
}
func (g *Generator) DeriveAddresses(accountID string, pathStrings []string) (map[string]AccountInfo, error) {
acc, err := g.findAccount(accountID)
if err != nil {
return nil, err
}
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
if err != nil {
return nil, err
}
pathAccountsInfo := make(map[string]AccountInfo)
for pathString, childAccount := range pathAccounts {
pathAccountsInfo[pathString] = childAccount.toAccountInfo()
}
return pathAccountsInfo, nil
}
func (g *Generator) StoreAccount(accountID string, password string) (AccountInfo, error) {
if g.am == nil {
return AccountInfo{}, ErrAccountManagerNotSet
}
acc, err := g.findAccount(accountID)
if err != nil {
return AccountInfo{}, err
}
return g.store(acc, password)
}
func (g *Generator) StoreDerivedAccounts(accountID string, password string, pathStrings []string) (map[string]AccountInfo, error) {
if g.am == nil {
return nil, ErrAccountManagerNotSet
}
acc, err := g.findAccount(accountID)
if err != nil {
return nil, err
}
pathAccounts, err := g.deriveChildAccounts(acc, pathStrings)
if err != nil {
return nil, err
}
pathAccountsInfo := make(map[string]AccountInfo)
for pathString, childAccount := range pathAccounts {
info, err := g.store(childAccount, password)
if err != nil {
return nil, err
}
pathAccountsInfo[pathString] = info
}
return pathAccountsInfo, nil
}
func (g *Generator) LoadAccount(address string, password string) (IdentifiedAccountInfo, error) {
if g.am == nil {
return IdentifiedAccountInfo{}, ErrAccountManagerNotSet
}
_, key, err := g.am.AddressToDecryptedAccount(address, password)
if err != nil {
return IdentifiedAccountInfo{}, err
}
if err := ValidateKeystoreExtendedKey(key); err != nil {
return IdentifiedAccountInfo{}, err
}
acc := &account{
privateKey: key.PrivateKey,
extendedKey: key.ExtendedKey,
}
id := g.addAccount(acc)
return acc.toIdentifiedAccountInfo(id), nil
}
func (g *Generator) deriveChildAccounts(acc *account, pathStrings []string) (map[string]*account, error) {
pathAccounts := make(map[string]*account)
for _, pathString := range pathStrings {
childAccount, err := g.deriveChildAccount(acc, pathString)
if err != nil {
return pathAccounts, err
}
pathAccounts[pathString] = childAccount
}
return pathAccounts, nil
}
func (g *Generator) deriveChildAccount(acc *account, pathString string) (*account, error) {
_, path, err := decodePath(pathString)
if err != nil {
return nil, err
}
if acc.extendedKey.IsZeroed() && len(path) == 0 {
return acc, nil
}
if acc.extendedKey.IsZeroed() {
return nil, ErrAccountCannotDeriveChildKeys
}
childExtendedKey, err := acc.extendedKey.Derive(path)
if err != nil {
return nil, err
}
return &account{
privateKey: childExtendedKey.ToECDSA(),
extendedKey: childExtendedKey,
}, nil
}
func (g *Generator) store(acc *account, password string) (AccountInfo, error) {
if acc.extendedKey != nil {
if _, _, err := g.am.ImportSingleExtendedKey(acc.extendedKey, password); err != nil {
return AccountInfo{}, err
}
} else {
if _, err := g.am.ImportAccount(acc.privateKey, password); err != nil {
return AccountInfo{}, err
}
}
g.Reset()
return acc.toAccountInfo(), nil
}
func (g *Generator) addAccount(acc *account) string {
g.Lock()
defer g.Unlock()
id := uuid.NewRandom().String()
g.accounts[id] = acc
return id
}
// Reset resets the accounts map removing all the accounts from memory.
func (g *Generator) Reset() {
g.Lock()
defer g.Unlock()
g.accounts = make(map[string]*account)
}
func (g *Generator) findAccount(accountID string) (*account, error) {
g.Lock()
defer g.Unlock()
acc, ok := g.accounts[accountID]
if !ok {
return nil, ErrAccountNotFoundByID
}
return acc, nil
}

View file

@ -0,0 +1,146 @@
package generator
import (
"fmt"
"strings"
"testing"
"github.com/ethereum/go-ethereum/crypto"
"github.com/stretchr/testify/assert"
)
var testAccount = struct {
mnemonic string
bip39Passphrase string
encriptionPassword string
extendedMasterKey string
bip44Key0 string
bip44PubKey0 string
bip44Address0 string
bip44Address1 string
}{
mnemonic: "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
bip39Passphrase: "TREZOR",
encriptionPassword: "TEST_PASSWORD",
extendedMasterKey: "xprv9s21ZrQH143K3h3fDYiay8mocZ3afhfULfb5GX8kCBdno77K4HiA15Tg23wpbeF1pLfs1c5SPmYHrEpTuuRhxMwvKDwqdKiGJS9XFKzUsAF",
bip44Key0: "0x62f1d86b246c81bdd8f6c166d56896a4a5e1eddbcaebe06480e5c0bc74c28224",
bip44PubKey0: "0x04986dee3b8afe24cb8ccb2ac23dac3f8c43d22850d14b809b26d6b8aa5a1f47784152cd2c7d9edd0ab20392a837464b5a750b2a7f3f06e6a5756b5211b6a6ed05",
bip44Address0: "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6",
bip44Address1: "0x7AF7283bd1462C3b957e8FAc28Dc19cBbF2FAdfe",
}
const testAccountJSONFile = `{
"address":"9c32f71d4db8fb9e1a58b0a80df79935e7256fa6",
"crypto":
{
"cipher":"aes-128-ctr","ciphertext":"8055b65d5e41ef467c0cfe52ce6beda7f8dbe689221c6c43be9e9401bf173004",
"cipherparams":{"iv":"738f002e5e5343e0bb0e1050e098f721"},
"kdf":"scrypt",
"kdfparams":{"dklen":32,"n":4096,"p":6,"r":8,"salt":"9a54fbe1439ac567bd05039f76907b2c2846364a38b2f6813bcdac5ab0ec9d18"},
"mac":"79d817cd21afd4944e70d804d7871d10cbd15f25c6755416f780f81c1588677e"
},
"id":"6202ced9-f0cd-42e4-bf21-6029cca0ea91",
"version":3
}`
func TestGenerator_Generate(t *testing.T) {
g := New(nil)
assert.Equal(t, 0, len(g.accounts))
accountsInfo, err := g.Generate(12, 5, "")
assert.NoError(t, err)
assert.Equal(t, 5, len(g.accounts))
for _, info := range accountsInfo {
words := strings.Split(info.Mnemonic, " ")
assert.Equal(t, 12, len(words))
}
}
func TestGenerator_ImportPrivateKey(t *testing.T) {
g := New(nil)
assert.Equal(t, 0, len(g.accounts))
info, err := g.ImportPrivateKey(testAccount.bip44Key0)
assert.NoError(t, err)
assert.Equal(t, 1, len(g.accounts))
assert.Equal(t, testAccount.bip44PubKey0, info.PublicKey)
assert.Equal(t, testAccount.bip44Address0, info.Address)
}
func TestGenerator_ImportMnemonic(t *testing.T) {
g := New(nil)
assert.Equal(t, 0, len(g.accounts))
info, err := g.ImportMnemonic(testAccount.mnemonic, testAccount.bip39Passphrase)
assert.NoError(t, err)
assert.Equal(t, 1, len(g.accounts))
key := g.accounts[info.ID]
assert.Equal(t, testAccount.extendedMasterKey, key.extendedKey.String())
}
func TestGenerator_ImportJSONKey(t *testing.T) {
g := New(nil)
assert.Equal(t, 0, len(g.accounts))
// wrong password
_, err := g.ImportJSONKey(testAccountJSONFile, "wrong-password")
assert.Error(t, err)
// right password
info, err := g.ImportJSONKey(testAccountJSONFile, testAccount.encriptionPassword)
assert.NoError(t, err)
assert.Equal(t, 1, len(g.accounts))
assert.Equal(t, testAccount.bip44Address0, info.Address)
key := g.accounts[info.ID]
keyHex := fmt.Sprintf("0x%x", crypto.FromECDSA(key.privateKey))
assert.Equal(t, testAccount.bip44Key0, keyHex)
}
func TestGenerator_DeriveAddresses(t *testing.T) {
g := New(nil)
assert.Equal(t, 0, len(g.accounts))
info, err := g.ImportMnemonic(testAccount.mnemonic, testAccount.bip39Passphrase)
assert.NoError(t, err)
assert.Equal(t, 1, len(g.accounts))
path0 := "m/44'/60'/0'/0/0"
path1 := "m/44'/60'/0'/0/1"
addresses, err := g.DeriveAddresses(info.ID, []string{path0, path1})
assert.NoError(t, err)
assert.Equal(t, testAccount.bip44Address0, addresses[path0].Address)
assert.Equal(t, testAccount.bip44Address1, addresses[path1].Address)
}
func TestGenerator_DeriveAddresses_FromImportedPrivateKey(t *testing.T) {
g := New(nil)
assert.Equal(t, 0, len(g.accounts))
key, err := crypto.GenerateKey()
assert.NoError(t, err)
hex := fmt.Sprintf("%#x", crypto.FromECDSA(key))
info, err := g.ImportPrivateKey(hex)
assert.NoError(t, err)
assert.Equal(t, 1, len(g.accounts))
// normal imported accounts cannot derive child accounts,
// but only the address/pubblic key of the current key.
paths := []string{"", "m"}
for _, path := range paths {
addresses, err := g.DeriveAddresses(info.ID, []string{path})
assert.NoError(t, err)
expectedAddress := crypto.PubkeyToAddress(key.PublicKey).Hex()
assert.Equal(t, expectedAddress, addresses[path].Address)
}
// cannot derive other child keys from a normal key
_, err = g.DeriveAddresses(info.ID, []string{"m/0/1/2"})
assert.Equal(t, ErrAccountCannotDeriveChildKeys, err)
}

View file

@ -0,0 +1,226 @@
package generator
import (
"fmt"
"io"
"strconv"
"strings"
)
type startingPoint int
const (
tokenMaster = 0x6D // char m
tokenSeparator = 0x2F // char /
tokenHardened = 0x27 // char '
tokenDot = 0x2E // char .
hardenedStart = 0x80000000 // 2^31
)
const (
startingPointMaster startingPoint = iota + 1
startingPointCurrent
startingPointParent
)
type parseFunc = func() error
type pathDecoder struct {
s string
r *strings.Reader
f parseFunc
pos int
path []uint32
start startingPoint
currentToken string
currentTokenHardened bool
}
func newPathDecoder(path string) (*pathDecoder, error) {
d := &pathDecoder{
s: path,
r: strings.NewReader(path),
}
if err := d.reset(); err != nil {
return nil, err
}
return d, nil
}
func (d *pathDecoder) reset() error {
_, err := d.r.Seek(0, io.SeekStart)
if err != nil {
return err
}
d.pos = 0
d.start = startingPointCurrent
d.f = d.parseStart
d.path = make([]uint32, 0)
d.resetCurrentToken()
return nil
}
func (d *pathDecoder) resetCurrentToken() {
d.currentToken = ""
d.currentTokenHardened = false
}
func (d *pathDecoder) parse() (startingPoint, []uint32, error) {
for {
err := d.f()
if err != nil {
if err == io.EOF {
err = nil
} else {
err = fmt.Errorf("error parsing derivation path %s; at position %d, %s", d.s, d.pos, err.Error())
}
return d.start, d.path, err
}
}
}
func (d *pathDecoder) readByte() (byte, error) {
b, err := d.r.ReadByte()
if err != nil {
return b, err
}
d.pos++
return b, nil
}
func (d *pathDecoder) unreadByte() error {
err := d.r.UnreadByte()
if err != nil {
return err
}
d.pos--
return nil
}
func (d *pathDecoder) parseStart() error {
b, err := d.readByte()
if err != nil {
return err
}
if b == tokenMaster {
d.start = startingPointMaster
d.f = d.parseSeparator
return nil
}
if b == tokenDot {
b2, err := d.readByte()
if err != nil {
return err
}
if b2 == tokenDot {
d.f = d.parseSeparator
d.start = startingPointParent
return nil
}
d.f = d.parseSeparator
d.start = startingPointCurrent
return d.unreadByte()
}
d.f = d.parseSegment
return d.unreadByte()
}
func (d *pathDecoder) saveSegment() error {
if len(d.currentToken) > 0 {
i, err := strconv.ParseUint(d.currentToken, 10, 32)
if err != nil {
return err
}
if i >= hardenedStart {
d.pos -= len(d.currentToken) - 1
return fmt.Errorf("index must be lower than 2^31, got %d", i)
}
if d.currentTokenHardened {
i += hardenedStart
}
d.path = append(d.path, uint32(i))
}
d.f = d.parseSegment
d.resetCurrentToken()
return nil
}
func (d *pathDecoder) parseSeparator() error {
b, err := d.readByte()
if err != nil {
return err
}
if b == tokenSeparator {
return d.saveSegment()
}
return fmt.Errorf("expected %s, got %s", string(tokenSeparator), string(b))
}
func (d *pathDecoder) parseSegment() error {
b, err := d.readByte()
if err == io.EOF {
if len(d.currentToken) == 0 {
return fmt.Errorf("expected number, got EOF")
}
if newErr := d.saveSegment(); newErr != nil {
return newErr
}
return err
}
if err != nil {
return err
}
if len(d.currentToken) > 0 && b == tokenSeparator {
return d.saveSegment()
}
if len(d.currentToken) > 0 && b == tokenHardened {
d.currentTokenHardened = true
d.f = d.parseSeparator
return nil
}
if b < 0x30 || b > 0x39 {
return fmt.Errorf("expected number, got %s", string(b))
}
d.currentToken = fmt.Sprintf("%s%s", d.currentToken, string(b))
return nil
}
func decodePath(str string) (startingPoint, []uint32, error) {
d, err := newPathDecoder(str)
if err != nil {
return 0, nil, err
}
return d.parse()
}

View file

@ -0,0 +1,86 @@
package generator
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func TestDecodePath(t *testing.T) {
scenarios := []struct {
path string
expectedPath []uint32
expectedStartingPoint startingPoint
err error
}{
{
path: "",
expectedPath: []uint32{},
expectedStartingPoint: startingPointCurrent,
},
{
path: "1",
expectedPath: []uint32{1},
expectedStartingPoint: startingPointCurrent,
},
{
path: "..",
expectedPath: []uint32{},
expectedStartingPoint: startingPointParent,
},
{
path: "m",
expectedPath: []uint32{},
expectedStartingPoint: startingPointMaster,
},
{
path: "m/1",
expectedPath: []uint32{1},
expectedStartingPoint: startingPointMaster,
},
{
path: "m/1/2",
expectedPath: []uint32{1, 2},
expectedStartingPoint: startingPointMaster,
},
{
path: "m/1/2'/3",
expectedPath: []uint32{1, 2147483650, 3},
expectedStartingPoint: startingPointMaster,
},
{
path: "m/",
err: fmt.Errorf("error parsing derivation path m/; at position 2, expected number, got EOF"),
},
{
path: "m/1//2",
err: fmt.Errorf("error parsing derivation path m/1//2; at position 5, expected number, got /"),
},
{
path: "m/1'2",
err: fmt.Errorf("error parsing derivation path m/1'2; at position 5, expected /, got 2"),
},
{
path: "m/'/2",
err: fmt.Errorf("error parsing derivation path m/'/2; at position 3, expected number, got '"),
},
{
path: "m/2147483648",
err: fmt.Errorf("error parsing derivation path m/2147483648; at position 3, index must be lower than 2^31, got 2147483648"),
},
}
for i, s := range scenarios {
t.Run(fmt.Sprintf("scenario %d", i), func(t *testing.T) {
startingP, path, err := decodePath(s.path)
if s.err == nil {
assert.NoError(t, err)
assert.Equal(t, s.expectedStartingPoint, startingP)
assert.Equal(t, s.expectedPath, path)
} else {
assert.Equal(t, s.err, err)
}
})
}
}

View file

@ -0,0 +1,46 @@
package generator
import (
"bytes"
"errors"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/extkeys"
)
var (
// ErrInvalidKeystoreExtendedKey is returned when the decrypted keystore file
// contains some old Status keys.
// The old version used to store the BIP44 account at index 0 as PrivateKey,
// and the BIP44 account at index 1 as ExtendedKey.
// The current version stores the same key as PrivateKey and ExtendedKey.
ErrInvalidKeystoreExtendedKey = errors.New("PrivateKey and ExtendedKey are different")
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
)
// ValidateKeystoreExtendedKey validates the keystore keys, checking that
// ExtendedKey is the extended key of PrivateKey.
func ValidateKeystoreExtendedKey(key *keystore.Key) error {
if key.ExtendedKey.IsZeroed() {
return nil
}
if !bytes.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA())) {
return ErrInvalidKeystoreExtendedKey
}
return nil
}
// MnemonicPhraseLengthToEntropyStrength returns the entropy strength for a given mnemonic length
func MnemonicPhraseLengthToEntropyStrength(length int) (extkeys.EntropyStrength, error) {
if length < 12 || length > 24 || length%3 != 0 {
return 0, ErrInvalidMnemonicPhraseLength
}
bitsLength := length * 11
checksumLength := bitsLength % 32
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
}

View file

@ -0,0 +1,48 @@
package generator
import (
"testing"
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/status-im/status-go/extkeys"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func generateTestKey(t *testing.T) *extkeys.ExtendedKey {
mnemonic := extkeys.NewMnemonic()
mnemonicPhrase, err := mnemonic.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
require.NoError(t, err)
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, ""))
require.NoError(t, err)
return masterExtendedKey
}
func TestValidateKeystoreExtendedKey(t *testing.T) {
extendedKey1 := generateTestKey(t)
extendedKey2 := generateTestKey(t)
// new keystore file format
key := &keystore.Key{
PrivateKey: extendedKey1.ToECDSA(),
ExtendedKey: extendedKey1,
}
assert.NoError(t, ValidateKeystoreExtendedKey(key))
// old keystore file format where the extended key was
// from another derivation path and not the same of the private key
oldKey := &keystore.Key{
PrivateKey: extendedKey1.ToECDSA(),
ExtendedKey: extendedKey2,
}
assert.Error(t, ValidateKeystoreExtendedKey(oldKey))
// normal key where we don't have an extended key
normalKey := &keystore.Key{
PrivateKey: extendedKey1.ToECDSA(),
ExtendedKey: nil,
}
assert.NoError(t, ValidateKeystoreExtendedKey(normalKey))
}

View file

@ -1,19 +1,15 @@
package account
import (
"errors"
"fmt"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/pborman/uuid"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/extkeys"
)
// ErrInvalidMnemonicPhraseLength is returned if the requested mnemonic length is invalid.
// Valid lengths are 12, 15, 18, 21, and 24.
var ErrInvalidMnemonicPhraseLength = errors.New("mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
// OnboardingAccount is returned during onboarding and contains its ID and the mnemonic to re-generate the same account Info keys.
type OnboardingAccount struct {
ID string `json:"id"`
@ -64,7 +60,7 @@ func (o *Onboarding) Account(id string) (*OnboardingAccount, error) {
}
func (o *Onboarding) generateAccount(mnemonicPhraseLength int) (*OnboardingAccount, error) {
entropyStrength, err := mnemonicPhraseLengthToEntropyStrenght(mnemonicPhraseLength)
entropyStrength, err := generator.MnemonicPhraseLengthToEntropyStrength(mnemonicPhraseLength)
if err != nil {
return nil, err
}
@ -115,14 +111,3 @@ func (o *Onboarding) deriveAccount(masterExtendedKey *extkeys.ExtendedKey, purpo
return address.Hex(), publicKeyHex, nil
}
func mnemonicPhraseLengthToEntropyStrenght(length int) (extkeys.EntropyStrength, error) {
if length < 12 || length > 24 || length%3 != 0 {
return 0, ErrInvalidMnemonicPhraseLength
}
bitsLength := length * 11
checksumLength := bitsLength % 32
return extkeys.EntropyStrength(bitsLength - checksumLength), nil
}

View file

@ -4,35 +4,10 @@ import (
"strings"
"testing"
"github.com/status-im/status-go/extkeys"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMnemonicPhraseLengthToEntropyStrenght(t *testing.T) {
scenarios := []struct {
phraseLength int
expectedStrength extkeys.EntropyStrength
expectedError error
}{
{12, 128, nil},
{15, 160, nil},
{18, 192, nil},
{21, 224, nil},
{24, 256, nil},
// invalid
{11, 0, ErrInvalidMnemonicPhraseLength},
{14, 0, ErrInvalidMnemonicPhraseLength},
{25, 0, ErrInvalidMnemonicPhraseLength},
}
for _, s := range scenarios {
strength, err := mnemonicPhraseLengthToEntropyStrenght(s.phraseLength)
assert.Equal(t, s.expectedError, err)
assert.Equal(t, s.expectedStrength, strength)
}
}
func TestOnboarding(t *testing.T) {
count := 2
wordsCount := 24

View file

@ -10,7 +10,8 @@ import (
// errors
var (
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
ErrInvalidAccountAddressOrKey = errors.New("cannot parse address or key to valid account address")
ErrInvalidMnemonicPhraseLength = errors.New("invalid mnemonic phrase length; valid lengths are 12, 15, 18, 21, and 24")
)
// Info contains wallet and chat addresses and public keys of an account.

View file

@ -6,6 +6,9 @@ import (
"github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/extkeys"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
@ -65,3 +68,27 @@ func (suite *AccountUtilsTestSuite) TestHex() {
func TestAccountUtilsTestSuite(t *testing.T) {
suite.Run(t, new(AccountUtilsTestSuite))
}
func TestMnemonicPhraseLengthToEntropyStrength(t *testing.T) {
scenarios := []struct {
phraseLength int
expectedStrength extkeys.EntropyStrength
expectedError error
}{
{12, 128, nil},
{15, 160, nil},
{18, 192, nil},
{21, 224, nil},
{24, 256, nil},
// invalid
{11, 0, ErrInvalidMnemonicPhraseLength},
{14, 0, ErrInvalidMnemonicPhraseLength},
{25, 0, ErrInvalidMnemonicPhraseLength},
}
for _, s := range scenarios {
strength, err := generator.MnemonicPhraseLengthToEntropyStrength(s.phraseLength)
assert.Equal(t, s.expectedError, err)
assert.Equal(t, s.expectedStrength, strength)
}
}

View file

@ -486,6 +486,7 @@ func (b *StatusBackend) Logout() error {
return err
}
}
if b.statusNode.Config().BrowsersConfig.Enabled {
svc, err := b.statusNode.BrowsersService()
switch err {

View file

@ -365,9 +365,14 @@ func (k *ExtendedKey) Neuter() (*ExtendedKey, error) {
}, nil
}
// IsZeroed returns true if key is nil or empty
func (k *ExtendedKey) IsZeroed() bool {
return k == nil || len(k.KeyData) == 0
}
// String returns the extended key as a human-readable base58-encoded string.
func (k *ExtendedKey) String() string {
if k == nil || len(k.KeyData) == 0 {
if k.IsZeroed() {
return EmptyExtendedKeyString
}

2
go.mod
View file

@ -30,6 +30,6 @@ require (
gopkg.in/natefinch/lumberjack.v2 v2.0.0
)
replace github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.3
replace github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.5
replace github.com/NaySoftware/go-fcm => github.com/status-im/go-fcm v1.0.0-status

2
go.sum
View file

@ -440,6 +440,8 @@ github.com/status-im/doubleratchet v2.0.0+incompatible h1:s77lF1lDubK0RKftxN2vH8
github.com/status-im/doubleratchet v2.0.0+incompatible/go.mod h1:1sqR0+yhiM/bd+wrdX79AOt2csZuJOni0nUDzKNuqOU=
github.com/status-im/go-ethereum v1.8.27-status.3 h1:h+CsF2Z/HyERLo5qTQnrK0RXHuDJ605hG2BbqNOLAAc=
github.com/status-im/go-ethereum v1.8.27-status.3/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
github.com/status-im/go-ethereum v1.8.27-status.5 h1:FS+1KwA97yWh9BtHse0BLfEWtldboSiTMA7nCavxtzc=
github.com/status-im/go-ethereum v1.8.27-status.5/go.mod h1:Ulij8LMpMvXnbnPcmDqrpI+iXoXSjxItuY/wmbasTZU=
github.com/status-im/go-fcm v1.0.0-status h1:eUNKm4ooAXdEf9/GaTYeTELna5aVMOEbzjbm2irQ0gY=
github.com/status-im/go-fcm v1.0.0-status/go.mod h1:0JGzul9SfemcbTdw0mkzLR42j+BTNci5aQWlI0o/uk8=
github.com/status-im/go-multiaddr-ethv4 v1.2.0 h1:OT84UsUzTCwguqCpJqkrCMiL4VZ1SvUtH9a5MsZupBk=

View file

@ -0,0 +1,261 @@
// +build e2e_test
package main
import (
"C"
"encoding/json"
"github.com/ethereum/go-ethereum/crypto"
mobile "github.com/status-im/status-go/mobile"
)
import (
"fmt"
"strings"
"testing"
"github.com/status-im/status-go/account/generator"
)
func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{}) {
var e struct {
Error *string `json:"error"`
}
json.Unmarshal([]byte(C.GoString(respJSON)), &e)
if e.Error != nil {
t.Errorf("unexpected response error: %s", *e.Error)
}
if err := json.Unmarshal([]byte(C.GoString(respJSON)), resp); err != nil {
t.Fatalf("error unmarshaling response to expected struct: %s", err)
}
}
func testMultiAccountGenerateDeriveAndStore(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
params := C.CString(`{
"n": 2,
"mnemonicPhraseLength": 24,
"bip39Passphrase": ""
}`)
// generate 2 random accounts
rawResp := MultiAccountGenerate(params)
var generateResp []generator.GeneratedAccountInfo
// check there's no error in the response
checkMultiAccountResponse(t, rawResp, &generateResp)
if len(generateResp) != 2 {
t.Errorf("expected 2 accounts created, got %d", len(generateResp))
return false
}
bip44DerivationPath := "m/44'/60'/0'/0/0"
eip1581DerivationPath := "m/43'/60'/1581'/0'/0"
paths := []string{bip44DerivationPath, eip1581DerivationPath}
// derive 2 child accounts for each account without storing them
for i := 0; i < len(generateResp); i++ {
info := generateResp[i]
mnemonicLength := len(strings.Split(info.Mnemonic, " "))
if mnemonicLength != 24 {
t.Errorf("expected mnemonic to have 24 words, got %d", mnemonicLength)
return false
}
if ok := testMultiAccountDeriveAddresses(t, info.ID, paths); !ok {
return false
}
}
// store 2 derived child accounts from the first account.
// after that all the generated account should be remove from memory.
if ok := testMultiAccountStoreDerived(t, generateResp[0].ID, paths); !ok {
return false
}
return true
}
func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo
params := mobile.MultiAccountDeriveAddressesParams{
AccountID: accountID,
Paths: paths,
}
paramsJSON, err := json.Marshal(&params)
if err != nil {
t.Errorf("error encoding MultiAccountDeriveAddressesParams")
return false
}
// derive addresses from account accountID
rawResp := MultiAccountDeriveAddresses(C.CString(string(paramsJSON)))
var deriveResp map[string]generator.AccountInfo
// check the response doesn't have errors
checkMultiAccountResponse(t, rawResp, &deriveResp)
if len(deriveResp) != 2 {
t.Errorf("expected 2 derived accounts info, got %d", len(deriveResp))
return false
}
// check that we have an address for each derivation path we used.
for _, path := range paths {
if _, ok := deriveResp[path]; !ok {
t.Errorf("results doesn't contain account info for path %s", path)
return false
}
}
return true
}
func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo
password := "test-multiaccount-password"
params := mobile.MultiAccountStoreDerivedParams{
MultiAccountDeriveAddressesParams: mobile.MultiAccountDeriveAddressesParams{
AccountID: accountID,
Paths: paths,
},
Password: password,
}
paramsJSON, err := json.Marshal(params)
if err != nil {
t.Errorf("error encoding MultiAccountStoreDerivedParams")
return false
}
// store one child account for each derivation path.
rawResp := MultiAccountStoreDerived(C.CString(string(paramsJSON)))
var storeResp map[string]generator.AccountInfo
// check that we don't have errors in the response
checkMultiAccountResponse(t, rawResp, &storeResp)
addresses := make([]string, 0)
for _, info := range storeResp {
addresses = append(addresses, info.Address)
}
if len(addresses) != 2 {
t.Errorf("expected 2 addresses, got %d", len(addresses))
return false
}
// for each stored account, check that we can decrypt it with the password we used.
dir := statusBackend.StatusNode().Config().DataDir
for _, address := range addresses {
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password)
if err != nil {
t.Errorf("failed to verify password on stored derived account")
}
}
return true
}
func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
paths := []string{"m/0", "m/1"}
params := mobile.MultiAccountGenerateAndDeriveAddressesParams{
MultiAccountGenerateParams: mobile.MultiAccountGenerateParams{
N: 2,
MnemonicPhraseLength: 12,
},
Paths: paths,
}
paramsJSON, err := json.Marshal(&params)
if err != nil {
t.Errorf("error encoding MultiAccountGenerateAndDeriveParams")
return false
}
// generate 2 random accounts and derive 2 accounts from each one.
rawResp := MultiAccountGenerateAndDeriveAddresses(C.CString(string(paramsJSON)))
var generateResp []generator.GeneratedAndDerivedAccountInfo
// check there's no error in the response
checkMultiAccountResponse(t, rawResp, &generateResp)
if len(generateResp) != 2 {
t.Errorf("expected 2 accounts created, got %d", len(generateResp))
return false
}
// check that for each account we have the 2 derived addresses
for _, info := range generateResp {
for _, path := range paths {
if _, ok := info.Derived[path]; !ok {
t.Errorf("results doesn't contain account info for path %s", path)
return false
}
}
}
return true
}
func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo
// to make sure that we start with empty account (which might have gotten populated during previous tests)
if err := statusBackend.Logout(); err != nil {
t.Fatal(err)
}
key, err := crypto.GenerateKey()
if err != nil {
t.Errorf("failed generating key")
}
hex := fmt.Sprintf("%#x", crypto.FromECDSA(key))
importParams := mobile.MultiAccountImportPrivateKeyParams{
PrivateKey: hex,
}
paramsJSON, err := json.Marshal(&importParams)
if err != nil {
t.Errorf("error encoding MultiAccountImportPrivateKeyParams")
return false
}
// import raw private key
rawResp := MultiAccountImportPrivateKey(C.CString(string(paramsJSON)))
var importResp generator.IdentifiedAccountInfo
// check the response doesn't have errors
checkMultiAccountResponse(t, rawResp, &importResp)
// prepare StoreAccount params
password := "test-multiaccount-imported-key-password"
storeParams := mobile.MultiAccountStoreAccountParams{
AccountID: importResp.ID,
Password: password,
}
paramsJSON, err = json.Marshal(storeParams)
if err != nil {
t.Errorf("error encoding MultiAccountStoreParams")
return false
}
// store the imported private key
rawResp = MultiAccountStoreAccount(C.CString(string(paramsJSON)))
var storeResp generator.AccountInfo
// check the response doesn't have errors
checkMultiAccountResponse(t, rawResp, &storeResp)
dir := statusBackend.StatusNode().Config().DataDir
_, err = statusBackend.AccountManager().VerifyAccountPassword(dir, storeResp.Address, password)
if err != nil {
t.Errorf("failed to verify password on stored derived account")
}
return true
}

View file

@ -146,6 +146,18 @@ func testExportedAPI(t *testing.T, done chan struct{}) {
"failed single transaction",
testFailedTransaction,
},
{
"MultiAccount - Generate/Derive/StoreDerived",
testMultiAccountGenerateDeriveAndStore,
},
{
"MultiAccount - GenerateAndDerive",
testMultiAccountGenerateAndDerive,
},
{
"MultiAccount - Import/Store",
testMultiAccountImportStore,
},
}
for _, test := range tests {
@ -1018,16 +1030,3 @@ func testValidateNodeConfig(t *testing.T, config string, fn func(*testing.T, API
fn(t, resp)
}
// PanicAfter throws panic() after waitSeconds, unless abort channel receives
// notification.
func PanicAfter(waitSeconds time.Duration, abort chan struct{}, desc string) {
go func() {
select {
case <-abort:
return
case <-time.After(waitSeconds):
panic("whatever you were doing takes toooo long: " + desc)
}
}()
}

141
lib/multiaccount.go Normal file
View file

@ -0,0 +1,141 @@
package main
// #include <stdlib.h>
import "C"
import (
"encoding/json"
mobile "github.com/status-im/status-go/mobile"
)
// MultiAccountGenerate generates account in memory without storing them.
//export MultiAccountGenerate
func MultiAccountGenerate(paramsJSON *C.char) *C.char {
var p mobile.MultiAccountGenerateParams
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().Generate(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(out))
}
// MultiAccountGenerateAndDeriveAddresses combines Generate and DeriveAddresses in one call.
//export MultiAccountGenerateAndDeriveAddresses
func MultiAccountGenerateAndDeriveAddresses(paramsJSON *C.char) *C.char {
var p mobile.MultiAccountGenerateAndDeriveAddressesParams
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().GenerateAndDeriveAddresses(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase, p.Paths)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(out))
}
// MultiAccountDeriveAddresses derive addresses from an account selected by ID, without storing them.
//export MultiAccountDeriveAddresses
func MultiAccountDeriveAddresses(paramsJSON *C.char) *C.char {
var p mobile.MultiAccountDeriveAddressesParams
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().DeriveAddresses(p.AccountID, p.Paths)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(out))
}
// MultiAccountStoreDerived derive accounts from the specified key and store them encrypted with the specified password.
//export MultiAccountStoreDerived
func MultiAccountStoreDerived(paramsJSON *C.char) *C.char {
var p mobile.MultiAccountStoreDerivedParams
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreDerivedAccounts(p.AccountID, p.Password, p.Paths)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(out))
}
// MultiAccountImportPrivateKey imports a raw private key without storing it.
//export MultiAccountImportPrivateKey
func MultiAccountImportPrivateKey(paramsJSON *C.char) *C.char {
var p mobile.MultiAccountImportPrivateKeyParams
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().ImportPrivateKey(p.PrivateKey)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(out))
}
// MultiAccountStoreAccount stores the select account.
//export MultiAccountStoreAccount
func MultiAccountStoreAccount(paramsJSON *C.char) *C.char {
var p mobile.MultiAccountStoreAccountParams
if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreAccount(p.AccountID, p.Password)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return C.CString(string(out))
}

167
mobile/multiaccount.go Normal file
View file

@ -0,0 +1,167 @@
package statusgo
import (
"encoding/json"
)
// MultiAccountGenerateParams are the params sent to MultiAccountGenerate.
type MultiAccountGenerateParams struct {
N int `json:"n"`
MnemonicPhraseLength int `json:"mnemonicPhraseLength"`
Bip39Passphrase string `json:"bip39Passphrase"`
}
// MultiAccountGenerateAndDeriveAddressesParams are the params sent to MultiAccountGenerateAndDeriveAddresses.
type MultiAccountGenerateAndDeriveAddressesParams struct {
MultiAccountGenerateParams
Paths []string `json:"paths"`
}
// MultiAccountDeriveAddressesParams are the params sent to MultiAccountDeriveAddresses.
type MultiAccountDeriveAddressesParams struct {
AccountID string `json:"accountID"`
Paths []string `json:"paths"`
}
// MultiAccountStoreDerivedParams are the params sent to MultiAccountStoreDerived.
type MultiAccountStoreDerivedParams struct {
MultiAccountDeriveAddressesParams
Password string `json:"password"`
}
// MultiAccountStoreAccountParams are the params sent to MultiAccountStoreAccount.
type MultiAccountStoreAccountParams struct {
AccountID string `json:"accountID"`
Password string `json:"password"`
}
// MultiAccountImportPrivateKeyParams are the params sent to MultiAccountImportPrivateKey.
type MultiAccountImportPrivateKeyParams struct {
PrivateKey string `json:"privateKey"`
}
// MultiAccountGenerate generates account in memory without storing them.
func MultiAccountGenerate(paramsJSON string) string {
var p MultiAccountGenerateParams
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().Generate(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return string(out)
}
// MultiAccountGenerateAndDeriveAddresses combines Generate and DeriveAddresses in one call.
func MultiAccountGenerateAndDeriveAddresses(paramsJSON string) string {
var p MultiAccountGenerateAndDeriveAddressesParams
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().GenerateAndDeriveAddresses(p.MnemonicPhraseLength, p.N, p.Bip39Passphrase, p.Paths)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return string(out)
}
// MultiAccountDeriveAddresses derive addresses from an account selected by ID, without storing them.
func MultiAccountDeriveAddresses(paramsJSON string) string {
var p MultiAccountDeriveAddressesParams
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().DeriveAddresses(p.AccountID, p.Paths)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return string(out)
}
// MultiAccountStoreDerived derive accounts from the specified key and store them encrypted with the specified password.
func MultiAccountStoreDerived(paramsJSON string) string {
var p MultiAccountStoreDerivedParams
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreDerivedAccounts(p.AccountID, p.Password, p.Paths)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return string(out)
}
// MultiAccountImportPrivateKey imports a raw private key without storing it.
func MultiAccountImportPrivateKey(paramsJSON string) string {
var p MultiAccountImportPrivateKeyParams
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().ImportPrivateKey(p.PrivateKey)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return string(out)
}
// MultiAccountStoreAccount stores the select account.
func MultiAccountStoreAccount(paramsJSON string) string {
var p MultiAccountStoreAccountParams
if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil {
return makeJSONResponse(err)
}
resp, err := statusBackend.AccountManager().AccountsGenerator().StoreAccount(p.AccountID, p.Password)
if err != nil {
return makeJSONResponse(err)
}
out, err := json.Marshal(resp)
if err != nil {
return makeJSONResponse(err)
}
return string(out)
}

View file

@ -112,6 +112,14 @@ func activateServices(stack *node.Node, config *params.NodeConfig, db *leveldb.D
}
}
if err := activateNodeServices(stack, config, db); err != nil {
return err
}
return nil
}
func activateNodeServices(stack *node.Node, config *params.NodeConfig, db *leveldb.DB) error {
// start Whisper service.
if err := activateShhService(stack, config, db); err != nil {
return fmt.Errorf("%v: %v", ErrWhisperServiceRegistrationFailure, err)

View file

@ -8,6 +8,7 @@ import (
"github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/account"
"github.com/status-im/status-go/extkeys"
e2e "github.com/status-im/status-go/t/e2e"
. "github.com/status-im/status-go/t/utils"
"github.com/stretchr/testify/suite"
@ -82,6 +83,58 @@ func (s *AccountsTestSuite) TestAccountsList() {
s.False(!subAccount2MatchesKey1 && !subAccount2MatchesKey2, "subAcount2 not returned")
}
func (s *AccountsTestSuite) TestImportSingleExtendedKey() {
s.StartTestBackend()
defer s.StopTestBackend()
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
s.NoError(err)
s.NotNil(keyStore)
// create a master extended key
mn := extkeys.NewMnemonic()
mnemonic, err := mn.MnemonicPhrase(extkeys.EntropyStrength128, extkeys.EnglishLanguage)
s.NoError(err)
extKey, err := extkeys.NewMaster(mn.MnemonicSeed(mnemonic, ""))
s.NoError(err)
derivedExtendedKey, err := extKey.EthBIP44Child(0)
s.NoError(err)
// import single extended key
password := "test-password-1"
addr, _, err := s.Backend.AccountManager().ImportSingleExtendedKey(derivedExtendedKey, password)
s.NoError(err)
_, key, err := s.Backend.AccountManager().AddressToDecryptedAccount(addr, password)
s.NoError(err)
s.Equal(crypto.FromECDSA(key.PrivateKey), crypto.FromECDSA(key.ExtendedKey.ToECDSA()))
}
func (s *AccountsTestSuite) TestImportAccount() {
s.StartTestBackend()
defer s.StopTestBackend()
keyStore, err := s.Backend.StatusNode().AccountKeyStore()
s.NoError(err)
s.NotNil(keyStore)
// create a private key
privateKey, err := crypto.GenerateKey()
s.NoError(err)
// import as normal account
password := "test-password-2"
addr, err := s.Backend.AccountManager().ImportAccount(privateKey, password)
s.Require().NoError(err)
_, key, err := s.Backend.AccountManager().AddressToDecryptedAccount(addr.String(), password)
s.NoError(err)
s.Equal(crypto.FromECDSA(privateKey), crypto.FromECDSA(key.PrivateKey))
s.True(key.ExtendedKey.IsZeroed())
}
func (s *AccountsTestSuite) TestCreateChildAccount() {
s.StartTestBackend()
defer s.StopTestBackend()

View file

@ -47,8 +47,12 @@ type Key struct {
// we only store privkey as pubkey/address can be derived from it
// privkey in this struct is always in plaintext
PrivateKey *ecdsa.PrivateKey
// extended key is the root node for new hardened children i.e. sub-accounts
// ExtendedKey is the extended key of the PrivateKey itself, and it's used
// to derive child keys.
ExtendedKey *extkeys.ExtendedKey
// SubAccountIndex is DEPRECATED
// It was use in Status to keep track of the number of sub-account created
// before having multi-account support.
// next index to be used for sub-account child derivation
SubAccountIndex uint32
}

View file

@ -38,6 +38,7 @@ import (
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/event"
"github.com/pborman/uuid"
"github.com/status-im/status-go/extkeys"
)
@ -459,14 +460,49 @@ func (ks *KeyStore) ImportECDSA(priv *ecdsa.PrivateKey, passphrase string) (acco
return ks.importKey(key, passphrase)
}
// ImportSingleExtendedKey imports an extended key setting it in both the PrivateKey and ExtendedKey fields
// of the Key struct.
// ImportExtendedKey is used in older version of Status where PrivateKey is set to be the BIP44 key at index 0,
// and ExtendedKey is the extended key of the BIP44 key at index 1.
func (ks *KeyStore) ImportSingleExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
privateKeyECDSA := extKey.ToECDSA()
id := uuid.NewRandom()
key := &Key{
Id: id,
Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey),
PrivateKey: privateKeyECDSA,
ExtendedKey: extKey,
}
// if account is already imported, return cached version
if ks.cache.hasAddress(key.Address) {
a := accounts.Account{
Address: key.Address,
}
ks.cache.maybeReload()
ks.cache.mu.Lock()
a, err := ks.cache.find(a)
ks.cache.mu.Unlock()
if err != nil {
zeroKey(key.PrivateKey)
return a, err
}
return a, nil
}
return ks.importKey(key, passphrase)
}
// 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.
// Deprecated: status-go is now using ImportSingleExtendedKey
func (ks *KeyStore) ImportExtendedKey(extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
return ks.ImportExtendedKeyForPurpose(extkeys.KeyPurposeWallet, extKey, passphrase)
}
// ImportExtendedKeyForPurpose 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.
// Deprecated: status-go is now using ImportSingleExtendedKey
func (ks *KeyStore) ImportExtendedKeyForPurpose(keyPurpose extkeys.KeyPurpose, extKey *extkeys.ExtendedKey, passphrase string) (accounts.Account, error) {
key, err := newKeyForPurposeFromExtendedKey(keyPurpose, extKey)
if err != nil {

View file

@ -198,6 +198,9 @@ func (s *dialstate) newTasks(nRunning int, peers map[enode.ID]*Peer, now time.Ti
case errNotWhitelisted, errSelf:
log.Warn("Removing static dial candidate", "id", t.dest.ID, "addr", &net.TCPAddr{IP: t.dest.IP(), Port: t.dest.TCP()}, "err", err)
delete(s.static, t.dest.ID())
case errRecentlyDialed:
expiry := s.hist.expiry(t.dest.ID())
log.Debug("peer was recently dialed", "enode", t.dest.String(), "expires at", expiry, "after", expiry.Sub(time.Now()))
case nil:
s.dialing[id] = t.flags
newtasks = append(newtasks, t)
@ -411,6 +414,16 @@ func (h dialHistory) contains(id enode.ID) bool {
}
return false
}
func (h dialHistory) expiry(id enode.ID) time.Time {
for _, v := range h {
if v.id == id {
return v.exp
}
}
return time.Time{}
}
func (h *dialHistory) expire(now time.Time) {
for h.Len() > 0 && h.min().exp.Before(now) {
heap.Pop(h)

View file

@ -693,6 +693,7 @@ running:
// ephemeral static peer list. Add it to the dialer,
// it will keep the node connected.
srv.log.Trace("Adding static node", "node", n)
dialstate.removeStatic(n)
dialstate.addStatic(n)
case n := <-srv.removestatic:
// This channel is used by RemovePeer to send a

2
vendor/modules.txt vendored
View file

@ -24,7 +24,7 @@ github.com/davecgh/go-spew/spew
github.com/deckarep/golang-set
# github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712
github.com/edsrzf/mmap-go
# github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.3
# github.com/ethereum/go-ethereum v1.8.27 => github.com/status-im/go-ethereum v1.8.27-status.5
github.com/ethereum/go-ethereum/accounts
github.com/ethereum/go-ethereum/accounts/keystore
github.com/ethereum/go-ethereum/common