diff --git a/src/app/boot/app_controller.nim b/src/app/boot/app_controller.nim index e7fb5c742a..255ca4c01b 100644 --- a/src/app/boot/app_controller.nim +++ b/src/app/boot/app_controller.nim @@ -215,7 +215,8 @@ proc newAppController*(statusFoundation: StatusFoundation): AppController = result.privacyService = privacy_service.newService(statusFoundation.events, result.settingsService, result.accountsService) result.savedAddressService = saved_address_service.newService(statusFoundation.events, result.networkService, result.settingsService) - result.devicesService = devices_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.accountsService) + result.devicesService = devices_service.newService(statusFoundation.events, statusFoundation.threadpool, + result.settingsService, result.accountsService, result.walletAccountService) result.mailserversService = mailservers_service.newService(statusFoundation.events, statusFoundation.threadpool, result.settingsService, result.nodeConfigurationService, statusFoundation.fleetConfiguration) result.nodeService = node_service.newService(statusFoundation.events, result.settingsService, result.nodeConfigurationService) diff --git a/src/app/core/signals/remote_signals/pairing.nim b/src/app/core/signals/remote_signals/pairing.nim index b7604cdeac..692a333c61 100644 --- a/src/app/core/signals/remote_signals/pairing.nim +++ b/src/app/core/signals/remote_signals/pairing.nim @@ -1,8 +1,8 @@ -import json, tables, chronicles +import json, tables, sequtils, sugar, chronicles import base -import ../../../../app_service/service/accounts/dto/accounts -import ../../../../app_service/service/devices/dto/installation -import ../../../../app_service/service/devices/dto/local_pairing_event +import app_service/service/accounts/dto/accounts +import app_service/service/devices/dto/installation +import app_service/service/devices/dto/local_pairing_event type LocalPairingSignal* = ref object of Signal @@ -11,6 +11,7 @@ type LocalPairingSignal* = ref object of Signal error*: string accountData*: LocalPairingAccountData installation*: InstallationDto + transferredKeypairs*: seq[string] ## seq[keypair_key_uid] proc fromEvent*(T: type LocalPairingSignal, event: JsonNode): LocalPairingSignal = result = LocalPairingSignal() @@ -29,5 +30,7 @@ proc fromEvent*(T: type LocalPairingSignal, event: JsonNode): LocalPairingSignal result.accountData = e["data"].toLocalPairingAccountData() of EventReceivedInstallation: result.installation = e["data"].toInstallationDto() + of EventReceivedKeystoreFiles: + result.transferredKeypairs = map(e["data"].getElems(), x => x.getStr()) else: discard diff --git a/src/app/modules/main/profile_section/devices/view.nim b/src/app/modules/main/profile_section/devices/view.nim index 2b4a4758a1..2856e302e7 100644 --- a/src/app/modules/main/profile_section/devices/view.nim +++ b/src/app/modules/main/profile_section/devices/view.nim @@ -27,7 +27,7 @@ QtObject: result.devicesLoadingError = false result.model = newModel() result.modelVariant = newQVariant(result.model) - result.localPairingStatus = newLocalPairingStatus() + result.localPairingStatus = newLocalPairingStatus(PairingType.AppSync, LocalPairingMode.Receiver) proc load*(self: View) = self.delegate.viewDidLoad() diff --git a/src/app/modules/startup/view.nim b/src/app/modules/startup/view.nim index 6e35156748..c5406b47ba 100644 --- a/src/app/modules/startup/view.nim +++ b/src/app/modules/startup/view.nim @@ -69,7 +69,7 @@ QtObject: result.loginAccountsModel = login_acc_model.newModel() result.loginAccountsModelVariant = newQVariant(result.loginAccountsModel) result.remainingAttempts = -1 - result.localPairingStatus = newLocalPairingStatus() + result.localPairingStatus = newLocalPairingStatus(PairingType.AppSync, LocalPairingMode.Receiver) signalConnect(result.currentStartupState, "backActionClicked()", result, "onBackActionClicked()", 2) signalConnect(result.currentStartupState, "primaryActionClicked()", result, "onPrimaryActionClicked()", 2) diff --git a/src/app_service/service/devices/async_tasks.nim b/src/app_service/service/devices/async_tasks.nim index 82d297a275..2d05f3e1a8 100644 --- a/src/app_service/service/devices/async_tasks.nim +++ b/src/app_service/service/devices/async_tasks.nim @@ -18,3 +18,8 @@ const asyncInputConnectionStringTask: Task = proc(argEncoded: string) {.gcsafe, let arg = decode[AsyncInputConnectionStringArg](argEncoded) let response = status_go.inputConnectionStringForBootstrapping(arg.connectionString, arg.configJSON) arg.finish(response) + +const asyncInputConnectionStringForImportingKeystoreTask: Task = proc(argEncoded: string) {.gcsafe, nimcall.} = + let arg = decode[AsyncInputConnectionStringArg](argEncoded) + let response = status_go.inputConnectionStringForImportingKeypairsKeystores(arg.connectionString, arg.configJSON) + arg.finish(response) diff --git a/src/app_service/service/devices/dto/local_pairing_event.nim b/src/app_service/service/devices/dto/local_pairing_event.nim index 33eba30323..f696e2cea2 100644 --- a/src/app_service/service/devices/dto/local_pairing_event.nim +++ b/src/app_service/service/devices/dto/local_pairing_event.nim @@ -6,23 +6,25 @@ import installation type EventType* {.pure.} = enum - EventUnknown = -1, - EventConnectionError = 0, - EventConnectionSuccess = 1, - EventTransferError = 2, - EventTransferSuccess = 3, - EventReceivedAccount = 4, - EventReceivedInstallation = 5 - EventProcessSuccess = 6, - EventProcessError = 7 + EventUnknown = -1 + EventConnectionError + EventConnectionSuccess + EventTransferError + EventTransferSuccess + EventReceivedAccount + EventReceivedInstallation + EventProcessSuccess + EventProcessError + EventReceivedKeystoreFiles type Action* {.pure.} = enum ActionUnknown = 0 - ActionConnect = 1, - ActionPairingAccount = 2, - ActionSyncDevice = 3, - ActionPairingInstallation = 4, + ActionConnect + ActionPairingAccount + ActionSyncDevice + ActionPairingInstallation + ActionKeystoreFilesTransfer type LocalPairingAccountData* = ref object @@ -38,6 +40,7 @@ type error*: string accountData*: LocalPairingAccountData installation*: InstallationDto + transferredKeypairs*: seq[string] ## seq[keypair_key_uid] proc parse*(self: string): EventType = case self: @@ -57,6 +60,8 @@ proc parse*(self: string): EventType = return EventReceivedAccount of "received-installation": return EventReceivedInstallation + of "received-keystore-files": + return EventReceivedKeystoreFiles else: return EventUnknown diff --git a/src/app_service/service/devices/dto/local_pairing_status.nim b/src/app_service/service/devices/dto/local_pairing_status.nim index 9da67ed48a..1a7e7577cd 100644 --- a/src/app_service/service/devices/dto/local_pairing_status.nim +++ b/src/app_service/service/devices/dto/local_pairing_status.nim @@ -3,6 +3,11 @@ import ../../accounts/dto/accounts import installation import local_pairing_event +type + PairingType* {.pure.} = enum + AppSync = 0 + KeypairSync + type LocalPairingState* {.pure.} = enum Idle = 0 @@ -18,29 +23,25 @@ type type LocalPairingStatus* = ref object of Args + pairingType*: PairingType mode*: LocalPairingMode state*: LocalPairingState account*: AccountDto password*: string chatKey*: string installation*: InstallationDto + transferredKeypairs*: seq[string] ## seq[keypair_key_uid] error*: string -proc reset*(self: LocalPairingStatus) = - self.mode = LocalPairingMode.Idle - self.state = LocalPairingState.Idle - self.error = "" - self.installation = InstallationDto() - -proc setup(self: LocalPairingStatus) = - self.reset() - proc delete*(self: LocalPairingStatus) = discard -proc newLocalPairingStatus*(): LocalPairingStatus = +proc newLocalPairingStatus*(pairingType: PairingType, mode: LocalPairingMode): LocalPairingStatus = new(result, delete) - result.setup() + result.pairingType = pairingType + result.mode = mode + result.state = LocalPairingState.Idle + result.installation = InstallationDto() proc update*(self: LocalPairingStatus, data: LocalPairingEventArgs) = @@ -54,6 +55,8 @@ proc update*(self: LocalPairingStatus, data: LocalPairingEventArgs) = self.chatKey = data.accountData.chatKey of EventReceivedInstallation: self.installation = data.installation + of EventReceivedKeystoreFiles: + self.transferredKeypairs = data.transferredKeypairs of EventConnectionError: self.state = LocalPairingState.Error of EventTransferError: @@ -67,15 +70,19 @@ proc update*(self: LocalPairingStatus, data: LocalPairingEventArgs) = return # Detect finished state - if (self.mode == LocalPairingMode.Sender and - data.eventType == EventProcessSuccess and - data.action == ActionPairingInstallation): - self.state = LocalPairingState.Finished + if self.mode == LocalPairingMode.Sender and + data.eventType == EventProcessSuccess: + if self.pairingType == PairingType.AppSync and + data.action == ActionPairingInstallation or + self.pairingType == PairingType.KeypairSync: + self.state = LocalPairingState.Finished - if (self.mode == LocalPairingMode.Receiver and - data.eventType == EventTransferSuccess and - data.action == ActionPairingInstallation): - self.state = LocalPairingState.Finished + if self.mode == LocalPairingMode.Receiver and + data.eventType == EventTransferSuccess: + if self.pairingType == PairingType.AppSync and + data.action == ActionPairingInstallation or + self.pairingType == PairingType.KeypairSync: + self.state = LocalPairingState.Finished if self.state == LocalPairingState.Finished: return diff --git a/src/app_service/service/devices/service.nim b/src/app_service/service/devices/service.nim index cf5e6fae80..5b7f2241f5 100644 --- a/src/app_service/service/devices/service.nim +++ b/src/app_service/service/devices/service.nim @@ -8,6 +8,7 @@ import ./dto/local_pairing_status import app_service/service/settings/service as settings_service import app_service/service/accounts/service as accounts_service +import app_service/service/wallet_account/service as wallet_account_service import app/global/global_singleton import app/core/[main] @@ -55,23 +56,26 @@ QtObject: threadpool: ThreadPool settingsService: settings_service.Service accountsService: accounts_service.Service + walletAccountService: wallet_account_service.Service localPairingStatus: LocalPairingStatus proc delete*(self: Service) = self.QObject.delete - self.localPairingStatus.delete + if not self.localPairingStatus.isNil: + self.localPairingStatus.delete proc newService*(events: EventEmitter, - threadpool: ThreadPool, - settingsService: settings_service.Service, - accountsService: accounts_service.Service): Service = - new(result, delete) - result.QObject.setup - result.events = events - result.threadpool = threadpool - result.settingsService = settingsService - result.accountsService = accountsService - result.localPairingStatus = newLocalPairingStatus() + threadpool: ThreadPool, + settingsService: settings_service.Service, + accountsService: accounts_service.Service, + walletAccountService: wallet_account_service.Service): Service = + new(result, delete) + result.QObject.setup + result.events = events + result.threadpool = threadpool + result.settingsService = settingsService + result.accountsService = accountsService + result.walletAccountService = walletAccountService proc updateLocalPairingStatus(self: Service, data: LocalPairingEventArgs) = self.localPairingStatus.update(data) @@ -98,15 +102,23 @@ QtObject: self.events.on(SignalType.LocalPairing.event) do(e:Args): let signalData = LocalPairingSignal(e) - if not signalData.accountData.isNil and signalData.accountData.keycardPairings.len > 0: - createKeycardPairingFile(signalData.accountData.keycardPairings) - let data = LocalPairingEventArgs( - eventType: signalData.eventType, - action: signalData.action, - accountData: signalData.accountData, - installation: signalData.installation, - error: signalData.error) - self.updateLocalPairingStatus(data) + if self.localPairingStatus.pairingType == PairingType.AppSync: + if not signalData.accountData.isNil and signalData.accountData.keycardPairings.len > 0: + createKeycardPairingFile(signalData.accountData.keycardPairings) + let data = LocalPairingEventArgs( + eventType: signalData.eventType, + action: signalData.action, + accountData: signalData.accountData, + installation: signalData.installation, + error: signalData.error) + self.updateLocalPairingStatus(data) + elif self.localPairingStatus.pairingType == PairingType.KeypairSync: + let data = LocalPairingEventArgs( + eventType: signalData.eventType, + action: signalData.action, + error: signalData.error, + transferredKeypairs: signalData.transferredKeypairs) + self.updateLocalPairingStatus(data) proc init*(self: Service) = self.doConnect() @@ -185,6 +197,10 @@ QtObject: self.updateLocalPairingStatus(data) proc validateConnectionString*(self: Service, connectionString: string): string = + if connectionString.len == 0: + result = "an empty connection string provided" + error "error", msg=result + return return status_go.validateConnectionString(connectionString) proc getConnectionStringForBootstrappingAnotherDevice*(self: Service, password: string, chatKey: string): string = @@ -209,8 +225,7 @@ QtObject: "timeout": 5 * 60 * 1000, } } - self.localPairingStatus.reset() - self.localPairingStatus.mode = LocalPairingMode.Sender + self.localPairingStatus = newLocalPairingStatus(PairingType.AppSync, LocalPairingMode.Sender) return status_go.getConnectionStringForBootstrappingAnotherDevice($configJSON) proc inputConnectionStringForBootstrapping*(self: Service, connectionString: string): string = @@ -226,8 +241,7 @@ QtObject: }, "clientConfig": %* {} } - self.localPairingStatus.reset() - self.localPairingStatus.mode = LocalPairingMode.Receiver + self.localPairingStatus = newLocalPairingStatus(PairingType.AppSync, LocalPairingMode.Receiver) let arg = AsyncInputConnectionStringArg( tptr: cast[ByteAddress](asyncInputConnectionStringTask), @@ -237,3 +251,127 @@ QtObject: configJSON: $configJSON ) self.threadpool.start(arg) + + proc validateKeyUids*(self: Service, keyUids: seq[string], validateForExport: bool): tuple[finalKeyUids: seq[string], err: string] = + if keyUids.len > 0: + for keyUid in keyUids: + let kp = self.walletAccountService.getKeypairByKeyUid(keyUid) + if kp.isNil: + result.err = "cannot resolve keypair for provided keyUid" + return + let migratedToKeycard = kp.keycards.len > 0 + if migratedToKeycard or kp.keypairType == KeypairTypeProfile: + result.err = "keypair is migrated to a keycard or refers to a profile keypair" + return + if validateForExport: + if kp.getOperability() == AccountNonOperable: + result.err = "cannot export non operable keypair" + return + elif kp.getOperability() != AccountNonOperable: + result.err = "keypair is already fully or partially operable" + return + result.finalKeyUids.add(kp.keyUid) + else: + let keypairs = self.walletAccountService.getKeypairs() + for kp in keypairs: + let migratedToKeycard = kp.keycards.len > 0 + if migratedToKeycard or + kp.keypairType == KeypairTypeProfile or + validateForExport and kp.getOperability() == AccountNonOperable or + not validateForExport and kp.getOperability() != AccountNonOperable: + continue + result.finalKeyUids.add(kp.keyUid) + + if result.finalKeyUids.len == 0: + result.err = "there is no valid keypair" + + ## Providing an empty array of keyUids means generating a connection string and transferring all non operable keypairs + proc generateConnectionStringForExportingKeypairsKeystores*(self: Service, keyUids: seq[string], password: string): tuple[res: string, err: string] = + if password.len == 0: + result.err = "emtpy password provided" + error "error", msg=result.err + return + let(finalKeyUids, err) = self.validateKeyUids(keyUids, validateForExport=true) + if err.len > 0: + result.err = err + error "error", msg=err + return + + let loggedInUserKeyUid = singletonInstance.userProfile.getKeyUid() + let keycardUser = singletonInstance.userProfile.getIsKeycardUser() + + var finalPassword = utils.hashPassword(password) + if keycardUser: + finalPassword = password + + let configJSON = %* { + "senderConfig": %* { + "keystorePath": joinPath(main_constants.ROOTKEYSTOREDIR, loggedInUserKeyUid), + "loggedInKeyUid": loggedInUserKeyUid, + "password": finalPassword, + "keypairsToExport": finalKeyUids, + }, + "serverConfig": %* { + "timeout": 5 * 60 * 1000, + } + } + self.localPairingStatus = newLocalPairingStatus(PairingType.KeypairSync, LocalPairingMode.Sender) + let response = status_go.getConnectionStringForExportingKeypairsKeystores($configJSON) + try: + let jsonObj = response.parseJson + if jsonObj.hasKey("error"): + return ("", jsonObj["error"].getStr) + except Exception: + return (response, "") + + proc inputConnectionStringForImportingKeystoreFinished*(self: Service, responseJson: string) {.slot.} = + try: + let jsonObj = responseJson.parseJson + if jsonObj.hasKey("error") and jsonObj["error"].getStr.len == 0: + info "keystore files successfully transferred" + self.walletAccountService.updateKeypairOperabilityInLocalStoreAndNotify(self.localPairingStatus.transferredKeypairs) + return + let errorDescription = jsonObj["error"].getStr + error "failed to start transferring keystore files", errorDescription + self.events.emit(SIGNAL_IMPORTED_KEYPAIRS, KeypairsArgs(error: errorDescription)) + except Exception as e: + error "unexpected error", msg=e.msg + + ## Providing an empty array of keyUids means expecting keystore files for all non operable keypairs to be received + proc inputConnectionStringForImportingKeypairsKeystores*(self: Service, keyUids: seq[string], connectionString: string, password: string): string = + if password.len == 0: + result = "emtpy password provided" + error "error", msg=result + return + let(finalKeyUids, err) = self.validateKeyUids(keyUids, validateForExport=false) + if err.len > 0: + result = err + error "error", msg=result + return + + let loggedInUserKeyUid = singletonInstance.userProfile.getKeyUid() + let keycardUser = singletonInstance.userProfile.getIsKeycardUser() + + var finalPassword = utils.hashPassword(password) + if keycardUser: + finalPassword = password + + let configJSON = %* { + "receiverConfig": %* { + "keystorePath": main_constants.ROOTKEYSTOREDIR, + "loggedInKeyUid": loggedInUserKeyUid, + "password": finalPassword, + "keypairsToImport": finalKeyUids, + }, + "clientConfig": %* {} + } + self.localPairingStatus = newLocalPairingStatus(PairingType.KeypairSync, LocalPairingMode.Receiver) + + let arg = AsyncInputConnectionStringArg( + tptr: cast[ByteAddress](asyncInputConnectionStringForImportingKeystoreTask), + vptr: cast[ByteAddress](self.vptr), + slot: "inputConnectionStringForImportingKeystoreFinished", + connectionString: connectionString, + configJSON: $configJSON + ) + self.threadpool.start(arg) \ No newline at end of file diff --git a/src/app_service/service/wallet_account/dto/keypair_dto.nim b/src/app_service/service/wallet_account/dto/keypair_dto.nim index 354ebda597..7725c8ceac 100644 --- a/src/app_service/service/wallet_account/dto/keypair_dto.nim +++ b/src/app_service/service/wallet_account/dto/keypair_dto.nim @@ -1,4 +1,4 @@ -import tables, json, strformat, strutils, chronicles +import tables, json, strformat, strutils, sequtils, sugar, chronicles import account_dto, keycard_dto @@ -73,3 +73,10 @@ proc `$`*(self: KeypairDto): string = """ result &= """ ]""" + +proc getOperability*(self: KeypairDto): string = + if self.accounts.any(x => x.operable == AccountNonOperable): + return AccountNonOperable + if self.accounts.any(x => x.operable == AccountPartiallyOperable): + return AccountPartiallyOperable + return AccountFullyOperable \ No newline at end of file diff --git a/src/app_service/service/wallet_account/service_account.nim b/src/app_service/service/wallet_account/service_account.nim index 41483186d8..3ed12b457e 100644 --- a/src/app_service/service/wallet_account/service_account.nim +++ b/src/app_service/service/wallet_account/service_account.nim @@ -215,14 +215,24 @@ proc removeAccountFromLocalStoreAndNotify(self: Service, address: string, notify if notify: self.events.emit(SIGNAL_WALLET_ACCOUNT_DELETED, AccountArgs(account: acc)) -proc updateKeypairOperabilityInLocalStoreAndNotify(self: Service, keyUid: string) = - let kp = self.getKeypairByKeyUid(keyUid) - if kp.isNil: - error "there is no known keypair", keyUid=keyUid, procName="updateKeypairOperabilityInLocalStoreAndNotify" +proc updateKeypairOperabilityInLocalStoreAndNotify*(self: Service, importedKeyUids: seq[string]) = + var updatedKeypairs: seq[KeypairDto] + let localKeypairs = self.getKeypairs() + for keyUid in importedKeyUids: + var foundKp: KeypairDto = nil + for kp in localKeypairs: + if keyUid == kp.keyUid: + foundKp = kp + break + if foundKp.isNil: + error "there is no known keypair", keyUid=keyUid, procName="updateKeypairOperabilityInLocalStoreAndNotify" + return + for acc in foundKp.accounts: + acc.operable = AccountFullyOperable + updatedKeypairs.add(foundKp) + if updatedKeypairs.len == 0: return - for acc in kp.accounts: - acc.operable = AccountFullyOperable - self.events.emit(SIGNAL_KEYPAIR_OPERABILITY_CHANGED, KeypairArgs(keypair: kp)) + self.events.emit(SIGNAL_IMPORTED_KEYPAIRS, KeypairsArgs(keypairs: updatedKeypairs)) proc updateAccountsPositions(self: Service) = let dbAccounts = getAccountsFromDb() @@ -326,7 +336,7 @@ proc makePrivateKeyKeypairFullyOperable*(self: Service, keyUid, privateKey, pass if not response.error.isNil: error "status-go error", procName="makePrivateKeyKeypairFullyOperable", errCode=response.error.code, errDesription=response.error.message return response.error.message - self.updateKeypairOperabilityInLocalStoreAndNotify(keyUid) + self.updateKeypairOperabilityInLocalStoreAndNotify(@[keyUid]) return "" except Exception as e: error "error: ", procName="makePrivateKeyKeypairFullyOperable", errName=e.name, errDesription=e.msg @@ -367,7 +377,7 @@ proc makeSeedPhraseKeypairFullyOperable*(self: Service, keyUid, mnemonic, passwo if not response.error.isNil: error "status-go error", procName="makeSeedPhraseKeypairFullyOperable", errCode=response.error.code, errDesription=response.error.message return response.error.message - self.updateKeypairOperabilityInLocalStoreAndNotify(keyUid) + self.updateKeypairOperabilityInLocalStoreAndNotify(@[keyUid]) return "" except Exception as e: error "error: ", procName="makeSeedPhraseKeypairFullyOperable", errName=e.name, errDesription=e.msg diff --git a/src/app_service/service/wallet_account/signals_and_payloads.nim b/src/app_service/service/wallet_account/signals_and_payloads.nim index 209e008162..34bb876c28 100644 --- a/src/app_service/service/wallet_account/signals_and_payloads.nim +++ b/src/app_service/service/wallet_account/signals_and_payloads.nim @@ -19,7 +19,7 @@ const SIGNAL_WALLET_ACCOUNT_PREFERRED_SHARING_CHAINS_UPDATED* = "walletAccount/p const SIGNAL_KEYPAIR_SYNCED* = "keypairSynced" const SIGNAL_KEYPAIR_NAME_CHANGED* = "keypairNameChanged" const SIGNAL_KEYPAIR_DELETED* = "keypairDeleted" -const SIGNAL_KEYPAIR_OPERABILITY_CHANGED* = "keypairOperabilityChanged" +const SIGNAL_IMPORTED_KEYPAIRS* = "importedKeypairs" const SIGNAL_NEW_KEYCARD_SET* = "newKeycardSet" const SIGNAL_KEYCARD_REBUILD* = "keycardRebuild" @@ -43,6 +43,10 @@ type KeypairArgs* = ref object of Args keyPairName*: string oldKeypairName*: string +type KeypairsArgs* = ref object of Args + keypairs*: seq[KeypairDto] + error*: string + type KeycardArgs* = ref object of Args success*: bool oldKeycardUid*: string diff --git a/vendor/nim-status-go b/vendor/nim-status-go index 0a0b5f0f32..f75bbc3831 160000 --- a/vendor/nim-status-go +++ b/vendor/nim-status-go @@ -1 +1 @@ -Subproject commit 0a0b5f0f320a7ed3adf53218e5b18fc41d0fbef0 +Subproject commit f75bbc383157285d8260365e3b95def7b259e340