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
This commit is contained in:
Igor Sirotin 2024-06-26 13:14:27 +02:00 committed by GitHub
parent 1cdcc0dcc2
commit 49eaabaca5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 674 additions and 218 deletions

View File

@ -1,6 +1,8 @@
package api package api
import ( import (
"crypto/ecdsa"
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes" signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
"github.com/status-im/status-go/eth-node/types" "github.com/status-im/status-go/eth-node/types"
@ -18,8 +20,8 @@ type StatusBackend interface {
// IsNodeRunning() bool // NOTE: Only used in tests // IsNodeRunning() bool // NOTE: Only used in tests
StartNode(config *params.NodeConfig) error // NOTE: Only used in canary StartNode(config *params.NodeConfig) error // NOTE: Only used in canary
StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string, conf *params.NodeConfig) error StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string, conf *params.NodeConfig) error
StartNodeWithAccount(acc multiaccounts.Account, password string, conf *params.NodeConfig) 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) error StartNodeWithAccountAndInitialConfig(account multiaccounts.Account, password string, settings settings.Settings, conf *params.NodeConfig, subaccs []*accounts.Account, chatKey *ecdsa.PrivateKey) error
StopNode() error StopNode() error
// RestartNode() error // NOTE: Only used in tests // RestartNode() error // NOTE: Only used in tests

View File

@ -34,6 +34,7 @@ import (
"github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/services/typeddata" "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" walletservice "github.com/status-im/status-go/services/wallet"
"github.com/status-im/status-go/signal" "github.com/status-im/status-go/signal"
"github.com/status-im/status-go/sqlite" "github.com/status-im/status-go/sqlite"
@ -1073,7 +1074,7 @@ func TestConvertAccount(t *testing.T) {
found = keystoreContainsFileForAccount(keyStoreDir, chatAddress) found = keystoreContainsFileForAccount(keyStoreDir, chatAddress)
require.True(t, found) require.True(t, found)
defaultSettings, err := defaultSettings(genAccInfo, derivedAccounts) defaultSettings, err := defaultSettings(genAccInfo.KeyUID, genAccInfo.Address, derivedAccounts)
require.NoError(t, err) require.NoError(t, err)
nodeConfig, err := defaultNodeConfig(defaultSettings.InstallationID, &requests.CreateAccount{ nodeConfig, err := defaultNodeConfig(defaultSettings.InstallationID, &requests.CreateAccount{
LogLevel: defaultSettings.LogLevel, LogLevel: defaultSettings.LogLevel,
@ -1135,7 +1136,7 @@ func TestConvertAccount(t *testing.T) {
err = backend.ensureAppDBOpened(account, password) err = backend.ensureAppDBOpened(account, password)
require.NoError(t, err) 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) require.NoError(t, err)
multiaccounts, err := backend.GetAccounts() multiaccounts, err := backend.GetAccounts()
require.NoError(t, err) require.NoError(t, err)
@ -1294,7 +1295,7 @@ func loginDesktopUser(t *testing.T, conf *params.NodeConfig) {
wg.Add(1) wg.Add(1)
go func() { go func() {
defer wg.Done() defer wg.Done()
err := b.StartNodeWithAccount(accounts[0], passwd, conf) err := b.StartNodeWithAccount(accounts[0], passwd, conf, nil)
require.NoError(t, err) require.NoError(t, err)
}() }()
@ -1682,3 +1683,168 @@ func TestCreateAccountPathsValidation(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, tmpdir, request.RootDataDir) 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)
}

View File

@ -21,6 +21,7 @@ import (
const pathWalletRoot = "m/44'/60'/0'/0" const pathWalletRoot = "m/44'/60'/0'/0"
const pathEIP1581 = "m/43'/60'/1581'" const pathEIP1581 = "m/43'/60'/1581'"
const pathDefaultChat = pathEIP1581 + "/0'/0" const pathDefaultChat = pathEIP1581 + "/0'/0"
const pathEncryption = pathEIP1581 + "/1'/0"
const pathDefaultWallet = pathWalletRoot + "/0" const pathDefaultWallet = pathWalletRoot + "/0"
const defaultMnemonicLength = 12 const defaultMnemonicLength = 12
const shardsTestClusterID = 16 const shardsTestClusterID = 16
@ -38,11 +39,11 @@ const DefaultListenAddr = ":0"
const DefaultMaxMessageDeliveryAttempts = 3 const DefaultMaxMessageDeliveryAttempts = 3
const DefaultVerifyTransactionChainID = 1 const DefaultVerifyTransactionChainID = 1
var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet} var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet, pathEncryption}
var DefaultFleet = params.FleetShardsTest 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 chatKeyString := derivedAddresses[pathDefaultChat].PublicKey
s := &settings.Settings{} s := &settings.Settings{}
@ -51,8 +52,8 @@ func defaultSettings(generatedAccountInfo generator.GeneratedAccountInfo, derive
s.LogLevel = &logLevel s.LogLevel = &logLevel
s.ProfilePicturesShowTo = settings.ProfilePicturesShowToEveryone s.ProfilePicturesShowTo = settings.ProfilePicturesShowToEveryone
s.ProfilePicturesVisibility = settings.ProfilePicturesVisibilityEveryone s.ProfilePicturesVisibility = settings.ProfilePicturesVisibilityEveryone
s.KeyUID = generatedAccountInfo.KeyUID s.KeyUID = keyUID
s.Address = types.HexToAddress(generatedAccountInfo.Address) s.Address = types.HexToAddress(address)
s.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address) s.WalletRootAddress = types.HexToAddress(derivedAddresses[pathWalletRoot].Address)
s.URLUnfurlingMode = settings.URLUnfurlingAlwaysAsk s.URLUnfurlingMode = settings.URLUnfurlingAlwaysAsk
@ -222,8 +223,11 @@ func defaultNodeConfig(installationID string, request *requests.CreateAccount, o
nodeConfig.LogDir = request.LogFilePath nodeConfig.LogDir = request.LogFilePath
nodeConfig.LogLevel = DefaultLogLevel nodeConfig.LogLevel = DefaultLogLevel
nodeConfig.DataDir = DefaultDataDir nodeConfig.DataDir = DefaultDataDir
nodeConfig.KeycardPairingDataFile = DefaultKeycardPairingDataFile
nodeConfig.ProcessBackedupMessages = false nodeConfig.ProcessBackedupMessages = false
nodeConfig.KeycardPairingDataFile = DefaultKeycardPairingDataFile
if request.KeycardPairingDataFile != nil {
nodeConfig.KeycardPairingDataFile = *request.KeycardPairingDataFile
}
if request.LogLevel != nil { if request.LogLevel != nil {
nodeConfig.LogLevel = *request.LogLevel nodeConfig.LogLevel = *request.LogLevel

View File

@ -2,9 +2,9 @@ package api
import ( import (
"context" "context"
"crypto/ecdsa"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math/big" "math/big"
"os" "os"
@ -15,8 +15,7 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"github.com/status-im/status-go/services/ens" "github.com/pkg/errors"
"github.com/status-im/status-go/sqlite"
"github.com/imdario/mergo" "github.com/imdario/mergo"
@ -48,10 +47,13 @@ import (
"github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/rpc" "github.com/status-im/status-go/rpc"
"github.com/status-im/status-go/server/pairing/statecontrol" "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/ext"
"github.com/status-im/status-go/services/personal" "github.com/status-im/status-go/services/personal"
"github.com/status-im/status-go/services/typeddata" "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/signal"
"github.com/status-im/status-go/sqlite"
"github.com/status-im/status-go/transactions" "github.com/status-im/status-go/transactions"
"github.com/status-im/status-go/walletdatabase" "github.com/status-im/status-go/walletdatabase"
) )
@ -454,11 +456,8 @@ func (b *GethStatusBackend) setupLogSettings() error {
return nil return nil
} }
// StartNodeWithKey instead of loading addresses from database this method derives address from key // Deprecated: Use StartNodeWithAccount instead.
// and uses it in application. func (b *GethStatusBackend) StartNodeWithKey(acc multiaccounts.Account, password string, keyHex string, nodecfg *params.NodeConfig) error {
// 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 {
if acc.KDFIterations == 0 { if acc.KDFIterations == 0 {
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(acc.KeyUID) kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(acc.KeyUID)
if err != nil { if err != nil {
@ -468,83 +467,21 @@ func (b *GethStatusBackend) startNodeWithKey(acc multiaccounts.Account, password
acc.KDFIterations = kdfIterations 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) chatKey, err := ethcrypto.HexToECDSA(keyHex)
if err != nil { if err != nil {
return err 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.startNodeWithAccount(acc, password, nodecfg, chatKey)
err := b.startNodeWithKey(acc, password, keyHex, nodecfg)
if err != nil { if err != nil {
// Stop node for clean up // Stop node for clean up
_ = b.StopNode() _ = b.StopNode()
return err
} }
// get logged in // get logged in
if !b.LocalPairingStateManager.IsPairing() { if b.LocalPairingStateManager.IsPairing() {
return b.LoggedIn(acc.KeyUID, err) return nil
} }
return nil return b.LoggedIn(acc.KeyUID, err)
} }
func (b *GethStatusBackend) OverwriteNodeConfigValues(conf *params.NodeConfig, n *params.NodeConfig) (*params.NodeConfig, error) { 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 // Stop node for clean up
_ = b.StopNode() _ = b.StopNode()
} }
if b.LocalPairingStateManager.IsPairing() {
return nil
}
return b.LoggedIn(request.KeyUID, err) return b.LoggedIn(request.KeyUID, err)
} }
@ -603,7 +543,20 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
return err 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{ acc := multiaccounts.Account{
KeyUID: request.KeyUID, KeyUID: request.KeyUID,
@ -614,9 +567,9 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber
} }
err := b.ensureDBsOpened(acc, password) err := b.ensureDBsOpened(acc, request.Password)
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to open database")
} }
defaultCfg := &params.NodeConfig{ defaultCfg := &params.NodeConfig{
@ -626,14 +579,14 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
defaultCfg.WalletConfig = buildWalletConfig(&request.WalletSecretsConfig) defaultCfg.WalletConfig = buildWalletConfig(&request.WalletSecretsConfig)
err = b.UpdateNodeConfigFleet(acc, password, defaultCfg) err = b.UpdateNodeConfigFleet(acc, request.Password, defaultCfg)
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to update node config fleet")
} }
err = b.loadNodeConfig(defaultCfg) err = b.loadNodeConfig(defaultCfg)
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to load node config")
} }
if request.RuntimeLogLevel != "" { if request.RuntimeLogLevel != "" {
@ -650,34 +603,34 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
err = b.setupLogSettings() err = b.setupLogSettings()
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to setup log settings")
} }
accountsDB, err := accounts.NewDB(b.appDB) accountsDB, err := accounts.NewDB(b.appDB)
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to create accounts db")
} }
multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB) multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB)
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to update account color hash and color id")
} }
b.account = multiAccount b.account = multiAccount
chatAddr, err := accountsDB.GetChatAddress() chatAddr, err := accountsDB.GetChatAddress()
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to get chat address")
} }
walletAddr, err := accountsDB.GetWalletAddress() walletAddr, err := accountsDB.GetWalletAddress()
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to get wallet address")
} }
watchAddrs, err := accountsDB.GetWalletAddresses() watchAddrs, err := accountsDB.GetWalletAddresses()
if err != nil { if err != nil {
return err return errors.Wrap(err, "failed to get wallet addresses")
} }
login := account.LoginParams{ login := account.LoginParams{
Password: password, Password: request.Password,
ChatAddress: chatAddr, ChatAddress: chatAddr,
WatchAddresses: watchAddrs, WatchAddresses: watchAddrs,
MainAccount: walletAddr, MainAccount: walletAddr,
@ -686,17 +639,35 @@ func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
err = b.StartNode(b.config) err = b.StartNode(b.config)
if err != nil { if err != nil {
b.log.Info("failed to start node") b.log.Info("failed to start node")
return err return errors.Wrap(err, "failed to start node")
} }
err = b.SelectAccount(login) if chatKey := request.ChatPrivateKey(); chatKey == nil {
if err != nil { err = b.SelectAccount(login)
return err 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()) err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
if err != nil { if err != nil {
b.log.Info("failed to update account") b.log.Error("failed to update account")
return err return errors.Wrap(err, "failed to update account")
} }
return nil return nil
@ -737,7 +708,8 @@ func (b *GethStatusBackend) UpdateNodeConfigFleet(acc multiaccounts.Account, pas
return nil 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) err := b.ensureDBsOpened(acc, password)
if err != nil { if err != nil {
return err return err
@ -793,10 +765,29 @@ func (b *GethStatusBackend) startNodeWithAccount(acc multiaccounts.Account, pass
return err return err
} }
err = b.SelectAccount(login) if chatKey == nil {
if err != nil { // Load account from keystore
return err 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()) err = b.multiaccountsDB.UpdateAccountTimestamp(acc.KeyUID, time.Now().Unix())
if err != nil { if err != nil {
b.log.Info("failed to update account") 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 { 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 { func (b *GethStatusBackend) StartNodeWithAccount(acc multiaccounts.Account, password string, nodecfg *params.NodeConfig, chatKey *ecdsa.PrivateKey) error {
err := b.startNodeWithAccount(acc, password, nodecfg) err := b.startNodeWithAccount(acc, password, nodecfg, chatKey)
if err != nil { if err != nil {
// Stop node for clean up // Stop node for clean up
_ = b.StopNode() _ = 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 // 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 // and should be handled gracefully here if it makes sense to run dummy node after
// logout // logout
_ = b.startNodeWithAccount(*account, password, nil) _ = b.startNodeWithAccount(*account, password, nil, nil)
} else { } 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 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 { if err != nil {
return nil, err 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 { if err != nil {
b.log.Error("start node", err) b.log.Error("start node", err)
return nil, 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) { func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error) {
@ -1319,44 +1383,104 @@ func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error)
return info.KeyUID, nil 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) info, err := b.generateAccountInfo(mnemonic)
if err != nil { if err != nil {
return nil, nil, nil, nil, err return nil, err
} }
keyStoreDir, err := b.initKeyStoreDirWithAccount(request.RootDataDir, info.KeyUID) keyStoreDir, err := b.initKeyStoreDirWithAccount(request.RootDataDir, info.KeyUID)
if err != nil { if err != nil {
return nil, nil, nil, nil, err return nil, err
}
account, info, err := b.generateAccount(*info, customizationColorClock, request)
if err != nil {
return nil, nil, nil, nil, err
} }
derivedAddresses, err := b.getDerivedAddresses(info.ID) derivedAddresses, err := b.getDerivedAddresses(info.ID)
if err != nil { 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 { if err != nil {
return nil, nil, nil, nil, err return nil, errors.Wrap(err, "failed to build account")
} }
processBackedupMessages := mnemonic != "" && fetchBackup response.settings, err = b.prepareSettings(request, input)
nodeConfig, err := b.prepareConfig(processBackedupMessages, account.KeyUID, keyStoreDir, request, opts...)
if err != nil { 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 { 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) { func (b *GethStatusBackend) initKeyStoreDirWithAccount(rootDataDir, keyUID string) (string, error) {
@ -1391,36 +1515,39 @@ func (b *GethStatusBackend) generateAccountInfo(mnemonic string) (*generator.Gen
return &info, nil return &info, nil
} }
func (b *GethStatusBackend) generateAccount(info generator.GeneratedAccountInfo, customizationColorClock uint64, request *requests.CreateAccount) (*multiaccounts.Account, *generator.GeneratedAccountInfo, error) { func (b *GethStatusBackend) storeAccount(id string, password string, paths []string) error {
err := b.OpenAccounts()
if err != nil {
b.log.Error("failed open accounts", "err", err)
return nil, nil, err
}
accountGenerator := b.accountManager.AccountsGenerator() accountGenerator := b.accountManager.AccountsGenerator()
_, err = accountGenerator.StoreAccount(info.ID, request.Password) _, err := accountGenerator.StoreAccount(id, password)
if err != nil { 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 { if err != nil {
return nil, nil, err return err
} }
account := multiaccounts.Account{ return nil
KeyUID: info.KeyUID, }
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, Name: request.DisplayName,
CustomizationColor: multiacccommon.CustomizationColor(request.CustomizationColor), CustomizationColor: multiacccommon.CustomizationColor(request.CustomizationColor),
CustomizationColorClock: customizationColorClock, CustomizationColorClock: input.customizationColorClock,
KDFIterations: request.KdfIterations, KDFIterations: request.KdfIterations,
Timestamp: time.Now().Unix(), Timestamp: time.Now().Unix(),
} }
if account.KDFIterations == 0 { if acc.KDFIterations == 0 {
account.KDFIterations = dbsetup.ReducedKDFIterationsNumber acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber
} }
if request.ImagePath != "" { if request.ImagePath != "" {
@ -1439,16 +1566,16 @@ func (b *GethStatusBackend) generateAccount(info generator.GeneratedAccountInfo,
imageCropRectangle.Ax, imageCropRectangle.Ay, imageCropRectangle.Bx, imageCropRectangle.By) imageCropRectangle.Ax, imageCropRectangle.Ay, imageCropRectangle.Bx, imageCropRectangle.By)
if err != nil { 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) { func (b *GethStatusBackend) prepareSettings(request *requests.CreateAccount, input *prepareAccountInput) (*settings.Settings, error) {
settings, err := defaultSettings(info, derivedAddresses) settings, err := defaultSettings(input.keyUID, input.address, input.derivedAddresses)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -1459,9 +1586,8 @@ func (b *GethStatusBackend) prepareSettings(info generator.GeneratedAccountInfo,
settings.CurrentNetwork = request.CurrentNetwork settings.CurrentNetwork = request.CurrentNetwork
settings.TestNetworksEnabled = request.TestNetworksEnabled settings.TestNetworksEnabled = request.TestNetworksEnabled
// If restoring an account, we don't set the mnemonic if !input.restoringAccount {
if mnemonic == "" { settings.Mnemonic = &input.mnemonic
settings.Mnemonic = &info.Mnemonic
settings.OmitTransfersHistoryScan = true settings.OmitTransfersHistoryScan = true
// TODO(rasom): uncomment it as soon as address will be properly // TODO(rasom): uncomment it as soon as address will be properly
// marked as shown on mobile client // marked as shown on mobile client
@ -1471,41 +1597,38 @@ func (b *GethStatusBackend) prepareSettings(info generator.GeneratedAccountInfo,
return settings, nil return settings, nil
} }
func (b *GethStatusBackend) prepareConfig(processBackedupMessages bool, installationID string, userKeyStoreDir string, request *requests.CreateAccount, opts ...params.Option) (*params.NodeConfig, error) { func (b *GethStatusBackend) prepareConfig(request *requests.CreateAccount, input *prepareAccountInput, installationID string) (*params.NodeConfig, error) {
nodeConfig, err := defaultNodeConfig(installationID, request, opts...) nodeConfig, err := defaultNodeConfig(installationID, request, input.opts...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
nodeConfig.ProcessBackedupMessages = processBackedupMessages nodeConfig.ProcessBackedupMessages = input.fetchBackup
// when we set nodeConfig.KeyStoreDir, value of nodeConfig.KeyStoreDir should not contain the rootDataDir // when we set nodeConfig.KeyStoreDir, value of nodeConfig.KeyStoreDir should not contain the rootDataDir
// loadNodeConfig will add rootDataDir to nodeConfig.KeyStoreDir // loadNodeConfig will add rootDataDir to nodeConfig.KeyStoreDir
nodeConfig.KeyStoreDir = userKeyStoreDir nodeConfig.KeyStoreDir = input.keyStoreDir
return nodeConfig, nil return nodeConfig, nil
} }
func (b *GethStatusBackend) prepareSubAccounts(mnemonic, keyUID string, derivedAddresses map[string]generator.AccountInfo, request *requests.CreateAccount) ([]*accounts.Account, error) { func (b *GethStatusBackend) prepareSubAccounts(request *requests.CreateAccount, input *prepareAccountInput) ([]*accounts.Account, error) {
walletDerivedAccount := derivedAddresses[pathDefaultWallet] walletDerivedAccount := input.derivedAddresses[pathDefaultWallet]
walletAccount := &accounts.Account{ walletAccount := &accounts.Account{
PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey), PublicKey: types.Hex2Bytes(walletDerivedAccount.PublicKey),
KeyUID: keyUID, KeyUID: input.keyUID,
Address: types.HexToAddress(walletDerivedAccount.Address), Address: types.HexToAddress(walletDerivedAccount.Address),
ColorID: multiacccommon.CustomizationColor(request.CustomizationColor), ColorID: multiacccommon.CustomizationColor(request.CustomizationColor),
Emoji: request.Emoji, Emoji: request.Emoji,
Wallet: true, Wallet: true,
Path: pathDefaultWallet, Path: pathDefaultWallet,
Name: walletAccountDefaultName, Name: walletAccountDefaultName,
AddressWasNotShown: !input.restoringAccount,
} }
if mnemonic == "" { chatDerivedAccount := input.derivedAddresses[pathDefaultChat]
walletAccount.AddressWasNotShown = true
}
chatDerivedAccount := derivedAddresses[pathDefaultChat]
chatAccount := &accounts.Account{ chatAccount := &accounts.Account{
PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey), PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey),
KeyUID: keyUID, KeyUID: input.keyUID,
Address: types.HexToAddress(chatDerivedAccount.Address), Address: types.HexToAddress(chatDerivedAccount.Address),
Name: request.DisplayName, Name: request.DisplayName,
Chat: true, Chat: true,
@ -1515,14 +1638,40 @@ func (b *GethStatusBackend) prepareSubAccounts(mnemonic, keyUID string, derivedA
return []*accounts.Account{walletAccount, chatAccount}, nil return []*accounts.Account{walletAccount, chatAccount}, nil
} }
func (b *GethStatusBackend) getDerivedAddresses(id string) (map[string]generator.AccountInfo, error) { func (b *GethStatusBackend) prepareForKeycard(request *requests.CreateAccount, input *prepareAccountInput, response *accountBundle) (*accountBundle, error) {
accountGenerator := b.accountManager.AccountsGenerator() if request.KeycardInstanceUID == "" {
derivedAddresses, err := accountGenerator.DeriveAddresses(id, paths) return response, nil
if err != nil {
return nil, err
} }
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. // CreateAccountAndLogin creates a new account and logs in with it.
@ -1535,18 +1684,26 @@ func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccoun
return nil, err 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 { if err != nil {
return nil, err 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 { if err != nil {
b.log.Error("start node", err) b.log.Error("start node", err)
return nil, err return nil, err
} }
return account, nil return response.account, nil
} }
func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error { func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error {
@ -1708,7 +1865,15 @@ func enrichMultiAccountByPublicKey(account *multiaccounts.Account, publicKey typ
return nil 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) err := enrichMultiAccountBySubAccounts(&account, subaccs)
if err != nil { if err != nil {
return err return err
@ -1731,15 +1896,15 @@ func (b *GethStatusBackend) SaveAccountAndStartNodeWithKey(account multiaccounts
// StartNodeWithAccountAndInitialConfig is used after account and config was generated. // 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 // 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. // status-go side this flow can be simplified.
// TODO: Consider passing accountBundle here directly
func (b *GethStatusBackend) StartNodeWithAccountAndInitialConfig( func (b *GethStatusBackend) StartNodeWithAccountAndInitialConfig(
account multiaccounts.Account, account multiaccounts.Account,
password string, password string,
settings settings.Settings, settings settings.Settings,
nodecfg *params.NodeConfig, nodecfg *params.NodeConfig,
subaccs []*accounts.Account, subaccs []*accounts.Account,
chatKey *ecdsa.PrivateKey,
) error { ) error {
b.log.Info("node config", "config", nodecfg)
err := enrichMultiAccountBySubAccounts(&account, subaccs) err := enrichMultiAccountBySubAccounts(&account, subaccs)
if err != nil { if err != nil {
return err return err
@ -1756,7 +1921,7 @@ func (b *GethStatusBackend) StartNodeWithAccountAndInitialConfig(
if err != nil { if err != nil {
return err 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 // TODO: change in `saveAccountsAndSettings` function param `subaccs []*accounts.Account` parameter to `profileKeypair *accounts.Keypair` parameter

View File

@ -22,6 +22,8 @@ import (
"github.com/status-im/status-go/protocol/requests" "github.com/status-im/status-go/protocol/requests"
"github.com/status-im/status-go/protocol/tt" "github.com/status-im/status-go/protocol/tt"
"github.com/status-im/status-go/services/utils" "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/status-im/status-go/wakuv2"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
@ -29,6 +31,7 @@ import (
type MessengerRawMessageResendTest struct { type MessengerRawMessageResendTest struct {
suite.Suite suite.Suite
logger *zap.Logger
aliceBackend *GethStatusBackend aliceBackend *GethStatusBackend
bobBackend *GethStatusBackend bobBackend *GethStatusBackend
aliceMessenger *protocol.Messenger aliceMessenger *protocol.Messenger
@ -43,9 +46,14 @@ func TestMessengerRawMessageResendTestSuite(t *testing.T) {
} }
func (s *MessengerRawMessageResendTest) SetupTest() { func (s *MessengerRawMessageResendTest) SetupTest() {
logger, err := zap.NewDevelopment() tutils.Init()
var err error
s.logger, err = zap.NewDevelopment()
s.Require().NoError(err) s.Require().NoError(err)
signal.SetMobileSignalHandler(nil)
exchangeNodeConfig := &wakuv2.Config{ exchangeNodeConfig := &wakuv2.Config{
Port: 0, Port: 0,
EnableDiscV5: true, EnableDiscV5: true,
@ -54,7 +62,7 @@ func (s *MessengerRawMessageResendTest) SetupTest() {
UseShardAsDefaultTopic: true, UseShardAsDefaultTopic: true,
DefaultShardPubsubTopic: shard.DefaultShardPubsubTopic(), 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(err)
s.Require().NoError(s.exchangeBootNode.Start()) s.Require().NoError(s.exchangeBootNode.Start())

View File

@ -449,7 +449,7 @@ func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error {
fmt.Println(nodeConfig) fmt.Println(nodeConfig)
accounts := []*accounts.Account{walletAccount, chatAccount} accounts := []*accounts.Account{walletAccount, chatAccount}
err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts) err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts, nil)
if err != nil { if err != nil {
logger.Error("start node", err) logger.Error("start node", err)
return err return err

View File

@ -498,7 +498,7 @@ func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error {
fmt.Println(nodeConfig) fmt.Println(nodeConfig)
accounts := []*accounts.Account{walletAccount, chatAccount} accounts := []*accounts.Account{walletAccount, chatAccount}
err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts) err = backend.StartNodeWithAccountAndInitialConfig(account, "", *settings, nodeConfig, accounts, nil)
if err != nil { if err != nil {
logger.Error("start node", err) logger.Error("start node", err)
return err return err

View File

@ -420,9 +420,9 @@ func ImportAccount(seedPhrase string, backend *api.GethStatusBackend) error {
fmt.Println(nodeConfig) fmt.Println(nodeConfig)
accounts := []*accounts.Account{walletAccount, chatAccount} accounts := []*accounts.Account{walletAccount, chatAccount}
if !exist { 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) { func retrieveMessagesLoop(messenger *protocol.Messenger, tick time.Duration) {

View File

@ -18,7 +18,7 @@ type Key struct {
// ExtendedKey is the extended key of the PrivateKey itself, and it's used // ExtendedKey is the extended key of the PrivateKey itself, and it's used
// to derive child keys. // to derive child keys.
ExtendedKey *extkeys.ExtendedKey ExtendedKey *extkeys.ExtendedKey
// SubAccountIndex is DEPRECATED // Deprecated: SubAccountIndex
// It was use in Status to keep track of the number of sub-account created // It was use in Status to keep track of the number of sub-account created
// before having multi-account support. // before having multi-account support.
SubAccountIndex uint32 SubAccountIndex uint32

View File

@ -237,7 +237,7 @@ func login(accountData, password, configJSON string) error {
return statusBackend.LoggedIn(account.KeyUID, err) return statusBackend.LoggedIn(account.KeyUID, err)
} }
err = statusBackend.StartNodeWithAccount(account, password, &conf) err = statusBackend.StartNodeWithAccount(account, password, &conf, nil)
if err != nil { if err != nil {
log.Error("failed to start a node", "key-uid", account.KeyUID, "error", err) log.Error("failed to start a node", "key-uid", account.KeyUID, "error", err)
return err return err
@ -339,7 +339,13 @@ func RestoreAccountAndLogin(requestJSON string) string {
api.RunAsync(func() error { api.RunAsync(func() error {
log.Debug("starting a node and restoring account") 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 { if err != nil {
log.Error("failed to restore account", "error", err) log.Error("failed to restore account", "error", err)
return err return err
@ -347,6 +353,7 @@ func RestoreAccountAndLogin(requestJSON string) string {
log.Debug("started a node, and restored account") log.Debug("started a node, and restored account")
return nil return nil
}) })
return makeJSONResponse(nil) return makeJSONResponse(nil)
} }
@ -382,7 +389,7 @@ func SaveAccountAndLogin(accountData, password, settingsJSON, configJSON, subacc
api.RunAsync(func() error { api.RunAsync(func() error {
log.Debug("starting a node, and saving account with configuration", "key-uid", account.KeyUID) 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 { if err != nil {
log.Error("failed to start node and save account", "key-uid", account.KeyUID, "error", err) log.Error("failed to start node and save account", "key-uid", account.KeyUID, "error", err)
return err return err
@ -419,7 +426,8 @@ func InitKeystore(keydir string) string {
return makeJSONResponse(err) 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 { func SaveAccountAndLoginWithKeycard(accountData, password, settingsJSON, configJSON, subaccountData string, keyHex string) string {
var account multiaccounts.Account var account multiaccounts.Account
err := json.Unmarshal([]byte(accountData), &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. // 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. // 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 { func LoginWithKeycard(accountData, password, keyHex string, configJSON string) string {
var account multiaccounts.Account var account multiaccounts.Account
err := json.Unmarshal([]byte(accountData), &account) err := json.Unmarshal([]byte(accountData), &account)

View File

@ -328,6 +328,9 @@ type NodeConfig struct {
KeyStoreDir string `validate:"required"` KeyStoreDir string `validate:"required"`
// KeycardPairingDataFile is the file where we keep keycard pairings data. // 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. // note: this field won't be saved into db, it's local to the device.
KeycardPairingDataFile string KeycardPairingDataFile string

View File

@ -74,6 +74,9 @@ type CreateAccount struct {
TelemetryServerURL string `json:"telemetryServerURL"` TelemetryServerURL string `json:"telemetryServerURL"`
APIConfig *APIConfig `json:"apiConfig"` APIConfig *APIConfig `json:"apiConfig"`
KeycardInstanceUID string `json:"keycardInstanceUID"`
KeycardPairingDataFile *string `json:"keycardPairingDataFile"`
} }
type WalletSecretsConfig struct { type WalletSecretsConfig struct {

View File

@ -1,18 +1,38 @@
package requests 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 { type Login struct {
Password string `json:"password"` Password string `json:"password"`
KeyUID string `json:"keyUid"` KeyUID string `json:"keyUid"`
KdfIterations int `json:"kdfIterations"` KdfIterations int `json:"kdfIterations"` // FIXME: KdfIterations should be loaded from multiaccounts db.
RuntimeLogLevel string `json:"runtimeLogLevel"` RuntimeLogLevel string `json:"runtimeLogLevel"`
WakuV2Nameserver string `json:"wakuV2Nameserver"` WakuV2Nameserver string `json:"wakuV2Nameserver"`
BandwidthStatsEnabled bool `json:"bandwidthStatsEnabled"` 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 WalletSecretsConfig
} }
@ -20,5 +40,24 @@ func (c *Login) Validate() error {
if c.KeyUID == "" { if c.KeyUID == "" {
return ErrLoginInvalidKeyUID return ErrLoginInvalidKeyUID
} }
if c.KeycardWhisperPrivateKey != "" {
_, err := parsePrivateKey(c.KeycardWhisperPrivateKey)
if err != nil {
return ErrLoginInvalidKeycardWhisperPrivateKey
}
}
return nil 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)
}

View File

@ -4,20 +4,46 @@ import (
"errors" "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 { type RestoreAccount struct {
Mnemonic string `json:"mnemonic"` Mnemonic string `json:"mnemonic"`
FetchBackup bool `json:"fetchBackup"`
// 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 CreateAccount
} }
func (c *RestoreAccount) Validate() error { func (c *RestoreAccount) Validate() error {
if len(c.Mnemonic) == 0 { if len(c.Mnemonic) == 0 && c.Keycard == nil {
return ErrRestoreAccountInvalidMnemonic return ErrRestoreAccountInvalidMnemonic
} }
if len(c.Mnemonic) > 0 && c.Keycard != nil {
return ErrRestoreAccountMnemonicAndKeycard
}
return c.CreateAccount.Validate(&CreateAccountValidation{ return c.CreateAccount.Validate(&CreateAccountValidation{
AllowEmptyDisplayName: true, 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"`
}

View File

@ -2,8 +2,12 @@ package pairing
import ( import (
"context" "context"
"crypto/ecdsa"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/status-im/status-go/api" "github.com/status-im/status-go/api"
"github.com/status-im/status-go/multiaccounts/accounts" "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 // because client don't know keyUID before received data, we need help client to update keystore dir
keystoreDir := filepath.Join(nodeConfig.KeyStoreDir, account.KeyUID) keystoreDir := filepath.Join(nodeConfig.KeyStoreDir, account.KeyUID)
nodeConfig.KeyStoreDir = keystoreDir nodeConfig.KeyStoreDir = keystoreDir
if accountPayload.exist {
if len(accountPayload.chatKey) == 0 { var chatKey *ecdsa.PrivateKey
err = s.backend.StartNodeWithAccount(*account, accountPayload.password, nodeConfig) if accountPayload.chatKey != "" {
} else { chatKeyHex := strings.Trim(accountPayload.chatKey, "0x")
err = s.backend.StartNodeWithKey(*account, accountPayload.password, accountPayload.chatKey, nodeConfig) chatKey, err = ethcrypto.HexToECDSA(chatKeyHex)
if err != nil {
return err
} }
}
if accountPayload.exist {
err = s.backend.StartNodeWithAccount(*account, accountPayload.password, nodeConfig, chatKey)
} else { } else {
accountManager := s.backend.AccountManager() accountManager := s.backend.AccountManager()
err = accountManager.InitKeystore(filepath.Join(nodeConfig.RootDataDir, keystoreDir)) 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.InstallationID = nodeConfig.ShhextConfig.InstallationID
rmp.setting.CurrentNetwork = settingCurrentNetwork rmp.setting.CurrentNetwork = settingCurrentNetwork
if len(accountPayload.chatKey) == 0 { err = s.backend.StartNodeWithAccountAndInitialConfig(*account, accountPayload.password, *rmp.setting, nodeConfig, rmp.profileKeypair.Accounts, chatKey)
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)
}
} }
if err != nil { if err != nil {
return err return err

View File

@ -140,7 +140,7 @@ func (s *SyncDeviceSuite) prepareBackendWithAccount(mnemonic, tmpdir string) *ap
} }
accounts := []*accounts.Account{walletAccount, chatAccount} 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) require.NoError(s.T(), err)
multiaccounts, err := backend.GetAccounts() multiaccounts, err := backend.GetAccounts()
require.NoError(s.T(), err) require.NoError(s.T(), err)

View File

@ -1,6 +1,7 @@
package wallet package wallet
import ( import (
"encoding/json"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath" "path/filepath"
@ -10,6 +11,11 @@ type KeycardPairings struct {
pairingsFile string pairingsFile string
} }
type KeycardPairing struct {
Key string `json:"key"`
Index int `json:"index"`
}
func NewKeycardPairings() *KeycardPairings { func NewKeycardPairings() *KeycardPairings {
return &KeycardPairings{} return &KeycardPairings{}
} }
@ -43,3 +49,22 @@ func (kp *KeycardPairings) SetPairingsJSONFileContent(content []byte) error {
return ioutil.WriteFile(kp.pairingsFile, content, 0600) 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
}