status-go/api/geth_backend.go

2742 lines
75 KiB
Go
Raw Normal View History

package api
import (
"context"
"crypto/ecdsa"
"database/sql"
"encoding/json"
"fmt"
"math/big"
2020-07-13 10:45:36 +00:00
"os"
"path/filepath"
2021-08-13 07:24:33 +00:00
"strings"
"sync"
"time"
"go.uber.org/zap"
"github.com/pkg/errors"
"github.com/imdario/mergo"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
ethcrypto "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/log"
2022-10-25 14:25:08 +00:00
signercore "github.com/ethereum/go-ethereum/signer/core/apitypes"
2020-01-02 09:10:19 +00:00
"github.com/status-im/status-go/account"
2023-03-21 17:02:04 +00:00
"github.com/status-im/status-go/account/generator"
"github.com/status-im/status-go/appdatabase"
"github.com/status-im/status-go/common/dbsetup"
2021-05-14 10:55:42 +00:00
"github.com/status-im/status-go/connection"
"github.com/status-im/status-go/eth-node/crypto"
"github.com/status-im/status-go/eth-node/types"
"github.com/status-im/status-go/images"
"github.com/status-im/status-go/logutils"
"github.com/status-im/status-go/multiaccounts"
"github.com/status-im/status-go/multiaccounts/accounts"
multiacccommon "github.com/status-im/status-go/multiaccounts/common"
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
"github.com/status-im/status-go/multiaccounts/settings"
"github.com/status-im/status-go/node"
"github.com/status-im/status-go/nodecfg"
"github.com/status-im/status-go/params"
"github.com/status-im/status-go/protocol"
identityutils "github.com/status-im/status-go/protocol/identity"
"github.com/status-im/status-go/protocol/identity/colorhash"
2023-03-16 14:49:25 +00:00
"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"
2022-02-10 15:15:27 +00:00
"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"
)
var (
// ErrWhisperClearIdentitiesFailure clearing whisper identities has failed.
ErrWhisperClearIdentitiesFailure = errors.New("failed to clear whisper identities")
// ErrWhisperIdentityInjectionFailure injecting whisper identities has failed.
ErrWhisperIdentityInjectionFailure = errors.New("failed to inject identity into Whisper")
// ErrWakuIdentityInjectionFailure injecting whisper identities has failed.
ErrWakuIdentityInjectionFailure = errors.New("failed to inject identity into waku")
// ErrUnsupportedRPCMethod is for methods not supported by the RPC interface
ErrUnsupportedRPCMethod = errors.New("method is unsupported by RPC interface")
// ErrRPCClientUnavailable is returned if an RPC client can't be retrieved.
// This is a normal situation when a node is stopped.
ErrRPCClientUnavailable = errors.New("JSON-RPC client is unavailable")
// ErrDBNotAvailable is returned if a method is called before the DB is available for usage
ErrDBNotAvailable = errors.New("DB is unavailable")
// ErrConfigNotAvailable is returned if a method is called before the nodeconfig is set
ErrConfigNotAvailable = errors.New("NodeConfig is not available")
)
var _ StatusBackend = (*GethStatusBackend)(nil)
// GethStatusBackend implements the Status.im service over go-ethereum
type GethStatusBackend struct {
mu sync.Mutex
// rootDataDir is the same for all networks.
rootDataDir string
appDB *sql.DB
walletDB *sql.DB
config *params.NodeConfig
statusNode *node.StatusNode
personalAPI *personal.PublicAPI
multiaccountsDB *multiaccounts.Database
account *multiaccounts.Account
accountManager *account.GethManager
transactor *transactions.Transactor
connectionState connection.State
appState appState
selectedAccountKeyID string
log log.Logger
allowAllRPC bool // used only for tests, disables api method restrictions
LocalPairingStateManager *statecontrol.ProcessStateManager
}
// NewGethStatusBackend create a new GethStatusBackend instance
func NewGethStatusBackend() *GethStatusBackend {
defer log.Info("Status backend initialized", "backend", "geth", "version", params.Version, "commit", params.GitCommit, "IpfsGatewayURL", params.IpfsGatewayURL)
2021-07-07 06:11:09 +00:00
backend := &GethStatusBackend{}
backend.initialize()
return backend
}
func (b *GethStatusBackend) initialize() {
2019-11-27 12:22:23 +00:00
accountManager := account.NewGethManager()
transactor := transactions.NewTransactor()
personalAPI := personal.NewAPI()
2021-12-21 15:44:37 +00:00
statusNode := node.New(transactor)
2021-07-07 06:11:09 +00:00
b.statusNode = statusNode
b.accountManager = accountManager
b.transactor = transactor
b.personalAPI = personalAPI
b.statusNode.SetMultiaccountsDB(b.multiaccountsDB)
b.log = log.New("package", "status-go/api.GethStatusBackend")
b.LocalPairingStateManager = new(statecontrol.ProcessStateManager)
b.LocalPairingStateManager.SetPairing(false)
}
// StatusNode returns reference to node manager
func (b *GethStatusBackend) StatusNode() *node.StatusNode {
return b.statusNode
}
// AccountManager returns reference to account manager
func (b *GethStatusBackend) AccountManager() *account.GethManager {
return b.accountManager
}
// Transactor returns reference to a status transactor
func (b *GethStatusBackend) Transactor() *transactions.Transactor {
return b.transactor
}
// SelectedAccountKeyID returns a Whisper key ID of the selected chat key pair.
func (b *GethStatusBackend) SelectedAccountKeyID() string {
return b.selectedAccountKeyID
}
// IsNodeRunning confirm that node is running
func (b *GethStatusBackend) IsNodeRunning() bool {
return b.statusNode.IsRunning()
}
// StartNode start Status node, fails if node is already started
func (b *GethStatusBackend) StartNode(config *params.NodeConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.startNode(config); err != nil {
signal.SendNodeCrashed(err)
return err
}
return nil
}
func (b *GethStatusBackend) UpdateRootDataDir(datadir string) {
b.mu.Lock()
defer b.mu.Unlock()
b.rootDataDir = datadir
}
func (b *GethStatusBackend) GetMultiaccountDB() *multiaccounts.Database {
return b.multiaccountsDB
}
func (b *GethStatusBackend) InitializeAccounts(rootDirectory string) error {
b.UpdateRootDataDir(rootDirectory)
manager := b.AccountManager()
keystoreDir := filepath.Join(rootDirectory, keystoreRelativePath)
if err := manager.InitKeystore(keystoreDir); err != nil {
return err
}
return b.OpenAccounts()
}
func (b *GethStatusBackend) OpenAccounts() error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB != nil {
return nil
}
db, err := multiaccounts.InitializeDB(filepath.Join(b.rootDataDir, "accounts.sql"))
if err != nil {
2020-10-13 12:32:47 +00:00
b.log.Error("failed to initialize accounts db", "err", err)
return err
}
b.multiaccountsDB = db
2021-07-07 06:11:09 +00:00
// Probably we should iron out a bit better how to create/dispose of the status-service
b.statusNode.SetMultiaccountsDB(db)
err = b.statusNode.StartMediaServerWithoutDB()
if err != nil {
b.log.Error("failed to start media server without app db", "err", err)
return err
}
return nil
}
func (b *GethStatusBackend) GetAccounts() ([]multiaccounts.Account, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return nil, errors.New("accounts db wasn't initialized")
}
return b.multiaccountsDB.GetAccounts()
}
func (b *GethStatusBackend) getAccountByKeyUID(keyUID string) (*multiaccounts.Account, error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return nil, errors.New("accounts db wasn't initialized")
}
as, err := b.multiaccountsDB.GetAccounts()
if err != nil {
return nil, err
}
for _, acc := range as {
if acc.KeyUID == keyUID {
return &acc, nil
}
}
return nil, fmt.Errorf("account with keyUID %s not found", keyUID)
}
func (b *GethStatusBackend) SaveAccount(account multiaccounts.Account) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return errors.New("accounts db wasn't initialized")
}
return b.multiaccountsDB.SaveAccount(account)
}
2022-07-06 16:12:49 +00:00
func (b *GethStatusBackend) DeleteMultiaccount(keyUID string, keyStoreDir string) error {
2020-07-13 10:45:36 +00:00
b.mu.Lock()
defer b.mu.Unlock()
if b.multiaccountsDB == nil {
return errors.New("accounts db wasn't initialized")
}
err := b.multiaccountsDB.DeleteAccount(keyUID)
if err != nil {
return err
}
appDbPath, err := b.getAppDBPath(keyUID)
if err != nil {
return err
}
walletDbPath, err := b.getWalletDBPath(keyUID)
if err != nil {
return err
}
2020-07-13 10:45:36 +00:00
dbFiles := []string{
filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql-shm", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql-wal", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db-shm", keyUID)),
filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db-wal", keyUID)),
appDbPath,
appDbPath + "-shm",
appDbPath + "-wal",
walletDbPath,
walletDbPath + "-shm",
walletDbPath + "-wal",
2020-07-13 10:45:36 +00:00
}
for _, path := range dbFiles {
if _, err := os.Stat(path); err == nil {
err = os.Remove(path)
if err != nil {
return err
}
}
}
if b.account != nil && b.account.KeyUID == keyUID {
// reset active account
b.account = nil
}
2020-07-13 10:45:36 +00:00
return os.RemoveAll(keyStoreDir)
}
2021-08-13 07:24:33 +00:00
func (b *GethStatusBackend) DeleteImportedKey(address, password, keyStoreDir string) error {
b.mu.Lock()
defer b.mu.Unlock()
err := filepath.Walk(keyStoreDir, func(path string, fileInfo os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.Contains(fileInfo.Name(), address) {
_, err := b.accountManager.VerifyAccountPassword(keyStoreDir, "0x"+address, password)
if err != nil {
b.log.Error("failed to verify account", "account", address, "error", err)
return err
}
return os.Remove(path)
}
return nil
})
return err
}
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
func (b *GethStatusBackend) runDBFileMigrations(account multiaccounts.Account, password string) (string, error) {
// Migrate file path to fix issue https://github.com/status-im/status-go/issues/2027
unsupportedPath := filepath.Join(b.rootDataDir, fmt.Sprintf("app-%x.sql", account.KeyUID))
v3Path := filepath.Join(b.rootDataDir, fmt.Sprintf("%s.db", account.KeyUID))
v4Path, err := b.getAppDBPath(account.KeyUID)
if err != nil {
return "", err
}
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
_, err = os.Stat(unsupportedPath)
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
if err == nil {
err := os.Rename(unsupportedPath, v3Path)
if err != nil {
return "", err
}
// rename journals as well, but ignore errors
_ = os.Rename(unsupportedPath+"-shm", v3Path+"-shm")
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
_ = os.Rename(unsupportedPath+"-wal", v3Path+"-wal")
}
if _, err = os.Stat(v3Path); err == nil {
2023-07-05 15:56:34 +00:00
if err := appdatabase.MigrateV3ToV4(v3Path, v4Path, password, account.KDFIterations, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished); err != nil {
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
_ = os.Remove(v4Path)
_ = os.Remove(v4Path + "-shm")
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
_ = os.Remove(v4Path + "-wal")
return "", errors.New("Failed to migrate v3 db to v4: " + err.Error())
}
_ = os.Remove(v3Path)
_ = os.Remove(v3Path + "-shm")
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
_ = os.Remove(v3Path + "-wal")
}
return v4Path, nil
}
func (b *GethStatusBackend) ensureDBsOpened(account multiaccounts.Account, password string) (err error) {
// After wallet DB initial migration, the tables moved to wallet DB are removed from appDB
// so better migrate wallet DB first to avoid removal if wallet DB migration fails
if err = b.ensureWalletDBOpened(account, password); err != nil {
return err
}
if err = b.ensureAppDBOpened(account, password); err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) ensureAppDBOpened(account multiaccounts.Account, password string) (err error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.appDB != nil {
return nil
}
if len(b.rootDataDir) == 0 {
return errors.New("root datadir wasn't provided")
}
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
dbFilePath, err := b.runDBFileMigrations(account, password)
if err != nil {
return errors.New("Failed to migrate db file: " + err.Error())
}
2024-05-28 11:59:54 +00:00
appdatabase.CurrentAppDBKeyUID = account.KeyUID
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
b.appDB, err = appdatabase.InitializeDB(dbFilePath, password, account.KDFIterations)
if err != nil {
b.log.Error("failed to initialize db", "err", err.Error())
return err
}
b.statusNode.SetAppDB(b.appDB)
return nil
}
func fileExists(path string) bool {
if _, err := os.Stat(path); errors.Is(err, os.ErrNotExist) {
return false
}
return true
}
func (b *GethStatusBackend) walletDBExists(keyUID string) bool {
path, err := b.getWalletDBPath(keyUID)
if err != nil {
return false
}
return fileExists(path)
}
func (b *GethStatusBackend) appDBExists(keyUID string) bool {
path, err := b.getAppDBPath(keyUID)
if err != nil {
return false
}
return fileExists(path)
}
func (b *GethStatusBackend) ensureWalletDBOpened(account multiaccounts.Account, password string) (err error) {
b.mu.Lock()
defer b.mu.Unlock()
if b.walletDB != nil {
return nil
}
dbWalletPath, err := b.getWalletDBPath(account.KeyUID)
if err != nil {
return err
}
b.walletDB, err = walletdatabase.InitializeDB(dbWalletPath, password, account.KDFIterations)
if err != nil {
b.log.Error("failed to initialize wallet db", "err", err.Error())
return err
}
b.statusNode.SetWalletDB(b.walletDB)
return nil
}
2022-02-27 15:23:07 +00:00
func (b *GethStatusBackend) setupLogSettings() error {
logSettings := logutils.LogSettings{
Enabled: b.config.LogEnabled,
MobileSystem: b.config.LogMobileSystem,
Level: b.config.LogLevel,
File: b.config.LogFile,
MaxSize: b.config.LogMaxSize,
MaxBackups: b.config.LogMaxBackups,
CompressRotated: b.config.LogCompressRotated,
}
if err := logutils.OverrideRootLogWithConfig(logSettings, false); err != nil {
return err
}
return nil
}
// 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 {
return err
}
acc.KDFIterations = kdfIterations
}
chatKey, err := ethcrypto.HexToECDSA(keyHex)
if err != nil {
return err
}
err = b.startNodeWithAccount(acc, password, nodecfg, chatKey)
if err != nil {
// Stop node for clean up
_ = b.StopNode()
}
2023-07-05 15:56:34 +00:00
// get logged in
if b.LocalPairingStateManager.IsPairing() {
return nil
}
return b.LoggedIn(acc.KeyUID, err)
}
func (b *GethStatusBackend) OverwriteNodeConfigValues(conf *params.NodeConfig, n *params.NodeConfig) (*params.NodeConfig, error) {
if err := mergo.Merge(conf, n, mergo.WithOverride); err != nil {
return nil, err
}
conf.Networks = n.Networks
if err := b.saveNodeConfig(conf); err != nil {
return nil, err
}
return conf, nil
}
func (b *GethStatusBackend) updateAccountColorHashAndColorID(keyUID string, accountsDB *accounts.Database) (*multiaccounts.Account, error) {
multiAccount, err := b.getAccountByKeyUID(keyUID)
if err != nil {
return nil, err
}
if multiAccount.ColorHash == nil {
keypair, err := accountsDB.GetKeypairByKeyUID(keyUID)
if err != nil {
return nil, err
}
publicKey := keypair.GetChatPublicKey()
if publicKey == nil {
return nil, errors.New("chat public key not found")
}
if err = enrichMultiAccountByPublicKey(multiAccount, publicKey); err != nil {
return nil, err
}
if err = b.multiaccountsDB.UpdateAccount(*multiAccount); err != nil {
return nil, err
}
}
return multiAccount, nil
}
func (b *GethStatusBackend) overrideNetworks(conf *params.NodeConfig, request *requests.Login) {
conf.Networks = setRPCs(defaultNetworks, &request.WalletSecretsConfig)
}
func (b *GethStatusBackend) LoginAccount(request *requests.Login) error {
err := b.loginAccount(request)
if err != nil {
// Stop node for clean up
_ = b.StopNode()
}
if b.LocalPairingStateManager.IsPairing() {
return nil
}
2023-07-05 15:56:34 +00:00
return b.LoggedIn(request.KeyUID, err)
}
func (b *GethStatusBackend) loginAccount(request *requests.Login) error {
if err := request.Validate(); err != nil {
return err
}
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,
KDFIterations: request.KdfIterations,
}
if acc.KDFIterations == 0 {
acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber
}
err := b.ensureDBsOpened(acc, request.Password)
if err != nil {
return errors.Wrap(err, "failed to open database")
}
defaultCfg := &params.NodeConfig{
// why we need this? relate PR: https://github.com/status-im/status-go/pull/4014
KeycardPairingDataFile: DefaultKeycardPairingDataFile,
}
2023-09-27 17:26:10 +00:00
defaultCfg.WalletConfig = buildWalletConfig(&request.WalletSecretsConfig)
err = b.UpdateNodeConfigFleet(acc, request.Password, defaultCfg)
2023-09-27 17:26:10 +00:00
if err != nil {
return errors.Wrap(err, "failed to update node config fleet")
2023-09-27 17:26:10 +00:00
}
err = b.loadNodeConfig(defaultCfg)
if err != nil {
return errors.Wrap(err, "failed to load node config")
}
2024-03-28 15:01:44 +00:00
if request.RuntimeLogLevel != "" {
b.config.LogLevel = request.RuntimeLogLevel
}
if b.config.WakuV2Config.Enabled && request.WakuV2Nameserver != "" {
b.config.WakuV2Config.Nameserver = request.WakuV2Nameserver
}
b.config.ShhextConfig.BandwidthStatsEnabled = request.BandwidthStatsEnabled
b.overrideNetworks(b.config, request)
err = b.setupLogSettings()
if err != nil {
return errors.Wrap(err, "failed to setup log settings")
}
accountsDB, err := accounts.NewDB(b.appDB)
if err != nil {
return errors.Wrap(err, "failed to create accounts db")
}
multiAccount, err := b.updateAccountColorHashAndColorID(acc.KeyUID, accountsDB)
if err != nil {
return errors.Wrap(err, "failed to update account color hash and color id")
}
b.account = multiAccount
chatAddr, err := accountsDB.GetChatAddress()
if err != nil {
return errors.Wrap(err, "failed to get chat address")
}
walletAddr, err := accountsDB.GetWalletAddress()
if err != nil {
return errors.Wrap(err, "failed to get wallet address")
}
watchAddrs, err := accountsDB.GetWalletAddresses()
if err != nil {
return errors.Wrap(err, "failed to get wallet addresses")
}
login := account.LoginParams{
Password: request.Password,
ChatAddress: chatAddr,
WatchAddresses: watchAddrs,
MainAccount: walletAddr,
}
err = b.StartNode(b.config)
if err != nil {
b.log.Info("failed to start node")
return errors.Wrap(err, "failed to start node")
}
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.Error("failed to update account")
return errors.Wrap(err, "failed to update account")
}
return nil
}
// UpdateNodeConfigFleet loads the fleet from the settings and updates the node configuration
// If the fleet in settings is empty, or not supported anymore, it will be overridden with the default fleet.
// In that case settings fleet value remain the same, only runtime node configuration is updated.
func (b *GethStatusBackend) UpdateNodeConfigFleet(acc multiaccounts.Account, password string, config *params.NodeConfig) error {
if config == nil {
return nil
}
err := b.ensureDBsOpened(acc, password)
if err != nil {
return err
}
accountSettings, err := b.GetSettings()
if err != nil {
return err
}
fleet := accountSettings.GetFleet()
if !params.IsFleetSupported(fleet) {
b.log.Warn("fleet is not supported, overriding with default value",
"fleet", fleet,
"defaultFleet", DefaultFleet)
fleet = DefaultFleet
}
err = SetFleet(fleet, config)
if err != nil {
return err
}
return nil
}
// 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
}
err = b.loadNodeConfig(inputNodeCfg)
if err != nil {
return err
}
2022-02-27 15:23:07 +00:00
err = b.setupLogSettings()
if err != nil {
return err
}
2022-02-27 15:23:07 +00:00
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
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
chatAddr, err := accountsDB.GetChatAddress()
if err != nil {
return err
}
walletAddr, err := accountsDB.GetWalletAddress()
if err != nil {
return err
}
status-im/status-react#9203 Faster tx fetching with less request *** How it worked before this PR on multiaccount creation: - On multiacc creation we scanned chain for eth and erc20 transfers. For each address of a new empty multiaccount this scan required 1. two `eth_getBalance` requests to find out that there is no any balance change between zero and the last block, for eth transfers 2. and `chain-size/100000` (currently ~100) `eth_getLogs` requests, for erc20 transfers - For some reason we scanned an address of the chat account as well, and also accounts were not deduplicated. So even for an empty multiacc we scanned chain twice for each chat and main wallet addresses, in result app had to execute about 400 requests. - As mentioned above, `eth_getBalance` requests were used to check if there were any eth transfers, and that caused empty history in case if user already used all available eth (so that both zero and latest blocks show 0 eth for an address). There might have been transactions but we wouldn't fetch/show them. - There was no upper limit for the number of rpc requests during the scan, so it could require indefinite number of requests; the scanning algorithm was written so that we persisted the whole history of transactions or tried to scan form the beginning again in case of failure, giving up only after 10 minutes of failures. In result addresses with sufficient number of transactions would never be fully scanned and during these 10 minutes app could use gigabytes of internet data. - Failures were caused by `eth_getBlockByNumber`/`eth_getBlockByHash` requests. These requests return significantly bigger responses than `eth_getBalance`/`eth_transactionsCount` and it is likely that execution of thousands of them in parallel caused failures for accounts with hundreds of transactions. Even for an account with 12k we could successfully determine blocks with transaction in a few minutes using `eth_getBalance` requests, but `eth_getBlock...` couldn't be processed for this acc. - There was no caching for for `eth_getBalance` requests, and this caused in average 3-4 times more such requests than is needed. *** How it works now on multiaccount creation: - On multiacc creation we scan chain for last ~30 eth transactions and then check erc20 in the range where these eth transactions were found. For an empty address in multiacc this means: 1. two `eth_getBalance` transactions to determine that there was no balance change between zero and the last block; two `eth_transactionsCount` requests to determine there are no outgoing transactions for this address; total 4 requests for eth transfers 2. 20 `eth_getLogs` for erc20 transfers. This number can be lowered, but that's not a big deal - Deduplication of addresses is added and also we don't scan chat account, so a new multiacc requires ~25 (we also request latest block number and probably execute a few other calls) request to determine that multiacc is empty (comparing to ~400 before) - In case if address contains transactions we: 1. determine the range which contains 20-25 outgoing eth/erc20 transactions. This usually requires up to 10 `eth_transactionCount` requests 2. then we scan chain for eth transfers using `eth_getBalance` and `eth_transactionCount` (for double checking zero balances) 3. we make sure that we do not scan db for more than 30 blocks with transfers. That's important for accounts with mostly incoming transactions, because the range found on the first step might contain any number of incoming transfers, but only 20-25 outgoing transactions 4. when we found ~30 blocks in a given range, we update initial range `from` block using the oldest found block 5. and now we scan db for erc20transfers using `eth_getLogs` `oldest-found-eth-block`-`latest-block`, we make not more than 20 calls 6. when all blocks which contain incoming/outgoing transfers for a given address are found, we save these blocks to db and mark that transfers from these blocks are still to be fetched 7. Then we select latest ~30 (the number can be adjusted) blocks from these which were found and fetch transfers, this requires 3-4 requests per transfer. 8. we persist scanned range so that we know were to start next time 9. we dispatch an event which tells client that transactions are found 10. client fetches latest 20 transfers - when user presses "fetch more" button we check if app's db contains next 20 transfers, if not we scan chain again and return transfers after small fixes
2019-12-18 11:01:46 +00:00
watchAddrs, err := accountsDB.GetWalletAddresses()
if err != nil {
return err
}
login := account.LoginParams{
Password: password,
ChatAddress: chatAddr,
WatchAddresses: watchAddrs,
MainAccount: walletAddr,
}
err = b.StartNode(b.config)
if err != nil {
2023-03-16 14:49:25 +00:00
b.log.Info("failed to start node")
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 {
2023-03-16 14:49:25 +00:00
b.log.Info("failed to update account")
return err
}
return nil
}
2023-11-10 15:12:54 +00:00
func (b *GethStatusBackend) accountsDB() (*accounts.Database, error) {
return accounts.NewDB(b.appDB)
}
2023-07-05 15:56:34 +00:00
func (b *GethStatusBackend) GetSettings() (*settings.Settings, error) {
2023-11-10 15:12:54 +00:00
accountsDB, err := b.accountsDB()
2023-07-05 15:56:34 +00:00
if err != nil {
return nil, err
}
2023-11-10 15:12:54 +00:00
settings, err := accountsDB.GetSettings()
2023-07-05 15:56:34 +00:00
if err != nil {
return nil, err
}
return &settings, nil
}
func (b *GethStatusBackend) GetEnsUsernames() ([]*ens.UsernameDetail, error) {
db := ens.NewEnsDatabase(b.appDB)
removed := false
return db.GetEnsUsernames(&removed)
}
func (b *GethStatusBackend) MigrateKeyStoreDir(acc multiaccounts.Account, password, oldDir, newDir string) error {
err := b.ensureDBsOpened(acc, password)
if err != nil {
return err
}
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
accountDB, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
accounts, err := accountDB.GetActiveAccounts()
if err != nil {
return err
}
settings, err := accountDB.GetSettings()
if err != nil {
return err
}
addresses := []string{settings.EIP1581Address.Hex(), settings.WalletRootAddress.Hex()}
for _, account := range accounts {
addresses = append(addresses, account.Address.Hex())
}
err = b.accountManager.MigrateKeyStoreDir(oldDir, newDir, addresses)
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) Login(keyUID, password string) error {
return b.startNodeWithAccount(multiaccounts.Account{KeyUID: keyUID}, password, nil, nil)
}
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()
}
2023-07-05 15:56:34 +00:00
// get logged in
if !b.LocalPairingStateManager.IsPairing() {
2023-07-05 15:56:34 +00:00
return b.LoggedIn(acc.KeyUID, err)
}
return err
2023-07-05 15:56:34 +00:00
}
func (b *GethStatusBackend) LoggedIn(keyUID string, err error) error {
if err != nil {
signal.SendLoggedIn(nil, nil, nil, err)
return err
2023-07-05 15:56:34 +00:00
}
settings, err := b.GetSettings()
if err != nil {
return err
}
account, err := b.getAccountByKeyUID(keyUID)
if err != nil {
return err
}
ensUsernames, err := b.GetEnsUsernames()
if err != nil {
return err
}
var ensUsernamesJSON json.RawMessage
if ensUsernames != nil {
ensUsernamesJSON, err = json.Marshal(ensUsernames)
if err != nil {
return err
}
}
signal.SendLoggedIn(account, settings, ensUsernamesJSON, nil)
2023-07-05 15:56:34 +00:00
return nil
}
2021-01-07 11:15:02 +00:00
func (b *GethStatusBackend) ExportUnencryptedDatabase(acc multiaccounts.Account, password, directory string) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.appDB != nil {
return nil
}
if len(b.rootDataDir) == 0 {
return errors.New("root datadir wasn't provided")
}
perf(sqlCipher): Increase cipher page size to 8192 (#3591) * perf(sqlCipher): Increase cipher page size to 8192 Increasing the cipher page size to 8192 requires DB re-encryption. The process is as follows: //Login to v3 DB PRAGMA key = 'key'; PRAGMA cipher_page_size = 1024"; // old Page size PRAGMA cipher_hmac_algorithm = HMAC_SHA1"; PRAGMA cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; PRAGMA kdf_iter = kdfIterationsNumber"; //Create V4 DB with increased page size ATTACH DATABASE 'newdb.db' AS newdb KEY 'key'; PRAGMA newdb.cipher_page_size = 8192; // new Page size PRAGMA newdb.cipher_hmac_algorithm = HMAC_SHA1"; // same as in v3 PRAGMA newdb.cipher_kdf_algorithm = PBKDF2_HMAC_SHA1"; // same as in v3 PRAGMA newdb.kdf_iter = kdfIterationsNumber"; // same as in v3 SELECT sqlcipher_export('newdb'); DETACH DATABASE newdb; //Login to V4 DB ... Worth noting: The DB migration will happen on the first successful login. The new DB version will have a different name to be able to distinguish between different DB versions.Versions naming mirrors sqlcipher major version (naming conventions used by sqlcipher), meaning that we're migrating from V3 to V4 DB (even if we're not fully aligned with V4 standards). The DB is not migrated to the v4 standard `SHA512` due to performance reasons. Our custom `SHA1` implementation is fully optimised for perfomance. * perf(sqlCipher): Fixing failing tests Update the new DB file format in Delete account, Change password and Decrypt database flows * perf(SQLCipher): Increase page size - send events to notify when the DB re-encryption starts/ends
2023-06-13 15:20:21 +00:00
dbPath, err := b.runDBFileMigrations(acc, password)
if err != nil {
return err
2021-01-07 11:15:02 +00:00
}
err = sqlite.DecryptDB(dbPath, directory, password, acc.KDFIterations)
2021-01-07 11:15:02 +00:00
if err != nil {
b.log.Error("failed to initialize db", "err", err)
return err
}
return nil
}
func (b *GethStatusBackend) ImportUnencryptedDatabase(acc multiaccounts.Account, password, databasePath string) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.appDB != nil {
return nil
}
path, err := b.getAppDBPath(acc.KeyUID)
if err != nil {
return err
}
2021-01-07 11:15:02 +00:00
err = sqlite.EncryptDB(databasePath, path, password, acc.KDFIterations, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished)
2021-01-07 11:15:02 +00:00
if err != nil {
b.log.Error("failed to initialize db", "err", err)
return err
}
return nil
}
func (b *GethStatusBackend) reEncryptKeyStoreDir(currentPassword string, newPassword string) error {
config := b.StatusNode().Config()
keyDir := ""
if config == nil {
keyDir = b.accountManager.Keydir
} else {
keyDir = config.KeyStoreDir
}
if keyDir != "" {
err := b.accountManager.ReEncryptKeyStoreDir(keyDir, currentPassword, newPassword)
if err != nil {
return fmt.Errorf("ReEncryptKeyStoreDir error: %v", err)
}
}
return nil
}
func (b *GethStatusBackend) ChangeDatabasePassword(keyUID string, password string, newPassword string) error {
account, err := b.multiaccountsDB.GetAccount(keyUID)
if err != nil {
return err
}
internalDbPath, err := dbsetup.GetDBFilename(b.appDB)
if err != nil {
return fmt.Errorf("failed to get database file name, %w", err)
}
appDBPath, err := b.getAppDBPath(keyUID)
if err != nil {
return err
}
isCurrentAccount := appDBPath == internalDbPath
restartNode := func() {
if isCurrentAccount {
if err != nil {
// TODO https://github.com/status-im/status-go/issues/3906
// Fix restarting node, as it always fails but the error is ignored
// 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, nil)
} else {
_ = b.startNodeWithAccount(*account, newPassword, nil, nil)
}
}
}
defer restartNode()
logout := func() {
if isCurrentAccount {
_ = b.Logout()
}
}
noLogout := func() {}
// First change app DB password, because it also reencrypts the keystore,
// otherwise if we call changeWalletDbPassword first and logout, we will fail
// to reencrypt the keystore
err = b.changeAppDBPassword(account, logout, password, newPassword)
if err != nil {
return err
}
// Already logged out but pass a param to decouple the logic for testing
err = b.changeWalletDBPassword(account, noLogout, password, newPassword)
if err != nil {
// Revert the password to original
err2 := b.changeAppDBPassword(account, noLogout, newPassword, password)
if err2 != nil {
log.Error("failed to revert app db password", "err", err2)
}
return err
}
return nil
}
func (b *GethStatusBackend) changeAppDBPassword(account *multiaccounts.Account, logout func(), password string, newPassword string) error {
tmpDbPath, cleanup, err := b.createTempDBFile("v4.db")
if err != nil {
return err
}
defer cleanup()
dbPath, err := b.getAppDBPath(account.KeyUID)
if err != nil {
return err
}
// Exporting database to a temporary file with a new password
err = sqlite.ExportDB(dbPath, password, account.KDFIterations, tmpDbPath, newPassword, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished)
if err != nil {
return err
}
err = b.reEncryptKeyStoreDir(password, newPassword)
if err != nil {
return err
}
// Replacing the old database with the new one requires closing all connections to the database
// This is done by stopping the node and restarting it with the new DB
logout()
// Replacing the old database files with the new ones, ignoring the wal and shm errors
replaceCleanup, err := replaceDBFile(dbPath, tmpDbPath)
if replaceCleanup != nil {
defer replaceCleanup()
}
if err != nil {
// Restore the old account
_ = b.reEncryptKeyStoreDir(newPassword, password)
return err
}
return nil
}
func (b *GethStatusBackend) changeWalletDBPassword(account *multiaccounts.Account, logout func(), password string, newPassword string) error {
tmpDbPath, cleanup, err := b.createTempDBFile("wallet.db")
if err != nil {
return err
}
defer cleanup()
dbPath, err := b.getWalletDBPath(account.KeyUID)
if err != nil {
return err
}
// Exporting database to a temporary file with a new password
err = sqlite.ExportDB(dbPath, password, account.KDFIterations, tmpDbPath, newPassword, signal.SendReEncryptionStarted, signal.SendReEncryptionFinished)
if err != nil {
return err
}
// Replacing the old database with the new one requires closing all connections to the database
// This is done by stopping the node and restarting it with the new DB
logout()
// Replacing the old database files with the new ones, ignoring the wal and shm errors
replaceCleanup, err := replaceDBFile(dbPath, tmpDbPath)
if replaceCleanup != nil {
defer replaceCleanup()
}
return err
}
func (b *GethStatusBackend) createTempDBFile(pattern string) (tmpDbPath string, cleanup func(), err error) {
if len(b.rootDataDir) == 0 {
err = errors.New("root datadir wasn't provided")
return
}
rootDataDir := b.rootDataDir
//On iOS, the rootDataDir value does not contain a trailing slash.
//This is causing an incorrectly formatted temporary file path to be generated, leading to an "operation not permitted" error.
//e.g. value of rootDataDir is `/var/mobile/.../12906D5A-E831-49E9-BBE7-5FFE8E805D8A/Library`,
//the file path generated is something like `/var/mobile/.../12906D5A-E831-49E9-BBE7-5FFE8E805D8A/123-v4.db`
//which removed `Library` from the path.
if !strings.HasSuffix(rootDataDir, "/") {
rootDataDir += "/"
}
file, err := os.CreateTemp(filepath.Dir(rootDataDir), "*-"+pattern)
if err != nil {
return
}
err = file.Close()
if err != nil {
return
}
tmpDbPath = file.Name()
cleanup = func() {
filePath := file.Name()
_ = os.Remove(filePath)
_ = os.Remove(filePath + "-wal")
_ = os.Remove(filePath + "-shm")
_ = os.Remove(filePath + "-journal")
}
return
}
func replaceDBFile(dbPath string, newDBPath string) (cleanup func(), err error) {
err = os.Rename(newDBPath, dbPath)
if err != nil {
return
}
cleanup = func() {
_ = os.Remove(dbPath + "-wal")
_ = os.Remove(dbPath + "-shm")
_ = os.Rename(newDBPath+"-wal", dbPath+"-wal")
_ = os.Rename(newDBPath+"-shm", dbPath+"-shm")
}
return
}
func (b *GethStatusBackend) ConvertToKeycardAccount(account multiaccounts.Account, s settings.Settings, keycardUID string, password string, newPassword string) error {
messenger := b.Messenger()
if messenger == nil {
return errors.New("cannot resolve messenger instance")
}
err := b.multiaccountsDB.UpdateAccountKeycardPairing(account.KeyUID, account.KeycardPairing)
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
err = b.ensureDBsOpened(account, password)
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
accountDB, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
keypair, err := accountDB.GetKeypairByKeyUID(account.KeyUID)
2021-07-20 11:48:10 +00:00
if err != nil {
if err == accounts.ErrDbKeypairNotFound {
return errors.New("cannot convert an unknown keypair")
}
2021-07-20 11:48:10 +00:00
return err
}
err = accountDB.SaveSettingField(settings.KeycardInstanceUID, s.KeycardInstanceUID)
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
err = accountDB.SaveSettingField(settings.KeycardPairedOn, s.KeycardPairedOn)
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
err = accountDB.SaveSettingField(settings.KeycardPairing, s.KeycardPairing)
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
err = accountDB.SaveSettingField(settings.Mnemonic, nil)
if err != nil {
return err
}
err = accountDB.SaveSettingField(settings.ProfileMigrationNeeded, false)
if err != nil {
return err
}
// This check is added due to mobile app cause it doesn't support a Keycard features as desktop app.
// We should remove the following line once mobile and desktop app align.
if len(keycardUID) > 0 {
displayName, err := accountDB.DisplayName()
if err != nil {
return err
}
position, err := accountDB.GetPositionForNextNewKeycard()
if err != nil {
return err
}
kc := accounts.Keycard{
KeycardUID: keycardUID,
KeycardName: displayName,
KeycardLocked: false,
KeyUID: account.KeyUID,
Position: position,
}
for _, acc := range keypair.Accounts {
kc.AccountsAddresses = append(kc.AccountsAddresses, acc.Address)
}
err = messenger.SaveOrUpdateKeycard(context.Background(), &kc)
if err != nil {
return err
}
}
masterAddress, err := accountDB.GetMasterAddress()
if err != nil {
return err
}
eip1581Address, err := accountDB.GetEIP1581Address()
if err != nil {
return err
}
walletRootAddress, err := accountDB.GetWalletRootAddress()
if err != nil {
return err
}
err = b.closeDBs()
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
err = b.ChangeDatabasePassword(account.KeyUID, password, newPassword)
if err != nil {
return err
}
// We need to delete all accounts for the Keycard which is being added
for _, acc := range keypair.Accounts {
err = b.accountManager.DeleteAccount(acc.Address)
if err != nil {
return err
}
}
err = b.accountManager.DeleteAccount(masterAddress)
if err != nil {
return err
}
err = b.accountManager.DeleteAccount(eip1581Address)
if err != nil {
return err
}
err = b.accountManager.DeleteAccount(walletRootAddress)
if err != nil {
return err
}
return nil
2021-07-20 11:48:10 +00:00
}
2023-10-10 16:12:38 +00:00
func (b *GethStatusBackend) RestoreAccountAndLogin(request *requests.RestoreAccount) (*multiaccounts.Account, error) {
2023-03-16 14:49:25 +00:00
if err := request.Validate(); err != nil {
2023-10-10 16:12:38 +00:00
return nil, err
2023-03-16 14:49:25 +00:00
}
response, err := b.generateOrImportAccount(request.Mnemonic, 0, request.FetchBackup, &request.CreateAccount)
if err != nil {
return nil, err
}
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 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
2023-03-21 17:02:04 +00:00
}
func (b *GethStatusBackend) GetKeyUIDByMnemonic(mnemonic string) (string, error) {
accountGenerator := b.accountManager.AccountsGenerator()
info, err := accountGenerator.ImportMnemonic(mnemonic, "")
if err != nil {
return "", err
}
return info.KeyUID, nil
}
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, err
}
2023-03-16 14:49:25 +00:00
keyStoreDir, err := b.initKeyStoreDirWithAccount(request.RootDataDir, info.KeyUID)
2023-03-16 14:49:25 +00:00
if err != nil {
return nil, err
}
derivedAddresses, err := b.getDerivedAddresses(info.ID)
if err != nil {
return nil, err
2023-03-16 14:49:25 +00:00
}
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, errors.Wrap(err, "failed to build account")
}
response.settings, err = b.prepareSettings(request, input)
if err != nil {
return nil, errors.Wrap(err, "failed to prepare settings")
}
response.nodeConfig, err = b.prepareConfig(request, input, response.settings.InstallationID)
if err != nil {
return nil, errors.Wrap(err, "failed to prepare node config")
}
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) {
b.UpdateRootDataDir(rootDataDir)
keystoreDir := keystoreRelativePath
userKeyStoreDir := filepath.Join(keystoreDir, keyUID)
// Initialize keystore dir with account
return userKeyStoreDir, b.accountManager.InitKeystore(filepath.Join(b.rootDataDir, userKeyStoreDir))
}
func (b *GethStatusBackend) generateAccountInfo(mnemonic string) (*generator.GeneratedAccountInfo, error) {
2023-03-21 17:02:04 +00:00
accountGenerator := b.accountManager.AccountsGenerator()
2023-03-16 14:49:25 +00:00
2023-03-21 17:02:04 +00:00
var info generator.GeneratedAccountInfo
var err error
2023-03-21 17:02:04 +00:00
if mnemonic == "" {
// generate 1(n) account with default mnemonic length and no passphrase
generatedAccountInfos, err := accountGenerator.Generate(defaultMnemonicLength, 1, "")
info = generatedAccountInfos[0]
2023-03-16 14:49:25 +00:00
2023-03-21 17:02:04 +00:00
if err != nil {
2023-10-10 16:12:38 +00:00
return nil, err
2023-03-21 17:02:04 +00:00
}
} else {
2023-03-16 14:49:25 +00:00
2023-03-21 17:02:04 +00:00
info, err = accountGenerator.ImportMnemonic(mnemonic, "")
if err != nil {
2023-10-10 16:12:38 +00:00
return nil, err
2023-03-21 17:02:04 +00:00
}
}
2023-03-16 14:49:25 +00:00
return &info, nil
}
func (b *GethStatusBackend) storeAccount(id string, password string, paths []string) error {
accountGenerator := b.accountManager.AccountsGenerator()
_, err := accountGenerator.StoreAccount(id, password)
2023-03-16 14:49:25 +00:00
if err != nil {
return err
2023-03-16 14:49:25 +00:00
}
_, err = accountGenerator.StoreDerivedAccounts(id, password, paths)
2023-10-10 16:12:38 +00:00
if err != nil {
return err
2023-03-29 11:36:13 +00:00
}
return nil
}
func (b *GethStatusBackend) buildAccount(request *requests.CreateAccount, input *prepareAccountInput) (*multiaccounts.Account, error) {
err := b.OpenAccounts()
2023-03-16 14:49:25 +00:00
if err != nil {
return nil, err
2023-03-16 14:49:25 +00:00
}
acc := &multiaccounts.Account{
KeyUID: input.keyUID,
Name: request.DisplayName,
CustomizationColor: multiacccommon.CustomizationColor(request.CustomizationColor),
CustomizationColorClock: input.customizationColorClock,
2024-03-28 15:01:44 +00:00
KDFIterations: request.KdfIterations,
Timestamp: time.Now().Unix(),
2024-03-28 15:01:44 +00:00
}
if acc.KDFIterations == 0 {
acc.KDFIterations = dbsetup.ReducedKDFIterationsNumber
2023-03-16 14:49:25 +00:00
}
2024-03-28 15:01:44 +00:00
if request.ImagePath != "" {
2024-03-28 15:01:44 +00:00
imageCropRectangle := request.ImageCropRectangle
if imageCropRectangle == nil {
// Default crop rectangle used by mobile
imageCropRectangle = &requests.ImageCropRectangle{
Ax: 0,
Ay: 0,
Bx: 1000,
By: 1000,
}
}
iis, err := images.GenerateIdentityImages(request.ImagePath,
imageCropRectangle.Ax, imageCropRectangle.Ay, imageCropRectangle.Bx, imageCropRectangle.By)
if err != nil {
return nil, err
}
acc.Images = iis
}
2023-03-16 14:49:25 +00:00
return acc, nil
}
func (b *GethStatusBackend) prepareSettings(request *requests.CreateAccount, input *prepareAccountInput) (*settings.Settings, error) {
settings, err := defaultSettings(input.keyUID, input.address, input.derivedAddresses)
2023-03-16 14:49:25 +00:00
if err != nil {
2023-10-10 16:12:38 +00:00
return nil, err
2023-03-16 14:49:25 +00:00
}
settings.DeviceName = request.DeviceName
settings.DisplayName = request.DisplayName
settings.PreviewPrivacy = request.PreviewPrivacy
settings.CurrentNetwork = request.CurrentNetwork
settings.TestNetworksEnabled = request.TestNetworksEnabled
2023-03-21 17:02:04 +00:00
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
//settings.MnemonicWasNotShown = true
2023-03-21 17:02:04 +00:00
}
2023-03-16 14:49:25 +00:00
return settings, nil
}
func (b *GethStatusBackend) prepareConfig(request *requests.CreateAccount, input *prepareAccountInput, installationID string) (*params.NodeConfig, error) {
nodeConfig, err := defaultNodeConfig(installationID, request, input.opts...)
2023-03-16 14:49:25 +00:00
if err != nil {
2023-10-10 16:12:38 +00:00
return nil, err
2023-03-16 14:49:25 +00:00
}
nodeConfig.ProcessBackedupMessages = input.fetchBackup
2023-03-16 14:49:25 +00:00
// when we set nodeConfig.KeyStoreDir, value of nodeConfig.KeyStoreDir should not contain the rootDataDir
// loadNodeConfig will add rootDataDir to nodeConfig.KeyStoreDir
nodeConfig.KeyStoreDir = input.keyStoreDir
2023-03-29 11:36:13 +00:00
return nodeConfig, nil
}
func (b *GethStatusBackend) prepareSubAccounts(request *requests.CreateAccount, input *prepareAccountInput) ([]*accounts.Account, error) {
walletDerivedAccount := input.derivedAddresses[pathDefaultWallet]
2023-03-16 14:49:25 +00:00
walletAccount := &accounts.Account{
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,
}
chatDerivedAccount := input.derivedAddresses[pathDefaultChat]
2023-03-16 14:49:25 +00:00
chatAccount := &accounts.Account{
PublicKey: types.Hex2Bytes(chatDerivedAccount.PublicKey),
KeyUID: input.keyUID,
2023-03-16 14:49:25 +00:00
Address: types.HexToAddress(chatDerivedAccount.Address),
Name: request.DisplayName,
2023-03-16 14:49:25 +00:00
Chat: true,
Path: pathDefaultChat,
}
return []*accounts.Account{walletAccount, chatAccount}, nil
}
func (b *GethStatusBackend) prepareForKeycard(request *requests.CreateAccount, input *prepareAccountInput, response *accountBundle) (*accountBundle, error) {
if request.KeycardInstanceUID == "" {
return response, nil
}
kp := wallet.NewKeycardPairings()
kp.SetKeycardPairingsFile(response.nodeConfig.KeycardPairingDataFile)
pairings, err := kp.GetPairings()
2023-03-16 14:49:25 +00:00
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")
2023-03-16 14:49:25 +00:00
}
return response, nil
}
func (b *GethStatusBackend) getDerivedAddresses(id string) (map[string]generator.AccountInfo, error) {
accountGenerator := b.accountManager.AccountsGenerator()
return accountGenerator.DeriveAddresses(id, paths)
2023-03-21 17:02:04 +00:00
}
2024-03-22 10:55:09 +00:00
// CreateAccountAndLogin creates a new account and logs in with it.
// NOTE: requests.CreateAccount is used for public, params.Option maybe used for internal usage.
func (b *GethStatusBackend) CreateAccountAndLogin(request *requests.CreateAccount, opts ...params.Option) (*multiaccounts.Account, error) {
2024-03-28 15:01:44 +00:00
validation := &requests.CreateAccountValidation{
AllowEmptyDisplayName: false,
}
if err := request.Validate(validation); err != nil {
2023-10-10 16:12:38 +00:00
return nil, err
2023-03-21 17:02:04 +00:00
}
response, err := b.generateOrImportAccount("", 1, false, request, opts...)
if err != nil {
return nil, err
}
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 response.account, nil
2023-03-16 14:49:25 +00:00
}
func (b *GethStatusBackend) ConvertToRegularAccount(mnemonic string, currPassword string, newPassword string) error {
messenger := b.Messenger()
if messenger == nil {
return errors.New("cannot resolve messenger instance")
}
mnemonicNoExtraSpaces := strings.Join(strings.Fields(mnemonic), " ")
accountInfo, err := b.accountManager.AccountsGenerator().ImportMnemonic(mnemonicNoExtraSpaces, "")
if err != nil {
return err
}
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(accountInfo.KeyUID)
if err != nil {
return err
}
err = b.ensureDBsOpened(multiaccounts.Account{KeyUID: accountInfo.KeyUID, KDFIterations: kdfIterations}, currPassword)
if err != nil {
return err
}
db, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
knownAccounts, err := db.GetActiveAccounts()
if err != nil {
return err
}
// We add these two paths, cause others will be added via `StoreAccount` function call
const pathWalletRoot = "m/44'/60'/0'/0"
const pathEIP1581 = "m/43'/60'/1581'"
var paths []string
paths = append(paths, pathWalletRoot, pathEIP1581)
for _, acc := range knownAccounts {
if accountInfo.KeyUID == acc.KeyUID {
paths = append(paths, acc.Path)
}
}
_, err = b.accountManager.AccountsGenerator().StoreAccount(accountInfo.ID, currPassword)
if err != nil {
return err
}
_, err = b.accountManager.AccountsGenerator().StoreDerivedAccounts(accountInfo.ID, currPassword, paths)
if err != nil {
return err
}
err = b.multiaccountsDB.UpdateAccountKeycardPairing(accountInfo.KeyUID, "")
if err != nil {
return err
}
err = messenger.DeleteAllKeycardsWithKeyUID(context.Background(), accountInfo.KeyUID)
if err != nil {
return err
}
err = db.SaveSettingField(settings.KeycardInstanceUID, "")
if err != nil {
return err
}
err = db.SaveSettingField(settings.KeycardPairedOn, 0)
if err != nil {
return err
}
err = db.SaveSettingField(settings.KeycardPairing, "")
if err != nil {
return err
}
err = db.SaveSettingField(settings.ProfileMigrationNeeded, false)
if err != nil {
return err
}
err = b.closeDBs()
if err != nil {
return err
}
return b.ChangeDatabasePassword(accountInfo.KeyUID, currPassword, newPassword)
}
2021-07-20 11:48:10 +00:00
func (b *GethStatusBackend) VerifyDatabasePassword(keyUID string, password string) error {
kdfIterations, err := b.multiaccountsDB.GetAccountKDFIterationsNumber(keyUID)
if err != nil {
return err
}
if !b.appDBExists(keyUID) || !b.walletDBExists(keyUID) {
return errors.New("One or more databases not created")
}
err = b.ensureDBsOpened(multiaccounts.Account{KeyUID: keyUID, KDFIterations: kdfIterations}, password)
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
err = b.closeDBs()
2021-07-20 11:48:10 +00:00
if err != nil {
return err
}
return nil
}
func enrichMultiAccountBySubAccounts(account *multiaccounts.Account, subaccs []*accounts.Account) error {
if account.ColorHash != nil && account.ColorID != 0 {
return nil
}
for i, acc := range subaccs {
subaccs[i].KeyUID = account.KeyUID
if acc.Chat {
pk := string(acc.PublicKey.Bytes())
colorHash, err := colorhash.GenerateFor(pk)
if err != nil {
return err
}
account.ColorHash = colorHash
colorID, err := identityutils.ToColorID(pk)
if err != nil {
return err
}
account.ColorID = colorID
break
}
}
return nil
}
func enrichMultiAccountByPublicKey(account *multiaccounts.Account, publicKey types.HexBytes) error {
pk := string(publicKey.Bytes())
colorHash, err := colorhash.GenerateFor(pk)
if err != nil {
return err
}
account.ColorHash = colorHash
colorID, err := identityutils.ToColorID(pk)
if err != nil {
return err
}
account.ColorID = colorID
return nil
}
// 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
}
err = b.SaveAccount(account)
if err != nil {
return err
}
err = b.ensureDBsOpened(account, password)
if err != nil {
return err
}
err = b.saveAccountsAndSettings(settings, nodecfg, subaccs)
if err != nil {
return err
}
return b.StartNodeWithKey(account, password, keyHex, nodecfg)
}
// 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,
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
settings settings.Settings,
nodecfg *params.NodeConfig,
2022-05-18 10:42:51 +00:00
subaccs []*accounts.Account,
chatKey *ecdsa.PrivateKey,
) error {
err := enrichMultiAccountBySubAccounts(&account, subaccs)
if err != nil {
return err
}
err = b.SaveAccount(account)
if err != nil {
return err
}
err = b.ensureDBsOpened(account, password)
if err != nil {
return err
}
err = b.saveAccountsAndSettings(settings, nodecfg, subaccs)
if err != nil {
return err
}
return b.StartNodeWithAccount(account, password, nodecfg, chatKey)
}
// TODO: change in `saveAccountsAndSettings` function param `subaccs []*accounts.Account` parameter to `profileKeypair *accounts.Keypair` parameter
2022-05-18 10:42:51 +00:00
func (b *GethStatusBackend) saveAccountsAndSettings(settings settings.Settings, nodecfg *params.NodeConfig, subaccs []*accounts.Account) error {
b.mu.Lock()
defer b.mu.Unlock()
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
accdb, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
err = accdb.CreateSettings(settings, *nodecfg)
if err != nil {
return err
}
// In case of setting up new account either way (creating new, importing seed phrase, keycard account...) we should not
// back up any data after login, as it was the case before, that's the reason why we're setting last backup time to the time
// when an account was created.
now := time.Now().Unix()
err = accdb.SetLastBackup(uint64(now))
if err != nil {
return err
}
keypair := &accounts.Keypair{
KeyUID: settings.KeyUID,
Name: settings.DisplayName,
Type: accounts.KeypairTypeProfile,
DerivedFrom: settings.Address.String(),
LastUsedDerivationIndex: 0,
}
// When creating a new account, the chat account should have position -1, cause it doesn't participate
// in the wallet view and default wallet account should be at position 0.
for _, acc := range subaccs {
if acc.Chat {
acc.Position = -1
}
if acc.Wallet {
acc.Position = 0
}
acc.Operable = accounts.AccountFullyOperable
keypair.Accounts = append(keypair.Accounts, acc)
}
return accdb.SaveOrUpdateKeypair(keypair)
}
func (b *GethStatusBackend) loadNodeConfig(inputNodeCfg *params.NodeConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
conf, err := nodecfg.GetNodeConfigFromDB(b.appDB)
if err != nil {
return err
}
if inputNodeCfg != nil {
2022-10-14 08:50:36 +00:00
// If an installationID is provided, we override it
if conf != nil && conf.ShhextConfig.InstallationID != "" {
inputNodeCfg.ShhextConfig.InstallationID = conf.ShhextConfig.InstallationID
}
conf, err = b.OverwriteNodeConfigValues(conf, inputNodeCfg)
if err != nil {
return err
}
}
// Start WakuV1 if WakuV2 is not enabled
conf.WakuConfig.Enabled = !conf.WakuV2Config.Enabled
// NodeConfig.Version should be taken from params.Version
// which is set at the compile time.
// What's cached is usually outdated so we overwrite it here.
conf.Version = params.Version
conf.RootDataDir = b.rootDataDir
conf.DataDir = filepath.Join(b.rootDataDir, conf.DataDir)
conf.KeyStoreDir = filepath.Join(b.rootDataDir, conf.KeyStoreDir)
if _, err = os.Stat(conf.RootDataDir); os.IsNotExist(err) {
if err := os.MkdirAll(conf.RootDataDir, os.ModePerm); err != nil {
b.log.Warn("failed to create data directory", zap.Error(err))
return err
}
}
if len(conf.LogDir) == 0 {
conf.LogFile = filepath.Join(b.rootDataDir, conf.LogFile)
} else {
conf.LogFile = filepath.Join(conf.LogDir, conf.LogFile)
}
b.config = conf
if inputNodeCfg != nil && inputNodeCfg.RuntimeLogLevel != "" {
b.config.LogLevel = inputNodeCfg.RuntimeLogLevel
}
return nil
}
func (b *GethStatusBackend) saveNodeConfig(n *params.NodeConfig) error {
err := nodecfg.SaveNodeConfig(b.appDB, n)
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) GetNodeConfig() (*params.NodeConfig, error) {
return nodecfg.GetNodeConfigFromDB(b.appDB)
}
func (b *GethStatusBackend) startNode(config *params.NodeConfig) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("node crashed on start: %v", err)
}
}()
b.log.Info("status-go version details", "version", params.Version, "commit", params.GitCommit)
b.log.Debug("starting node with config", "config", config)
// Update config with some defaults.
if err := config.UpdateWithDefaults(); err != nil {
return err
}
// Updating node config
b.config = config
b.log.Debug("updated config with defaults", "config", config)
// Start by validating configuration
if err := config.Validate(); err != nil {
return err
}
if b.accountManager.GetManager() == nil {
err = b.accountManager.InitKeystore(config.KeyStoreDir)
if err != nil {
return err
}
}
manager := b.accountManager.GetManager()
if manager == nil {
return errors.New("ethereum accounts.Manager is nil")
}
if err = b.statusNode.StartWithOptions(config, node.StartOptions{
// The peers discovery protocols are started manually after
// `node.ready` signal is sent.
// It was discussed in https://github.com/status-im/status-go/pull/1333.
StartDiscovery: false,
AccountsManager: manager,
}); err != nil {
return
}
b.accountManager.SetRPCClient(b.statusNode.RPCClient(), rpc.DefaultCallTimeout)
signal.SendNodeStarted()
b.transactor.SetNetworkID(config.NetworkID)
b.transactor.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout)
2021-07-12 09:44:26 +00:00
b.personalAPI.SetRPC(b.statusNode.RPCClient(), rpc.DefaultCallTimeout)
if err = b.registerHandlers(); err != nil {
b.log.Error("Handler registration failed", "err", err)
return
}
b.log.Info("Handlers registered")
// Handle a case when a node is stopped and resumed.
// If there is no account selected, an error is returned.
if _, err := b.accountManager.SelectedChatAccount(); err == nil {
if err := b.injectAccountsIntoServices(); err != nil {
return err
}
} else if err != account.ErrNoAccountSelected {
return err
}
if b.statusNode.WalletService() != nil {
b.statusNode.WalletService().KeycardPairings().SetKeycardPairingsFile(config.KeycardPairingDataFile)
}
signal.SendNodeReady()
if err := b.statusNode.StartDiscovery(); err != nil {
return err
}
return nil
}
// StopNode stop Status node. Stopped node cannot be resumed.
func (b *GethStatusBackend) StopNode() error {
b.mu.Lock()
defer b.mu.Unlock()
return b.stopNode()
}
func (b *GethStatusBackend) stopNode() error {
2021-07-07 06:11:09 +00:00
if b.statusNode == nil || !b.IsNodeRunning() {
return nil
}
if !b.LocalPairingStateManager.IsPairing() {
defer signal.SendNodeStopped()
}
2021-07-07 06:11:09 +00:00
return b.statusNode.Stop()
}
// RestartNode restart running Status node, fails if node is not running
func (b *GethStatusBackend) RestartNode() error {
b.mu.Lock()
defer b.mu.Unlock()
if !b.IsNodeRunning() {
return node.ErrNoRunningNode
}
if err := b.stopNode(); err != nil {
return err
}
return b.startNode(b.config)
}
// ResetChainData remove chain data from data directory.
// Node is stopped, and new node is started, with clean data directory.
func (b *GethStatusBackend) ResetChainData() error {
b.mu.Lock()
defer b.mu.Unlock()
if err := b.stopNode(); err != nil {
return err
}
// config is cleaned when node is stopped
if err := b.statusNode.ResetChainData(b.config); err != nil {
return err
}
signal.SendChainDataRemoved()
return b.startNode(b.config)
}
// CallRPC executes public RPC requests on node's in-proc RPC server.
func (b *GethStatusBackend) CallRPC(inputJSON string) (string, error) {
client := b.statusNode.RPCClient()
if client == nil {
return "", ErrRPCClientUnavailable
}
return client.CallRaw(inputJSON), nil
}
// CallPrivateRPC executes public and private RPC requests on node's in-proc RPC server.
func (b *GethStatusBackend) CallPrivateRPC(inputJSON string) (string, error) {
client := b.statusNode.RPCClient()
if client == nil {
return "", ErrRPCClientUnavailable
}
return client.CallRaw(inputJSON), nil
}
// SendTransaction creates a new transaction and waits until it's complete.
func (b *GethStatusBackend) SendTransaction(sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) {
verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password)
if err != nil {
return hash, err
}
return b.transactor.SendTransaction(sendArgs, verifiedAccount)
}
func (b *GethStatusBackend) SendTransactionWithChainID(chainID uint64, sendArgs transactions.SendTxArgs, password string) (hash types.Hash, err error) {
verifiedAccount, err := b.getVerifiedWalletAccount(sendArgs.From.String(), password)
if err != nil {
return hash, err
}
return b.transactor.SendTransactionWithChainID(chainID, sendArgs, verifiedAccount)
}
func (b *GethStatusBackend) SendTransactionWithSignature(sendArgs transactions.SendTxArgs, sig []byte) (hash types.Hash, err error) {
txWithSignature, err := b.transactor.BuildTransactionWithSignature(b.transactor.NetworkID(), sendArgs, sig)
if err != nil {
return hash, err
}
return b.transactor.SendTransactionWithSignature(common.Address(sendArgs.From), sendArgs.Symbol, sendArgs.MultiTransactionID, txWithSignature)
}
// HashTransaction validate the transaction and returns new sendArgs and the transaction hash.
func (b *GethStatusBackend) HashTransaction(sendArgs transactions.SendTxArgs) (transactions.SendTxArgs, types.Hash, error) {
return b.transactor.HashTransaction(sendArgs)
}
// SignMessage checks the pwd vs the selected account and passes on the signParams
// to personalAPI for message signature
func (b *GethStatusBackend) SignMessage(rpcParams personal.SignParams) (types.HexBytes, error) {
verifiedAccount, err := b.getVerifiedWalletAccount(rpcParams.Address, rpcParams.Password)
if err != nil {
return types.HexBytes{}, err
}
return b.personalAPI.Sign(rpcParams, verifiedAccount)
}
// Recover calls the personalAPI to return address associated with the private
// key that was used to calculate the signature in the message
func (b *GethStatusBackend) Recover(rpcParams personal.RecoverParams) (types.Address, error) {
return b.personalAPI.Recover(rpcParams)
}
// SignTypedData accepts data and password. Gets verified account and signs typed data.
func (b *GethStatusBackend) SignTypedData(typed typeddata.TypedData, address string, password string) (types.HexBytes, error) {
account, err := b.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
sig, err := typeddata.Sign(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
2020-10-29 07:46:02 +00:00
// SignTypedDataV4 accepts data and password. Gets verified account and signs typed data.
func (b *GethStatusBackend) SignTypedDataV4(typed signercore.TypedData, address string, password string) (types.HexBytes, error) {
account, err := b.getVerifiedWalletAccount(address, password)
if err != nil {
return types.HexBytes{}, err
}
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
sig, err := typeddata.SignTypedDataV4(typed, account.AccountKey.PrivateKey, chain)
if err != nil {
return types.HexBytes{}, err
}
return types.HexBytes(sig), err
}
// HashTypedData generates the hash of TypedData.
func (b *GethStatusBackend) HashTypedData(typed typeddata.TypedData) (types.Hash, error) {
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
hash, err := typeddata.ValidateAndHash(typed, chain)
if err != nil {
return types.Hash{}, err
}
return types.Hash(hash), err
}
2020-10-29 07:46:02 +00:00
// HashTypedDataV4 generates the hash of TypedData.
func (b *GethStatusBackend) HashTypedDataV4(typed signercore.TypedData) (types.Hash, error) {
chain := new(big.Int).SetUint64(b.StatusNode().Config().NetworkID)
hash, err := typeddata.HashTypedDataV4(typed, chain)
if err != nil {
return types.Hash{}, err
}
return types.Hash(hash), err
}
func (b *GethStatusBackend) getVerifiedWalletAccount(address, password string) (*account.SelectedExtKey, error) {
config := b.StatusNode().Config()
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
db, err := accounts.NewDB(b.appDB)
if err != nil {
b.log.Error("failed to create new *Database instance", "error", err)
return nil, err
}
2019-12-11 13:59:37 +00:00
exists, err := db.AddressExists(types.HexToAddress(address))
if err != nil {
b.log.Error("failed to query db for a given address", "address", address, "error", err)
return nil, err
}
if !exists {
b.log.Error("failed to get a selected account", "err", transactions.ErrInvalidTxSender)
return nil, transactions.ErrAccountDoesntExist
}
key, err := b.accountManager.VerifyAccountPassword(config.KeyStoreDir, address, password)
2022-07-06 16:12:49 +00:00
if _, ok := err.(*account.ErrCannotLocateKeyFile); ok {
key, err = b.generatePartialAccountKey(db, address, password)
if err != nil {
return nil, err
}
}
if err != nil {
b.log.Error("failed to verify account", "account", address, "error", err)
return nil, err
}
return &account.SelectedExtKey{
Address: key.Address,
AccountKey: key,
}, nil
}
2022-07-06 16:12:49 +00:00
func (b *GethStatusBackend) generatePartialAccountKey(db *accounts.Database, address string, password string) (*types.Key, error) {
dbPath, err := db.GetPath(types.HexToAddress(address))
path := "m/" + dbPath[strings.LastIndex(dbPath, "/")+1:]
if err != nil {
b.log.Error("failed to get path for given account address", "account", address, "error", err)
return nil, err
}
rootAddress, err := db.GetWalletRootAddress()
if err != nil {
return nil, err
}
info, err := b.accountManager.AccountsGenerator().LoadAccount(rootAddress.Hex(), password)
if err != nil {
return nil, err
}
masterID := info.ID
accInfosMap, err := b.accountManager.AccountsGenerator().StoreDerivedAccounts(masterID, password, []string{path})
if err != nil {
return nil, err
}
_, key, err := b.accountManager.AddressToDecryptedAccount(accInfosMap[path].Address, password)
if err != nil {
return nil, err
}
return key, nil
}
// registerHandlers attaches Status callback handlers to running node
func (b *GethStatusBackend) registerHandlers() error {
var clients []*rpc.Client
if c := b.StatusNode().RPCClient(); c != nil {
clients = append(clients, c)
} else {
return errors.New("RPC client unavailable")
}
for _, client := range clients {
client.RegisterHandler(
params.AccountsMethodName,
func(context.Context, uint64, ...interface{}) (interface{}, error) {
return b.accountManager.Accounts()
},
)
if b.allowAllRPC {
// this should only happen in unit-tests, this variable is not available outside this package
continue
}
client.RegisterHandler(params.SendTransactionMethodName, unsupportedMethodHandler)
client.RegisterHandler(params.PersonalSignMethodName, unsupportedMethodHandler)
client.RegisterHandler(params.PersonalRecoverMethodName, unsupportedMethodHandler)
}
return nil
}
func unsupportedMethodHandler(ctx context.Context, chainID uint64, rpcParams ...interface{}) (interface{}, error) {
return nil, ErrUnsupportedRPCMethod
}
// ConnectionChange handles network state changes logic.
func (b *GethStatusBackend) ConnectionChange(typ string, expensive bool) {
b.mu.Lock()
defer b.mu.Unlock()
2021-05-14 10:55:42 +00:00
state := connection.State{
2022-03-28 10:10:40 +00:00
Type: connection.NewType(typ),
Expensive: expensive,
}
2021-05-14 10:55:42 +00:00
if typ == connection.None {
state.Offline = true
}
b.log.Info("Network state change", "old", b.connectionState, "new", state)
b.connectionState = state
b.statusNode.ConnectionChanged(state)
// logic of handling state changes here
// restart node? force peers reconnect? etc
}
// AppStateChange handles app state changes (background/foreground).
// state values: see https://facebook.github.io/react-native/docs/appstate.html
func (b *GethStatusBackend) AppStateChange(state string) {
var messenger *protocol.Messenger
s, err := parseAppState(state)
if err != nil {
log.Error("AppStateChange failed, ignoring", "error", err)
return
}
b.appState = s
if b.statusNode == nil {
log.Warn("statusNode nil, not reporting app state change")
return
}
if b.statusNode.WakuExtService() != nil {
messenger = b.statusNode.WakuExtService().Messenger()
}
if b.statusNode.WakuV2ExtService() != nil {
messenger = b.statusNode.WakuV2ExtService().Messenger()
}
if messenger == nil {
log.Warn("messenger nil, not reporting app state change")
return
}
if s == appStateForeground {
messenger.ToForeground()
} else {
messenger.ToBackground()
}
// TODO: put node in low-power mode if the app is in background (or inactive)
// and normal mode if the app is in foreground.
}
func (b *GethStatusBackend) StopLocalNotifications() error {
if b.statusNode == nil {
return nil
}
return b.statusNode.StopLocalNotifications()
}
func (b *GethStatusBackend) StartLocalNotifications() error {
if b.statusNode == nil {
return nil
}
return b.statusNode.StartLocalNotifications()
}
// Logout clears whisper identities.
func (b *GethStatusBackend) Logout() error {
b.mu.Lock()
defer b.mu.Unlock()
2024-01-30 11:45:08 +00:00
b.log.Debug("logging out")
err := b.cleanupServices()
if err != nil {
return err
}
err = b.closeDBs()
if err != nil {
return err
}
b.AccountManager().Logout()
b.account = nil
2021-07-07 06:11:09 +00:00
if b.statusNode != nil {
if err := b.statusNode.Stop(); err != nil {
return err
}
b.statusNode = nil
}
2024-01-30 11:45:08 +00:00
if !b.LocalPairingStateManager.IsPairing() {
signal.SendNodeStopped()
}
2021-07-07 06:11:09 +00:00
// re-initialize the node, at some point we should better manage the lifecycle
b.initialize()
err = b.statusNode.StartMediaServerWithoutDB()
if err != nil {
b.log.Error("failed to start media server without app db", "err", err)
return err
}
return nil
}
// cleanupServices stops parts of services that doesn't managed by a node and removes injected data from services.
func (b *GethStatusBackend) cleanupServices() error {
b.selectedAccountKeyID = ""
if b.statusNode == nil {
return nil
}
return b.statusNode.Cleanup()
}
func (b *GethStatusBackend) closeDBs() error {
err := b.closeWalletDB()
if err != nil {
return err
}
return b.closeAppDB()
}
func (b *GethStatusBackend) closeAppDB() error {
if b.appDB != nil {
err := b.appDB.Close()
if err != nil {
return err
}
b.appDB = nil
return nil
}
return nil
}
func (b *GethStatusBackend) closeWalletDB() error {
if b.walletDB != nil {
err := b.walletDB.Close()
if err != nil {
return err
}
b.walletDB = nil
}
return nil
}
// SelectAccount selects current wallet and chat accounts, by verifying that each address has corresponding account which can be decrypted
// using provided password. Once verification is done, the decrypted chat key is injected into Whisper (as a single identity,
// all previous identities are removed).
func (b *GethStatusBackend) SelectAccount(loginParams account.LoginParams) error {
b.mu.Lock()
defer b.mu.Unlock()
b.AccountManager().RemoveOnboarding()
err := b.accountManager.SelectAccount(loginParams)
if err != nil {
return err
}
if loginParams.MultiAccount != nil {
b.account = loginParams.MultiAccount
}
if err := b.injectAccountsIntoServices(); err != nil {
return err
}
return nil
}
2020-12-09 14:03:43 +00:00
func (b *GethStatusBackend) GetActiveAccount() (*multiaccounts.Account, error) {
2020-12-09 14:45:59 +00:00
if b.account == nil {
2020-12-09 14:36:40 +00:00
return nil, errors.New("master key account is nil in the GethStatusBackend")
2020-12-09 14:45:59 +00:00
}
2020-12-09 14:03:43 +00:00
return b.account, nil
}
func (b *GethStatusBackend) LocalPairingStarted() error {
if b.account == nil {
return errors.New("master key account is nil in the GethStatusBackend")
}
accountDB, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
return accountDB.MnemonicWasShown()
}
2022-02-10 15:15:27 +00:00
func (b *GethStatusBackend) injectAccountsIntoWakuService(w types.WakuKeyManager, st *ext.Service) error {
chatAccount, err := b.accountManager.SelectedChatAccount()
if err != nil {
return err
}
identity := chatAccount.AccountKey.PrivateKey
2020-12-09 14:03:43 +00:00
acc, err := b.GetActiveAccount()
if err != nil {
return err
}
2022-02-10 15:15:27 +00:00
if err := w.DeleteKeyPairs(); err != nil { // err is not possible; method return value is incorrect
return err
}
b.selectedAccountKeyID, err = w.AddKeyPair(identity)
if err != nil {
return ErrWakuIdentityInjectionFailure
}
2022-02-10 15:15:27 +00:00
if st != nil {
if err := st.InitProtocol(b.statusNode.GethNode().Config().Name, identity, b.appDB, b.walletDB, b.statusNode.HTTPServer(), b.multiaccountsDB, acc, b.accountManager, b.statusNode.RPCClient(), b.statusNode.WalletService(), b.statusNode.CommunityTokensService(), b.statusNode.WakuV2Service(), logutils.ZapLogger()); err != nil {
return err
}
2022-02-10 15:15:27 +00:00
// Set initial connection state
st.ConnectionChanged(b.connectionState)
2022-02-10 15:15:27 +00:00
messenger := st.Messenger()
// Init public status api
b.statusNode.StatusPublicService().Init(messenger)
2022-07-06 16:12:49 +00:00
b.statusNode.AccountService().Init(messenger)
2022-02-10 15:15:27 +00:00
// Init chat service
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
accDB, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
b.statusNode.ChatService(accDB).Init(messenger)
b.statusNode.EnsService().Init(messenger.SyncEnsNamesWithDispatchMessage)
b.statusNode.CommunityTokensService().Init(messenger)
2022-02-10 15:15:27 +00:00
}
2021-08-05 13:27:47 +00:00
2022-02-10 15:15:27 +00:00
return nil
}
2021-05-14 10:55:42 +00:00
2022-02-10 15:15:27 +00:00
func (b *GethStatusBackend) injectAccountsIntoServices() error {
if b.statusNode.WakuService() != nil {
return b.injectAccountsIntoWakuService(b.statusNode.WakuService(), func() *ext.Service {
if b.statusNode.WakuExtService() == nil {
return nil
}
return b.statusNode.WakuExtService().Service
}())
}
2022-02-10 15:15:27 +00:00
if b.statusNode.WakuV2Service() != nil {
return b.injectAccountsIntoWakuService(b.statusNode.WakuV2Service(), func() *ext.Service {
if b.statusNode.WakuV2ExtService() == nil {
return nil
}
return b.statusNode.WakuV2ExtService().Service
}())
}
return nil
}
// ExtractGroupMembershipSignatures extract signatures from tuples of content/signature
func (b *GethStatusBackend) ExtractGroupMembershipSignatures(signaturePairs [][2]string) ([]string, error) {
return crypto.ExtractSignatures(signaturePairs)
}
// SignGroupMembership signs a piece of data containing membership information
func (b *GethStatusBackend) SignGroupMembership(content string) (string, error) {
selectedChatAccount, err := b.accountManager.SelectedChatAccount()
if err != nil {
return "", err
}
return crypto.SignStringAsHex(content, selectedChatAccount.AccountKey.PrivateKey)
}
func (b *GethStatusBackend) Messenger() *protocol.Messenger {
node := b.StatusNode()
if node != nil {
accountService := node.AccountService()
if accountService != nil {
return accountService.GetMessenger()
}
}
return nil
}
// SignHash exposes vanilla ECDSA signing for signing a message for Swarm
func (b *GethStatusBackend) SignHash(hexEncodedHash string) (string, error) {
hash, err := hexutil.Decode(hexEncodedHash)
if err != nil {
return "", fmt.Errorf("SignHash: could not unmarshal the input: %v", err)
}
chatAccount, err := b.accountManager.SelectedChatAccount()
if err != nil {
return "", fmt.Errorf("SignHash: could not select account: %v", err.Error())
}
signature, err := ethcrypto.Sign(hash, chatAccount.AccountKey.PrivateKey)
if err != nil {
return "", fmt.Errorf("SignHash: could not sign the hash: %v", err)
}
hexEncodedSignature := types.EncodeHex(signature)
return hexEncodedSignature, nil
}
2022-03-17 18:06:02 +00:00
func (b *GethStatusBackend) SwitchFleet(fleet string, conf *params.NodeConfig) error {
if b.appDB == nil {
return ErrDBNotAvailable
}
Sync Settings (#2478) * Sync Settings * Added valueHandlers and Database singleton Some issues remain, need a way to comparing incoming sql.DB to check if the connection is to a different file or not. Maybe make singleton instance per filename * Added functionality to check the sqlite filename * Refactor of Database.SaveSyncSettings to be used as a handler * Implemented inteface for setting sync protobuf factories * Refactored and completed adhoc send setting sync * Tidying up * Immutability refactor * Refactor settings into dedicated package * Breakout structs * Tidy up * Refactor of bulk settings sync * Bug fixes * Addressing feedback * Fix code dropped during rebase * Fix for db closed * Fix for node config related crashes * Provisional fix for type assertion - issue 2 * Adding robust type assertion checks * Partial fix for null literal db storage and json encoding * Fix for passively handling nil sql.DB, and checking if elem has len and if len is 0 * Added test for preferred name behaviour * Adding saved sync settings to MessengerResponse * Completed granular initial sync and clock from network on save * add Settings to isEmpty * Refactor of protobufs, partially done * Added syncSetting receiver handling, some bug fixes * Fix for sticker packs * Implement inactive flag on sync protobuf factory * Refactor of types and structs * Added SettingField.CanSync functionality * Addressing rebase artifact * Refactor of Setting SELECT queries * Refactor of string return queries * VERSION bump and migration index bump * Deactiveate Sync Settings * Deactiveated preferred_name and send_status_updates Co-authored-by: Andrea Maria Piana <andrea.maria.piana@gmail.com>
2022-03-23 18:47:00 +00:00
accountDB, err := accounts.NewDB(b.appDB)
if err != nil {
return err
}
err = accountDB.SaveSetting("fleet", fleet)
2022-03-17 18:06:02 +00:00
if err != nil {
return err
}
err = nodecfg.SaveNodeConfig(b.appDB, conf)
if err != nil {
return err
}
return nil
}
func (b *GethStatusBackend) getAppDBPath(keyUID string) (string, error) {
if len(b.rootDataDir) == 0 {
return "", errors.New("root datadir wasn't provided")
}
return filepath.Join(b.rootDataDir, fmt.Sprintf("%s-v4.db", keyUID)), nil
}
func (b *GethStatusBackend) getWalletDBPath(keyUID string) (string, error) {
if len(b.rootDataDir) == 0 {
return "", errors.New("root datadir wasn't provided")
}
return filepath.Join(b.rootDataDir, fmt.Sprintf("%s-wallet.db", keyUID)), nil
}