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:
parent
dcb0fa5262
commit
2b96aa5456
|
@ -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.
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
}
|
||||
|
||||
return true
|
||||
addresses[path] = info.Address
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue