create random accounts in memory for onboarding (#1464)

* add account onboarding struct

* add onboarding to account manager

* allow resetting onboarding

* add onboarding functions to lib and mobile

* fix lint warnings

* update mnemonic test

* remove unused fmt

* reset onboarding before selecting account

* expose ResetOnboaring to lib and mobile

* refactoring

* add comment

* update StartOnboarding function

* remove unused var

* update VERSION

* fix returned accounts slice
This commit is contained in:
Andrea Franz 2019-06-27 00:28:16 +02:00 committed by GitHub
parent e28d4ef1a3
commit dd17860302
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 438 additions and 6 deletions

View File

@ -1 +1 @@
0.29.0-beta.0
0.29.0-beta.1

View File

@ -26,6 +26,8 @@ var (
ErrAccountToKeyMappingFailure = errors.New("cannot retrieve a valid key for a given account")
ErrNoAccountSelected = errors.New("no account has been selected, please login")
ErrInvalidMasterKeyCreated = errors.New("can not create master extended key")
ErrOnboardingNotStarted = errors.New("onboarding must be started before choosing an account")
ErrOnboardingAccountNotFound = errors.New("cannot find onboarding account with the given id")
)
// GethServiceProvider provides required geth services.
@ -39,6 +41,9 @@ type Manager struct {
geth GethServiceProvider
mu sync.RWMutex
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()
}
@ -365,6 +370,55 @@ func (m *Manager) Accounts() ([]gethcommon.Address, error) {
return filtered, nil
}
// StartOnboarding starts the onboarding process generating accountsCount accounts and returns a slice of OnboardingAccount.
func (m *Manager) StartOnboarding(accountsCount, mnemonicPhraseLength int) ([]*OnboardingAccount, error) {
m.mu.Lock()
defer m.mu.Unlock()
onboarding, err := NewOnboarding(accountsCount, mnemonicPhraseLength)
if err != nil {
return nil, err
}
m.onboarding = onboarding
return m.onboarding.Accounts(), nil
}
// RemoveOnboarding reset the current onboarding struct setting it to nil and deleting the accounts from memory.
func (m *Manager) RemoveOnboarding() {
m.mu.Lock()
defer m.mu.Unlock()
m.onboarding = nil
}
// ImportOnboardingAccount imports the account specified by id and encrypts it with password.
func (m *Manager) ImportOnboardingAccount(id string, password string) (Info, string, error) {
var info Info
m.mu.Lock()
defer m.mu.Unlock()
if m.onboarding == nil {
return info, "", ErrOnboardingNotStarted
}
acc, err := m.onboarding.Account(id)
if err != nil {
return info, "", err
}
info, err = m.RecoverAccount(password, acc.mnemonic)
if err != nil {
return info, "", err
}
m.onboarding = nil
return info, acc.mnemonic, nil
}
// refreshSelectedWalletAccount re-populates list of sub-accounts of the currently selected account (if any)
func (m *Manager) refreshSelectedWalletAccount() {
if m.selectedWalletAccount == nil {

View File

@ -224,6 +224,46 @@ func (s *ManagerTestSuite) TestRecoverAccount() {
s.Equal(errKeyStore, err)
}
func (s *ManagerTestSuite) TestOnboarding() {
// try to choose an account before starting onboarding
_, _, err := s.accManager.ImportOnboardingAccount("test-id", "test-password")
s.Equal(ErrOnboardingNotStarted, err)
// generates 5 random accounts
count := 5
accounts, err := s.accManager.StartOnboarding(count, 24)
s.Require().NoError(err)
s.Equal(count, len(accounts))
// try to choose an account with an undefined id
_, _, err = s.accManager.ImportOnboardingAccount("test-id", "test-password")
s.Equal(ErrOnboardingAccountNotFound, err)
// choose one account and encrypt it with password
password := "test-onboarding-account"
account := accounts[0]
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
info, mnemonic, err := s.accManager.ImportOnboardingAccount(account.ID, password)
s.Require().NoError(err)
s.Equal(account.Info, info)
s.Equal(account.mnemonic, mnemonic)
s.Nil(s.accManager.onboarding)
// try to decrypt it with password to check if it's been imported correctly
s.gethServiceProvider.EXPECT().AccountKeyStore().Return(s.keyStore, nil)
decAccount, _, err := s.accManager.AddressToDecryptedAccount(info.WalletAddress, password)
s.Require().NoError(err)
s.Equal(info.WalletAddress, decAccount.Address.Hex())
// try resetting onboarding
_, err = s.accManager.StartOnboarding(count, 24)
s.Require().NoError(err)
s.NotNil(s.accManager.onboarding)
s.accManager.RemoveOnboarding()
s.Nil(s.accManager.onboarding)
}
func (s *ManagerTestSuite) TestSelectAccount() {
testCases := []struct {
name string

128
account/onboarding.go Normal file
View File

@ -0,0 +1,128 @@
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/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"`
mnemonic string
Info Info `json:"info"`
}
// Onboarding is a struct contains a slice of OnboardingAccount.
type Onboarding struct {
accounts map[string]*OnboardingAccount
}
// NewOnboarding returns a new onboarding struct generating n accounts.
func NewOnboarding(n, mnemonicPhraseLength int) (*Onboarding, error) {
onboarding := &Onboarding{
accounts: make(map[string]*OnboardingAccount),
}
for i := 0; i < n; i++ {
account, err := onboarding.generateAccount(mnemonicPhraseLength)
if err != nil {
return nil, err
}
onboarding.accounts[account.ID] = account
}
return onboarding, nil
}
// Accounts return the list of OnboardingAccount generated.
func (o *Onboarding) Accounts() []*OnboardingAccount {
accounts := make([]*OnboardingAccount, 0)
for _, a := range o.accounts {
accounts = append(accounts, a)
}
return accounts
}
// Account returns an OnboardingAccount by id.
func (o *Onboarding) Account(id string) (*OnboardingAccount, error) {
account, ok := o.accounts[id]
if !ok {
return nil, ErrOnboardingAccountNotFound
}
return account, nil
}
func (o *Onboarding) generateAccount(mnemonicPhraseLength int) (*OnboardingAccount, error) {
entropyStrength, err := mnemonicPhraseLengthToEntropyStrenght(mnemonicPhraseLength)
if err != nil {
return nil, err
}
mnemonic := extkeys.NewMnemonic()
mnemonicPhrase, err := mnemonic.MnemonicPhrase(entropyStrength, extkeys.EnglishLanguage)
if err != nil {
return nil, fmt.Errorf("can not create mnemonic seed: %v", err)
}
masterExtendedKey, err := extkeys.NewMaster(mnemonic.MnemonicSeed(mnemonicPhrase, ""))
if err != nil {
return nil, fmt.Errorf("can not create master extended key: %v", err)
}
walletAddress, walletPubKey, err := o.deriveAccount(masterExtendedKey, extkeys.KeyPurposeWallet, 0)
if err != nil {
return nil, err
}
info := Info{
WalletAddress: walletAddress,
WalletPubKey: walletPubKey,
ChatAddress: walletAddress,
ChatPubKey: walletPubKey,
}
uuid := uuid.NewRandom().String()
account := &OnboardingAccount{
ID: uuid,
mnemonic: mnemonicPhrase,
Info: info,
}
return account, nil
}
func (o *Onboarding) deriveAccount(masterExtendedKey *extkeys.ExtendedKey, purpose extkeys.KeyPurpose, index uint32) (string, string, error) {
extendedKey, err := masterExtendedKey.ChildForPurpose(purpose, index)
if err != nil {
return "", "", err
}
privateKeyECDSA := extendedKey.ToECDSA()
address := crypto.PubkeyToAddress(privateKeyECDSA.PublicKey)
publicKeyHex := hexutil.Encode(crypto.FromECDSAPub(&privateKeyECDSA.PublicKey))
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

@ -0,0 +1,55 @@
package account
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
o, _ := NewOnboarding(count, wordsCount)
assert.Equal(t, count, len(o.accounts))
for id, a := range o.accounts {
words := strings.Split(a.mnemonic, " ")
assert.Equal(t, wordsCount, len(words))
assert.NotEmpty(t, a.Info.WalletAddress)
assert.NotEmpty(t, a.Info.WalletPubKey)
assert.NotEmpty(t, a.Info.ChatAddress)
assert.NotEmpty(t, a.Info.ChatPubKey)
retrieved, err := o.Account(id)
require.NoError(t, err)
assert.Equal(t, a, retrieved)
}
}

View File

@ -493,6 +493,8 @@ func (b *StatusBackend) Logout() error {
// reSelectAccount selects previously selected account, often, after node restart.
func (b *StatusBackend) reSelectAccount() error {
b.AccountManager().RemoveOnboarding()
selectedChatAccount, err := b.AccountManager().SelectedChatAccount()
if selectedChatAccount == nil || err == account.ErrNoAccountSelected {
return nil
@ -518,6 +520,8 @@ func (b *StatusBackend) SelectAccount(walletAddress, chatAddress, password strin
b.mu.Lock()
defer b.mu.Unlock()
b.AccountManager().RemoveOnboarding()
err := b.accountManager.SelectAccount(walletAddress, chatAddress, password)
if err != nil {
return err

View File

@ -40,11 +40,11 @@ const (
totalAvailableLanguages
)
type entropyStrength int
type EntropyStrength int
// Valid entropy strengths
const (
EntropyStrength128 entropyStrength = 128 + 32*iota
EntropyStrength128 EntropyStrength = 128 + 32*iota
EntropyStrength160
EntropyStrength192
EntropyStrength224
@ -129,7 +129,7 @@ func (m *Mnemonic) MnemonicSeed(mnemonic string, password string) []byte {
}
// MnemonicPhrase returns a human readable seed for BIP32 Hierarchical Deterministic Wallets
func (m *Mnemonic) MnemonicPhrase(strength entropyStrength, language Language) (string, error) {
func (m *Mnemonic) MnemonicPhrase(strength EntropyStrength, language Language) (string, error) {
wordList, err := m.WordList(language)
if err != nil {
return "", err

View File

@ -45,7 +45,7 @@ func TestMnemonicPhrase(t *testing.T) {
mnemonic := NewMnemonic()
// test strength validation
strengths := []entropyStrength{127, 129, 257}
strengths := []EntropyStrength{127, 129, 257}
for _, s := range strengths {
_, err := mnemonic.MnemonicPhrase(s, EnglishLanguage)
if err != ErrInvalidEntropyStrength {

View File

@ -234,6 +234,72 @@ func RecoverAccount(password, mnemonic *C.char) *C.char {
return C.CString(string(outBytes))
}
// StartOnboarding initialize the onboarding with n random accounts
//export StartOnboarding
func StartOnboarding(n, mnemonicPhraseLength C.int) *C.char {
out := struct {
Accounts []OnboardingAccount `json:"accounts"`
Error string `json:"error"`
}{
Accounts: make([]OnboardingAccount, 0),
}
accounts, err := statusBackend.AccountManager().StartOnboarding(int(n), int(mnemonicPhraseLength))
if err != nil {
fmt.Fprintln(os.Stderr, err)
out.Error = err.Error()
}
if err == nil {
for _, account := range accounts {
out.Accounts = append(out.Accounts, OnboardingAccount{
ID: account.ID,
Address: account.Info.WalletAddress,
PubKey: account.Info.WalletPubKey,
WalletAddress: account.Info.WalletAddress,
WalletPubKey: account.Info.WalletPubKey,
ChatAddress: account.Info.ChatAddress,
ChatPubKey: account.Info.ChatPubKey,
})
}
}
outBytes, _ := json.Marshal(out)
return C.CString(string(outBytes))
}
// ImportOnboardingAccount re-creates and imports an account created during onboarding.
//export ImportOnboardingAccount
func ImportOnboardingAccount(id, password *C.char) *C.char {
info, mnemonic, err := statusBackend.AccountManager().ImportOnboardingAccount(C.GoString(id), C.GoString(password))
errString := ""
if err != nil {
fmt.Fprintln(os.Stderr, err)
errString = err.Error()
}
out := AccountInfo{
Address: info.WalletAddress,
PubKey: info.WalletPubKey,
WalletAddress: info.WalletAddress,
WalletPubKey: info.WalletPubKey,
ChatAddress: info.ChatAddress,
ChatPubKey: info.ChatPubKey,
Mnemonic: mnemonic,
Error: errString,
}
outBytes, _ := json.Marshal(out)
return C.CString(string(outBytes))
}
// RemoveOnboarding resets the current onboarding removing from memory all the generated keys.
//export RemoveOnboarding
func RemoveOnboarding() {
statusBackend.AccountManager().RemoveOnboarding()
}
//VerifyAccountPassword verifies account password
//export VerifyAccountPassword
func VerifyAccountPassword(keyStoreDir, address, password *C.char) *C.char {

View File

@ -74,6 +74,17 @@ type AccountInfo struct {
Error string `json:"error"`
}
// OnboardingAccount represents accounts info generated for the onboarding.
type OnboardingAccount struct {
ID string `json:"id"`
Address string `json:"address"` // DEPRECATED
PubKey string `json:"pubkey"` // DEPRECATED
WalletAddress string `json:"walletAddress"`
WalletPubKey string `json:"walletPubKey"`
ChatAddress string `json:"chatAddress"`
ChatPubKey string `json:"chatPubKey"`
}
// SendDataNotificationResult is a JSON returned from notify message.
type SendDataNotificationResult struct {
Status bool `json:"status"`

View File

@ -238,6 +238,69 @@ func RecoverAccount(password, mnemonic string) string {
return string(outBytes)
}
// StartOnboarding initialize the onboarding with n random accounts
func StartOnboarding(n, mnemonicPhraseLength int) string {
out := struct {
Accounts []OnboardingAccount `json:"accounts"`
Error string `json:"error"`
}{
Accounts: make([]OnboardingAccount, 0),
}
accounts, err := statusBackend.AccountManager().StartOnboarding(n, mnemonicPhraseLength)
if err != nil {
fmt.Fprintln(os.Stderr, err)
out.Error = err.Error()
}
if err == nil {
for _, account := range accounts {
out.Accounts = append(out.Accounts, OnboardingAccount{
ID: account.ID,
Address: account.Info.WalletAddress,
PubKey: account.Info.WalletPubKey,
WalletAddress: account.Info.WalletAddress,
WalletPubKey: account.Info.WalletPubKey,
ChatAddress: account.Info.ChatAddress,
ChatPubKey: account.Info.ChatPubKey,
})
}
}
outBytes, _ := json.Marshal(out)
return string(outBytes)
}
//ImportOnboardingAccount re-creates and imports an account created during onboarding.
func ImportOnboardingAccount(id, password string) string {
info, mnemonic, err := statusBackend.AccountManager().ImportOnboardingAccount(id, password)
errString := ""
if err != nil {
fmt.Fprintln(os.Stderr, err)
errString = err.Error()
}
out := AccountInfo{
Address: info.WalletAddress,
PubKey: info.WalletPubKey,
WalletAddress: info.WalletAddress,
WalletPubKey: info.WalletPubKey,
ChatAddress: info.ChatAddress,
ChatPubKey: info.ChatPubKey,
Mnemonic: mnemonic,
Error: errString,
}
outBytes, _ := json.Marshal(out)
return string(outBytes)
}
// RemoveOnboarding resets the current onboarding removing from memory all the generated keys.
func RemoveOnboarding() {
statusBackend.AccountManager().RemoveOnboarding()
}
// VerifyAccountPassword verifies account password.
func VerifyAccountPassword(keyStoreDir, address, password string) string {
_, err := statusBackend.AccountManager().VerifyAccountPassword(keyStoreDir, address, password)

View File

@ -74,6 +74,17 @@ type AccountInfo struct {
Error string `json:"error"`
}
// OnboardingAccount represents accounts info generated for the onboarding.
type OnboardingAccount struct {
ID string `json:"id"`
Address string `json:"address"` // DEPRECATED
PubKey string `json:"pubkey"` // DEPRECATED
WalletAddress string `json:"walletAddress"`
WalletPubKey string `json:"walletPubKey"`
ChatAddress string `json:"chatAddress"`
ChatPubKey string `json:"chatPubKey"`
}
// NotifyResult is a JSON returned from notify message.
type NotifyResult struct {
Status bool `json:"status"`