add multi-account LoadAccount, ImportMnemonic, and Reset functions (#1542)

* add multi-account LoadAccount and Reset functions

* add MultiAccountImportMnemonic

* rename StoreDerived to StoreDerivedAccounts

* add docs
This commit is contained in:
Andrea Franz 2019-07-26 11:33:38 +02:00 committed by GitHub
parent dcb0fa5262
commit 2b96aa5456
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 310 additions and 30 deletions

View File

@ -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.

View File

@ -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(&params)
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(&params)
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
}

View File

@ -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",

View File

@ -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)
}

View File

@ -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)
}