From dd178603025cf69b895b772597a98db6dd9f0143 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Thu, 27 Jun 2019 00:28:16 +0200 Subject: [PATCH] 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 --- VERSION | 2 +- account/accounts.go | 56 +++++++++++++++- account/accounts_test.go | 40 ++++++++++++ account/onboarding.go | 128 +++++++++++++++++++++++++++++++++++++ account/onboarding_test.go | 55 ++++++++++++++++ api/backend.go | 4 ++ extkeys/mnemonic.go | 6 +- extkeys/mnemonic_test.go | 2 +- lib/library.go | 66 +++++++++++++++++++ lib/types.go | 11 ++++ mobile/status.go | 63 ++++++++++++++++++ mobile/types.go | 11 ++++ 12 files changed, 438 insertions(+), 6 deletions(-) create mode 100644 account/onboarding.go create mode 100644 account/onboarding_test.go diff --git a/VERSION b/VERSION index 9b8c71ab8..f735712f8 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.29.0-beta.0 +0.29.0-beta.1 diff --git a/account/accounts.go b/account/accounts.go index 465f646cf..b31a21bee 100644 --- a/account/accounts.go +++ b/account/accounts.go @@ -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. @@ -38,7 +40,10 @@ type GethServiceProvider interface { type Manager struct { geth GethServiceProvider - mu sync.RWMutex + 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 { diff --git a/account/accounts_test.go b/account/accounts_test.go index 236c37515..76b8a4eb6 100644 --- a/account/accounts_test.go +++ b/account/accounts_test.go @@ -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 diff --git a/account/onboarding.go b/account/onboarding.go new file mode 100644 index 000000000..e07a6393a --- /dev/null +++ b/account/onboarding.go @@ -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 +} diff --git a/account/onboarding_test.go b/account/onboarding_test.go new file mode 100644 index 000000000..eaad0c077 --- /dev/null +++ b/account/onboarding_test.go @@ -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) + } +} diff --git a/api/backend.go b/api/backend.go index 07a1b84c1..e77d0b0e4 100644 --- a/api/backend.go +++ b/api/backend.go @@ -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 diff --git a/extkeys/mnemonic.go b/extkeys/mnemonic.go index 7578e228e..e44c2aa88 100644 --- a/extkeys/mnemonic.go +++ b/extkeys/mnemonic.go @@ -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 diff --git a/extkeys/mnemonic_test.go b/extkeys/mnemonic_test.go index 13eb7655d..ee71ad063 100644 --- a/extkeys/mnemonic_test.go +++ b/extkeys/mnemonic_test.go @@ -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 { diff --git a/lib/library.go b/lib/library.go index 568b228b8..cd53a9b9e 100644 --- a/lib/library.go +++ b/lib/library.go @@ -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 { diff --git a/lib/types.go b/lib/types.go index 932ac6750..3392ab2fd 100644 --- a/lib/types.go +++ b/lib/types.go @@ -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"` diff --git a/mobile/status.go b/mobile/status.go index 1941793a3..074f711e7 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -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) diff --git a/mobile/types.go b/mobile/types.go index 6504f0d0b..597bd29b3 100644 --- a/mobile/types.go +++ b/mobile/types.go @@ -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"`