diff --git a/mobile/status.go b/mobile/status.go index 409a6cb3b..f53db17e8 100644 --- a/mobile/status.go +++ b/mobile/status.go @@ -1140,6 +1140,36 @@ func InputConnectionStringForBootstrappingAnotherDevice(cs, configJSON string) s return makeJSONResponse(err) } +// GetConnectionStringForExportingKeypairsKeystores starts a pairing.SenderServer +// then generates a pairing.ConnectionParams. Used when the device is Logged in and therefore has Account keys +// and the device might not have a camera, to transfer kestore files of provided key uids. +func GetConnectionStringForExportingKeypairsKeystores(configJSON string) string { + if configJSON == "" { + return makeJSONResponse(fmt.Errorf("no config given, SendingServerConfig is expected")) + } + + cs, err := pairing.StartUpKeystoreFilesSenderServer(statusBackend, configJSON) + if err != nil { + return makeJSONResponse(err) + } + return cs +} + +// InputConnectionStringForImportingKeypairsKeystores starts a pairing.ReceiverClient +// The given server.ConnectionParams string will determine the server.Mode +// Used when the device is Logged in and has Account keys and has a camera to read a QR code +// +// Example: A mobile device (device with a camera) receiving account data from +// a device with a screen (mobile or desktop devices) +func InputConnectionStringForImportingKeypairsKeystores(cs, configJSON string) string { + if configJSON == "" { + return makeJSONResponse(fmt.Errorf("no config given, ReceiverClientConfig is expected")) + } + + err := pairing.StartUpKeystoreFilesReceivingClient(statusBackend, cs, configJSON) + return makeJSONResponse(err) +} + func ValidateConnectionString(cs string) string { err := pairing.ValidateConnectionString(cs) if err == nil { diff --git a/server/pairing/client.go b/server/pairing/client.go index a04aeac45..7df581a33 100644 --- a/server/pairing/client.go +++ b/server/pairing/client.go @@ -482,3 +482,112 @@ func StartUpReceivingClient(backend *api.GethStatusBackend, cs, configJSON strin } return c.sendInstallationData() } + +/* +|-------------------------------------------------------------------------- +| ReceiverClient +|-------------------------------------------------------------------------- +*/ + +type KeystoreFilesReceiverClient struct { + *BaseClient + + keystoreFilesReceiver PayloadReceiver +} + +func NewKeystoreFilesReceiverClient(backend *api.GethStatusBackend, c *ConnectionParams, config *KeystoreFilesReceiverClientConfig) (*KeystoreFilesReceiverClient, error) { + bc, err := NewBaseClient(c) + if err != nil { + return nil, err + } + + logger := logutils.ZapLogger().Named("ReceiverClient") + pe := NewPayloadEncryptor(c.aesKey) + + kfrc, err := NewKeystoreFilesPayloadReceiver(backend, pe, config.ReceiverConfig, logger) + if err != nil { + return nil, err + } + + return &KeystoreFilesReceiverClient{ + BaseClient: bc, + keystoreFilesReceiver: kfrc, + }, nil +} + +func (c *KeystoreFilesReceiverClient) receiveKeystoreFilesData() error { + c.baseAddress.Path = pairingSendAccount + req, err := http.NewRequest(http.MethodGet, c.baseAddress.String(), nil) + if err != nil { + return err + } + + err = c.challengeTaker.DoChallenge(req) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer}) + return err + } + + resp, err := c.Do(req) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer}) + return err + } + + if resp.StatusCode != http.StatusOK { + err = fmt.Errorf("[client] status not ok when receiving account data, received '%s'", resp.Status) + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer}) + return err + } + + payload, err := io.ReadAll(resp.Body) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventTransferError, Error: err.Error(), Action: ActionKeystoreFilesTransfer}) + return err + } + signal.SendLocalPairingEvent(Event{Type: EventTransferSuccess, Action: ActionKeystoreFilesTransfer}) + + err = c.keystoreFilesReceiver.Receive(payload) + if err != nil { + signal.SendLocalPairingEvent(Event{Type: EventProcessError, Error: err.Error(), Action: ActionKeystoreFilesTransfer}) + return err + } + signal.SendLocalPairingEvent(Event{Type: EventProcessSuccess, Action: ActionKeystoreFilesTransfer}) + return nil +} + +// setupKeystoreFilesReceivingClient creates a new ReceiverClient after parsing string inputs +func setupKeystoreFilesReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) (*KeystoreFilesReceiverClient, error) { + ccp := new(ConnectionParams) + err := ccp.FromString(cs) + if err != nil { + return nil, err + } + + conf := NewKeystoreFilesReceiverClientConfig() + err = json.Unmarshal([]byte(configJSON), conf) + if err != nil { + return nil, err + } + err = validateKeystoreFilesConfig(backend, conf) + if err != nil { + return nil, err + } + + return NewKeystoreFilesReceiverClient(backend, ccp, conf) +} + +// StartUpKeystoreFilesReceivingClient creates a KeystoreFilesReceiverClient and triggers all `receive` calls in sequence to the KeystoreFilesSenderServer +func StartUpKeystoreFilesReceivingClient(backend *api.GethStatusBackend, cs, configJSON string) error { + c, err := setupKeystoreFilesReceivingClient(backend, cs, configJSON) + if err != nil { + return err + } + + err = c.getChallenge() + if err != nil { + return err + } + + return c.receiveKeystoreFilesData() +} diff --git a/server/pairing/common.go b/server/pairing/common.go index 2333e7dcd..9d07b9563 100644 --- a/server/pairing/common.go +++ b/server/pairing/common.go @@ -13,6 +13,7 @@ import ( "gopkg.in/go-playground/validator.v9" "github.com/status-im/status-go/account/generator" + "github.com/status-im/status-go/api" "github.com/status-im/status-go/eth-node/keystore" ) @@ -174,3 +175,82 @@ func emptyDir(dir string) error { } return nil } + +func validateReceivedKeystoreFiles(expectedKeys []string, keys map[string][]byte, password string) error { + if len(expectedKeys) != len(keys) { + return fmt.Errorf("one or more keystore files were not sent") + } + + for _, searchKey := range expectedKeys { + found := false + for key := range keys { + if strings.Contains(key, strings.ToLower(searchKey)) { + found = true + break + } + } + if !found { + return fmt.Errorf("one or more expected keystore files are not found among the sent files") + } + } + + return validateKeys(keys, password) +} + +func validateKeystoreFilesConfig(backend *api.GethStatusBackend, conf interface{}) error { + var ( + loggedInKeyUID string + password string + numOfKeypairs int + keystorePath string + ) + + switch c := conf.(type) { + case *KeystoreFilesSenderServerConfig: + loggedInKeyUID = c.SenderConfig.LoggedInKeyUID + password = c.SenderConfig.Password + numOfKeypairs = len(c.SenderConfig.KeypairsToExport) + keystorePath = c.SenderConfig.KeystorePath + case *KeystoreFilesReceiverClientConfig: + loggedInKeyUID = c.ReceiverConfig.LoggedInKeyUID + password = c.ReceiverConfig.Password + numOfKeypairs = len(c.ReceiverConfig.KeypairsToImport) + keystorePath = c.ReceiverConfig.KeystorePath + default: + return fmt.Errorf("unknown config type: %v", reflect.TypeOf(conf)) + } + + accountService := backend.StatusNode().AccountService() + if accountService == nil { + return fmt.Errorf("cannot resolve accounts service instance") + } + + if !accountService.GetMessenger().HasPairedDevices() { + return fmt.Errorf("there are no known paired devices") + } + + selectedAccount, err := backend.GetActiveAccount() + if err != nil { + return err + } + + if selectedAccount.KeyUID != loggedInKeyUID { + return fmt.Errorf("configuration is not meant for the logged in account") + } + + if selectedAccount.KeycardPairing == "" { + if !accountService.VerifyPassword(password) { + return fmt.Errorf("provided password is not correct") + } + } + + if numOfKeypairs == 0 { + return fmt.Errorf("it should be at least a single keypair set a keystore files are transferred for") + } + + if keystorePath == "" { + return fmt.Errorf("keyStorePath can not be empty") + } + + return nil +} diff --git a/server/pairing/config.go b/server/pairing/config.go index b0ec62ca5..596f60e10 100644 --- a/server/pairing/config.go +++ b/server/pairing/config.go @@ -41,6 +41,22 @@ type ReceiverConfig struct { LoggedInKeyUID string `json:"-"` } +type KeystoreFilesConfig struct { + KeystorePath string `json:"keystorePath" validate:"required,keystorepath"` + LoggedInKeyUID string `json:"loggedInKeyUid" validate:"required,keyuid"` + Password string `json:"password" validate:"required"` +} + +type KeystoreFilesSenderConfig struct { + KeystoreFilesConfig + KeypairsToExport []string `json:"keypairsToExport" validate:"required"` +} + +type KeystoreFilesReceiverConfig struct { + KeystoreFilesConfig + KeypairsToImport []string `json:"keypairsToImport" validate:"required"` +} + type ServerConfig struct { // Timeout the number of milliseconds after which the pairing server will automatically terminate Timeout uint `json:"timeout" validate:"omitempty,gte=0"` @@ -61,6 +77,11 @@ type SenderServerConfig struct { ServerConfig *ServerConfig `json:"serverConfig" validate:"omitempty,dive"` } +type KeystoreFilesSenderServerConfig struct { + SenderConfig *KeystoreFilesSenderConfig `json:"senderConfig" validate:"required"` + ServerConfig *ServerConfig `json:"serverConfig" validate:"omitempty,dive"` +} + type SenderClientConfig struct { SenderConfig *SenderConfig `json:"senderConfig" validate:"required"` ClientConfig *ClientConfig `json:"clientConfig"` @@ -71,6 +92,11 @@ type ReceiverClientConfig struct { ClientConfig *ClientConfig `json:"clientConfig"` } +type KeystoreFilesReceiverClientConfig struct { + ReceiverConfig *KeystoreFilesReceiverConfig `json:"receiverConfig" validate:"required"` + ClientConfig *ClientConfig `json:"clientConfig"` +} + type ReceiverServerConfig struct { ReceiverConfig *ReceiverConfig `json:"receiverConfig" validate:"required"` ServerConfig *ServerConfig `json:"serverConfig" validate:"omitempty,dive"` @@ -83,6 +109,13 @@ func NewSenderServerConfig() *SenderServerConfig { } } +func NewKeystoreFilesSenderServerConfig() *KeystoreFilesSenderServerConfig { + return &KeystoreFilesSenderServerConfig{ + SenderConfig: new(KeystoreFilesSenderConfig), + ServerConfig: new(ServerConfig), + } +} + func NewSenderClientConfig() *SenderClientConfig { return &SenderClientConfig{ SenderConfig: new(SenderConfig), @@ -97,6 +130,13 @@ func NewReceiverClientConfig() *ReceiverClientConfig { } } +func NewKeystoreFilesReceiverClientConfig() *KeystoreFilesReceiverClientConfig { + return &KeystoreFilesReceiverClientConfig{ + ReceiverConfig: new(KeystoreFilesReceiverConfig), + ClientConfig: new(ClientConfig), + } +} + func NewReceiverServerConfig() *ReceiverServerConfig { return &ReceiverServerConfig{ ReceiverConfig: new(ReceiverConfig), diff --git a/server/pairing/events.go b/server/pairing/events.go index 31cc9f7b5..855d3921e 100644 --- a/server/pairing/events.go +++ b/server/pairing/events.go @@ -17,9 +17,10 @@ const ( // Only Receiver side - EventReceivedAccount EventType = "received-account" - EventProcessSuccess EventType = "process-success" - EventProcessError EventType = "process-error" + EventReceivedAccount EventType = "received-account" + EventProcessSuccess EventType = "process-success" + EventProcessError EventType = "process-error" + EventReceivedKeystoreFiles EventType = "received-keystore-files" ) // Event is a type for transfer events. @@ -38,6 +39,7 @@ const ( ActionSyncDevice ActionPairingInstallation ActionPeerDiscovery + ActionKeystoreFilesTransfer ) type AccountData struct { diff --git a/server/pairing/payload_management.go b/server/pairing/payload_management.go index f4948a7f4..1fd68edc9 100644 --- a/server/pairing/payload_management.go +++ b/server/pairing/payload_management.go @@ -45,13 +45,16 @@ func NewPairingPayloadMarshaller(ap *AccountPayload, logger *zap.Logger) *Accoun } func (ppm *AccountPayloadMarshaller) MarshalProtobuf() ([]byte, error) { - return proto.Marshal(&protobuf.LocalPairingPayload{ + lpp := &protobuf.LocalPairingPayload{ Keys: ppm.accountKeysToProtobuf(), - Multiaccount: ppm.multiaccount.ToProtobuf(), Password: ppm.password, ChatKey: ppm.chatKey, KeycardPairings: ppm.keycardPairings, - }) + } + if ppm.multiaccount != nil { + lpp.Multiaccount = ppm.multiaccount.ToProtobuf() + } + return proto.Marshal(lpp) } func (ppm *AccountPayloadMarshaller) accountKeysToProtobuf() []*protobuf.LocalPairingPayload_Key { @@ -79,7 +82,9 @@ func (ppm *AccountPayloadMarshaller) UnmarshalProtobuf(data []byte) error { } ppm.accountKeysFromProtobuf(pb.Keys) - ppm.multiaccountFromProtobuf(pb.Multiaccount) + if pb.Multiaccount != nil { + ppm.multiaccountFromProtobuf(pb.Multiaccount) + } ppm.password = pb.Password ppm.chatKey = pb.ChatKey ppm.keycardPairings = pb.KeycardPairings diff --git a/server/pairing/payload_mounter.go b/server/pairing/payload_mounter.go index cdebe2236..24006c3e2 100644 --- a/server/pairing/payload_mounter.go +++ b/server/pairing/payload_mounter.go @@ -1,10 +1,14 @@ package pairing import ( + "fmt" + "strings" + "go.uber.org/zap" "github.com/status-im/status-go/api" "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/accounts" ) type PayloadMounter interface { @@ -243,3 +247,93 @@ func NewPayloadMounters(logger *zap.Logger, pe *PayloadEncryptor, backend *api.G imr := NewInstallationPayloadMounterReceiver(pe, backend, config.DeviceType) return am, rmm, imr, nil } + +/* +|-------------------------------------------------------------------------- +| KeystoreFilesPayload +|-------------------------------------------------------------------------- +*/ + +func NewKeystoreFilesPayloadMounter(backend *api.GethStatusBackend, pe *PayloadEncryptor, config *KeystoreFilesSenderConfig, logger *zap.Logger) (*BasePayloadMounter, error) { + l := logger.Named("KeystoreFilesPayloadLoader") + l.Debug("fired", zap.Any("config", config)) + + pe = pe.Renew() + + // A new SHARED AccountPayload + p := new(AccountPayload) + kfpl, err := NewKeystoreFilesPayloadLoader(backend, p, config) + if err != nil { + return nil, err + } + + return NewBasePayloadMounter( + kfpl, + NewPairingPayloadMarshaller(p, l), + pe, + ), nil +} + +type KeystoreFilesPayloadLoader struct { + *AccountPayload + + keystorePath string + loggedInKeyUID string + keystoreFilesToTransfer []string +} + +func NewKeystoreFilesPayloadLoader(backend *api.GethStatusBackend, p *AccountPayload, config *KeystoreFilesSenderConfig) (*KeystoreFilesPayloadLoader, error) { + if config == nil { + return nil, fmt.Errorf("empty keystore files sender config") + } + + kfpl := &KeystoreFilesPayloadLoader{ + AccountPayload: p, + keystorePath: config.KeystorePath, + loggedInKeyUID: config.LoggedInKeyUID, + } + + kfpl.password = config.Password + + accountService := backend.StatusNode().AccountService() + + for _, keyUID := range config.KeypairsToExport { + kp, err := accountService.GetKeypairByKeyUID(keyUID) + if err != nil { + return nil, err + } + + if kp.Type == accounts.KeypairTypeSeed { + kfpl.keystoreFilesToTransfer = append(kfpl.keystoreFilesToTransfer, kp.DerivedFrom[2:]) + } + + for _, acc := range kp.Accounts { + kfpl.keystoreFilesToTransfer = append(kfpl.keystoreFilesToTransfer, acc.Address.Hex()[2:]) + } + } + + return kfpl, nil +} + +func (kfpl *KeystoreFilesPayloadLoader) Load() error { + kfpl.keys = make(map[string][]byte) + err := loadKeys(kfpl.keys, kfpl.keystorePath) + if err != nil { + return err + } + + // Create a new map to filter keys + filteredMap := make(map[string][]byte) + for _, searchKey := range kfpl.keystoreFilesToTransfer { + for key, value := range kfpl.keys { + if strings.Contains(key, strings.ToLower(searchKey)) { + filteredMap[key] = value + break + } + } + } + + kfpl.keys = filteredMap + + return validateKeys(kfpl.keys, kfpl.password) +} diff --git a/server/pairing/payload_receiver.go b/server/pairing/payload_receiver.go index 71fdf70bc..7d703c578 100644 --- a/server/pairing/payload_receiver.go +++ b/server/pairing/payload_receiver.go @@ -5,12 +5,14 @@ import ( "io/ioutil" "os" "path/filepath" + "strings" "go.uber.org/multierr" "go.uber.org/zap" "github.com/status-im/status-go/api" "github.com/status-im/status-go/multiaccounts" + "github.com/status-im/status-go/multiaccounts/accounts" "github.com/status-im/status-go/params" "github.com/status-im/status-go/signal" ) @@ -346,3 +348,128 @@ func NewPayloadReceivers(logger *zap.Logger, pe *PayloadEncryptor, backend *api. imr := NewInstallationPayloadMounterReceiver(pe, backend, config.DeviceType) return ar, rmr, imr, nil } + +/* +|-------------------------------------------------------------------------- +| KeystoreFilesPayload +|-------------------------------------------------------------------------- +*/ + +func NewKeystoreFilesPayloadReceiver(backend *api.GethStatusBackend, e *PayloadEncryptor, config *KeystoreFilesReceiverConfig, logger *zap.Logger) (*BasePayloadReceiver, error) { + l := logger.Named("KeystoreFilesPayloadManager") + l.Debug("fired", zap.Any("config", config)) + + e = e.Renew() + + // A new SHARED AccountPayload + p := new(AccountPayload) + + kfps, err := NewKeystoreFilesPayloadStorer(backend, p, config) + if err != nil { + return nil, err + } + + return NewBasePayloadReceiver(e, NewPairingPayloadMarshaller(p, l), kfps, + func() { + data := len(p.keys) + signal.SendLocalPairingEvent(Event{Type: EventReceivedKeystoreFiles, Action: ActionKeystoreFilesTransfer, Data: data}) + }, + ), nil +} + +type KeystoreFilesPayloadStorer struct { + *AccountPayload + + keystorePath string + loggedInKeyUID string + expectedKeystoreFilesToReceive []string +} + +func NewKeystoreFilesPayloadStorer(backend *api.GethStatusBackend, p *AccountPayload, config *KeystoreFilesReceiverConfig) (*KeystoreFilesPayloadStorer, error) { + if config == nil { + return nil, fmt.Errorf("empty keystore files receiver config") + } + + kfps := &KeystoreFilesPayloadStorer{ + AccountPayload: p, + keystorePath: config.KeystorePath, + loggedInKeyUID: config.LoggedInKeyUID, + } + + accountService := backend.StatusNode().AccountService() + + for _, keyUID := range config.KeypairsToImport { + kp, err := accountService.GetKeypairByKeyUID(keyUID) + if err != nil { + return nil, err + } + + if kp.Type == accounts.KeypairTypeSeed { + kfps.expectedKeystoreFilesToReceive = append(kfps.expectedKeystoreFilesToReceive, kp.DerivedFrom[2:]) + } + + for _, acc := range kp.Accounts { + kfps.expectedKeystoreFilesToReceive = append(kfps.expectedKeystoreFilesToReceive, acc.Address.Hex()[2:]) + } + } + + return kfps, nil +} + +func (kfps *KeystoreFilesPayloadStorer) Store() error { + err := validateReceivedKeystoreFiles(kfps.expectedKeystoreFilesToReceive, kfps.keys, kfps.password) + if err != nil { + return err + } + + return kfps.storeKeys(kfps.keystorePath) +} + +func (kfps *KeystoreFilesPayloadStorer) storeKeys(keyStorePath string) error { + if keyStorePath == "" { + return fmt.Errorf("keyStorePath can not be empty") + } + + _, lastDir := filepath.Split(keyStorePath) + + // If lastDir == keystoreDir we presume we need to create the rest of the keystore path + // else we presume the provided keystore is valid + if lastDir == keystoreDir { + keyStorePath = filepath.Join(keyStorePath, kfps.loggedInKeyUID) + _, err := os.Stat(keyStorePath) + if os.IsNotExist(err) { + err := os.MkdirAll(keyStorePath, 0700) + if err != nil { + return err + } + } else if err != nil { + return err + } + } + + for name, data := range kfps.keys { + found := false + for _, key := range kfps.expectedKeystoreFilesToReceive { + if strings.Contains(name, strings.ToLower(key)) { + found = true + } + } + if !found { + continue + } + + err := ioutil.WriteFile(filepath.Join(keyStorePath, name), data, 0600) + if err != nil { + writeErr := fmt.Errorf("failed to write key to path '%s' : %w", filepath.Join(keyStorePath, name), err) + // If we get an error on any of the key files attempt to revert + err := emptyDir(keyStorePath) + if err != nil { + // If we get an error when trying to empty the dir combine the write error and empty error + emptyDirErr := fmt.Errorf("failed to revert and cleanup storeKeys : %w", err) + return multierr.Combine(writeErr, emptyDirErr) + } + return writeErr + } + } + return nil +} diff --git a/server/pairing/server.go b/server/pairing/server.go index fb1c031cb..8afb0db9b 100644 --- a/server/pairing/server.go +++ b/server/pairing/server.go @@ -296,3 +296,84 @@ func StartUpReceiverServer(backend *api.GethStatusBackend, configJSON string) (s return cp.ToString(), nil } + +/* +|-------------------------------------------------------------------------- +| type KeystoreFilesSenderServer struct { +|-------------------------------------------------------------------------- +*/ + +type KeystoreFilesSenderServer struct { + *BaseServer + keystoreFilesMounter PayloadMounter +} + +func NewKeystoreFilesSenderServer(backend *api.GethStatusBackend, config *KeystoreFilesSenderServerConfig) (*KeystoreFilesSenderServer, error) { + logger := logutils.ZapLogger().Named("SenderServer") + e := NewPayloadEncryptor(config.ServerConfig.EK) + + bs, err := NewBaseServer(logger, e, config.ServerConfig) + if err != nil { + return nil, err + } + + kfm, err := NewKeystoreFilesPayloadMounter(backend, e, config.SenderConfig, logger) + if err != nil { + return nil, err + } + + return &KeystoreFilesSenderServer{ + BaseServer: bs, + keystoreFilesMounter: kfm, + }, nil +} + +func (s *KeystoreFilesSenderServer) startSendingData() error { + s.SetHandlers(server.HandlerPatternMap{ + pairingChallenge: handlePairingChallenge(s.challengeGiver), + pairingSendAccount: middlewareChallenge(s.challengeGiver, handleSendAccount(s.GetLogger(), s.keystoreFilesMounter)), + }) + return s.Start() +} + +// MakeFullSenderServer generates a fully configured and randomly seeded KeystoreFilesSenderServer +func MakeKeystoreFilesSenderServer(backend *api.GethStatusBackend, config *KeystoreFilesSenderServerConfig) (*KeystoreFilesSenderServer, error) { + err := MakeServerConfig(config.ServerConfig) + if err != nil { + return nil, err + } + + return NewKeystoreFilesSenderServer(backend, config) +} + +// StartUpKeystoreFilesSenderServer generates a KeystoreFilesSenderServer, starts the sending server +// and returns the ConnectionParams string to allow a ReceiverClient to make a successful connection. +func StartUpKeystoreFilesSenderServer(backend *api.GethStatusBackend, configJSON string) (string, error) { + conf := NewKeystoreFilesSenderServerConfig() + err := json.Unmarshal([]byte(configJSON), conf) + if err != nil { + return "", err + } + + err = validateKeystoreFilesConfig(backend, conf) + if err != nil { + return "", err + } + + ps, err := MakeKeystoreFilesSenderServer(backend, conf) + if err != nil { + return "", err + } + + err = ps.startSendingData() + if err != nil { + return "", err + } + + cp, err := ps.MakeConnectionParams() + if err != nil { + return "", err + } + + return cp.ToString(), nil +} diff --git a/server/pairing/sync_device_test.go b/server/pairing/sync_device_test.go index 08b32e286..7ac47b273 100644 --- a/server/pairing/sync_device_test.go +++ b/server/pairing/sync_device_test.go @@ -3,7 +3,10 @@ package pairing import ( "context" "encoding/json" + "fmt" + "os" "path/filepath" + "strings" "testing" "time" @@ -29,20 +32,25 @@ import ( "github.com/status-im/status-go/protocol/identity/alias" "github.com/status-im/status-go/protocol/protobuf" "github.com/status-im/status-go/protocol/requests" + accservice "github.com/status-im/status-go/services/accounts" "github.com/status-im/status-go/services/browsers" "github.com/status-im/status-go/sqlite" ) const ( - pathWalletRoot = "m/44'/60'/0'/0" - pathEIP1581 = "m/43'/60'/1581'" - pathDefaultChat = pathEIP1581 + "/0'/0" - pathDefaultWallet = pathWalletRoot + "/0" - currentNetwork = "mainnet_rpc" - socialLinkURL = "https://github.com/status-im" - ensUsername = "bob.stateofus.eth" - ensChainID = 1 - publicChatID = "localpairtest" + pathWalletRoot = "m/44'/60'/0'/0" + pathEIP1581 = "m/43'/60'/1581'" + pathDefaultChat = pathEIP1581 + "/0'/0" + pathDefaultWallet = pathWalletRoot + "/0" + currentNetwork = "mainnet_rpc" + socialLinkURL = "https://github.com/status-im" + ensUsername = "bob.stateofus.eth" + ensChainID = 1 + publicChatID = "localpairtest" + profileMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon" + seedPhraseMnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + path0 = "m/44'/60'/0'/0/0" + path1 = "m/44'/60'/0'/0/1" ) var paths = []string{pathWalletRoot, pathEIP1581, pathDefaultChat, pathDefaultWallet} @@ -68,13 +76,25 @@ func (s *SyncDeviceSuite) SetupTest() { s.pairThreeDevicesTmpdir = s.T().TempDir() } -func (s *SyncDeviceSuite) prepareBackendWithAccount(tmpdir string) *api.GethStatusBackend { +func (s *SyncDeviceSuite) prepareBackendWithAccount(mnemonic, tmpdir string) *api.GethStatusBackend { backend := s.prepareBackendWithoutAccount(tmpdir) accountManager := backend.AccountManager() - generator := accountManager.AccountsGenerator() - generatedAccountInfos, err := generator.GenerateAndDeriveAddresses(12, 1, "", paths) - require.NoError(s.T(), err) - generatedAccountInfo := generatedAccountInfos[0] + accGenerator := accountManager.AccountsGenerator() + + var ( + generatedAccountInfo generator.GeneratedAndDerivedAccountInfo + err error + ) + if len(mnemonic) > 0 { + generatedAccountInfo.GeneratedAccountInfo, err = accGenerator.ImportMnemonic(mnemonic, "") + require.NoError(s.T(), err) + generatedAccountInfo.Derived, err = accGenerator.DeriveAddresses(generatedAccountInfo.ID, paths) + require.NoError(s.T(), err) + } else { + generatedAccountInfos, err := accGenerator.GenerateAndDeriveAddresses(12, 1, "", paths) + require.NoError(s.T(), err) + generatedAccountInfo = generatedAccountInfos[0] + } account := multiaccounts.Account{ KeyUID: generatedAccountInfo.KeyUID, KDFIterations: sqlite.ReducedKDFIterationsNumber, @@ -84,7 +104,7 @@ func (s *SyncDeviceSuite) prepareBackendWithAccount(tmpdir string) *api.GethStat err = backend.OpenAccounts() require.NoError(s.T(), err) derivedAddresses := generatedAccountInfo.Derived - _, err = generator.StoreDerivedAccounts(generatedAccountInfo.ID, s.password, paths) + _, err = accGenerator.StoreDerivedAccounts(generatedAccountInfo.ID, s.password, paths) require.NoError(s.T(), err) settings, err := defaultSettings(generatedAccountInfo.GeneratedAccountInfo, derivedAddresses, nil) @@ -256,7 +276,7 @@ func (s *SyncDeviceSuite) checkMutualContact(backend *api.GethStatusBackend, con func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsSender() { clientTmpDir := filepath.Join(s.clientAsSenderTmpdir, "client") - clientBackend := s.prepareBackendWithAccount(clientTmpDir) + clientBackend := s.prepareBackendWithAccount("", clientTmpDir) serverTmpDir := filepath.Join(s.clientAsSenderTmpdir, "server") serverBackend := s.prepareBackendWithoutAccount(serverTmpDir) defer func() { @@ -380,7 +400,7 @@ func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsReceiver() { ctx := context.TODO() serverTmpDir := filepath.Join(s.clientAsReceiverTmpdir, "server") - serverBackend := s.prepareBackendWithAccount(serverTmpDir) + serverBackend := s.prepareBackendWithAccount("", serverTmpDir) defer func() { require.NoError(s.T(), clientBackend.Logout()) require.NoError(s.T(), serverBackend.Logout()) @@ -512,13 +532,13 @@ func (s *SyncDeviceSuite) TestPairingSyncDeviceClientAsReceiver() { func (s *SyncDeviceSuite) TestPairingThreeDevices() { bobTmpDir := filepath.Join(s.pairThreeDevicesTmpdir, "bob") - bobBackend := s.prepareBackendWithAccount(bobTmpDir) + bobBackend := s.prepareBackendWithAccount("", bobTmpDir) bobMessenger := bobBackend.Messenger() _, err := bobMessenger.Start() s.Require().NoError(err) alice1TmpDir := filepath.Join(s.pairThreeDevicesTmpdir, "alice1") - alice1Backend := s.prepareBackendWithAccount(alice1TmpDir) + alice1Backend := s.prepareBackendWithAccount("", alice1TmpDir) alice1Messenger := alice1Backend.Messenger() _, err = alice1Messenger.Start() s.Require().NoError(err) @@ -527,7 +547,7 @@ func (s *SyncDeviceSuite) TestPairingThreeDevices() { alice2Backend := s.prepareBackendWithoutAccount(alice2TmpDir) alice3TmpDir := filepath.Join(s.pairThreeDevicesTmpdir, "alice3") - alice3Backend := s.prepareBackendWithAccount(alice3TmpDir) + alice3Backend := s.prepareBackendWithAccount("", alice3TmpDir) defer func() { require.NoError(s.T(), bobBackend.Logout()) @@ -688,3 +708,160 @@ func buildTestMessage(chat *protocol.Chat) *common.Message { return message } + +func (s *SyncDeviceSuite) getSeedPhraseKeypairForTest(backend *api.GethStatusBackend, server bool) *accounts.Keypair { + generatedAccount, err := backend.AccountManager().AccountsGenerator().ImportMnemonic(seedPhraseMnemonic, "") + require.NoError(s.T(), err) + generatedDerivedAccs, err := backend.AccountManager().AccountsGenerator().DeriveAddresses(generatedAccount.ID, []string{path0, path1}) + require.NoError(s.T(), err) + + seedPhraseKp := &accounts.Keypair{ + KeyUID: generatedAccount.KeyUID, + Name: "SeedPhraseImported", + Type: accounts.KeypairTypeSeed, + DerivedFrom: generatedAccount.Address, + } + i := 0 + for path, ga := range generatedDerivedAccs { + acc := &accounts.Account{ + Address: types.HexToAddress(ga.Address), + KeyUID: generatedAccount.KeyUID, + Wallet: false, + Chat: false, + Type: accounts.AccountTypeSeed, + Path: path, + PublicKey: types.HexBytes(ga.PublicKey), + Name: fmt.Sprintf("Acc_%d", i), + Operable: accounts.AccountFullyOperable, + Emoji: fmt.Sprintf("Emoji_%d", i), + ColorID: "blue", + } + if !server { + acc.Operable = accounts.AccountNonOperable + } + seedPhraseKp.Accounts = append(seedPhraseKp.Accounts, acc) + i++ + } + + return seedPhraseKp +} + +func (s *SyncDeviceSuite) TestTransferringKeystoreFiles() { + ctx := context.TODO() + + serverTmpDir := filepath.Join(s.clientAsReceiverTmpdir, "server") + serverBackend := s.prepareBackendWithAccount(profileMnemonic, serverTmpDir) + + clientTmpDir := filepath.Join(s.clientAsReceiverTmpdir, "client") + clientBackend := s.prepareBackendWithAccount(profileMnemonic, clientTmpDir) + defer func() { + require.NoError(s.T(), clientBackend.Logout()) + require.NoError(s.T(), serverBackend.Logout()) + }() + + serverBackend.Messenger().SetLocalPairing(true) + clientBackend.Messenger().SetLocalPairing(true) + + serverActiveAccount, err := serverBackend.GetActiveAccount() + require.NoError(s.T(), err) + + clientActiveAccount, err := clientBackend.GetActiveAccount() + require.NoError(s.T(), err) + + require.True(s.T(), serverActiveAccount.KeyUID == clientActiveAccount.KeyUID) + + serverSeedPhraseKp := s.getSeedPhraseKeypairForTest(serverBackend, true) + serverAccountsAPI := serverBackend.StatusNode().AccountService().APIs()[1].Service.(*accservice.API) + err = serverAccountsAPI.ImportMnemonic(ctx, seedPhraseMnemonic, s.password) + require.NoError(s.T(), err, "importing mnemonic for new keypair on server") + err = serverAccountsAPI.AddKeypair(ctx, s.password, serverSeedPhraseKp) + require.NoError(s.T(), err, "saving seed phrase keypair on server with keystore files created") + + clientSeedPhraseKp := s.getSeedPhraseKeypairForTest(serverBackend, true) + clientAccountsAPI := clientBackend.StatusNode().AccountService().APIs()[1].Service.(*accservice.API) + err = clientAccountsAPI.SaveKeypair(ctx, clientSeedPhraseKp) + require.NoError(s.T(), err, "saving seed phrase keypair on client without keystore files") + + containsKeystoreFile := func(directory, key string) bool { + files, err := os.ReadDir(directory) + if err != nil { + return false + } + + for _, file := range files { + if strings.Contains(file.Name(), strings.ToLower(key)) { + return true + } + } + return false + } + + // check server - server should contain keystore files for imported seed phrase + serverKeystorePath := filepath.Join(serverTmpDir, keystoreDir, serverActiveAccount.KeyUID) + require.True(s.T(), containsKeystoreFile(serverKeystorePath, serverSeedPhraseKp.DerivedFrom[2:])) + for _, acc := range serverSeedPhraseKp.Accounts { + require.True(s.T(), containsKeystoreFile(serverKeystorePath, acc.Address.String()[2:])) + } + + // check client - client should not contain keystore files for imported seed phrase + clientKeystorePath := filepath.Join(clientTmpDir, keystoreDir, clientActiveAccount.KeyUID) + require.False(s.T(), containsKeystoreFile(clientKeystorePath, clientSeedPhraseKp.DerivedFrom[2:])) + for _, acc := range clientSeedPhraseKp.Accounts { + require.False(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:])) + } + + // prepare sender + var config = KeystoreFilesSenderServerConfig{ + SenderConfig: &KeystoreFilesSenderConfig{ + KeystoreFilesConfig: KeystoreFilesConfig{ + KeystorePath: serverKeystorePath, + LoggedInKeyUID: serverActiveAccount.KeyUID, + Password: s.password, + }, + KeypairsToExport: []string{serverSeedPhraseKp.KeyUID}, + }, + ServerConfig: new(ServerConfig), + } + configBytes, err := json.Marshal(config) + require.NoError(s.T(), err) + cs, err := StartUpKeystoreFilesSenderServer(serverBackend, string(configBytes)) + require.NoError(s.T(), err) + + // prepare receiver + clientPayloadSourceConfig := KeystoreFilesReceiverClientConfig{ + ReceiverConfig: &KeystoreFilesReceiverConfig{ + KeystoreFilesConfig: KeystoreFilesConfig{ + KeystorePath: clientKeystorePath, + LoggedInKeyUID: clientActiveAccount.KeyUID, + Password: s.password, + }, + KeypairsToImport: []string{serverSeedPhraseKp.KeyUID}, + }, + ClientConfig: new(ClientConfig), + } + clientConfigBytes, err := json.Marshal(clientPayloadSourceConfig) + require.NoError(s.T(), err) + err = StartUpKeystoreFilesReceivingClient(clientBackend, cs, string(clientConfigBytes)) + require.NoError(s.T(), err) + + // check client - client should contain keystore files for imported seed phrase + accountManager := clientBackend.AccountManager() + accGenerator := accountManager.AccountsGenerator() + require.True(s.T(), containsKeystoreFile(clientKeystorePath, clientSeedPhraseKp.DerivedFrom[2:])) + for _, acc := range clientSeedPhraseKp.Accounts { + require.True(s.T(), containsKeystoreFile(clientKeystorePath, acc.Address.String()[2:])) + } + + // reinit keystore on client + require.NoError(s.T(), accountManager.InitKeystore(clientKeystorePath)) + + // check keystore on client + genAccInfo, err := accGenerator.LoadAccount(clientSeedPhraseKp.DerivedFrom, s.password) + require.NoError(s.T(), err) + require.Equal(s.T(), clientSeedPhraseKp.KeyUID, genAccInfo.KeyUID) + for _, acc := range clientSeedPhraseKp.Accounts { + genAccInfo, err := accGenerator.LoadAccount(acc.Address.String(), s.password) + require.NoError(s.T(), err) + require.Equal(s.T(), acc.Address.String(), genAccInfo.Address) + } +} diff --git a/services/accounts/service.go b/services/accounts/service.go index b308a1307..27ec8b38d 100644 --- a/services/accounts/service.go +++ b/services/accounts/service.go @@ -82,3 +82,12 @@ func (s *Service) GetSettings() (settings.Settings, error) { func (s *Service) GetMessenger() *protocol.Messenger { return s.messenger } + +func (s *Service) VerifyPassword(password string) bool { + address, err := s.db.GetChatAddress() + if err != nil { + return false + } + _, err = s.manager.VerifyAccountPassword(s.config.KeyStoreDir, address.Hex(), password) + return err == nil +}