From 2b96aa54563e54ffd4403e09b110d6da4d611341 Mon Sep 17 00:00:00 2001 From: Andrea Franz Date: Fri, 26 Jul 2019 11:33:38 +0200 Subject: [PATCH] add multi-account LoadAccount, ImportMnemonic, and Reset functions (#1542) * add multi-account LoadAccount and Reset functions * add MultiAccountImportMnemonic * rename StoreDerived to StoreDerivedAccounts * add docs --- account/generator/README.md | 43 +++++++++ lib/library_test_multiaccount.go | 160 +++++++++++++++++++++++++++---- lib/library_test_utils.go | 8 +- lib/multiaccount.go | 59 +++++++++++- mobile/multiaccount.go | 70 +++++++++++++- 5 files changed, 310 insertions(+), 30 deletions(-) create mode 100644 account/generator/README.md diff --git a/account/generator/README.md b/account/generator/README.md new file mode 100644 index 000000000..eee765985 --- /dev/null +++ b/account/generator/README.md @@ -0,0 +1,43 @@ +# Account Generator + +The Account Generator is used to generate, import, derive child keys, and store accounts. +It is instantiated in the `account.Manager` struct and it's accessible from the `lib` and `mobile` +package through functions with the `MultiAccount` prefix: + +* MultiAccountGenerate +* MultiAccountGenerateAndDeriveAddresses +* MultiAccountImportMnemonic +* MultiAccountDeriveAddresses +* MultiAccountStoreDerivedAccounts +* MultiAccountImportPrivateKey +* MultiAccountStoreAccount +* MultiAccountLoadAccount +* MultiAccountReset + + +Using `Generate` and `ImportMnemonic`, a master key is loaded in memory and a random temporarily id is returned. +Bare in mind these accounts are not saved. They are in memory until `StoreAccount` or `StoreDerivedAccounts` are called. +Calling `Reset` or restarting the application will remove everything from memory. +Logging-in and Logging-out will do the same. + +Since `Generate` and `ImportMnemonic` create extended keys, we can use those keys to derive new child keys. +`MultiAccountDeriveAddresses(id, paths)` returns a list of addresses/pubKey, one for each path. +This can be used to check balances on those addresses and show them to the user. + +Once the user is happy with some specific derivation paths, we can store them using `StoreDerivedAccounts(id, passwordToEncryptKey, paths)`. +`StoreDerivedAccounts` returns an address/pubKey for each path. The address can be use in the future to load them in memory again. +Calling `StoreDerivedAccounts` will encrypt and store the keys, each one in a keystore json file, and remove all the keys from memory. +Since they are derived from an extended key, they are extended keys too, so they can be used in the future to derive more child keys. +`StoreAccount` stores the key identified by its ID, so in case the key comes from `Generate` or `ImportPrivateKey`, it will store the master key. +In general we want to avoid saving master keys, so we should only use `StoreDerivedAccounts` for extended keys, and `StoreAccount` for normal keys. + +Calling `Load(address, password)` will unlock the key specified by addresses using password, and load it in memory. +`Load` returns a new id that can be used again with DeriveAddresses, `StoreAccount`, and `StoreDerivedAccounts`. + +`ImportPrivateKey` imports a raw private key specified by its hex form. +It's not an extended key, so it can't be used to derive child addresses. +You can call `DeriveAddresses` to derive the address/pubKey of a normal key passing an empty string as derivation path. +`StoreAccount` will save the key without deriving a child key. + + + diff --git a/lib/library_test_multiaccount.go b/lib/library_test_multiaccount.go index 63f630331..c216aa5f4 100644 --- a/lib/library_test_multiaccount.go +++ b/lib/library_test_multiaccount.go @@ -17,9 +17,27 @@ import ( "github.com/status-im/status-go/account/generator" ) +func checkMultiAccountErrorResponse(t *testing.T, respJSON *C.char, expectedError string) { + var e struct { + Error *string `json:"error,omitempty"` + } + + if err := json.Unmarshal([]byte(C.GoString(respJSON)), &e); err != nil { + t.Fatalf("error unmarshaling error response") + } + + if e.Error == nil { + t.Fatalf("unexpected empty error. expected %s, got nil", expectedError) + } + + if *e.Error != expectedError { + t.Fatalf("unexpected error. expected %s, got %+v", expectedError, *e.Error) + } +} + func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{}) { var e struct { - Error *string `json:"error"` + Error *string `json:"error,omitempty"` } json.Unmarshal([]byte(C.GoString(respJSON)), &e) @@ -32,7 +50,7 @@ func checkMultiAccountResponse(t *testing.T, respJSON *C.char, resp interface{}) } } -func testMultiAccountGenerateDeriveAndStore(t *testing.T) bool { //nolint: gocyclo +func testMultiAccountGenerateDeriveStoreLoadReset(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) @@ -68,21 +86,91 @@ func testMultiAccountGenerateDeriveAndStore(t *testing.T) bool { //nolint: gocyc return false } - if ok := testMultiAccountDeriveAddresses(t, info.ID, paths); !ok { + if _, ok := testMultiAccountDeriveAddresses(t, info.ID, paths, false); !ok { return false } } + password := "multi-account-test-password" + // 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 { + addresses, ok := testMultiAccountStoreDerived(t, generateResp[0].ID, password, paths) + if !ok { + return false + } + + loadedIDs := make([]string, 0) + + // unlock and load all stored accounts. + for _, address := range addresses { + loadedID, ok := testMultiAccountLoadAccount(t, address, password) + if !ok { + return false + } + + loadedIDs = append(loadedIDs, loadedID) + + if _, ok := testMultiAccountDeriveAddresses(t, loadedID, paths, false); !ok { + return false + } + } + + rawResp = MultiAccountReset() + + // try again deriving addresses. + // it should fail because reset should remove all the accounts from memory. + for _, loadedID := range loadedIDs { + if _, ok := testMultiAccountDeriveAddresses(t, loadedID, paths, true); !ok { + t.Errorf("account is still in memory, expected Reset to remove all accounts") + return false + } + } + + return true +} + +func testMultiAccountImportMnemonicAndDerive(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) + } + + mnemonicPhrase := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + bip39Passphrase := "TREZOR" + params := mobile.MultiAccountImportMnemonicParams{ + MnemonicPhrase: mnemonicPhrase, + Bip39Passphrase: bip39Passphrase, + } + + paramsJSON, err := json.Marshal(¶ms) + if err != nil { + t.Errorf("error encoding MultiAccountImportMnemonicParams") + return false + } + + // import mnemonic + rawResp := MultiAccountImportMnemonic(C.CString(string(paramsJSON))) + var importResp generator.IdentifiedAccountInfo + // check the response doesn't have errors + checkMultiAccountResponse(t, rawResp, &importResp) + + bip44DerivationPath := "m/44'/60'/0'/0/0" + expectedBip44Address := "0x9c32F71D4DB8Fb9e1A58B0a80dF79935e7256FA6" + addresses, ok := testMultiAccountDeriveAddresses(t, importResp.ID, []string{bip44DerivationPath}, false) + if !ok { + return false + } + + if addresses[bip44DerivationPath] != expectedBip44Address { + t.Errorf("unexpected address; expected %s, got %s", expectedBip44Address, addresses[bip44DerivationPath]) return false } return true } -func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo +func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []string, expectAccountNotFoundError bool) (map[string]string, bool) { //nolint: gocyclo params := mobile.MultiAccountDeriveAddressesParams{ AccountID: accountID, Paths: paths, @@ -91,34 +179,44 @@ func testMultiAccountDeriveAddresses(t *testing.T, accountID string, paths []str paramsJSON, err := json.Marshal(¶ms) if err != nil { t.Errorf("error encoding MultiAccountDeriveAddressesParams") - return false + return nil, false } // derive addresses from account accountID rawResp := MultiAccountDeriveAddresses(C.CString(string(paramsJSON))) + + if expectAccountNotFoundError { + checkMultiAccountErrorResponse(t, rawResp, "account not found") + return nil, true + } + 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 + if len(deriveResp) != len(paths) { + t.Errorf("expected %d derived accounts info, got %d", len(paths), len(deriveResp)) + return nil, false } + addresses := make(map[string]string) + // check that we have an address for each derivation path we used. for _, path := range paths { - if _, ok := deriveResp[path]; !ok { + info, ok := deriveResp[path] + if !ok { t.Errorf("results doesn't contain account info for path %s", path) - return false + return nil, false } + + addresses[path] = info.Address } - return true + return addresses, true } -func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string) bool { //nolint: gocyclo - password := "test-multiaccount-password" +func testMultiAccountStoreDerived(t *testing.T, accountID string, password string, paths []string) ([]string, bool) { //nolint: gocyclo - params := mobile.MultiAccountStoreDerivedParams{ + params := mobile.MultiAccountStoreDerivedAccountsParams{ MultiAccountDeriveAddressesParams: mobile.MultiAccountDeriveAddressesParams{ AccountID: accountID, Paths: paths, @@ -129,11 +227,11 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string paramsJSON, err := json.Marshal(params) if err != nil { t.Errorf("error encoding MultiAccountStoreDerivedParams") - return false + return nil, false } // store one child account for each derivation path. - rawResp := MultiAccountStoreDerived(C.CString(string(paramsJSON))) + rawResp := MultiAccountStoreDerivedAccounts(C.CString(string(paramsJSON))) var storeResp map[string]generator.AccountInfo // check that we don't have errors in the response @@ -145,7 +243,7 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string if len(addresses) != 2 { t.Errorf("expected 2 addresses, got %d", len(addresses)) - return false + return nil, false } // for each stored account, check that we can decrypt it with the password we used. @@ -154,10 +252,11 @@ func testMultiAccountStoreDerived(t *testing.T, accountID string, paths []string _, err = statusBackend.AccountManager().VerifyAccountPassword(dir, address, password) if err != nil { t.Errorf("failed to verify password on stored derived account") + return nil, false } } - return true + return addresses, true } func testMultiAccountGenerateAndDerive(t *testing.T) bool { //nolint: gocyclo @@ -259,3 +358,26 @@ func testMultiAccountImportStore(t *testing.T) bool { //nolint: gocyclo return true } + +func testMultiAccountLoadAccount(t *testing.T, address string, password string) (string, bool) { //nolint: gocyclo + t.Log("loading account") + params := mobile.MultiAccountLoadAccountParams{ + Address: address, + Password: password, + } + + paramsJSON, err := json.Marshal(params) + if err != nil { + t.Errorf("error encoding MultiAccountLoadAccountParams") + return "", false + } + + // load the account in memory + rawResp := MultiAccountLoadAccount(C.CString(string(paramsJSON))) + var loadResp generator.IdentifiedAccountInfo + + // check that we don't have errors in the response + checkMultiAccountResponse(t, rawResp, &loadResp) + + return loadResp.ID, true +} diff --git a/lib/library_test_utils.go b/lib/library_test_utils.go index b3f119bc9..de56f9935 100644 --- a/lib/library_test_utils.go +++ b/lib/library_test_utils.go @@ -147,8 +147,12 @@ func testExportedAPI(t *testing.T, done chan struct{}) { testFailedTransaction, }, { - "MultiAccount - Generate/Derive/StoreDerived", - testMultiAccountGenerateDeriveAndStore, + "MultiAccount - Generate/Derive/StoreDerived/Load/Reset", + testMultiAccountGenerateDeriveStoreLoadReset, + }, + { + "MultiAccount - ImportMnemonic/Derive", + testMultiAccountImportMnemonicAndDerive, }, { "MultiAccount - GenerateAndDerive", diff --git a/lib/multiaccount.go b/lib/multiaccount.go index 440d81212..85ecf700f 100644 --- a/lib/multiaccount.go +++ b/lib/multiaccount.go @@ -74,10 +74,10 @@ func MultiAccountDeriveAddresses(paramsJSON *C.char) *C.char { 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 +// MultiAccountStoreDerivedAccounts derive accounts from the specified key and store them encrypted with the specified password. +//export MultiAccountStoreDerivedAccounts +func MultiAccountStoreDerivedAccounts(paramsJSON *C.char) *C.char { + var p mobile.MultiAccountStoreDerivedAccountsParams if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil { return makeJSONResponse(err) @@ -118,6 +118,28 @@ func MultiAccountImportPrivateKey(paramsJSON *C.char) *C.char { return C.CString(string(out)) } +// MultiAccountImportMnemonic imports an account derived from the mnemonic phrase and the Bip39Passphrase storing it. +//export MultiAccountImportMnemonic +func MultiAccountImportMnemonic(paramsJSON *C.char) *C.char { + var p mobile.MultiAccountImportMnemonicParams + + if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil { + return makeJSONResponse(err) + } + + resp, err := statusBackend.AccountManager().AccountsGenerator().ImportMnemonic(p.MnemonicPhrase, p.Bip39Passphrase) + 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 { @@ -139,3 +161,32 @@ func MultiAccountStoreAccount(paramsJSON *C.char) *C.char { return C.CString(string(out)) } + +// MultiAccountLoadAccount loads in memory the account specified by address unlocking it with password. +//export MultiAccountLoadAccount +func MultiAccountLoadAccount(paramsJSON *C.char) *C.char { + var p mobile.MultiAccountLoadAccountParams + + if err := json.Unmarshal([]byte(C.GoString(paramsJSON)), &p); err != nil { + return makeJSONResponse(err) + } + + resp, err := statusBackend.AccountManager().AccountsGenerator().LoadAccount(p.Address, p.Password) + if err != nil { + return makeJSONResponse(err) + } + + out, err := json.Marshal(resp) + if err != nil { + return makeJSONResponse(err) + } + + return C.CString(string(out)) +} + +// MultiAccountReset remove all the multi-account keys from memory. +//export MultiAccountReset +func MultiAccountReset() *C.char { + statusBackend.AccountManager().AccountsGenerator().Reset() + return makeJSONResponse(nil) +} diff --git a/mobile/multiaccount.go b/mobile/multiaccount.go index ad340ddc2..efacf710a 100644 --- a/mobile/multiaccount.go +++ b/mobile/multiaccount.go @@ -23,8 +23,8 @@ type MultiAccountDeriveAddressesParams struct { Paths []string `json:"paths"` } -// MultiAccountStoreDerivedParams are the params sent to MultiAccountStoreDerived. -type MultiAccountStoreDerivedParams struct { +// MultiAccountStoreDerivedAccountsParams are the params sent to MultiAccountStoreDerivedAccounts. +type MultiAccountStoreDerivedAccountsParams struct { MultiAccountDeriveAddressesParams Password string `json:"password"` } @@ -40,6 +40,18 @@ type MultiAccountImportPrivateKeyParams struct { PrivateKey string `json:"privateKey"` } +// MultiAccountLoadAccountParams are the params sent to MultiAccountLoadAccount. +type MultiAccountLoadAccountParams struct { + Address string `json:"address"` + Password string `json:"password"` +} + +// MultiAccountImportMnemonicParams are the params sent to MultiAccountImportMnemonic. +type MultiAccountImportMnemonicParams struct { + MnemonicPhrase string `json:"mnemonicPhrase"` + Bip39Passphrase string `json:"Bip39Passphrase"` +} + // MultiAccountGenerate generates account in memory without storing them. func MultiAccountGenerate(paramsJSON string) string { var p MultiAccountGenerateParams @@ -103,9 +115,9 @@ func MultiAccountDeriveAddresses(paramsJSON string) string { 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 +// MultiAccountStoreDerivedAccounts derive accounts from the specified key and store them encrypted with the specified password. +func MultiAccountStoreDerivedAccounts(paramsJSON string) string { + var p MultiAccountStoreDerivedAccountsParams if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil { return makeJSONResponse(err) @@ -145,6 +157,27 @@ func MultiAccountImportPrivateKey(paramsJSON string) string { return string(out) } +// MultiAccountImportMnemonic imports an account derived from the mnemonic phrase and the Bip39Passphrase storing it. +func MultiAccountImportMnemonic(paramsJSON string) string { + var p MultiAccountImportMnemonicParams + + if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil { + return makeJSONResponse(err) + } + + resp, err := statusBackend.AccountManager().AccountsGenerator().ImportMnemonic(p.MnemonicPhrase, p.Bip39Passphrase) + 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 @@ -165,3 +198,30 @@ func MultiAccountStoreAccount(paramsJSON string) string { return string(out) } + +// MultiAccountLoadAccount loads in memory the account specified by address unlocking it with password. +func MultiAccountLoadAccount(paramsJSON string) string { + var p MultiAccountLoadAccountParams + + if err := json.Unmarshal([]byte(paramsJSON), &p); err != nil { + return makeJSONResponse(err) + } + + resp, err := statusBackend.AccountManager().AccountsGenerator().LoadAccount(p.Address, p.Password) + if err != nil { + return makeJSONResponse(err) + } + + out, err := json.Marshal(resp) + if err != nil { + return makeJSONResponse(err) + } + + return string(out) +} + +// MultiAccountReset remove all the multi-account keys from memory. +func MultiAccountReset() string { + statusBackend.AccountManager().AccountsGenerator().Reset() + return makeJSONResponse(nil) +}