From 49eaabaca5100368c5b39fb8c107aad2535371e5 Mon Sep 17 00:00:00 2001 From: Igor Sirotin Date: Wed, 26 Jun 2024 13:14:27 +0200 Subject: [PATCH] feat: adapt create/restore/login endpoints for keycard usage (#5311) * chore_: remove duplicated `StartNodeWithKey` * feat(KeycardPairing)_: added GetPairings method * chore_: simplify startNode... methods * chore_: added encryption path to be derived * fix_: error handling in StartNodeWithKey * feat_: added keycard properties to CreateAccount * feat_: moved KeycardWhisperPrivateKey to LoginAccount * fix_: LoginAccount during local pairing * feat_: added chat key handling to loginAccount * chore_: struct response from generateOrImportAccount * fix_: do not store keycard account to keystore * feat_: added Mnemonic parameter to LoginAccount * chore_: wrap loginAccount errors * feat_: RestoreKeycardAccountAndLogin endpoint * chore_: merge RestoreKeycardAccountRequest into RestoreAccountRequest * fix_: TestRestoreKeycardAccountAndLogin * fix_: MessengerRawMessageResendTest * chore_: cleanup * chore_: cleanup according to pr comments * chore_: better doc for Login.Mnemonic * chore_: add/fix comments * fix_: lint --- api/backend.go | 6 +- api/backend_test.go | 172 +++++++- api/defaults.go | 14 +- api/geth_backend.go | 523 +++++++++++++++-------- api/messenger_raw_message_resend_test.go | 12 +- cmd/ping-community/main.go | 2 +- cmd/populate-db/main.go | 2 +- cmd/spiff-workflow/main.go | 4 +- eth-node/types/key.go | 2 +- mobile/status.go | 17 +- params/config.go | 3 + protocol/requests/create_account.go | 3 + protocol/requests/login.go | 45 +- protocol/requests/restore_account.go | 34 +- server/pairing/raw_message_handler.go | 26 +- server/pairing/sync_device_test.go | 2 +- services/wallet/keycard_pairings.go | 25 ++ 17 files changed, 674 insertions(+), 218 deletions(-) diff --git a/api/backend.go b/api/backend.go index b3d88dfb3..148b82e55 100644 --- a/api/backend.go +++ b/api/backend.go @@ -1,6 +1,8 @@ package api import ( + "crypto/ecdsa" + signercore "github.com/ethereum/go-ethereum/signer/core/apitypes" "github.com/status-im/status-go/eth-node/types" @@ -18,8 +20,8 @@ type StatusBackend interface { // IsNodeRunning() bool // NOTE: Only used in tests StartNode(config *params.NodeConfig) error // NOTE: Only used in canary StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string, conf *params.NodeConfig) error - StartNodeWithAccount(acc multiaccounts.Account, password string, conf *params.NodeConfig) error - StartNodeWithAccountAndInitialConfig(account multiaccounts.Account, password string, settings settings.Settings, conf *params.NodeConfig, subaccs []*accounts.Account) error + StartNodeWithAccount(acc multiaccounts.Account, password string, conf *params.NodeConfig, chatKey *ecdsa.PrivateKey) error + StartNodeWithAccountAndInitialConfig(account multiaccounts.Account, password string, settings settings.Settings, conf *params.NodeConfig, subaccs []*accounts.Account, chatKey *ecdsa.PrivateKey) error StopNode() error // RestartNode() error // NOTE: Only used in tests diff --git a/api/backend_test.go b/api/backend_test.go index 96c5fce54..9c46c796a 100644 --- a/api/backend_test.go +++ b/api/backend_test.go @@ -34,6 +34,7 @@ import ( "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/services/typeddata" + "github.com/status-im/status-go/services/wallet" walletservice "github.com/status-im/status-go/services/wallet" "github.com/status-im/status-go/signal" "github.com/status-im/status-go/sqlite" @@ -1073,7 +1074,7 @@ func TestConvertAccount(t *testing.T) { found = keystoreContainsFileForAccount(keyStoreDir, chatAddress) require.True(t, found) - defaultSettings, err := defaultSettings(genAccInfo, derivedAccounts) + defaultSettings, err := defaultSettings(genAccInfo.KeyUID, genAccInfo.Address, derivedAccounts) require.NoError(t, err) nodeConfig, err := defaultNodeConfig(defaultSettings.InstallationID, &requests.CreateAccount{ LogLevel: defaultSettings.LogLevel, @@ -1135,7 +1136,7 @@ func TestConvertAccount(t *testing.T) { err = backend.ensureAppDBOpened(account, password) require.NoError(t, err) - err = backend.StartNodeWithAccountAndInitialConfig(account, password, *defaultSettings, nodeConfig, profileKeypair.Accounts) + err = backend.StartNodeWithAccountAndInitialConfig(account, password, *defaultSettings, nodeConfig, profileKeypair.Accounts, nil) require.NoError(t, err) multiaccounts, err := backend.GetAccounts() require.NoError(t, err) @@ -1294,7 +1295,7 @@ func loginDesktopUser(t *testing.T, conf *params.NodeConfig) { wg.Add(1) go func() { defer wg.Done() - err := b.StartNodeWithAccount(accounts[0], passwd, conf) + err := b.StartNodeWithAccount(accounts[0], passwd, conf, nil) require.NoError(t, err) }() @@ -1682,3 +1683,168 @@ func TestCreateAccountPathsValidation(t *testing.T) { require.NoError(t, err) require.Equal(t, tmpdir, request.RootDataDir) } + +func TestRestoreKeycardAccountAndLogin(t *testing.T) { + utils.Init() + tmpdir := t.TempDir() + + exampleKeycardEvent := map[string]interface{}{ + "error": "", + "instanceUID": "a84599394887b742eed9a99d3834a797", + "applicationInfo": map[string]interface{}{ + "initialized": false, + "instanceUID": "", + "version": 0, + "availableSlots": 0, + "keyUID": "", + }, + "seedPhraseIndexes": []interface{}{}, + "freePairingSlots": 0, + "keyUid": "0x579324c53f347e18961c775a00ec13ed7d59a225b1859d5125ff36b450b8778d", + "pinRetries": 0, + "pukRetries": 0, + "cardMetadata": map[string]interface{}{ + "name": "", + "walletAccounts": []interface{}{}, + }, + "generatedWalletAccount": map[string]interface{}{ + "address": "", + "publicKey": "", + "privateKey": "", + }, + "generatedWalletAccounts": []interface{}{}, + "txSignature": map[string]interface{}{ + "r": "", + "s": "", + "v": "", + }, + "eip1581Key": map[string]interface{}{ + "address": "0xA8d50f0B3bc581298446be8FBfF5c71684Ea6c01", + "publicKey": "0x040d7e6e3761ab3d17c220e484ede2f3fa02998b859d4d0e9d34216c6e41b03dc94996fdea23a9233092cee50a768e7428d5de7bd42e8e32c10d6b0e36b10f0e7a", + "privateKey": "", + }, + "encryptionKey": map[string]interface{}{ + "address": "0x1ec12f2b323ddDD076A1127cEc8FA0B592c46cD3", + "publicKey": "0x04c4b16f670b51702dc130673bf9c64ffd1f69383cef2127dfa05031b9b1359120f7342134af9a350465126a85e87cb003b7c4f93d2ba2ff98bb73277b119c7a87", + "privateKey": "68c830d5b327382a65e6c302594744ec0d28b01d1ea8124f49714f05c9625ddd"}, + "masterKey": map[string]interface{}{ + "address": "0xbf9dE86774051537b2192Ce9c8d2496f129bA24b", + "publicKey": "0x040d909a07ecca18bbfa7d53d10a86bd956f54b8b446eabd94940e642ae18421b516ec5b63677c4ce65e0e266b58bdb716d8266b25356154eb61713ecb23824075", + "privateKey": "", + }, + "walletKey": map[string]interface{}{ + "address": "0xB9E1998e1A8854887CA327D1aF5894B6CB0AC07D", + "publicKey": "0x04c16e7748f34e0ab2c9c13350d7872d928e942934dd8b8abd3fb12b8c742a5ee8cf0919731e800907068afec25f577bde3a9c534795e359ee48097e4e55f4aaca", + "privateKey": "", + }, + "walletRootKey": map[string]interface{}{ + "address": "0xFf59db9F2f97Db7104A906C390D33C342a1309C8", + "publicKey": "0x04c436532398e19ed14b4eb41545b82014435d60e7db4449a371fd80d0d5cd557f60d81f6c2b35ca5440aa60934c23b70489b0e7963e63ec66b51a7e52db711262", + "privateKey": "", + }, + "whisperKey": map[string]interface{}{ + "address": "0xBa122B9c0Ef560813b5D2C0961094aC36289f846", + "publicKey": "0x0441468c39b579259676350b9736b01cdadb740f67bfd022fa2b985123b1d66fc3191cfe73205e3d3d84148f0248f9a2978afeeda16d7c3db90bd2579f0de33459", + "privateKey": "5a42b4f15ff1a5da95d116442ce11a31e9020f562224bf60b1d8d3a99d90653d", + }, + "masterKeyAddress": "", + } + + exampleRequest := map[string]interface{}{ + "mnemonic": "", + "fetchBackup": true, + "createAccountRequest": map[string]interface{}{ + "rootDataDir": tmpdir, + "kdfIterations": 256000, + "deviceName": "", + "displayName": "", + "password": "0xc5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + "imagePath": "", + "imageCropRectangle": map[string]interface{}{ + "ax": 0, "ay": 0, "bx": 0, "by": 0}, + "customizationColor": "primary", + "emoji": "", + "wakuV2Nameserver": nil, + "wakuV2LightClient": false, + "logLevel": "DEBUG", + "logFilePath": "", + "logEnabled": false, + "previewPrivacy": true, + "verifyTransactionURL": nil, + "verifyENSURL": nil, + "verifyENSContractAddress": nil, + "verifyTransactionChainID": nil, + "upstreamConfig": "", + "networkID": nil, + "walletSecretsConfig": map[string]interface{}{ + "poktToken": "1234567890", + "infuraToken": "1234567890", + "infuraSecret": "", + "openseaApiKey": "", + "raribleMainnetApiKey": "", + "raribleTestnetApiKey": "", + "alchemyEthereumMainnetToken": "", + "alchemyEthereumGoerliToken": "", + "alchemyEthereumSepoliaToken": "", + "alchemyArbitrumMainnetToken": "", + "alchemyArbitrumGoerliToken": "", + "alchemyArbitrumSepoliaToken": "", + "alchemyOptimismMainnetToken": "", + "alchemyOptimismGoerliToken": "", + "alchemyOptimismSepoliaToken": "", + }, + "torrentConfigEnabled": false, + "torrentConfigPort": 0, + "keycardInstanceUID": "a84599394887b742eed9a99d3834a797", + "keycardPairingDataFile": path.Join(tmpdir, DefaultKeycardPairingDataFile), + }, + } + + require.NotNil(t, exampleKeycardEvent) + require.NotNil(t, exampleRequest) + + conf, err := params.NewNodeConfig(tmpdir, 1777) + require.NoError(t, err) + + backend := NewGethStatusBackend() + + require.NoError(t, backend.AccountManager().InitKeystore(conf.KeyStoreDir)) + backend.UpdateRootDataDir(conf.DataDir) + + require.NoError(t, backend.OpenAccounts()) + + keycardPairingDataFile := exampleRequest["createAccountRequest"].(map[string]interface{})["keycardPairingDataFile"].(string) + + kp := wallet.NewKeycardPairings() + kp.SetKeycardPairingsFile(keycardPairingDataFile) + + err = kp.SetPairingsJSONFileContent([]byte(`{"a84599394887b742eed9a99d3834a797":{"key":"785d52957b05482477728380d9b4bbb5dc9a8ed978ab4a4098e1a279c855d3c6","index":1}}`)) + require.NoError(t, err) + + request := &requests.RestoreAccount{ + Keycard: &requests.KeycardData{ + KeyUID: exampleKeycardEvent["keyUid"].(string), + Address: exampleKeycardEvent["masterKey"].(map[string]interface{})["address"].(string), + WhisperPrivateKey: exampleKeycardEvent["whisperKey"].(map[string]interface{})["privateKey"].(string), + WhisperPublicKey: exampleKeycardEvent["whisperKey"].(map[string]interface{})["publicKey"].(string), + WhisperAddress: exampleKeycardEvent["whisperKey"].(map[string]interface{})["address"].(string), + WalletPublicKey: exampleKeycardEvent["walletKey"].(map[string]interface{})["publicKey"].(string), + WalletAddress: exampleKeycardEvent["walletKey"].(map[string]interface{})["address"].(string), + WalletRootAddress: exampleKeycardEvent["walletRootKey"].(map[string]interface{})["address"].(string), + Eip1581Address: exampleKeycardEvent["eip1581Key"].(map[string]interface{})["address"].(string), + EncryptionPublicKey: exampleKeycardEvent["encryptionKey"].(map[string]interface{})["publicKey"].(string), + }, + CreateAccount: requests.CreateAccount{ + DisplayName: "User-1", + Password: "password123", + CustomizationColor: "#ffffff", + RootDataDir: tmpdir, + KeycardInstanceUID: exampleKeycardEvent["instanceUID"].(string), + KeycardPairingDataFile: &keycardPairingDataFile, + }, + } + + acc, err := backend.RestoreKeycardAccountAndLogin(request) + require.NoError(t, err) + require.NotNil(t, acc) +} diff --git a/api/defaults.go b/api/defaults.go index 5d33f2f50..504228250 100644 --- a/api/defaults.go +++ b/api/defaults.go @@ -21,6 +21,7 @@ import ( const pathWalletRoot = "m/44'/60'/0'/0" const pathEIP1581 = "m/43'/60'/1581'" const pathDefaultChat = pathEIP1581 + "/0'/0" +const pathEncryption = pathEIP1581 + "/1'/0" const pathDefaultWallet = pathWalletRoot + "/0" const defaultMnemonicLength = 12 const shardsTestClusterID = 16 @@ -38,11 +39,11 @@ const DefaultListenAddr = ":0" const DefaultMaxMessageDeliveryAttempts = 3 const DefaultVerifyTransactionChainID = 1 -var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet} +var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet, pathEncryption} var DefaultFleet = params.FleetShardsTest -func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo) (*settings.Settings, error) { +func defaultSettings(keyUID string, address string, derivedAddresses map[string]generator.AccountInfo) (*settings.Settings, error) { chatKeyString := derivedAddresses[pathDefaultChat].PublicKey s := &settings.Settings{} @@ -51,8 +52,8 @@ func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derive s.LogLevel = &logLevel s.ProfilePicturesShowTo = settings.ProfilePicturesShowToEveryone s.ProfilePicturesVisibility = settings.ProfilePicturesVisibilityEveryone - s.KeyUID = generatedAccountInfo.KeyUID - s.Address = types.HexToAddress(generatedAccountInfo.Address) + s.KeyUID = keyUID + s.Address = types.HexToAddress(address) s.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address) s.URLUnfurlingMode = settings.URLUnfurlingAlwaysAsk @@ -222,8 +223,11 @@ func defaultNodeConfig(installationID string, request *requests.CreateAccount, o nodeConfig.LogDir = request.LogFilePath nodeConfig.LogLevel = DefaultLogLevel nodeConfig.DataDir = DefaultDataDir - nodeConfig.KeycardPairingDataFile = DefaultKeycardPairingDataFile nodeConfig.ProcessBackedupMessages = false + nodeConfig.KeycardPairingDataFile = DefaultKeycardPairingDataFile + if request.KeycardPairingDataFile != nil { + nodeConfig.KeycardPairingDataFile = *request.KeycardPairingDataFile + } if request.LogLevel != nil { nodeConfig.LogLevel = *request.LogLevel diff --git a/api/geth_backend.go b/api/geth_backend.go index 47fe42665..c1cbb1dc8 100644 --- a/api/geth_backend.go +++ b/api/geth_backend.go @@ -2,9 +2,9 @@ package api import ( "context" + "crypto/ecdsa" "database/sql" "encoding/json" - "errors" "fmt" "math/big" "os" @@ -15,8 +15,7 @@ import ( "go.uber.org/zap" - "github.com/status-im/status-go/services/ens" - "github.com/status-im/status-go/sqlite" + "github.com/pkg/errors" "github.com/imdario/mergo" @@ -48,10 +47,13 @@ import ( "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/rpc" "github.com/status-im/status-go/server/pairing/statecontrol" + "github.com/status-im/status-go/services/ens" "github.com/status-im/status-go/services/ext" "github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/typeddata" + "github.com/status-im/status-go/services/wallet" "github.com/status-im/status-go/signal" + "github.com/status-im/status-go/sqlite" "github.com/status-im/status-go/transactions" "github.com/status-im/status-go/walletdatabase" ) @@ -454,11 +456,8 @@ func (b *GethStatusBackend) setupLogSettings() error { return nil } -// StartNodeWithKey instead of loading addresses from database this method derives address from key -// and uses it in application. -// TODO: we should use a proper struct with optional values instead of duplicating the regular functions -// with small variants for keycard, this created too many bugs -func (b *GethStatusBackend) startNodeWithKey(acc multiaccounts.Account, password string, keyHex string, inputNodeCfg *params.NodeConfig) error { +// Deprecated: Use StartNodeWithAccount instead. +func (b *GethStatusBackend) StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string, nodecfg *params.NodeConfig) error { if acc.KDFIterations == 0 { kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(acc.KeyUID) if err != nil { @@ -468,83 +467,21 @@ func (b *GethStatusBackend) startNodeWithKey(acc multiaccounts.Account, password acc.KDFIterations = kdfIterations } - err := b.ensureDBsOpened(acc, password) - if err != nil { - return err - } - - err = b.loadNodeConfig(inputNodeCfg) - if err != nil { - return err - } - - err = b.setupLogSettings() - if err != nil { - return err - } - - accountsDB, err := accounts.NewDB(b.appDB) - if err != nil { - return err - } - - if acc.ColorHash == nil { - multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB) - if err != nil { - return err - } - acc = *multiAccount - } - - b.account = &acc - - walletAddr, err := accountsDB.GetWalletAddress() - if err != nil { - return err - } - watchAddrs, err := accountsDB.GetAddresses() - if err != nil { - return err - } chatKey, err := ethcrypto.HexToECDSA(keyHex) if err != nil { return err } - err = b.StartNode(b.config) - if err != nil { - return err - } - if err := b.accountManager.SetChatAccount(chatKey); err != nil { - return err - } - _, err = b.accountManager.SelectedChatAccount() - if err != nil { - return err - } - b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...) - err = b.injectAccountsIntoServices() - if err != nil { - return err - } - err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) - if err != nil { - return err - } - return nil -} -func (b *GethStatusBackend) StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string, nodecfg *params.NodeConfig) error { - err := b.startNodeWithKey(acc, password, keyHex, nodecfg) + err = b.startNodeWithAccount(acc, password, nodecfg, chatKey) if err != nil { // Stop node for clean up _ = b.StopNode() - return err } // get logged in - if !b.LocalPairingStateManager.IsPairing() { - return b.LoggedIn(acc.KeyUID, err) + if b.LocalPairingStateManager.IsPairing() { + return nil } - return nil + return b.LoggedIn(acc.KeyUID, err) } func (b *GethStatusBackend) OverwriteNodeConfigValues(conf *params.NodeConfig, n *params.NodeConfig) (*params.NodeConfig, error) { @@ -595,6 +532,9 @@ func (b *GethStatusBackend) LoginAccount(request *requests.Login) error { // Stop node for clean up _ = b.StopNode() } + if b.LocalPairingStateManager.IsPairing() { + return nil + } return b.LoggedIn(request.KeyUID, err) } @@ -603,7 +543,20 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error { return err } - password := request.Password + if request.Mnemonic != "" { + info, err := b.generateAccountInfo(request.Mnemonic) + if err != nil { + return errors.Wrap(err, "failed to generate account info") + } + + derivedAddresses, err := b.getDerivedAddresses(info.ID) + if err != nil { + return errors.Wrap(err, "failed to get derived addresses") + } + + request.Password = derivedAddresses[pathEncryption].PublicKey + request.KeycardWhisperPrivateKey = derivedAddresses[pathDefaultChat].PrivateKey + } acc := multiaccounts.Account{ KeyUID: request.KeyUID, @@ -614,9 +567,9 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error { acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber } - err := b.ensureDBsOpened(acc, password) + err := b.ensureDBsOpened(acc, request.Password) if err != nil { - return err + return errors.Wrap(err, "failed to open database") } defaultCfg := ¶ms.NodeConfig{ @@ -626,14 +579,14 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error { defaultCfg.WalletConfig = buildWalletConfig(&request.WalletSecretsConfig) - err = b.UpdateNodeConfigFleet(acc, password, defaultCfg) + err = b.UpdateNodeConfigFleet(acc, request.Password, defaultCfg) if err != nil { - return err + return errors.Wrap(err, "failed to update node config fleet") } err = b.loadNodeConfig(defaultCfg) if err != nil { - return err + return errors.Wrap(err, "failed to load node config") } if request.RuntimeLogLevel != "" { @@ -650,34 +603,34 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error { err = b.setupLogSettings() if err != nil { - return err + return errors.Wrap(err, "failed to setup log settings") } accountsDB, err := accounts.NewDB(b.appDB) if err != nil { - return err + return errors.Wrap(err, "failed to create accounts db") } multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB) if err != nil { - return err + return errors.Wrap(err, "failed to update account color hash and color id") } b.account = multiAccount chatAddr, err := accountsDB.GetChatAddress() if err != nil { - return err + return errors.Wrap(err, "failed to get chat address") } walletAddr, err := accountsDB.GetWalletAddress() if err != nil { - return err + return errors.Wrap(err, "failed to get wallet address") } watchAddrs, err := accountsDB.GetWalletAddresses() if err != nil { - return err + return errors.Wrap(err, "failed to get wallet addresses") } login := account.LoginParams{ - Password: password, + Password: request.Password, ChatAddress: chatAddr, WatchAddresses: watchAddrs, MainAccount: walletAddr, @@ -686,17 +639,35 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error { err = b.StartNode(b.config) if err != nil { b.log.Info("failed to start node") - return err + return errors.Wrap(err, "failed to start node") } - err = b.SelectAccount(login) - if err != nil { - return err + if chatKey := request.ChatPrivateKey(); chatKey == nil { + err = b.SelectAccount(login) + if err != nil { + return errors.Wrap(err, "failed to select account") + } + } else { + // In case of keycard, we don't have a keystore, instead we have private key loaded from the keycard + if err := b.accountManager.SetChatAccount(chatKey); err != nil { + return errors.Wrap(err, "failed to set chat account") + } + _, err = b.accountManager.SelectedChatAccount() + if err != nil { + return errors.Wrap(err, "failed to get selected chat account") + } + + b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...) + err = b.injectAccountsIntoServices() + if err != nil { + return errors.Wrap(err, "failed to inject accounts into services") + } } + err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) if err != nil { - b.log.Info("failed to update account") - return err + b.log.Error("failed to update account") + return errors.Wrap(err, "failed to update account") } return nil @@ -737,7 +708,8 @@ func (b *GethStatusBackend) UpdateNodeConfigFleet(acc multiaccounts.Account, pas return nil } -func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, password string, inputNodeCfg *params.NodeConfig) error { +// Deprecated: Use loginAccount instead +func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, password string, inputNodeCfg *params.NodeConfig, chatKey *ecdsa.PrivateKey) error { err := b.ensureDBsOpened(acc, password) if err != nil { return err @@ -793,10 +765,29 @@ func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, pass return err } - err = b.SelectAccount(login) - if err != nil { - return err + if chatKey == nil { + // Load account from keystore + err = b.SelectAccount(login) + if err != nil { + return err + } + } else { + // In case of keycard, we don't have keystore, but we directly have the private key + if err := b.accountManager.SetChatAccount(chatKey); err != nil { + return err + } + _, err = b.accountManager.SelectedChatAccount() + if err != nil { + return err + } + + b.accountManager.SetAccountAddresses(walletAddr, watchAddrs...) + err = b.injectAccountsIntoServices() + if err != nil { + return err + } } + err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix()) if err != nil { b.log.Info("failed to update account") @@ -861,11 +852,11 @@ func (b *GethStatusBackend) MigrateKeyStoreDir(acc multiaccounts.Account, passwo } func (b *GethStatusBackend) Login(keyUID, password string) error { - return b.startNodeWithAccount(multiaccounts.Account{KeyUID: keyUID}, password, nil) + return b.startNodeWithAccount(multiaccounts.Account{KeyUID: keyUID}, password, nil, nil) } -func (b *GethStatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string, nodecfg *params.NodeConfig) error { - err := b.startNodeWithAccount(acc, password, nodecfg) +func (b *GethStatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string, nodecfg *params.NodeConfig, chatKey *ecdsa.PrivateKey) error { + err := b.startNodeWithAccount(acc, password, nodecfg, chatKey) if err != nil { // Stop node for clean up _ = b.StopNode() @@ -993,9 +984,9 @@ func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password strin // because UI calls Logout and Quit afterwards. It should not be UI-dependent // and should be handled gracefully here if it makes sense to run dummy node after // logout - _ = b.startNodeWithAccount(*account, password, nil) + _ = b.startNodeWithAccount(*account, password, nil, nil) } else { - _ = b.startNodeWithAccount(*account, newPassword, nil) + _ = b.startNodeWithAccount(*account, newPassword, nil, nil) } } } @@ -1294,18 +1285,91 @@ func (b *GethStatusBackend) RestoreAccountAndLogin(request *requests.RestoreAcco return nil, err } - account, settings, nodeConfig, subAccounts, err := b.generateOrImportAccount(request.Mnemonic, 0, request.FetchBackup, &request.CreateAccount) + response, err := b.generateOrImportAccount(request.Mnemonic, 0, request.FetchBackup, &request.CreateAccount) if err != nil { return nil, err } - err = b.StartNodeWithAccountAndInitialConfig(*account, request.Password, *settings, nodeConfig, subAccounts) + err = b.StartNodeWithAccountAndInitialConfig( + *response.account, + request.Password, + *response.settings, + response.nodeConfig, + response.subAccounts, + response.chatPrivateKey, + ) + if err != nil { b.log.Error("start node", err) return nil, err } - return account, nil + return response.account, nil +} + +func (b *GethStatusBackend) RestoreKeycardAccountAndLogin(request *requests.RestoreAccount) (*multiaccounts.Account, error) { + if err := request.Validate(); err != nil { + return nil, err + } + + keyStoreDir, err := b.initKeyStoreDirWithAccount(request.RootDataDir, request.Keycard.KeyUID) + if err != nil { + return nil, err + } + + derivedAddresses := map[string]generator.AccountInfo{ + pathDefaultChat: { + Address: request.Keycard.WhisperAddress, + PublicKey: request.Keycard.WhisperPublicKey, + PrivateKey: request.Keycard.WhisperPrivateKey, + }, + pathWalletRoot: { + Address: request.Keycard.WalletRootAddress, + }, + pathDefaultWallet: { + Address: request.Keycard.WalletAddress, + PublicKey: request.Keycard.WalletPublicKey, + }, + pathEIP1581: { + Address: request.Keycard.Eip1581Address, + }, + pathEncryption: { + PublicKey: request.Keycard.EncryptionPublicKey, + }, + } + + input := &prepareAccountInput{ + customizationColorClock: 0, + accountID: "", // empty for keycard + keyUID: request.Keycard.KeyUID, + address: request.Keycard.Address, + mnemonic: "", + restoringAccount: true, + derivedAddresses: derivedAddresses, + fetchBackup: request.FetchBackup, // WARNING: Ensure this value is correct + keyStoreDir: keyStoreDir, + } + + response, err := b.prepareNodeAccount(&request.CreateAccount, input) + if err != nil { + return nil, err + } + + err = b.StartNodeWithAccountAndInitialConfig( + *response.account, + request.Password, + *response.settings, + response.nodeConfig, + response.subAccounts, + response.chatPrivateKey, //request.WhisperPrivateKey, + ) + + if err != nil { + b.log.Error("start node", err) + return nil, errors.Wrap(err, "failed to start node") + } + + return response.account, nil } func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error) { @@ -1319,44 +1383,104 @@ func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error) return info.KeyUID, nil } -func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, fetchBackup bool, request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, *settings.Settings, *params.NodeConfig, []*accounts.Account, error) { +type prepareAccountInput struct { + customizationColorClock uint64 + accountID string + keyUID string + address string + mnemonic string + restoringAccount bool + derivedAddresses map[string]generator.AccountInfo + fetchBackup bool + keyStoreDir string + opts []params.Option +} + +type accountBundle struct { + account *multiaccounts.Account + settings *settings.Settings + nodeConfig *params.NodeConfig + subAccounts []*accounts.Account + chatPrivateKey *ecdsa.PrivateKey +} + +func (b *GethStatusBackend) generateOrImportAccount(mnemonic string, customizationColorClock uint64, fetchBackup bool, request *requests.CreateAccount, opts ...params.Option) (*accountBundle, error) { info, err := b.generateAccountInfo(mnemonic) if err != nil { - return nil, nil, nil, nil, err + return nil, err } keyStoreDir, err := b.initKeyStoreDirWithAccount(request.RootDataDir, info.KeyUID) if err != nil { - return nil, nil, nil, nil, err - } - - account, info, err := b.generateAccount(*info, customizationColorClock, request) - if err != nil { - return nil, nil, nil, nil, err + return nil, err } derivedAddresses, err := b.getDerivedAddresses(info.ID) if err != nil { - return nil, nil, nil, nil, err + return nil, err } - settings, err := b.prepareSettings(*info, derivedAddresses, request, mnemonic) + input := &prepareAccountInput{ + customizationColorClock: customizationColorClock, + accountID: info.ID, + keyUID: info.KeyUID, + address: info.Address, + mnemonic: info.Mnemonic, + restoringAccount: mnemonic != "", + derivedAddresses: derivedAddresses, + fetchBackup: fetchBackup, + keyStoreDir: keyStoreDir, + opts: opts, + } + + return b.prepareNodeAccount(request, input) +} + +func (b *GethStatusBackend) prepareNodeAccount(request *requests.CreateAccount, input *prepareAccountInput) (*accountBundle, error) { + var err error + response := &accountBundle{} + + if request.KeycardInstanceUID != "" { + request.Password = input.derivedAddresses[pathEncryption].PublicKey + } + + // NOTE: I intentionally left this condition separately and not an `else` branch. Technically it's an `else`, + // but the statements inside are not the opposite statement of the first statement. It's just kinda like this: + // - replace password when we're using keycard + // - store account when we're not using keycard + if request.KeycardInstanceUID == "" { + err = b.storeAccount(input.accountID, request.Password, paths) + if err != nil { + return nil, err + } + } + + response.account, err = b.buildAccount(request, input) if err != nil { - return nil, nil, nil, nil, err + return nil, errors.Wrap(err, "failed to build account") } - processBackedupMessages := mnemonic != "" && fetchBackup - nodeConfig, err := b.prepareConfig(processBackedupMessages, account.KeyUID, keyStoreDir, request, opts...) + response.settings, err = b.prepareSettings(request, input) if err != nil { - return nil, nil, nil, nil, err + return nil, errors.Wrap(err, "failed to prepare settings") } - subAccounts, err := b.prepareSubAccounts(mnemonic, account.KeyUID, derivedAddresses, request) + response.nodeConfig, err = b.prepareConfig(request, input, response.settings.InstallationID) if err != nil { - return nil, nil, nil, nil, err + return nil, errors.Wrap(err, "failed to prepare node config") } - return account, settings, nodeConfig, subAccounts, nil + response.subAccounts, err = b.prepareSubAccounts(request, input) + if err != nil { + return nil, errors.Wrap(err, "failed to prepare sub accounts") + } + + response, err = b.prepareForKeycard(request, input, response) + if err != nil { + return nil, errors.Wrap(err, "failed to prepare for keycard") + } + + return response, nil } func (b *GethStatusBackend) initKeyStoreDirWithAccount(rootDataDir, keyUID string) (string, error) { @@ -1391,36 +1515,39 @@ func (b *GethStatusBackend) generateAccountInfo(mnemonic string) (*generator.Gen return &info, nil } -func (b *GethStatusBackend) generateAccount(info generator.GeneratedAccountInfo, customizationColorClock uint64, request *requests.CreateAccount) (*multiaccounts.Account, *generator.GeneratedAccountInfo, error) { - err := b.OpenAccounts() - if err != nil { - b.log.Error("failed open accounts", "err", err) - return nil, nil, err - } - +func (b *GethStatusBackend) storeAccount(id string, password string, paths []string) error { accountGenerator := b.accountManager.AccountsGenerator() - _, err = accountGenerator.StoreAccount(info.ID, request.Password) + _, err := accountGenerator.StoreAccount(id, password) if err != nil { - return nil, nil, err + return err } - _, err = accountGenerator.StoreDerivedAccounts(info.ID, request.Password, paths) + _, err = accountGenerator.StoreDerivedAccounts(id, password, paths) if err != nil { - return nil, nil, err + return err } - account := multiaccounts.Account{ - KeyUID: info.KeyUID, + return nil +} + +func (b *GethStatusBackend) buildAccount(request *requests.CreateAccount, input *prepareAccountInput) (*multiaccounts.Account, error) { + err := b.OpenAccounts() + if err != nil { + return nil, err + } + + acc := &multiaccounts.Account{ + KeyUID: input.keyUID, Name: request.DisplayName, CustomizationColor: multiacccommon.CustomizationColor(request.CustomizationColor), - CustomizationColorClock: customizationColorClock, + CustomizationColorClock: input.customizationColorClock, KDFIterations: request.KdfIterations, Timestamp: time.Now().Unix(), } - if account.KDFIterations == 0 { - account.KDFIterations = dbsetup.ReducedKDFIterationsNumber + if acc.KDFIterations == 0 { + acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber } if request.ImagePath != "" { @@ -1439,16 +1566,16 @@ func (b *GethStatusBackend) generateAccount(info generator.GeneratedAccountInfo, imageCropRectangle.Ax, imageCropRectangle.Ay, imageCropRectangle.Bx, imageCropRectangle.By) if err != nil { - return nil, nil, err + return nil, err } - account.Images = iis + acc.Images = iis } - return &account, &info, nil + return acc, nil } -func (b *GethStatusBackend) prepareSettings(info generator.GeneratedAccountInfo, derivedAddresses map[string]generator.AccountInfo, request *requests.CreateAccount, mnemonic string) (*settings.Settings, error) { - settings, err := defaultSettings(info, derivedAddresses) +func (b *GethStatusBackend) prepareSettings(request *requests.CreateAccount, input *prepareAccountInput) (*settings.Settings, error) { + settings, err := defaultSettings(input.keyUID, input.address, input.derivedAddresses) if err != nil { return nil, err } @@ -1459,9 +1586,8 @@ func (b *GethStatusBackend) prepareSettings(info generator.GeneratedAccountInfo, settings.CurrentNetwork = request.CurrentNetwork settings.TestNetworksEnabled = request.TestNetworksEnabled - // If restoring an account, we don't set the mnemonic - if mnemonic == "" { - settings.Mnemonic = &info.Mnemonic + if !input.restoringAccount { + settings.Mnemonic = &input.mnemonic settings.OmitTransfersHistoryScan = true // TODO(rasom): uncomment it as soon as address will be properly // marked as shown on mobile client @@ -1471,41 +1597,38 @@ func (b *GethStatusBackend) prepareSettings(info generator.GeneratedAccountInfo, return settings, nil } -func (b *GethStatusBackend) prepareConfig(processBackedupMessages bool, installationID string, userKeyStoreDir string, request *requests.CreateAccount, opts ...params.Option) (*params.NodeConfig, error) { - nodeConfig, err := defaultNodeConfig(installationID, request, opts...) +func (b *GethStatusBackend) prepareConfig(request *requests.CreateAccount, input *prepareAccountInput, installationID string) (*params.NodeConfig, error) { + nodeConfig, err := defaultNodeConfig(installationID, request, input.opts...) if err != nil { return nil, err } - nodeConfig.ProcessBackedupMessages = processBackedupMessages + nodeConfig.ProcessBackedupMessages = input.fetchBackup // when we set nodeConfig.KeyStoreDir, value of nodeConfig.KeyStoreDir should not contain the rootDataDir // loadNodeConfig will add rootDataDir to nodeConfig.KeyStoreDir - nodeConfig.KeyStoreDir = userKeyStoreDir + nodeConfig.KeyStoreDir = input.keyStoreDir return nodeConfig, nil } -func (b *GethStatusBackend) prepareSubAccounts(mnemonic, keyUID string, derivedAddresses map[string]generator.AccountInfo, request *requests.CreateAccount) ([]*accounts.Account, error) { - walletDerivedAccount := derivedAddresses[pathDefaultWallet] +func (b *GethStatusBackend) prepareSubAccounts(request *requests.CreateAccount, input *prepareAccountInput) ([]*accounts.Account, error) { + walletDerivedAccount := input.derivedAddresses[pathDefaultWallet] walletAccount := &accounts.Account{ - PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey), - KeyUID: keyUID, - Address: types.HexToAddress(walletDerivedAccount.Address), - ColorID: multiacccommon.CustomizationColor(request.CustomizationColor), - Emoji: request.Emoji, - Wallet: true, - Path: pathDefaultWallet, - Name: walletAccountDefaultName, + PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey), + KeyUID: input.keyUID, + Address: types.HexToAddress(walletDerivedAccount.Address), + ColorID: multiacccommon.CustomizationColor(request.CustomizationColor), + Emoji: request.Emoji, + Wallet: true, + Path: pathDefaultWallet, + Name: walletAccountDefaultName, + AddressWasNotShown: !input.restoringAccount, } - if mnemonic == "" { - walletAccount.AddressWasNotShown = true - } - - chatDerivedAccount := derivedAddresses[pathDefaultChat] + chatDerivedAccount := input.derivedAddresses[pathDefaultChat] chatAccount := &accounts.Account{ PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey), - KeyUID: keyUID, + KeyUID: input.keyUID, Address: types.HexToAddress(chatDerivedAccount.Address), Name: request.DisplayName, Chat: true, @@ -1515,14 +1638,40 @@ func (b *GethStatusBackend) prepareSubAccounts(mnemonic, keyUID string, derivedA return []*accounts.Account{walletAccount, chatAccount}, nil } -func (b *GethStatusBackend) getDerivedAddresses(id string) (map[string]generator.AccountInfo, error) { - accountGenerator := b.accountManager.AccountsGenerator() - derivedAddresses, err := accountGenerator.DeriveAddresses(id, paths) - if err != nil { - return nil, err +func (b *GethStatusBackend) prepareForKeycard(request *requests.CreateAccount, input *prepareAccountInput, response *accountBundle) (*accountBundle, error) { + if request.KeycardInstanceUID == "" { + return response, nil } - return derivedAddresses, nil + kp := wallet.NewKeycardPairings() + kp.SetKeycardPairingsFile(response.nodeConfig.KeycardPairingDataFile) + pairings, err := kp.GetPairings() + if err != nil { + return nil, errors.Wrap(err, "failed to get keycard pairings") + } + + keycard, ok := pairings[request.KeycardInstanceUID] + if !ok { + return nil, errors.New("keycard not found in pairings file") + } + + response.settings.KeycardInstanceUID = request.KeycardInstanceUID + response.settings.KeycardPairedOn = time.Now().Unix() + response.settings.KeycardPairing = keycard.Key + response.account.KeycardPairing = keycard.Key + + privateKeyHex := strings.TrimPrefix(input.derivedAddresses[pathDefaultChat].PrivateKey, "0x") + response.chatPrivateKey, err = crypto.HexToECDSA(privateKeyHex) + if err != nil { + return nil, errors.Wrap(err, "failed to parse chat private key hex") + } + + return response, nil +} + +func (b *GethStatusBackend) getDerivedAddresses(id string) (map[string]generator.AccountInfo, error) { + accountGenerator := b.accountManager.AccountsGenerator() + return accountGenerator.DeriveAddresses(id, paths) } // CreateAccountAndLogin creates a new account and logs in with it. @@ -1535,18 +1684,26 @@ func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccoun return nil, err } - account, settings, nodeConfig, subAccounts, err := b.generateOrImportAccount("", 1, false, request, opts...) + response, err := b.generateOrImportAccount("", 1, false, request, opts...) if err != nil { return nil, err } - err = b.StartNodeWithAccountAndInitialConfig(*account, request.Password, *settings, nodeConfig, subAccounts) + err = b.StartNodeWithAccountAndInitialConfig( + *response.account, + request.Password, + *response.settings, + response.nodeConfig, + response.subAccounts, + response.chatPrivateKey, + ) + if err != nil { b.log.Error("start node", err) return nil, err } - return account, nil + return response.account, nil } func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error { @@ -1708,7 +1865,15 @@ func enrichMultiAccountByPublicKey(account *multiaccounts.Account, publicKey typ return nil } -func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(account multiaccounts.Account, password string, settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account, keyHex string) error { +// Deprecated: Use CreateAccountAndLogin instead +func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey( + account multiaccounts.Account, + password string, + settings settings.Settings, + nodecfg *params.NodeConfig, + subaccs []*accounts.Account, + keyHex string, +) error { err := enrichMultiAccountBySubAccounts(&account, subaccs) if err != nil { return err @@ -1731,15 +1896,15 @@ func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(account multiaccounts // StartNodeWithAccountAndInitialConfig is used after account and config was generated. // In current setup account name and config is generated on the client side. Once/if it will be generated on // status-go side this flow can be simplified. +// TODO: Consider passing accountBundle here directly func (b *GethStatusBackend) StartNodeWithAccountAndInitialConfig( account multiaccounts.Account, password string, settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account, + chatKey *ecdsa.PrivateKey, ) error { - b.log.Info("node config", "config", nodecfg) - err := enrichMultiAccountBySubAccounts(&account, subaccs) if err != nil { return err @@ -1756,7 +1921,7 @@ func (b *GethStatusBackend) StartNodeWithAccountAndInitialConfig( if err != nil { return err } - return b.StartNodeWithAccount(account, password, nodecfg) + return b.StartNodeWithAccount(account, password, nodecfg, chatKey) } // TODO: change in `saveAccountsAndSettings` function param `subaccs []*accounts.Account` parameter to `profileKeypair *accounts.Keypair` parameter diff --git a/api/messenger_raw_message_resend_test.go b/api/messenger_raw_message_resend_test.go index 7f03fd79b..2e2c7bcc7 100644 --- a/api/messenger_raw_message_resend_test.go +++ b/api/messenger_raw_message_resend_test.go @@ -22,6 +22,8 @@ import ( "github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/tt" "github.com/status-im/status-go/services/utils" + "github.com/status-im/status-go/signal" + tutils "github.com/status-im/status-go/t/utils" "github.com/status-im/status-go/wakuv2" "github.com/stretchr/testify/suite" @@ -29,6 +31,7 @@ import ( type MessengerRawMessageResendTest struct { suite.Suite + logger *zap.Logger aliceBackend *GethStatusBackend bobBackend *GethStatusBackend aliceMessenger *protocol.Messenger @@ -43,9 +46,14 @@ func TestMessengerRawMessageResendTestSuite(t *testing.T) { } func (s *MessengerRawMessageResendTest) SetupTest() { - logger, err := zap.NewDevelopment() + tutils.Init() + + var err error + s.logger, err = zap.NewDevelopment() s.Require().NoError(err) + signal.SetMobileSignalHandler(nil) + exchangeNodeConfig := &wakuv2.Config{ Port: 0, EnableDiscV5: true, @@ -54,7 +62,7 @@ func (s *MessengerRawMessageResendTest) SetupTest() { UseShardAsDefaultTopic: true, DefaultShardPubsubTopic: shard.DefaultShardPubsubTopic(), } - s.exchangeBootNode, err = wakuv2.New(nil, "", exchangeNodeConfig, logger.Named("pxServerNode"), nil, nil, nil, nil) + s.exchangeBootNode, err = wakuv2.New(nil, "", exchangeNodeConfig, s.logger.Named("pxServerNode"), nil, nil, nil, nil) s.Require().NoError(err) s.Require().NoError(s.exchangeBootNode.Start()) diff --git a/cmd/ping-community/main.go b/cmd/ping-community/main.go index 4f0029680..de5a4e0bc 100644 --- a/cmd/ping-community/main.go +++ b/cmd/ping-community/main.go @@ -449,7 +449,7 @@ func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error { fmt.Println(nodeConfig) accounts := []*accounts.Account{walletAccount, chatAccount} - err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts) + err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts, nil) if err != nil { logger.Error("start node", err) return err diff --git a/cmd/populate-db/main.go b/cmd/populate-db/main.go index 70e10cda6..7e118283d 100644 --- a/cmd/populate-db/main.go +++ b/cmd/populate-db/main.go @@ -498,7 +498,7 @@ func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error { fmt.Println(nodeConfig) accounts := []*accounts.Account{walletAccount, chatAccount} - err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts) + err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts, nil) if err != nil { logger.Error("start node", err) return err diff --git a/cmd/spiff-workflow/main.go b/cmd/spiff-workflow/main.go index b5693fd4e..dccaabd41 100644 --- a/cmd/spiff-workflow/main.go +++ b/cmd/spiff-workflow/main.go @@ -420,9 +420,9 @@ func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error { fmt.Println(nodeConfig) accounts := []*accounts.Account{walletAccount, chatAccount} if !exist { - return backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts) + return backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts, nil) } - return backend.StartNodeWithAccount(account, "", nodeConfig) + return backend.StartNodeWithAccount(account, "", nodeConfig, nil) } func retrieveMessagesLoop(messenger *protocol.Messenger, tick time.Duration) { diff --git a/eth-node/types/key.go b/eth-node/types/key.go index b74b16a8c..b3dc004fe 100644 --- a/eth-node/types/key.go +++ b/eth-node/types/key.go @@ -18,7 +18,7 @@ type Key struct { // ExtendedKey is the extended key of the PrivateKey itself, and it's used // to derive child keys. ExtendedKey *extkeys.ExtendedKey - // SubAccountIndex is DEPRECATED + // Deprecated: SubAccountIndex // It was use in Status to keep track of the number of sub-account created // before having multi-account support. SubAccountIndex uint32 diff --git a/mobile/status.go b/mobile/status.go index cff4d74de..1a67eaeee 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -237,7 +237,7 @@ func login(accountData, password, configJSON string) error { return statusBackend.LoggedIn(account.KeyUID, err) } - err = statusBackend.StartNodeWithAccount(account, password, &conf) + err = statusBackend.StartNodeWithAccount(account, password, &conf, nil) if err != nil { log.Error("failed to start a node", "key-uid", account.KeyUID, "error", err) return err @@ -339,7 +339,13 @@ func RestoreAccountAndLogin(requestJSON string) string { api.RunAsync(func() error { log.Debug("starting a node and restoring account") - _, err := statusBackend.RestoreAccountAndLogin(&request) + + if request.Keycard != nil { + _, err = statusBackend.RestoreKeycardAccountAndLogin(&request) + } else { + _, err = statusBackend.RestoreAccountAndLogin(&request) + } + if err != nil { log.Error("failed to restore account", "error", err) return err @@ -347,6 +353,7 @@ func RestoreAccountAndLogin(requestJSON string) string { log.Debug("started a node, and restored account") return nil }) + return makeJSONResponse(nil) } @@ -382,7 +389,7 @@ func SaveAccountAndLogin(accountData, password, settingsJSON, configJSON, subacc api.RunAsync(func() error { log.Debug("starting a node, and saving account with configuration", "key-uid", account.KeyUID) - err := statusBackend.StartNodeWithAccountAndInitialConfig(account, password, settings, &conf, subaccs) + err := statusBackend.StartNodeWithAccountAndInitialConfig(account, password, settings, &conf, subaccs, nil) if err != nil { log.Error("failed to start node and save account", "key-uid", account.KeyUID, "error", err) return err @@ -419,7 +426,8 @@ func InitKeystore(keydir string) string { return makeJSONResponse(err) } -// SaveAccountAndLoginWithKeycard saves account in status-go database.. +// SaveAccountAndLoginWithKeycard saves account in status-go database. +// Deprecated: Use CreateAndAccountAndLogin with required keycard properties. func SaveAccountAndLoginWithKeycard(accountData, password, settingsJSON, configJSON, subaccountData string, keyHex string) string { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) @@ -457,6 +465,7 @@ func SaveAccountAndLoginWithKeycard(accountData, password, settingsJSON, configJ // LoginWithKeycard initializes an account with a chat key and encryption key used for PFS. // It purges all the previous identities from Whisper, and injects the key as shh identity. +// Deprecated: Use LoginAccount instead. func LoginWithKeycard(accountData, password, keyHex string, configJSON string) string { var account multiaccounts.Account err := json.Unmarshal([]byte(accountData), &account) diff --git a/params/config.go b/params/config.go index 436f18227..8c0cfd213 100644 --- a/params/config.go +++ b/params/config.go @@ -328,6 +328,9 @@ type NodeConfig struct { KeyStoreDir string `validate:"required"` // KeycardPairingDataFile is the file where we keep keycard pairings data. + // It's specified by clients (and not in status-go) when creating a new account, + // because this file is initialized by status-keycard-go and we need to use it before initializing the node. + // I guess proper way would be to ask status-go for the file path, or just duplicate the file path in both backend and client. // note: this field won't be saved into db, it's local to the device. KeycardPairingDataFile string diff --git a/protocol/requests/create_account.go b/protocol/requests/create_account.go index ce16d11bf..d44f0ae84 100644 --- a/protocol/requests/create_account.go +++ b/protocol/requests/create_account.go @@ -74,6 +74,9 @@ type CreateAccount struct { TelemetryServerURL string `json:"telemetryServerURL"` APIConfig *APIConfig `json:"apiConfig"` + + KeycardInstanceUID string `json:"keycardInstanceUID"` + KeycardPairingDataFile *string `json:"keycardPairingDataFile"` } type WalletSecretsConfig struct { diff --git a/protocol/requests/login.go b/protocol/requests/login.go index 13441fc98..1b0f871da 100644 --- a/protocol/requests/login.go +++ b/protocol/requests/login.go @@ -1,18 +1,38 @@ package requests -import "errors" +import ( + "crypto/ecdsa" + "errors" + "strings" -var ErrLoginInvalidKeyUID = errors.New("login: invalid key-uid") + "github.com/status-im/status-go/eth-node/crypto" +) + +var ( + ErrLoginInvalidKeyUID = errors.New("login: invalid key-uid") + ErrLoginInvalidKeycardWhisperPrivateKey = errors.New("login: invalid keycard whisper private key") +) type Login struct { Password string `json:"password"` KeyUID string `json:"keyUid"` - KdfIterations int `json:"kdfIterations"` + KdfIterations int `json:"kdfIterations"` // FIXME: KdfIterations should be loaded from multiaccounts db. RuntimeLogLevel string `json:"runtimeLogLevel"` WakuV2Nameserver string `json:"wakuV2Nameserver"` BandwidthStatsEnabled bool `json:"bandwidthStatsEnabled"` + KeycardWhisperPrivateKey string `json:"keycardWhisperPrivateKey"` + + // Mnemonic allows to log in to an account when password is lost. + // This is needed for the "Lost keycard -> Start using without keycard" flow, when a keycard account database + // exists locally, but now the keycard is lost. In this case client is responsible for calling + // `convertToRegularAccount` after a successful login. This could be improved in the future. + // When non-empty, mnemonic is used to generate required keypairs and: + // - Password is ignored and replaced with encryption public key + // - KeycardWhisperPrivateKey is ignored and replaced with chat private key + Mnemonic string `json:"mnemonic"` + WalletSecretsConfig } @@ -20,5 +40,24 @@ func (c *Login) Validate() error { if c.KeyUID == "" { return ErrLoginInvalidKeyUID } + + if c.KeycardWhisperPrivateKey != "" { + _, err := parsePrivateKey(c.KeycardWhisperPrivateKey) + if err != nil { + return ErrLoginInvalidKeycardWhisperPrivateKey + } + } + return nil } + +func (c *Login) ChatPrivateKey() *ecdsa.PrivateKey { + // Skip error check, as it's already validated in Validate + privateKey, _ := parsePrivateKey(c.KeycardWhisperPrivateKey) + return privateKey +} + +func parsePrivateKey(privateKeyHex string) (*ecdsa.PrivateKey, error) { + privateKeyHex = strings.TrimPrefix(privateKeyHex, "0x") + return crypto.HexToECDSA(privateKeyHex) +} diff --git a/protocol/requests/restore_account.go b/protocol/requests/restore_account.go index e6bf54861..fd0be680a 100644 --- a/protocol/requests/restore_account.go +++ b/protocol/requests/restore_account.go @@ -4,20 +4,46 @@ import ( "errors" ) -var ErrRestoreAccountInvalidMnemonic = errors.New("restore-account: invalid mnemonic") +var ( + ErrRestoreAccountInvalidMnemonic = errors.New("restore-account: no mnemonic or keycard is set") + ErrRestoreAccountMnemonicAndKeycard = errors.New("restore-account: both mnemonic and keycard info are set") +) type RestoreAccount struct { - Mnemonic string `json:"mnemonic"` - FetchBackup bool `json:"fetchBackup"` + Mnemonic string `json:"mnemonic"` + + // Keycard info can be set instead of Mnemonic. + // This is to log in using a keycard with existing account. + Keycard *KeycardData `json:"keycard"` + + FetchBackup bool `json:"fetchBackup"` + CreateAccount } func (c *RestoreAccount) Validate() error { - if len(c.Mnemonic) == 0 { + if len(c.Mnemonic) == 0 && c.Keycard == nil { return ErrRestoreAccountInvalidMnemonic } + if len(c.Mnemonic) > 0 && c.Keycard != nil { + return ErrRestoreAccountMnemonicAndKeycard + } + return c.CreateAccount.Validate(&CreateAccountValidation{ AllowEmptyDisplayName: true, }) } + +type KeycardData struct { + KeyUID string `json:"keyUID"` + Address string `json:"address"` + WhisperPrivateKey string `json:"whisperPrivateKey"` + WhisperPublicKey string `json:"whisperPublicKey"` + WhisperAddress string `json:"whisperAddress"` + WalletPublicKey string `json:"walletPublicKey"` + WalletAddress string `json:"walletAddress"` + WalletRootAddress string `json:"walletRootAddress"` + Eip1581Address string `json:"eip1581Address"` + EncryptionPublicKey string `json:"encryptionPublicKey"` +} diff --git a/server/pairing/raw_message_handler.go b/server/pairing/raw_message_handler.go index cce308460..2f53507ec 100644 --- a/server/pairing/raw_message_handler.go +++ b/server/pairing/raw_message_handler.go @@ -2,8 +2,12 @@ package pairing import ( "context" + "crypto/ecdsa" "fmt" "path/filepath" + "strings" + + ethcrypto "github.com/ethereum/go-ethereum/crypto" "github.com/status-im/status-go/api" "github.com/status-im/status-go/multiaccounts/accounts" @@ -93,12 +97,18 @@ func (s *SyncRawMessageHandler) HandleRawMessage(accountPayload *AccountPayload, // because client don't know keyUID before received data, we need help client to update keystore dir keystoreDir := filepath.Join(nodeConfig.KeyStoreDir, account.KeyUID) nodeConfig.KeyStoreDir = keystoreDir - if accountPayload.exist { - if len(accountPayload.chatKey) == 0 { - err = s.backend.StartNodeWithAccount(*account, accountPayload.password, nodeConfig) - } else { - err = s.backend.StartNodeWithKey(*account, accountPayload.password, accountPayload.chatKey, nodeConfig) + + var chatKey *ecdsa.PrivateKey + if accountPayload.chatKey != "" { + chatKeyHex := strings.Trim(accountPayload.chatKey, "0x") + chatKey, err = ethcrypto.HexToECDSA(chatKeyHex) + if err != nil { + return err } + } + + if accountPayload.exist { + err = s.backend.StartNodeWithAccount(*account, accountPayload.password, nodeConfig, chatKey) } else { accountManager := s.backend.AccountManager() err = accountManager.InitKeystore(filepath.Join(nodeConfig.RootDataDir, keystoreDir)) @@ -109,11 +119,7 @@ func (s *SyncRawMessageHandler) HandleRawMessage(accountPayload *AccountPayload, rmp.setting.InstallationID = nodeConfig.ShhextConfig.InstallationID rmp.setting.CurrentNetwork = settingCurrentNetwork - if len(accountPayload.chatKey) == 0 { - err = s.backend.StartNodeWithAccountAndInitialConfig(*account, accountPayload.password, *rmp.setting, nodeConfig, rmp.profileKeypair.Accounts) - } else { - err = s.backend.SaveAccountAndStartNodeWithKey(*account, accountPayload.password, *rmp.setting, nodeConfig, rmp.profileKeypair.Accounts, accountPayload.chatKey) - } + err = s.backend.StartNodeWithAccountAndInitialConfig(*account, accountPayload.password, *rmp.setting, nodeConfig, rmp.profileKeypair.Accounts, chatKey) } if err != nil { return err diff --git a/server/pairing/sync_device_test.go b/server/pairing/sync_device_test.go index bb59e2949..8e37ef406 100644 --- a/server/pairing/sync_device_test.go +++ b/server/pairing/sync_device_test.go @@ -140,7 +140,7 @@ func (s *SyncDeviceSuite) prepareBackendWithAccount(mnemonic, tmpdir string) *ap } accounts := []*accounts.Account{walletAccount, chatAccount} - err = backend.StartNodeWithAccountAndInitialConfig(account, s.password, *settings, nodeConfig, accounts) + err = backend.StartNodeWithAccountAndInitialConfig(account, s.password, *settings, nodeConfig, accounts, nil) require.NoError(s.T(), err) multiaccounts, err := backend.GetAccounts() require.NoError(s.T(), err) diff --git a/services/wallet/keycard_pairings.go b/services/wallet/keycard_pairings.go index dd2bf4ac0..9eaafa20e 100644 --- a/services/wallet/keycard_pairings.go +++ b/services/wallet/keycard_pairings.go @@ -1,6 +1,7 @@ package wallet import ( + "encoding/json" "io/ioutil" "os" "path/filepath" @@ -10,6 +11,11 @@ type KeycardPairings struct { pairingsFile string } +type KeycardPairing struct { + Key string `json:"key"` + Index int `json:"index"` +} + func NewKeycardPairings() *KeycardPairings { return &KeycardPairings{} } @@ -43,3 +49,22 @@ func (kp *KeycardPairings) SetPairingsJSONFileContent(content []byte) error { return ioutil.WriteFile(kp.pairingsFile, content, 0600) } + +func (kp *KeycardPairings) GetPairings() (map[string]KeycardPairing, error) { + content, err := kp.GetPairingsJSONFileContent() + if err != nil { + return nil, err + } + + if len(content) == 0 { + return nil, os.ErrNotExist + } + + pairings := make(map[string]KeycardPairing) + err = json.Unmarshal(content, &pairings) + if err != nil { + return nil, err + } + + return pairings, nil +}