diff --git a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved index 23f7bb755..ba7a595ad 100644 --- a/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/FlowCrypt.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/realm/realm-cocoa", "state": { "branch": null, - "revision": "9f43d0da902c55b493d6c8bb63203764caa8acbe", - "version": "10.18.0" + "revision": "7ca0ce1dd58553d5be1ec9cc7283b068c256979d", + "version": "10.17.0" } }, { diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index db33db1a5..e93e5e3c5 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -22,6 +22,7 @@ private struct ComposedDraft: Equatable { let contextToSend: ComposeMessageContext } +@MainActor final class ComposeViewController: TableNodeViewController { private enum Constants { static let endTypingCharacters = [",", " ", "\n", ";"] @@ -47,6 +48,7 @@ final class ComposeViewController: TableNodeViewController { private let filesManager: FilesManagerType private let photosManager: PhotosManagerType private let keyService: KeyServiceType + private let keyMethods: KeyMethodsType private let service: ServiceActor private let passPhraseService: PassPhraseService @@ -76,7 +78,8 @@ final class ComposeViewController: TableNodeViewController { filesManager: FilesManagerType = FilesManager(), photosManager: PhotosManagerType = PhotosManager(), keyService: KeyServiceType = KeyService(), - passPhraseService: PassPhraseService = PassPhraseService() + passPhraseService: PassPhraseService = PassPhraseService(), + keyMethods: KeyMethodsType = KeyMethods() ) { self.email = email self.notificationCenter = notificationCenter @@ -88,6 +91,7 @@ final class ComposeViewController: TableNodeViewController { self.filesManager = filesManager self.photosManager = photosManager self.keyService = keyService + self.keyMethods = keyMethods self.service = ServiceActor( composeMessageService: composeMessageService, contactsService: contactsService, @@ -154,55 +158,52 @@ final class ComposeViewController: TableNodeViewController { // MARK: - Drafts extension ComposeViewController { @objc private func startTimer() { - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } - self.saveDraftTimer = Timer.scheduledTimer( - timeInterval: 1, - target: self, - selector: #selector(self.saveDraftIfNeeded), - userInfo: nil, - repeats: true) - self.saveDraftTimer?.fire() + saveDraftTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in + self?.saveDraftIfNeeded() } + saveDraftTimer?.fire() } @objc private func stopTimer() { saveDraftTimer?.invalidate() saveDraftTimer = nil - + saveDraftIfNeeded() } private func shouldSaveDraft() -> Bool { + // todo - that draft should only be saved when one of the fields are dirty (edited by user). Right now, a draft may get saved right after rendering compose (or reply), which is annoying. let newDraft = ComposedDraft(email: email, input: input, contextToSend: contextToSend) - guard let oldDraft = composedLatestDraft else { composedLatestDraft = newDraft return true } - let result = newDraft != oldDraft composedLatestDraft = newDraft return result } - @objc private func saveDraftIfNeeded() { + private func saveDraftIfNeeded() { + guard shouldSaveDraft() else { return } Task { - guard shouldSaveDraft() else { return } do { let signingPrv = try await prepareSigningKey() - let messagevalidationResult = composeMessageService.validateMessage( + let sendableMsg = try await composeMessageService.validateAndProduceSendableMsg( input: input, contextToSend: contextToSend, email: email, includeAttachments: false, signingPrv: signingPrv ) - guard case let .success(message) = messagevalidationResult else { - return + try await composeMessageService.encryptAndSaveDraft(message: sendableMsg, threadId: input.threadId) + } catch { + if !(error is MessageValidationError) { + // no need to save or notify user if validation error + // for other errors show toast + // todo - should make sure that the toast doesn't hide the keyboard. Also should be toasted on top when keyboard open? + showToast("Error saving draft: \(error.localizedDescription)") } - try await composeMessageService.encryptAndSaveDraft(message: message, threadId: input.threadId) - } catch {} + } } } } @@ -329,8 +330,10 @@ extension ComposeViewController { Task { do { let key = try await prepareSigningKey() - sendMessage(key) - } catch {} + try await sendMessage(key) + } catch { + handle(error: error) + } } } } @@ -338,41 +341,65 @@ extension ComposeViewController { // MARK: - Message Sending extension ComposeViewController { - private func prepareSigningKey() async throws -> PrvKeyInfo { - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - guard let signingKey = try? keyService.getSigningKey() else { - let message = "No available private key has your user id \"\(email)\" in it. Please import the appropriate private key." - showAlert(message: message) - continuation.resume(throwing: MessageServiceError.unknown) - return - } + @MainActor private func prepareSigningKey() async throws -> PrvKeyInfo { + guard let signingKey = try await keyService.getSigningKey() else { + throw AppErr.general("None of your private keys have your user id \"\(email)\". Please import the appropriate key.") + } - guard let passphrase = signingKey.passphrase else { - let alert = AlertsFactory.makePassPhraseAlert( - onCancel: { [weak self] in - self?.showAlert(message: "Passphrase is required for message signing") - continuation.resume(throwing: MessageServiceError.unknown) - }, - onCompletion: { [weak self] passPhrase in - // save passphrase - let keyInfo = signingKey.copy(with: passPhrase) - self?.savePassPhrases(value: passPhrase, with: [keyInfo]) - continuation.resume(returning: keyInfo) - }) - present(alert, animated: true, completion: nil) - return - } - continuation.resume(returning: signingKey.copy(with: passphrase)) + guard let existingPassPhrase = signingKey.passphrase else { + return signingKey.copy(with: try await self.requestMissingPassPhraseWithModal(for: signingKey)) + } + + return signingKey.copy(with: existingPassPhrase) + } + + private func requestMissingPassPhraseWithModal(for signingKey: PrvKeyInfo) async throws -> String { + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + let alert = AlertsFactory.makePassPhraseAlert( + onCancel: { + continuation.resume(throwing: AppErr.user("Passphrase is required for message signing")) + }, + onCompletion: { [weak self] passPhrase in + guard let self = self else { + continuation.resume(throwing: AppErr.nilSelf) + return + } + Task { + do { + let matched = try await self.handlePassPhraseEntry(passPhrase, for: signingKey) + if matched { + continuation.resume(returning: passPhrase) + } else { + throw AppErr.user("This pass phrase did not match your signing private key") + } + } catch { + continuation.resume(throwing: error) + } + } + } + ) + present(alert, animated: true, completion: nil) } } - private func savePassPhrases(value passPhrase: String, with privateKeys: [PrvKeyInfo]) { - privateKeys - .map { PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) } - .forEach { self.passPhraseService.savePassPhrase(with: $0, storageMethod: .memory) } + private func handlePassPhraseEntry(_ passPhrase: String, for signingKey: PrvKeyInfo) async throws -> Bool { + // since pass phrase was entered (an inconvenient thing for user to do), + // let's find all keys that match and save the pass phrase for all + let allKeys = try await self.keyService.getPrvKeyInfo() + guard allKeys.isNotEmpty else { + // tom - todo - nonsensical error type choice https://github.com/FlowCrypt/flowcrypt-ios/issues/859 + // I copied it from another usage, but has to be changed + throw KeyServiceError.retrieve + } + let matchingKeys = try await self.keyMethods.filterByPassPhraseMatch(keys: allKeys, passPhrase: passPhrase) + // save passphrase for all matching keys + self.passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) + // now figure out if the pass phrase also matched the signing prv itself + let matched = matchingKeys.first(where: { $0.fingerprints.first == signingKey.fingerprints.first }) + return matched != nil// true if the pass phrase matched signing key } - private func sendMessage(_ signingKey: PrvKeyInfo) { + private func sendMessage(_ signingKey: PrvKeyInfo) async throws { view.endEditing(true) navigationItem.rightBarButtonItem?.isEnabled = false @@ -380,55 +407,37 @@ extension ComposeViewController { showSpinner(spinnerTitle.localized) let selectedRecipients = contextToSend.recipients.filter(\.state.isSelected) - selectedRecipients.forEach(evaluate) + for selectedRecipient in selectedRecipients { + evaluate(recipient: selectedRecipient) + } // TODO: - fix for spinner // https://github.com/FlowCrypt/flowcrypt-ios/issues/291 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - guard let self = self else { return } - let result = self.composeMessageService.validateMessage( - input: self.input, - contextToSend: self.contextToSend, - email: self.email, - signingPrv: signingKey - ) - switch result { - case .success(let message): - UIApplication.shared.isIdleTimerDisabled = true - self.encryptAndSend(message) - case .failure(let error): - self.handle(error: error) - } - } - } - - private func encryptAndSend(_ message: SendableMsg) { - Task { - do { - try await service.encryptAndSend(message: message, - threadId: input.threadId, - progressHandler: { [weak self] progress in - self?.updateSpinner(progress: progress) - }) - handleSuccessfullySentMessage() - } catch { - if let error = error as? ComposeMessageError { - handle(error: error) - } + try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100ms + let sendableMsg = try await self.composeMessageService.validateAndProduceSendableMsg( + input: self.input, + contextToSend: self.contextToSend, + email: self.email, + signingPrv: signingKey + ) + UIApplication.shared.isIdleTimerDisabled = true + try await service.encryptAndSend( + message: sendableMsg, + threadId: input.threadId, + progressHandler: { [weak self] progress in + self?.updateSpinner(progress: progress) } - } + ) + handleSuccessfullySentMessage() } - private func handle(error: ComposeMessageError) { + private func handle(error: Error) { UIApplication.shared.isIdleTimerDisabled = false hideSpinner() navigationItem.rightBarButtonItem?.isEnabled = true - - let message = "compose_error".localized - + "\n\n" - + error.description - - showAlert(message: message) + let err = error as? ComposeMessageError + let description = err?.description ?? error.localizedDescription + showAlert(message: "compose_error".localized + "\n\n" + description) } private func handleSuccessfullySentMessage() { diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index b08ea5e8d..5c5f7adec 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -61,14 +61,17 @@ final class MessageViewController: TableNodeViewController { private var input: MessageViewController.Input private let decorator: MessageViewDecorator private let messageService: MessageService + private let messageProvider: MessageProvider private let messageOperationsProvider: MessageOperationsProvider private let trashFolderProvider: TrashFolderProviderType private let filesManager: FilesManagerType + private let serviceActor: ServiceActor private var processedMessage: ProcessedMessage = .empty init( messageService: MessageService = MessageService(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, + messageProvider: MessageProvider = MailProvider.shared.messageProvider, decorator: MessageViewDecorator = MessageViewDecorator(dateFormatter: DateFormatter()), trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), filesManager: FilesManagerType = FilesManager(), @@ -82,6 +85,11 @@ final class MessageViewController: TableNodeViewController { self.trashFolderProvider = trashFolderProvider self.onCompletion = completion self.filesManager = filesManager + self.messageProvider = messageProvider + self.serviceActor = ServiceActor( + messageService: messageService, + messageProvider: messageProvider + ) super.init(node: TableNode()) } @@ -144,35 +152,31 @@ final class MessageViewController: TableNodeViewController { extension MessageViewController { private func fetchDecryptAndRenderMsg() { showSpinner("loading_title".localized, isUserInteractionEnabled: true) - - Promise { [weak self] in - guard let self = self else { return } - let promise = self.messageService.getAndProcessMessage( - with: self.input.objMessage, - folder: self.input.path - ) - let message = try awaitPromise(promise) - self.processedMessage = message - } - .then(on: .main) { [weak self] in - self?.handleReceivedMessage() - } - .catch(on: .main) { [weak self] error in - self?.handleError(error) + Task { + do { + processedMessage = try await serviceActor.fetchDecryptAndRenderMsg(message: input.objMessage, path: input.path) + handleReceivedMessage() + } catch { + handleError(error) + } } } - private func validateMessage(rawMimeData: Data, with passPhrase: String) { + private func handlePassPhraseEntry(rawMimeData: Data, with passPhrase: String) { showSpinner("loading_title".localized, isUserInteractionEnabled: true) - - messageService.validateMessage(rawMimeData: rawMimeData, with: passPhrase) - .then(on: .main) { [weak self] message in - self?.processedMessage = message - self?.handleReceivedMessage() - } - .catch(on: .main) { [weak self] error in - self?.handleError(error) + Task { + do { + let matched = try await serviceActor.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) + if matched { + processedMessage = try await serviceActor.decryptAndProcessMessage(mime: rawMimeData) + handleReceivedMessage() + } else { + handleWrongPathPhrase(for: rawMimeData, with: passPhrase) + } + } catch { + handleError(error) } + } } private func handleReceivedMessage() { @@ -210,8 +214,8 @@ extension MessageViewController { hideSpinner() switch error as? MessageServiceError { - case let .missedPassPhrase(rawMimeData): - handleMissedPassPhrase(for: rawMimeData) + case let .missingPassPhrase(rawMimeData): + handleMissingPassPhrase(for: rawMimeData) case let .wrongPassPhrase(rawMimeData, passPhrase): handleWrongPathPhrase(for: rawMimeData, with: passPhrase) case let .keyMismatch(rawMimeData): @@ -231,13 +235,13 @@ extension MessageViewController { } } - private func handleMissedPassPhrase(for rawMimeData: Data) { + private func handleMissingPassPhrase(for rawMimeData: Data) { let alert = AlertsFactory.makePassPhraseAlert( onCancel: { [weak self] in self?.navigationController?.popViewController(animated: true) }, onCompletion: { [weak self] passPhrase in - self?.validateMessage(rawMimeData: rawMimeData, with: passPhrase) + self?.handlePassPhraseEntry(rawMimeData: rawMimeData, with: passPhrase) }) present(alert, animated: true, completion: nil) @@ -249,7 +253,7 @@ extension MessageViewController { self?.navigationController?.popViewController(animated: true) }, onCompletion: { [weak self] passPhrase in - self?.validateMessage(rawMimeData: rawMimeData, with: passPhrase) + self?.handlePassPhraseEntry(rawMimeData: rawMimeData, with: passPhrase) }) present(alert, animated: true, completion: nil) } @@ -523,3 +527,28 @@ extension MessageViewController: UIDocumentPickerDelegate { present(alert, animated: true) } } + +// TODO temporary solution for background execution problem +private actor ServiceActor { + private let messageService: MessageService + private let messageProvider: MessageProvider + + init(messageService: MessageService, + messageProvider: MessageProvider) { + self.messageService = messageService + self.messageProvider = messageProvider + } + + func fetchDecryptAndRenderMsg(message: Message, path: String) async throws -> ProcessedMessage { + let rawMimeData = try awaitPromise(messageProvider.fetchMsg(message: message, folder: path)) + return try await messageService.decryptAndProcessMessage(mime: rawMimeData) + } + + func checkAndPotentiallySaveEnteredPassPhrase(_ passPhrase: String) async throws -> Bool { + return try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) + } + + func decryptAndProcessMessage(mime: Data) async throws -> ProcessedMessage { + return try await messageService.decryptAndProcessMessage(mime: mime) + } +} diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift index 2596922b5..d942646d7 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift @@ -62,7 +62,14 @@ extension ContactsListViewController { } private func fetchContacts() { - recipients = contactsProvider.getAllRecipients() + Task { + do { + self.recipients = try await contactsProvider.getAllRecipients() + } catch { + self.showToast("Failed to load recipients: \(error.localizedDescription)") + } + } + } } diff --git a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift index 174c5cdab..e174b46a6 100644 --- a/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift +++ b/FlowCrypt/Controllers/Settings/KeySettings/Key List/KeySettingsViewController.swift @@ -15,6 +15,7 @@ import FlowCryptUI * - User can proceed to importing keys *SetupManuallyImportKeyViewController* * - User can see detail information for the key in *KeyDetailViewController* */ +@MainActor final class KeySettingsViewController: TableNodeViewController { private var keys: [KeyDetails] = [] private let decorator: KeySettingsViewDecorator @@ -44,9 +45,14 @@ final class KeySettingsViewController: TableNodeViewController { node.delegate = self node.dataSource = self node.reloadData() - - loadKeysFromStorageAndRender() setupNavigationBar() + Task { + do { + try await loadKeysFromStorageAndRender() + } catch { + handleCommon(error: error) + } + } } private func setupNavigationBar() { @@ -61,14 +67,9 @@ final class KeySettingsViewController: TableNodeViewController { } extension KeySettingsViewController { - private func loadKeysFromStorageAndRender() { - switch keyService.getPrvKeyDetails() { - case let .failure(error): - handleCommon(error: error) - case let .success(keys): - self.keys = keys - node.reloadData() - } + private func loadKeysFromStorageAndRender() async throws { + self.keys = try await keyService.getPrvKeyDetails() + await node.reloadData() } } diff --git a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift index 62441a4b3..f0bec4316 100644 --- a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift @@ -133,34 +133,29 @@ extension SetupBackupsViewController { .becomeFirstResponder() } - private func recoverAccount(with backups: [KeyDetails], and passPhrase: String) { + private func recoverAccount(with backups: [KeyDetails], and passPhrase: String) async throws { logger.logInfo("Start recoverAccount with \(backups.count)") - let matchingKeyBackups = Set(keyMethods.filterByPassPhraseMatch(keys: backups, passPhrase: passPhrase)) - + let matchingKeyBackups = Set(try await keyMethods.filterByPassPhraseMatch(keys: backups, passPhrase: passPhrase)) logger.logInfo("matchingKeyBackups = \(matchingKeyBackups.count)") guard matchingKeyBackups.isNotEmpty else { showAlert(message: "setup_wrong_pass_phrase_retry".localized) return } - if storageMethod == .memory { // save pass phrase matchingKeyBackups .map { - PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) + PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { passPhraseService.savePassPhrase(with: $0, storageMethod: storageMethod) } } - // save keys keyStorage.addKeys(keyDetails: Array(matchingKeyBackups), passPhrase: storageMethod == .persistent ? passPhrase : nil, source: .backup, for: user.email) - - moveToMainFlow() } private func handleButtonPressed() { @@ -176,9 +171,17 @@ extension SetupBackupsViewController { // TODO: - fix for spinner // https://github.com/FlowCrypt/flowcrypt-ios/issues/291 - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - guard let self = self else { return } - self.recoverAccount(with: self.fetchedEncryptedKeys, and: passPhrase) + Task { + do { + try await Task.sleep(nanoseconds: 100 * 1_000_000) // 100 ms + try await self.recoverAccount(with: self.fetchedEncryptedKeys, and: passPhrase) + moveToMainFlow() + } catch { + hideSpinner() + showAlert(error: error, message: "Failed to set up account", onOk: { + // todo - what to do? maybe nothing, since they should now see the same button again that they can press again + }) + } } } diff --git a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift index f7388c3da..00d8caada 100644 --- a/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupCreatePassphraseAbstractViewController.swift @@ -137,51 +137,43 @@ extension SetupCreatePassphraseAbstractViewController { extension SetupCreatePassphraseAbstractViewController { - func validateAndConfirmNewPassPhraseOrReject(passPhrase: String) -> Promise { - Promise { [weak self] in - guard let self = self else { throw AppErr.nilSelf } - - let strength = try self.core.zxcvbnStrengthBar(passPhrase: passPhrase) - - guard strength.word.pass else { - throw CreateKeyError.weakPassPhrase(strength) - } - - let confirmPassPhrase = try awaitPromise(self.awaitUserPassPhraseEntry()) - - guard confirmPassPhrase != nil else { - throw CreateKeyError.conformingPassPhraseError - } - - guard confirmPassPhrase == passPhrase else { - throw CreateKeyError.doesntMatch - } + func validateAndConfirmNewPassPhraseOrReject(passPhrase: String) async throws { + let strength = try await self.core.zxcvbnStrengthBar(passPhrase: passPhrase) + guard strength.word.pass else { + throw CreateKeyError.weakPassPhrase(strength) + } + let confirmPassPhrase = try await self.awaitUserPassPhraseEntry() + guard confirmPassPhrase != nil else { + throw CreateKeyError.conformingPassPhraseError + } + guard confirmPassPhrase == passPhrase else { + throw CreateKeyError.doesntMatch } } - private func awaitUserPassPhraseEntry() -> Promise { - Promise(on: .main) { [weak self] resolve, _ in - guard let self = self else { throw AppErr.nilSelf } - let alert = UIAlertController( - title: "Pass Phrase", - message: "Confirm Pass Phrase", - preferredStyle: .alert - ) - - alert.addTextField { textField in - textField.isSecureTextEntry = true - textField.accessibilityLabel = "textField" - } + private func awaitUserPassPhraseEntry() async throws -> String? { + return await withCheckedContinuation { (continuation: CheckedContinuation) in + DispatchQueue.main.async { + let alert = UIAlertController( + title: "Pass Phrase", + message: "Confirm Pass Phrase", + preferredStyle: .alert + ) - alert.addAction(UIAlertAction(title: "cancel".localized, style: .default) { _ in - resolve(nil) - }) + alert.addTextField { textField in + textField.isSecureTextEntry = true + textField.accessibilityLabel = "textField" + } - alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { [weak alert] _ in - resolve(alert?.textFields?[0].text) - }) + alert.addAction(UIAlertAction(title: "cancel".localized, style: .default) { _ in + continuation.resume(returning: nil) + }) + alert.addAction(UIAlertAction(title: "ok".localized, style: .default) { [weak alert] _ in + continuation.resume(returning: alert?.textFields?[0].text) + }) - self.present(alert, animated: true, completion: nil) + self.present(alert, animated: true, completion: nil) + } } } } diff --git a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift index 060cf0d20..9bf577908 100644 --- a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift @@ -28,13 +28,13 @@ final class SetupEKMKeyViewController: SetupCreatePassphraseAbstractViewControll override var parts: [SetupCreatePassphraseAbstractViewController.Parts] { SetupCreatePassphraseAbstractViewController.Parts.ekmKeysSetup } - private let keys: [CoreRes.ParseKeys] + private let keys: [KeyDetails] private lazy var logger = Logger.nested(in: Self.self, with: .setup) init( user: UserId, - keys: [CoreRes.ParseKeys] = [], + keys: [KeyDetails] = [], core: Core = .shared, router: GlobalRouterType = GlobalRouter(), decorator: SetupViewDecorator = SetupViewDecorator(), @@ -62,7 +62,20 @@ final class SetupEKMKeyViewController: SetupCreatePassphraseAbstractViewControll } override func setupAccount(with passphrase: String) { - setupAccountWithKeysFetchedFromEkm(with: passphrase) + Task { + do { + try await setupAccountWithKeysFetchedFromEkm(with: passphrase) + hideSpinner() + moveToMainFlow() + } catch { + hideSpinner() + + let isErrorHandled = self.handleCommon(error: error) + if !isErrorHandled { + showAlert(error: error, message: "Could not finish setup, please try again") + } + } + } } override func setupUI() { @@ -75,50 +88,30 @@ final class SetupEKMKeyViewController: SetupCreatePassphraseAbstractViewControll extension SetupEKMKeyViewController { - private func setupAccountWithKeysFetchedFromEkm(with passPhrase: String) { - Promise { [weak self] in - guard let self = self else { return } - self.showSpinner() - - try awaitPromise(self.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase)) - - var allFingerprints: [String] = [] - try self.keys.forEach { key in - try key.keyDetails.forEach { keyDetail in - guard let privateKey = keyDetail.private else { - throw CreatePassphraseWithExistingKeyError.noPrivateKey - } - let encryptedPrv = try self.core.encryptKey( - armoredPrv: privateKey, - passphrase: passPhrase - ) - let parsedKey = try self.core.parseKeys(armoredOrBinary: encryptedPrv.encryptedKey.data()) - self.keyStorage.addKeys(keyDetails: parsedKey.keyDetails, - passPhrase: self.storageMethod == .persistent ? passPhrase : nil, - source: .ekm, - for: self.user.email) - allFingerprints.append(contentsOf: parsedKey.keyDetails.flatMap { $0.fingerprints }) - } - } - - if self.storageMethod == .memory { - let passPhrase = PassPhrase(value: passPhrase, fingerprints: allFingerprints.unique()) - self.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: self.storageMethod) + private func setupAccountWithKeysFetchedFromEkm(with passPhrase: String) async throws { + self.showSpinner() + try await self.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase) + var allFingerprints: [String] = [] + for keyDetail in self.keys { + guard let privateKey = keyDetail.private else { + throw CreatePassphraseWithExistingKeyError.noPrivateKey } + let encryptedPrv = try await self.core.encryptKey( + armoredPrv: privateKey, + passphrase: passPhrase + ) + let parsedKey = try await self.core.parseKeys(armoredOrBinary: encryptedPrv.encryptedKey.data()) + self.keyStorage.addKeys( + keyDetails: parsedKey.keyDetails, + passPhrase: self.storageMethod == .persistent ? passPhrase : nil, + source: .ekm, + for: self.user.email + ) + allFingerprints.append(contentsOf: parsedKey.keyDetails.flatMap { $0.fingerprints }) } - .then(on: .main) { [weak self] in - self?.hideSpinner() - self?.moveToMainFlow() - } - .catch(on: .main) { [weak self] error in - guard let self = self else { return } - self.hideSpinner() - - let isErrorHandled = self.handleCommon(error: error) - - if !isErrorHandled { - self.showAlert(error: error, message: "Could not finish setup, please try again") - } + if self.storageMethod == .memory { + let passPhrase = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: allFingerprints.unique()) + self.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: self.storageMethod) } } } diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift index 657eff856..a748bcdcd 100644 --- a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -133,7 +133,7 @@ private actor Service { viewController: ViewController) async throws { let userId = try getUserId() - try awaitPromise(await viewController.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase)) + try await viewController.validateAndConfirmNewPassPhraseOrReject(passPhrase: passPhrase) let encryptedPrv = try await core.generateKey(passphrase: passPhrase, variant: .curve25519, userIds: [userId]) try await backupService.backupToInbox(keys: [encryptedPrv.key], for: user) @@ -144,7 +144,7 @@ private actor Service { for: user.email) if storageMethod == .memory { - let passPhrase = PassPhrase(value: passPhrase, fingerprints: encryptedPrv.key.fingerprints) + let passPhrase = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: encryptedPrv.key.fingerprints) passPhraseService.savePassPhrase(with: passPhrase, storageMethod: .memory) } diff --git a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift index e57a43ce9..f52cd9a2d 100644 --- a/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupInitialViewController.swift @@ -321,7 +321,7 @@ extension SetupInitialViewController { let viewController = SetupGenerateKeyViewController(user: user) navigationController?.pushViewController(viewController, animated: true) } - private func proceedToSetupWithEKMKeys(keys: [CoreRes.ParseKeys]) { + private func proceedToSetupWithEKMKeys(keys: [KeyDetails]) { let viewController = SetupEKMKeyViewController(user: user, keys: keys) navigationController?.pushViewController(viewController, animated: true) } diff --git a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift index 2b340b86e..729ccf98d 100644 --- a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift @@ -176,7 +176,15 @@ extension SetupManuallyEnterPassPhraseViewController: ASTableDelegate, ASTableDa insets: self.decorator.insets.buttonInsets ) return ButtonCellNode(input: input) { [weak self] in - self?.handleContinueAction() + guard let self = self else { return } + Task { + do { + try await self.handleContinueAction() + } catch { + self.handleCommon(error: error) + } + } + } case .chooseAnother: return ButtonCellNode(input: .chooseAnotherAccount) { [weak self] in @@ -209,32 +217,24 @@ extension SetupManuallyEnterPassPhraseViewController: ASTableDelegate, ASTableDa // MARK: - Actions extension SetupManuallyEnterPassPhraseViewController { - private func handleContinueAction() { + private func handleContinueAction() async throws { view.endEditing(true) guard let passPhrase = passPhrase else { return } - guard passPhrase.isNotEmpty else { showAlert(message: "setup_enter_pass_phrase".localized) return } showSpinner() - - let matchingKeys = keyMethods.filterByPassPhraseMatch( + let matchingKeys = try await keyMethods.filterByPassPhraseMatch( keys: fetchedKeys, passPhrase: passPhrase ) - guard matchingKeys.isNotEmpty else { showAlert(message: "setup_wrong_pass_phrase_retry".localized) return } - - switch keyService.getPrvKeyDetails() { - case let .failure(error): - handleCommon(error: error) - case let .success(existedKeys): - importKeys(with: existedKeys, and: passPhrase) - } + let keyDetails = try await keyService.getPrvKeyDetails() + importKeys(with: keyDetails, and: passPhrase) } private func importKeys(with existedKeys: [KeyDetails], and passPhrase: String) { @@ -247,7 +247,7 @@ extension SetupManuallyEnterPassPhraseViewController { if storageMethod == .memory { keysToUpdate .map { - PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) + PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { passPhraseService.updatePassPhrase(with: $0, storageMethod: storageMethod) @@ -255,7 +255,7 @@ extension SetupManuallyEnterPassPhraseViewController { newKeysToAdd .map { - PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) + PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { passPhraseService.savePassPhrase(with: $0, storageMethod: storageMethod) diff --git a/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift index 26a13c5c3..fcba07ec9 100644 --- a/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyImportKeyViewController.swift @@ -125,7 +125,14 @@ extension SetupManuallyImportKeyViewController: ASTableDelegate, ASTableDataSour insets: self.decorator.insets.buttonInsets ) return ButtonCellNode(input: input) { [weak self] in - self?.proceedToKeyImportFromPasteboard() + guard let self = self else { return } + Task { + do { + try await self.proceedToKeyImportFromPasteboard() + } catch { + self.userInfoMessage = error.localizedDescription + } + } } .then { $0.isButtonEnabled = self.pasteboard.hasStrings @@ -159,25 +166,20 @@ extension SetupManuallyImportKeyViewController { present(documentInteractionController, animated: true, completion: nil) } - private func proceedToKeyImportFromPasteboard() { + private func proceedToKeyImportFromPasteboard() async throws { guard let armoredKey = pasteboard.string else { return } - parseUserProvided(data: Data(armoredKey.utf8)) - } - - private func parseUserProvided(data keyData: Data) { - do { - let keys = try core.parseKeys(armoredOrBinary: keyData) - let privateKey = keys.keyDetails.filter { $0.private != nil } - let user = dataService.email ?? "unknown_title".localized - - if privateKey.isEmpty { - userInfoMessage = "import_no_backups_clipboard".localized + user - } else { - userInfoMessage = "Found \(privateKey.count) key\(privateKey.count > 1 ? "s" : "")" - proceedToPassPhrase(with: user, keys: privateKey) - } - } catch { - userInfoMessage = error.localizedDescription + try await parseUserProvided(data: Data(armoredKey.utf8)) + } + + private func parseUserProvided(data keyData: Data) async throws { + let keys = try await core.parseKeys(armoredOrBinary: keyData) + let privateKey = keys.keyDetails.filter { $0.private != nil } + let user = dataService.email ?? "unknown_title".localized + if privateKey.isEmpty { + userInfoMessage = "import_no_backups_clipboard".localized + user + } else { + userInfoMessage = "Found \(privateKey.count) key\(privateKey.count > 1 ? "s" : "")" + proceedToPassPhrase(with: user, keys: privateKey) } } @@ -215,7 +217,14 @@ extension SetupManuallyImportKeyViewController: UIDocumentPickerDelegate { document.open { [weak self] success in guard success else { assertionFailure("Failed to open doc"); return } guard let metadata = document.data else { assertionFailure("Failed to fetch data"); return } - self?.parseUserProvided(data: metadata) + guard let self = self else { return} + Task { + do { + try await self.parseUserProvided(data: metadata) + } catch { + self.userInfoMessage = error.localizedDescription + } + } } } } diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index acbbc1dc4..2ec387773 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -46,11 +46,11 @@ enum CoreError: LocalizedError, Equatable { } protocol KeyDecrypter { - func decryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.DecryptKey + func decryptKey(armoredPrv: String, passphrase: String) async throws -> CoreRes.DecryptKey } protocol KeyParser { - func parseKeys(armoredOrBinary: Data) throws -> CoreRes.ParseKeys + func parseKeys(armoredOrBinary: Data) async throws -> CoreRes.ParseKeys } final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { @@ -64,30 +64,30 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { private dynamic var started = false private dynamic var ready = false - private let queue = DispatchQueue(label: "com.flowcrypt.core", qos: .background) + private let queue = DispatchQueue(label: "com.flowcrypt.core", qos: .default) // todo - try also with .userInitiated private lazy var logger = Logger.nested(in: Self.self, with: "Js") private init() {} - func version() throws -> CoreRes.Version { - let r = try call("version", jsonDict: nil, data: nil) + func version() async throws -> CoreRes.Version { + let r = try await call("version", jsonDict: nil, data: nil) return try r.json.decodeJson(as: CoreRes.Version.self) } // MARK: Keys - func parseKeys(armoredOrBinary: Data) throws -> CoreRes.ParseKeys { - let r = try call("parseKeys", jsonDict: [String: String](), data: armoredOrBinary) + func parseKeys(armoredOrBinary: Data) async throws -> CoreRes.ParseKeys { + let r = try await call("parseKeys", jsonDict: [String: String](), data: armoredOrBinary) return try r.json.decodeJson(as: CoreRes.ParseKeys.self) } - func decryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.DecryptKey { - let r = try call("decryptKey", jsonDict: ["armored": armoredPrv, "passphrases": [passphrase]], data: nil) + func decryptKey(armoredPrv: String, passphrase: String) async throws -> CoreRes.DecryptKey { + let r = try await call("decryptKey", jsonDict: ["armored": armoredPrv, "passphrases": [passphrase]], data: nil) return try r.json.decodeJson(as: CoreRes.DecryptKey.self) } - func encryptKey(armoredPrv: String, passphrase: String) throws -> CoreRes.EncryptKey { - let r = try call("encryptKey", jsonDict: ["armored": armoredPrv, "passphrase": passphrase], data: nil) + func encryptKey(armoredPrv: String, passphrase: String) async throws -> CoreRes.EncryptKey { + let r = try await call("encryptKey", jsonDict: ["armored": armoredPrv, "passphrase": passphrase], data: nil) return try r.json.decodeJson(as: CoreRes.EncryptKey.self) } @@ -97,29 +97,29 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { "variant": String(variant.rawValue), "userIds": try userIds.map { try $0.toJsonEncodedDict() } ] - let r = try call("generateKey", jsonDict: request, data: nil) + let r = try await call("generateKey", jsonDict: request, data: nil) return try r.json.decodeJson(as: CoreRes.GenerateKey.self) } // MARK: Files - public func decryptFile(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?) throws -> CoreRes.DecryptFile { + public func decryptFile(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?) async throws -> CoreRes.DecryptFile { let json: [String : Any?]? = [ "keys": try keys.map { try $0.toJsonEncodedDict() }, "msgPwd": msgPwd ] - let decrypted = try call("decryptFile", jsonDict: json, data: encrypted) + let decrypted = try await call("decryptFile", jsonDict: json, data: encrypted) let meta = try decrypted.json.decodeJson(as: CoreRes.DecryptFileMeta.self) return CoreRes.DecryptFile(name: meta.name, content: decrypted.data) } - public func encryptFile(pubKeys: [String]?, fileData: Data, name: String) throws -> CoreRes.EncryptFile { + public func encryptFile(pubKeys: [String]?, fileData: Data, name: String) async throws -> CoreRes.EncryptFile { let json: [String: Any?]? = [ "pubKeys": pubKeys, "name": name ] - let encrypted = try call( + let encrypted = try await call( "encryptFile", jsonDict: json, data: fileData @@ -127,13 +127,13 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return CoreRes.EncryptFile(encryptedFile: encrypted.data) } - func parseDecryptMsg(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?, isEmail: Bool) throws -> CoreRes.ParseDecryptMsg { + func parseDecryptMsg(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?, isEmail: Bool) async throws -> CoreRes.ParseDecryptMsg { let json: [String : Any?]? = [ "keys": try keys.map { try $0.toJsonEncodedDict() }, "isEmail": isEmail, "msgPwd": msgPwd ] - let parsed = try call( + let parsed = try await call( "parseDecryptMsg", jsonDict: json, data: encrypted @@ -167,7 +167,7 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { ] } - let r = try call("composeEmail", jsonDict: [ + let r = try await call("composeEmail", jsonDict: [ "text": msg.text, "to": msg.to, "cc": msg.cc, @@ -183,8 +183,8 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return CoreRes.ComposeEmail(mimeEncoded: r.data) } - func zxcvbnStrengthBar(passPhrase: String) throws -> CoreRes.ZxcvbnStrengthBar { - let r = try call("zxcvbnStrengthBar", jsonDict: ["value": passPhrase, "purpose": "passphrase"], data: nil) + func zxcvbnStrengthBar(passPhrase: String) async throws -> CoreRes.ZxcvbnStrengthBar { + let r = try await call("zxcvbnStrengthBar", jsonDict: ["value": passPhrase, "purpose": "passphrase"], data: nil) return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self) } @@ -216,8 +216,8 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { } } - func gmailBackupSearch(for email: String) throws -> String { - let response = try call("gmailBackupSearch", jsonDict: ["acctEmail": email], data: nil) + func gmailBackupSearch(for email: String) async throws -> String { + let response = try await call("gmailBackupSearch", jsonDict: ["acctEmail": email], data: nil) let result = try response.json.decodeJson(as: GmailBackupSearchResponse.self) return result.query } @@ -227,45 +227,55 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType { } // MARK: Private calls - private func call(_ endpoint: String, jsonDict: [String: Any?]?, data: Data?) throws -> RawRes { - try call(endpoint, jsonData: try JSONSerialization.data(withJSONObject: jsonDict ?? [String: String]()), data: data ?? Data()) + private func call(_ endpoint: String, jsonDict: [String: Any?]?, data: Data?) async throws -> RawRes { + return try await call(endpoint, jsonData: try JSONSerialization.data(withJSONObject: jsonDict ?? [String: String]()), data: data ?? Data()) } - private func call(_ endpoint: String, jsonEncodable: Encodable, data: Data) throws -> RawRes { - try call(endpoint, jsonData: try jsonEncodable.toJsonData(), data: data) + private func call(_ endpoint: String, jsonEncodable: Encodable, data: Data) async throws -> RawRes { + return try await call(endpoint, jsonData: try jsonEncodable.toJsonData(), data: data) } - private func call(_ endpoint: String, jsonData: Data, data: Data) throws -> RawRes { - try blockUntilReadyOrThrow() - cb_last_value = nil - jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array(data), cb_catcher!]) - guard - let resJsonData = cb_last_value?.0.data(using: .utf8), - let rawResponse = cb_last_value?.1 - else { - throw CoreError.format("JavaScript callback response not available") - } - - let error = try? resJsonData.decodeJson(as: CoreRes.Error.self) - if let error = error { - let errMsg = "------ js err -------\nCore \(endpoint):\n\(error.error.message)\n\(error.error.stack ?? "no stack")\n------- end js err -----" - logger.logError(errMsg) - - throw CoreError(coreError: error) + private func call(_ endpoint: String, jsonData: Data, data: Data) async throws -> RawRes { + try await sleepUntilReadyOrThrow() + return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + // tom - todo - currently there is only one callback storage variable "cb_last_value" + // for all JavaScript calls, and so we have to synchronize + // all calls into JavaScript to happen serially, else + // the return values would be undefined when used concurrently + // see https://github.com/FlowCrypt/flowcrypt-ios/issues/852 + // A possible solution would be to only synchronize returning o fthe callbac values into some dict. But I'm unsure if JavaScript is otherwise safe to call concurrently, so for now we'll do the safer thing. + queue.async { + self.cb_last_value = nil + self.jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array(data), self.cb_catcher!]) + guard + let resJsonData = self.cb_last_value?.0.data(using: .utf8), + let rawResponse = self.cb_last_value?.1 + else { + self.logger.logError("could not see callback response, got cb_last_value: \(String(describing: self.cb_last_value))") + continuation.resume(throwing: CoreError.format("JavaScript callback response not available")) + return + } + let error = try? resJsonData.decodeJson(as: CoreRes.Error.self) + if let error = error { + let errMsg = "------ js err -------\nCore \(endpoint):\n\(error.error.message)\n\(error.error.stack ?? "no stack")\n------- end js err -----" + self.logger.logError(errMsg) + continuation.resume(throwing: CoreError(coreError: error)) + return + } + continuation.resume(returning: RawRes(json: resJsonData, data: Data(rawResponse))) + } } - - return RawRes(json: resJsonData, data: Data(rawResponse)) } - private func blockUntilReadyOrThrow() throws { - // This will block the thread for up to 1000ms if the app was just started and Core method was called before JSContext is ready + private func sleepUntilReadyOrThrow() async throws { + // This will block the task for up to 1000ms if the app was just started and Core method was called before JSContext is ready // It should only affect the user if Core method was called within 500-800ms of starting the app let start = DispatchTime.now() while !ready { if start.millisecondsSince > 1000 { // already waited for 1000 ms, give up throw CoreError.notReady("App Core not ready yet") } - usleep(50000) // 50ms + await Task.sleep(50 * 1_000_000) // 50ms } } diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index 0830dba8d..25270ee35 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -214,15 +214,15 @@ extension EncryptedStorage { // MARK: - PassPhrase extension EncryptedStorage: PassPhraseStorageType { func save(passPhrase: PassPhrase) { - updateKeys(with: passPhrase.primaryFingerprint, passphrase: passPhrase.value) + updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: passPhrase.value) } func update(passPhrase: PassPhrase) { - updateKeys(with: passPhrase.primaryFingerprint, passphrase: passPhrase.value) + updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: passPhrase.value) } func remove(passPhrase: PassPhrase) { - updateKeys(with: passPhrase.primaryFingerprint, passphrase: nil) + updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: nil) } func getPassPhrases() -> [PassPhrase] { diff --git a/FlowCrypt/Functionality/Mail Provider/Backup Provider/Gmail+Backup.swift b/FlowCrypt/Functionality/Mail Provider/Backup Provider/Gmail+Backup.swift index a11faad39..df6bf3466 100644 --- a/FlowCrypt/Functionality/Mail Provider/Backup Provider/Gmail+Backup.swift +++ b/FlowCrypt/Functionality/Mail Provider/Backup Provider/Gmail+Backup.swift @@ -13,7 +13,7 @@ extension GmailService: BackupProvider { func searchBackups(for email: String) async throws -> Data { do { logger.logVerbose("will begin searching for backups") - let query = try backupSearchQueryProvider.makeBackupQuery(for: email) + let query = try await backupSearchQueryProvider.makeBackupQuery(for: email) let backupMessages = try await searchExpression(using: MessageSearchContext(expression: query)) logger.logVerbose("searching done, found \(backupMessages.count) backup messages") let uniqueMessages = Set(backupMessages) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 155fb6d8e..04447a0ca 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -44,7 +44,7 @@ extension ProcessedMessage { // MARK: - MessageService enum MessageServiceError: Error { - case missedPassPhrase(_ rawMimeData: Data) + case missingPassPhrase(_ rawMimeData: Data) case wrongPassPhrase(_ rawMimeData: Data, _ passPhrase: String) case keyMismatch(_ rawMimeData: Data) // Could not fetch keys @@ -59,118 +59,90 @@ final class MessageService { private let messageProvider: MessageProvider private let keyService: KeyServiceType + private let keyMethods: KeyMethodsType private let passPhraseService: PassPhraseServiceType private let core: Core + private let logger: Logger init( messageProvider: MessageProvider = MailProvider.shared.messageProvider, keyService: KeyServiceType = KeyService(), core: Core = Core.shared, - passPhraseService: PassPhraseServiceType = PassPhraseService() + passPhraseService: PassPhraseServiceType = PassPhraseService(), + keyMethods: KeyMethodsType = KeyMethods() ) { self.messageProvider = messageProvider self.keyService = keyService self.core = core self.passPhraseService = passPhraseService + self.logger = Logger.nested(in: Self.self, with: "MessageService") + self.keyMethods = keyMethods } - func validateMessage(rawMimeData: Data, with passPhrase: String) -> Promise { - Promise { [weak self] resolve, reject in - guard let self = self else { return } - - let keys = try self.keyService.getPrvKeyInfo().get() - guard keys.isNotEmpty else { - return reject(MessageServiceError.emptyKeys) - } - - let keysWithFilledPassPhrase = keys.map { $0.copy(with: passPhrase) } - let keysToSave = keys.filter { $0.passphrase == passPhrase } - - self.savePassPhrases(value: passPhrase, with: keysToSave) - - let decrypted = try self.core.parseDecryptMsg( - encrypted: rawMimeData, - keys: keysWithFilledPassPhrase, - msgPwd: nil, - isEmail: true - ) - - guard !self.hasWrongPassPhraseError(decrypted) else { - return reject(MessageServiceError.wrongPassPhrase(rawMimeData, passPhrase)) - } - - let processedMessage = try self.processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) - resolve(processedMessage) + func checkAndPotentiallySaveEnteredPassPhrase(_ passPhrase: String) async throws -> Bool { + let keys = try await self.keyService.getPrvKeyInfo() + guard keys.isNotEmpty else { + throw MessageServiceError.emptyKeys } + let keysWithoutPassPhrases = keys.filter { $0.passphrase == nil } + let matchingKeys = try await self.keyMethods.filterByPassPhraseMatch( + keys: keysWithoutPassPhrases, + passPhrase: passPhrase + ) + self.passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) + return matchingKeys.isNotEmpty } - private func savePassPhrases(value passPhrase: String, with privateKeys: [PrvKeyInfo]) { - privateKeys - .map { PassPhrase(value: passPhrase, fingerprints: $0.fingerprints) } - .forEach { self.passPhraseService.savePassPhrase(with: $0, storageMethod: .memory) } - } - - func getAndProcessMessage(with input: Message, folder: String) -> Promise { - Promise { [weak self] resolve, reject in - guard let self = self else { return } - - let rawMimeData = try awaitPromise( - self.messageProvider.fetchMsg(message: input, folder: folder) - ) - - let keys = try self.keyService.getPrvKeyInfo().get() - guard keys.isNotEmpty else { - return reject(MessageServiceError.emptyKeys) - } - - let decrypted = try self.core.parseDecryptMsg( - encrypted: rawMimeData, - keys: keys, - msgPwd: nil, - isEmail: true - ) - - guard !self.hasWrongPassPhraseError(decrypted) else { - return reject(MessageServiceError.missedPassPhrase(rawMimeData)) - } + func decryptAndProcessMessage(mime rawMimeData: Data) async throws -> ProcessedMessage { + let keys = try await self.keyService.getPrvKeyInfo() + guard keys.isNotEmpty else { + throw MessageServiceError.emptyKeys + } + let decrypted = try await self.core.parseDecryptMsg( + encrypted: rawMimeData, + keys: keys, + msgPwd: nil, + isEmail: true + ) + guard !self.hasMsgBlockThatNeedsPassPhrase(decrypted) else { + throw MessageServiceError.missingPassPhrase(rawMimeData) + } - let processedMessage = try self.processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) - switch processedMessage.messageType { - case .error(let errorType): - switch errorType { - case .needPassphrase: - reject(MessageServiceError.missedPassPhrase(rawMimeData)) - case .keyMismatch: - reject(MessageServiceError.keyMismatch(rawMimeData)) - default: - reject(MessageServiceError.unknown) - } - case .plain, .encrypted: - resolve(processedMessage) + let processedMessage = try await self.processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) + switch processedMessage.messageType { + case .error(let errorType): + switch errorType { + case .needPassphrase: + throw MessageServiceError.missingPassPhrase(rawMimeData) + case .keyMismatch: + throw MessageServiceError.keyMismatch(rawMimeData) + default: + throw MessageServiceError.unknown } + case .plain, .encrypted: + return processedMessage } } - private func processMessage(rawMimeData: Data, with decrypted: CoreRes.ParseDecryptMsg, keys: [PrvKeyInfo]) throws -> ProcessedMessage { + private func processMessage(rawMimeData: Data, with decrypted: CoreRes.ParseDecryptMsg, keys: [PrvKeyInfo]) async throws -> ProcessedMessage { let decryptErrBlocks = decrypted.blocks .filter { $0.decryptErr != nil } - - let attachments = try decrypted.blocks + let attachmentBlocks = decrypted.blocks .filter(\.isAttachmentBlock) - .compactMap { block -> MessageAttachment? in - guard let attMeta = block.attMeta else { return nil } - var name = attMeta.name - let size = attMeta.length - var data = attMeta.data - - if block.type == .encryptedAtt { - data = (try core.decryptFile(encrypted: data, keys: keys, msgPwd: nil).content) - name = name.deletingPathExtension - } - - return MessageAttachment(name: name, size: size, data: data) + var attachments: [MessageAttachment] = [] + for block in attachmentBlocks { + guard let meta = block.attMeta else { continue } + var name = meta.name + var data = meta.data + var size = meta.length + if block.type == .encryptedAtt { // decrypt + let decrypted = try await core.decryptFile(encrypted: data, keys: keys, msgPwd: nil) + data = decrypted.content + name = decrypted.name + size = decrypted.content.count } - + attachments.append(MessageAttachment(name: name, size: size, data: data)) + } let messageType: ProcessedMessage.MessageType let text: String @@ -192,8 +164,13 @@ final class MessageService { ) } - private func hasWrongPassPhraseError(_ msg: CoreRes.ParseDecryptMsg) -> Bool { - msg.blocks.first(where: { $0.decryptErr?.error.type == .needPassphrase }) != nil + private func hasMsgBlockThatNeedsPassPhrase(_ msg: CoreRes.ParseDecryptMsg) -> Bool { + let maybeBlock = msg.blocks.first(where: { $0.decryptErr?.error.type == .needPassphrase }) + guard let block = maybeBlock, let decryptErr = block.decryptErr else { + return false + } + logger.logInfo("missing pass phrase for one of longids \(decryptErr.longids)") + return true } } diff --git a/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/GmailSearchExpressionGenerator.swift b/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/GmailSearchExpressionGenerator.swift index c1269e977..079de7406 100644 --- a/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/GmailSearchExpressionGenerator.swift +++ b/FlowCrypt/Functionality/Mail Provider/SearchMessage Provider/GmailSearchExpressionGenerator.swift @@ -9,7 +9,7 @@ import Foundation protocol GmailBackupSearchQueryProviderType { - func makeBackupQuery(for email: String) throws -> String + func makeBackupQuery(for email: String) async throws -> String } final class GmailBackupSearchQueryProvider: GmailBackupSearchQueryProviderType { @@ -19,7 +19,7 @@ final class GmailBackupSearchQueryProvider: GmailBackupSearchQueryProviderType { self.core = core } - func makeBackupQuery(for email: String) throws -> String { - try core.gmailBackupSearch(for: email) + func makeBackupQuery(for email: String) async throws -> String { + return try await core.gmailBackupSearch(for: email) } } diff --git a/FlowCrypt/Functionality/Pgp/KeyMethods.swift b/FlowCrypt/Functionality/Pgp/KeyMethods.swift index 97ebdf58d..c13e12765 100644 --- a/FlowCrypt/Functionality/Pgp/KeyMethods.swift +++ b/FlowCrypt/Functionality/Pgp/KeyMethods.swift @@ -10,7 +10,8 @@ import FlowCryptCommon import Foundation protocol KeyMethodsType { - func filterByPassPhraseMatch(keys: [KeyDetails], passPhrase: String) -> [KeyDetails] + func filterByPassPhraseMatch(keys: [KeyDetails], passPhrase: String) async throws -> [KeyDetails] + func filterByPassPhraseMatch(keys: [PrvKeyInfo], passPhrase: String) async throws -> [PrvKeyInfo] } final class KeyMethods: KeyMethodsType { @@ -21,27 +22,38 @@ final class KeyMethods: KeyMethodsType { self.decrypter = decrypter } - func filterByPassPhraseMatch(keys: [KeyDetails], passPhrase: String) -> [KeyDetails] { + // todo - join these two methods into one + func filterByPassPhraseMatch(keys: [PrvKeyInfo], passPhrase: String) async throws -> [PrvKeyInfo] { let logger = Logger.nested(in: Self.self, with: .core) - - guard keys.isNotEmpty else { - logger.logInfo("Keys are empty") - return [] + var matching: [PrvKeyInfo] = [] + for key in keys { + do { + _ = try await self.decrypter.decryptKey(armoredPrv: key.private, passphrase: passPhrase) + matching.append(key) + logger.logInfo("pass phrase matches for key: \(key.fingerprints.first ?? "missing fingerprint")") + } catch { + logger.logInfo("pass phrase does not match for key: \(key.fingerprints.first ?? "missing fingerprint")") + } } + return matching + } - return keys.compactMap { key -> KeyDetails? in + // todo - join these two methods into one. Maybe drop this one and keep the PrvKeyInfo method? + func filterByPassPhraseMatch(keys: [KeyDetails], passPhrase: String) async throws -> [KeyDetails] { + let logger = Logger.nested(in: Self.self, with: .core) + var matching: [KeyDetails] = [] + for key in keys { guard let privateKey = key.private else { - logger.logInfo("Filtered not private key") - return nil + throw KeyServiceError.expectedPrivateGotPublic } - do { - _ = try self.decrypter.decryptKey(armoredPrv: privateKey, passphrase: passPhrase) - return key + _ = try await self.decrypter.decryptKey(armoredPrv: privateKey, passphrase: passPhrase) + matching.append(key) + logger.logInfo("pass phrase matches for key: \(key.primaryFingerprint)") } catch { - logger.logInfo("Filtered not decrypted key") - return nil + logger.logInfo("pass phrase does not match for key: \(key.primaryFingerprint)") } } + return matching } } diff --git a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift index 28becfd9f..623e04218 100644 --- a/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift +++ b/FlowCrypt/Functionality/Services/Backup Services/BackupService.swift @@ -31,9 +31,8 @@ final class BackupService { extension BackupService: BackupServiceType { func fetchBackupsFromInbox(for userId: UserId) async throws -> [KeyDetails] { let backupData = try await self.backupProvider.searchBackups(for: userId.email) - do { - let parsed = try core.parseKeys(armoredOrBinary: backupData) + let parsed = try await core.parseKeys(armoredOrBinary: backupData) let keys = parsed.keyDetails.filter { $0.private != nil } return keys } catch { diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageAttachment.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageAttachment.swift index 65e050861..40d5de175 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageAttachment.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageAttachment.swift @@ -64,4 +64,8 @@ extension ComposeMessageAttachment { self.size = data.count self.type = fileURL.mimeType } + + func toSendableMsgAttachment() -> SendableMsg.Attachment { + return SendableMsg.Attachment( name: self.name, type: self.type, base64: self.data.base64EncodedString()) + } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 24c3e0a75..aa77a06d7 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -10,6 +10,7 @@ import Combine import FlowCryptUI import Foundation import GoogleAPIClientForREST_Gmail +import FlowCryptCommon struct ComposeMessageContext: Equatable { var message: String? @@ -37,6 +38,7 @@ final class ComposeMessageService { private let contactsService: ContactsServiceType private let core: CoreComposeMessageType & KeyParser private let draftGateway: DraftGateway? + private let logger: Logger init( messageGateway: MessageGateway = MailProvider.shared.messageSender, @@ -50,38 +52,38 @@ final class ComposeMessageService { self.dataService = dataService self.contactsService = contactsService self.core = core + self.logger = Logger.nested(in: Self.self, with: "ComposeMessageService") } - // MARK: - Validation - func validateMessage( + func validateAndProduceSendableMsg( input: ComposeMessageInput, contextToSend: ComposeMessageContext, email: String, includeAttachments: Bool = true, signingPrv: PrvKeyInfo? - ) -> Result { + ) async throws -> SendableMsg { let recipients = contextToSend.recipients guard recipients.isNotEmpty else { - return .failure(.validationError(.emptyRecipient)) + throw MessageValidationError.emptyRecipient } let emails = recipients.map(\.email) let emptyEmails = emails.filter { !$0.hasContent } guard emails.isNotEmpty, emptyEmails.isEmpty else { - return .failure(.validationError(.emptyRecipient)) + throw MessageValidationError.emptyRecipient } guard emails.filter({ !$0.isValidEmail }).isEmpty else { - return .failure(.validationError(.invalidEmailRecipient)) + throw MessageValidationError.invalidEmailRecipient } guard input.isReply || contextToSend.subject?.hasContent ?? false else { - return .failure(.validationError(.emptySubject)) + throw MessageValidationError.emptySubject } guard let text = contextToSend.message, text.hasContent else { - return .failure(.validationError(.emptyMessage)) + throw MessageValidationError.emptyMessage } let subject = input.subjectReplyTitle @@ -89,60 +91,56 @@ final class ComposeMessageService { ?? "(no subject)" guard let myPubKey = self.dataService.publicKey() else { - return .failure(.validationError(.missedPublicKey)) + throw MessageValidationError.missedPublicKey } - let sendableAttachments: [SendableMsg.Attachment] = contextToSend.attachments - .map { composeAttachment in - return SendableMsg.Attachment( - name: composeAttachment.name, - type: composeAttachment.type, - base64: composeAttachment.data.base64EncodedString() - ) - } - - return getPubKeys(for: recipients) - .mapError { ComposeMessageError.validationError($0) } - .map { allRecipientPubs in - let replyToMimeMsg = input.replyToMime - .flatMap { String(data: $0, encoding: .utf8) } - - return SendableMsg( - text: text, - to: recipients.map(\.email), - cc: [], - bcc: [], - from: email, - subject: subject, - replyToMimeMsg: replyToMimeMsg, - atts: sendableAttachments, - pubKeys: allRecipientPubs + [myPubKey], - signingPrv: signingPrv - ) - } + let sendableAttachments: [SendableMsg.Attachment] = includeAttachments + ? contextToSend.attachments.map { $0.toSendableMsgAttachment() } + : [] + + let allRecipientPubs = try await getPubKeys(for: recipients) + let replyToMimeMsg = input.replyToMime + .flatMap { String(data: $0, encoding: .utf8) } + return SendableMsg( + text: text, + to: recipients.map(\.email), + cc: [], + bcc: [], + from: email, + subject: subject, + replyToMimeMsg: replyToMimeMsg, + atts: sendableAttachments, + pubKeys: [myPubKey] + allRecipientPubs, + signingPrv: signingPrv + ) } - private func getPubKeys(for recipients: [ComposeMessageRecipient]) -> Result<[String], MessageValidationError> { - let recipientsWithKeys = recipients.map { recipient -> RecipientWithSortedPubKeys in - let keyDetails = contactsService.retrievePubKeys(for: recipient.email) - .compactMap { try? self.core.parseKeys(armoredOrBinary: $0.data()) } - .flatMap { $0.keyDetails } - return RecipientWithSortedPubKeys(email: recipient.email, keyDetails: keyDetails) + private func getPubKeys(for recipients: [ComposeMessageRecipient]) async throws -> [String] { + var recipientsWithKeys: [RecipientWithSortedPubKeys] = [] + for recipient in recipients { + let armoredPubkeys = contactsService.retrievePubKeys(for: recipient.email).joined(separator: "\n") + let parsed = try await self.core.parseKeys(armoredOrBinary: armoredPubkeys.data()) + recipientsWithKeys.append(RecipientWithSortedPubKeys(email: recipient.email, keyDetails: parsed.keyDetails)) } - - return validate(recipients: recipientsWithKeys) + return try validate(recipients: recipientsWithKeys) } - private func validate(recipients: [RecipientWithSortedPubKeys]) -> Result<[String], MessageValidationError> { + private func validate(recipients: [RecipientWithSortedPubKeys]) throws -> [String] { func contains(keyState: PubKeyState) -> Bool { recipients.first(where: { $0.keyState == keyState }) != nil } - - guard !contains(keyState: .empty) else { return .failure(.noPubRecipients) } - guard !contains(keyState: .expired) else { return .failure(.expiredKeyRecipients) } - guard !contains(keyState: .revoked) else { return .failure(.revokedKeyRecipients) } - - return .success(recipients.flatMap(\.activePubKeys).map(\.armored)) + logger.logDebug("validate recipients: \(recipients)") + logger.logDebug("validate recipient keyStates: \(recipients.map { $0.keyState })") + guard !contains(keyState: .empty) else { + throw MessageValidationError.noPubRecipients + } + guard !contains(keyState: .expired) else { + throw MessageValidationError.expiredKeyRecipients + } + guard !contains(keyState: .revoked) else { + throw MessageValidationError.revokedKeyRecipients + } + return recipients.flatMap(\.activePubKeys).map(\.armored) } private var draft: GTLRGmail_Draft? @@ -160,6 +158,7 @@ final class ComposeMessageService { func getDraft(with identifier: String) { Task { + // tom - unsure what this does? the result is unused let draft = try await draftGateway?.getDraft(with: identifier) } } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift index d4055ad28..ee4777351 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/KeyService.swift @@ -7,22 +7,25 @@ // import Foundation +import FlowCryptCommon protocol KeyServiceType { - func getPrvKeyDetails() -> Result<[KeyDetails], KeyServiceError> - func getPrvKeyInfo() -> Result<[PrvKeyInfo], KeyServiceError> - func getSigningKey() throws -> PrvKeyInfo? + func getPrvKeyDetails() async throws -> [KeyDetails] + func getPrvKeyInfo() async throws -> [PrvKeyInfo] + func getSigningKey() async throws -> PrvKeyInfo? } enum KeyServiceError: Error { - case unexpected, parsingError, retrieve, missingCurrentUserEmail + case unexpected, parsingError, retrieve, missingCurrentUserEmail, expectedPrivateGotPublic } final class KeyService: KeyServiceType { + let coreService: Core = .shared let storage: KeyStorageType let passPhraseService: PassPhraseServiceType let currentUserEmail: () -> (String?) + let logger: Logger init( storage: KeyStorageType = KeyDataStorage(), @@ -32,93 +35,91 @@ final class KeyService: KeyServiceType { self.storage = storage self.passPhraseService = passPhraseService self.currentUserEmail = currentUserEmail + self.logger = Logger.nested(in: Self.self, with: "KeyService") } /// Use to get list of keys (including missing pass phrases keys) - func getPrvKeyDetails() -> Result<[KeyDetails], KeyServiceError> { + func getPrvKeyDetails() async throws -> [KeyDetails] { guard let email = currentUserEmail() else { - return .failure(.missingCurrentUserEmail) + throw KeyServiceError.missingCurrentUserEmail } - let privateKeys = storage.keysInfo() .filter { $0.account == email } .map(\.private) - - let keyDetails = privateKeys - .compactMap { - try? coreService.parseKeys(armoredOrBinary: $0.data()) - } - .map(\.keyDetails) - .flatMap { $0 } - - guard keyDetails.count == privateKeys.count else { - return .failure(.parsingError) + let parsed = try await coreService.parseKeys( + armoredOrBinary: privateKeys.joined(separator: "\n").data() + ) + guard parsed.keyDetails.count == privateKeys.count else { + throw KeyServiceError.parsingError } - - return .success(keyDetails) + return parsed.keyDetails } /// Use to get list of PrvKeyInfo - func getPrvKeyInfo() -> Result<[PrvKeyInfo], KeyServiceError> { + func getPrvKeyInfo() async throws -> [PrvKeyInfo] { guard let email = currentUserEmail() else { - return .failure(.missingCurrentUserEmail) + throw KeyServiceError.missingCurrentUserEmail } - let keysInfo = storage.keysInfo() .filter { $0.account == email } - let storedPassPhrases = passPhraseService.getPassPhrases() - let privateKeys = keysInfo .map { keyInfo -> PrvKeyInfo in let passphrase = storedPassPhrases .filter { $0.value.isNotEmpty } - .first(where: { $0.primaryFingerprint == keyInfo.primaryFingerprint })? + .first(where: { $0.primaryFingerprintOfAssociatedKey == keyInfo.primaryFingerprint })? .value - return PrvKeyInfo(keyInfo: keyInfo, passphrase: passphrase) } - - return .success(privateKeys) + return privateKeys } - func getSigningKey() throws -> PrvKeyInfo? { + func getSigningKey() async throws -> PrvKeyInfo? { guard let email = currentUserEmail() else { - return nil + logger.logError("no current user email") + throw AppErr.noCurrentUser } - - let keysInfo = storage.keysInfo() - .filter { $0.account == email } - - guard let foundKey = try findKeyByUserEmail(keysInfo: keysInfo, email: email) else { + // get keys associated with this account, freeze them to pass across threads + let keysInfo = storage.keysInfo().filter { $0.account == email }.map { object -> KeyInfo in + guard object.realm != nil else { return object } + return object.freeze() + } + guard let foundKey = try await findKeyByUserEmail(keysInfo: keysInfo, email: email) else { return nil } let storedPassPhrases = passPhraseService.getPassPhrases() let passphrase = storedPassPhrases .filter { $0.value.isNotEmpty } - .first(where: { $0.primaryFingerprint == foundKey.primaryFingerprint })? + .first(where: { $0.primaryFingerprintOfAssociatedKey == foundKey.primaryFingerprint })? .value return PrvKeyInfo(keyInfo: foundKey, passphrase: passphrase) } - private func findKeyByUserEmail(keysInfo: [KeyInfo], email: String) throws -> KeyInfo? { - let keys: [(KeyInfo, KeyDetails?)] = try keysInfo.map { - let parsedKeys = try self.coreService.parseKeys( - armoredOrBinary: $0.`private`.data() + private func findKeyByUserEmail(keysInfo: [KeyInfo], email: String) async throws -> KeyInfo? { + // todo - should be refactored with https://github.com/FlowCrypt/flowcrypt-ios/issues/812 + logger.logDebug("findKeyByUserEmail: found \(keysInfo.count) candidate prvs in storage, searching by:\(email)") + var keys: [(KeyInfo, KeyDetails)] = [] + for keyInfo in keysInfo { + let parsedKeys = try await coreService.parseKeys( + armoredOrBinary: keyInfo.`private`.data() ) - return ($0, parsedKeys.keyDetails.first) + guard let parsedKey = parsedKeys.keyDetails.first else { + throw KeyServiceError.parsingError + } +// logger.logDebug("findKeyByUserEmail: PrvKeyInfo from storage has primary fingerprint \(keyInfo.primaryFingerprint) vs parsed key \(parsedKey.primaryFingerprint) and has emails \(parsedKey.pgpUserEmails)") + keys.append((keyInfo, parsedKey)) } - - if let primaryEmailMatch = keys.first(where: { $0.1?.pgpUserEmails.first == email }) { + if let primaryEmailMatch = keys.first(where: { $0.1.pgpUserEmails.first?.lowercased() == email.lowercased() }) { + logger.logDebug("findKeyByUserEmail: found key \(primaryEmailMatch.1.primaryFingerprint) by primary email match") return primaryEmailMatch.0 } - - if let anyEmailMatch = keys.first(where: { $0.1?.pgpUserEmails.contains(email) == true }) { - return anyEmailMatch.0 + if let alternativeEmailMatch = keys.first(where: { $0.1.pgpUserEmails.map { $0.lowercased() }.contains(email.lowercased()) == true }) { + logger.logDebug("findKeyByUserEmail: found key \(alternativeEmailMatch.1.primaryFingerprint) by alternative email match") + return alternativeEmailMatch.0 } - + logger.logDebug("findKeyByUserEmail: could not match any key") return nil } } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift index 2901c783d..e2552d061 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift @@ -12,29 +12,32 @@ import UIKit // MARK: - Data Object struct PassPhrase: Codable, Hashable, Equatable { let value: String - let fingerprints: [String] + let fingerprintsOfAssociatedKey: [String] let date: Date? - var primaryFingerprint: String { - fingerprints[0] + var primaryFingerprintOfAssociatedKey: String { + fingerprintsOfAssociatedKey[0] } - init(value: String, fingerprints: [String], date: Date? = nil) { + init(value: String, fingerprintsOfAssociatedKey: [String], date: Date? = nil) { self.value = value - self.fingerprints = fingerprints + self.fingerprintsOfAssociatedKey = fingerprintsOfAssociatedKey self.date = date } func withUpdatedDate() -> PassPhrase { - PassPhrase(value: self.value, fingerprints: self.fingerprints, date: Date()) + PassPhrase(value: self.value, fingerprintsOfAssociatedKey: self.fingerprintsOfAssociatedKey, date: Date()) } + // (tom) todo - this is a confusing thing to do + // when comparing pass phrases to one another, you would expect that it's compared by the pass phrase string itself, and not by primary fingerprint of the associated key. I understand this is being used somewhere, but I suggest to refactor it to avoid defining this == overload. static func == (lhs: PassPhrase, rhs: PassPhrase) -> Bool { - lhs.primaryFingerprint == rhs.primaryFingerprint + lhs.primaryFingerprintOfAssociatedKey == rhs.primaryFingerprintOfAssociatedKey } + // similarly here func hash(into hasher: inout Hasher) { - hasher.combine(primaryFingerprint) + hasher.combine(primaryFingerprintOfAssociatedKey) } } @@ -43,7 +46,7 @@ extension PassPhrase { guard let passphrase = keyInfo.passphrase else { return nil } self.init(value: passphrase, - fingerprints: Array(keyInfo.allFingerprints)) + fingerprintsOfAssociatedKey: Array(keyInfo.allFingerprints)) } } @@ -61,6 +64,7 @@ protocol PassPhraseServiceType { func getPassPhrases() -> [PassPhrase] func savePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) func updatePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) + func savePassPhrasesInMemory(_ passPhrase: String, for privateKeys: [PrvKeyInfo]) } final class PassPhraseService: PassPhraseServiceType { @@ -81,24 +85,21 @@ final class PassPhraseService: PassPhraseServiceType { } func savePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) { + logger.logInfo("\(storageMethod): saving passphrase for key \(passPhrase.primaryFingerprintOfAssociatedKey)") switch storageMethod { case .persistent: - logger.logInfo("Save passphrase to storage") encryptedStorage.save(passPhrase: passPhrase) case .memory: - logger.logInfo("Save passphrase in memory") - - inMemoryStorage.save(passPhrase: passPhrase) - - let alreadySaved = encryptedStorage.getPassPhrases() - - if alreadySaved.contains(where: { $0.primaryFingerprint == passPhrase.primaryFingerprint }) { + if encryptedStorage.getPassPhrases().contains(where: { $0.primaryFingerprintOfAssociatedKey == passPhrase.primaryFingerprintOfAssociatedKey }) { + logger.logInfo("\(StorageMethod.persistent): removing pass phrase from for key \(passPhrase.primaryFingerprintOfAssociatedKey)") encryptedStorage.remove(passPhrase: passPhrase) } + inMemoryStorage.save(passPhrase: passPhrase) } } func updatePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) { + logger.logInfo("\(storageMethod): updating passphrase for key \(passPhrase.primaryFingerprintOfAssociatedKey)") switch storageMethod { case .persistent: encryptedStorage.update(passPhrase: passPhrase) @@ -110,4 +111,12 @@ final class PassPhraseService: PassPhraseServiceType { func getPassPhrases() -> [PassPhrase] { encryptedStorage.getPassPhrases() + inMemoryStorage.getPassPhrases() } + + func savePassPhrasesInMemory(_ passPhrase: String, for privateKeys: [PrvKeyInfo]) { + for privateKey in privateKeys { + let pp = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: privateKey.fingerprints) + savePassPhrase(with: pp, storageMethod: StorageMethod.memory) + } + } + } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift index 4e7156639..29034bcda 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift @@ -44,7 +44,8 @@ struct ContactsService: ContactsServiceType { extension ContactsService: ContactsProviderType { func searchContact(with email: String) async throws -> RecipientWithSortedPubKeys { - guard let contact = localContactsProvider.searchRecipient(with: email) else { + let contact = try await localContactsProvider.searchRecipient(with: email) + guard let contact = contact else { let recipient = try await pubLookup.lookup(with: email) localContactsProvider.save(recipient: recipient) return recipient diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index 392f762df..41ab53e04 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -12,12 +12,12 @@ import RealmSwift protocol LocalContactsProviderType: PublicKeyProvider { func updateLastUsedDate(for email: String) - func searchRecipient(with email: String) -> RecipientWithSortedPubKeys? + func searchRecipient(with email: String) async throws -> RecipientWithSortedPubKeys? func searchEmails(query: String) -> [String] func save(recipient: RecipientWithSortedPubKeys) func remove(recipient: RecipientWithSortedPubKeys) func updateKeys(for recipient: RecipientWithSortedPubKeys) - func getAllRecipients() -> [RecipientWithSortedPubKeys] + func getAllRecipients() async throws -> [RecipientWithSortedPubKeys] } struct LocalContactsProvider { @@ -74,9 +74,10 @@ extension LocalContactsProvider: LocalContactsProviderType { } } - func searchRecipient(with email: String) -> RecipientWithSortedPubKeys? { + func searchRecipient(with email: String) async throws -> RecipientWithSortedPubKeys? { guard let recipientObject = find(with: email) else { return nil } - return parseRecipient(from: recipientObject) + // TODO temporary fix for Realm thread problem + return try await parseRecipient(from: recipientObject.freeze()) } func searchEmails(query: String) -> [String] { @@ -86,11 +87,13 @@ extension LocalContactsProvider: LocalContactsProviderType { .map(\.email) } - func getAllRecipients() -> [RecipientWithSortedPubKeys] { - localContactsCache.realm - .objects(RecipientObject.self) - .map(parseRecipient) - .sorted(by: { $0.email > $1.email }) + func getAllRecipients() async throws -> [RecipientWithSortedPubKeys] { + let objects = localContactsCache.realm.objects(RecipientObject.self) + var recipients: [RecipientWithSortedPubKeys] = [] + for object in objects { + recipients.append(try await parseRecipient(from: object)) + } + return recipients.sorted(by: { $0.email > $1.email }) } func removePubKey(with fingerprint: String, for email: String) { @@ -111,11 +114,12 @@ extension LocalContactsProvider { forPrimaryKey: email) } - private func parseRecipient(from object: RecipientObject) -> RecipientWithSortedPubKeys { - let keyDetails = object.pubKeys - .compactMap { try? core.parseKeys(armoredOrBinary: $0.armored.data()).keyDetails } - .flatMap { $0 } - return RecipientWithSortedPubKeys(object, keyDetails: Array(keyDetails)) + private func parseRecipient(from object: RecipientObject) async throws -> RecipientWithSortedPubKeys { + let armoredToParse = object.pubKeys + .map { $0.armored } + .joined(separator: "\n") + let parsed = try await core.parseKeys(armoredOrBinary: armoredToParse.data()) + return RecipientWithSortedPubKeys(object, keyDetails: parsed.keyDetails) } private func add(pubKey: PubKey, to recipient: RecipientObject) { diff --git a/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift b/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift index 6bdfc61c2..583a0eb07 100644 --- a/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Private Key Services/EmailKeyManagerApi.swift @@ -18,7 +18,7 @@ enum EmailKeyManagerApiError: Error { } enum EmailKeyManagerApiResult { - case success(keys: [CoreRes.ParseKeys]) + case success(keys: [KeyDetails]) case noKeys case keysAreNotDecrypted } @@ -79,19 +79,19 @@ actor EmailKeyManagerApi: EmailKeyManagerApiType { return .noKeys } - let privateKeys = decryptedPrivateKeysResponse.privateKeys - .map { $0.decryptedPrivateKey.data() } - let parsedPrivateKeys = privateKeys - .compactMap { try? core.parseKeys(armoredOrBinary: $0) } - let areKeysDecrypted = parsedPrivateKeys - .compactMap { $0.keyDetails.map { $0.isFullyDecrypted } } - .flatMap { $0 } - + let privateKeysArmored = decryptedPrivateKeysResponse.privateKeys + .map { $0.decryptedPrivateKey } + .joined(separator: "\n") + .data() + let parsedPrivateKeys = try await core.parseKeys(armoredOrBinary: privateKeysArmored) + // todo - check that parsedPrivateKeys don't contain public keys + let areKeysDecrypted = parsedPrivateKeys.keyDetails + .compactMap { $0.isFullyDecrypted } if areKeysDecrypted.contains(false) { return .keysAreNotDecrypted } - return .success(keys: parsedPrivateKeys) + return .success(keys: parsedPrivateKeys.keyDetails) } private func getPrivateKeysUrlString() -> String? { diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift index fdafd383a..382b4de04 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/AttesterApi.swift @@ -61,7 +61,7 @@ extension AttesterApi { let res = try await ApiCall.asyncCall(request) if res.status >= 200, res.status <= 299 { - return try core.parseKeys(armoredOrBinary: res.data).keyDetails + return try await core.parseKeys(armoredOrBinary: res.data).keyDetails } if res.status == 404 { diff --git a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift index a6c1be71c..166a912d7 100644 --- a/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift +++ b/FlowCrypt/Functionality/Services/Remote Pub Key Services/WkdApi.swift @@ -47,25 +47,21 @@ class WkdApi: WkdApiType { else { return nil } - var response: (hasPolicy: Bool, key: Data?)? response = try await urlLookup(advancedUrl) if response?.hasPolicy == true && response?.key == nil { return nil } - if response?.key == nil { response = try await urlLookup(directUrl) if response?.key == nil { return nil } } - guard let binaryKeysData = response?.key else { return nil } - - return try? core.parseKeys(armoredOrBinary: binaryKeysData) + return try await core.parseKeys(armoredOrBinary: binaryKeysData) } } diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index c14a579cd..16af131b2 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -7,6 +7,7 @@ import Combine @testable import FlowCrypt +import FlowCryptCommon import XCTest final class FlowCryptCoreTests: XCTestCase { @@ -22,8 +23,8 @@ final class FlowCryptCoreTests: XCTestCase { // the tests below - func testVersions() throws { - let r = try core.version() + func testVersions() async throws { + let r = try await core.version() XCTAssertEqual(r.app_version, "iOS 0.2") } @@ -41,8 +42,8 @@ final class FlowCryptCoreTests: XCTestCase { XCTAssertEqual(r.key.ids.count, 2) } - func testZxcvbnStrengthBarWeak() throws { - let r = try core.zxcvbnStrengthBar(passPhrase: "nothing much") + func testZxcvbnStrengthBarWeak() async throws { + let r = try await core.zxcvbnStrengthBar(passPhrase: "nothing much") XCTAssertEqual(r.word.word, CoreRes.ZxcvbnStrengthBar.WordDetails.Word.weak) XCTAssertEqual(r.word.pass, false) XCTAssertEqual(r.word.color, CoreRes.ZxcvbnStrengthBar.WordDetails.Color.red) @@ -50,8 +51,8 @@ final class FlowCryptCoreTests: XCTestCase { XCTAssertEqual(r.time, "less than a second") } - func testZxcvbnStrengthBarStrong() throws { - let r = try core.zxcvbnStrengthBar(passPhrase: "this one is seriously over the top strong pwd") + func testZxcvbnStrengthBarStrong() async throws { + let r = try await core.zxcvbnStrengthBar(passPhrase: "this one is seriously over the top strong pwd") XCTAssertEqual(r.word.word, CoreRes.ZxcvbnStrengthBar.WordDetails.Word.perfect) XCTAssertEqual(r.word.pass, true) XCTAssertEqual(r.word.color, CoreRes.ZxcvbnStrengthBar.WordDetails.Color.green) @@ -59,8 +60,8 @@ final class FlowCryptCoreTests: XCTestCase { XCTAssertEqual(r.time, "millennia") } - func testParseKeys() throws { - let r = try core.parseKeys(armoredOrBinary: TestData.k0.pub.data(using: .utf8)! + [10] + TestData.k1.prv.data(using: .utf8)!) + func testParseKeys() async throws { + let r = try await core.parseKeys(armoredOrBinary: TestData.k0.pub.data(using: .utf8)! + [10] + TestData.k1.prv.data(using: .utf8)!) XCTAssertEqual(r.format, CoreRes.ParseKeys.Format.armored) XCTAssertEqual(r.keyDetails.count, 2) // k0 k is public @@ -82,17 +83,24 @@ final class FlowCryptCoreTests: XCTestCase { // todo - could test user ids } - func testDecryptKeyWithCorrectPassPhrase() throws { - let decryptKeyRes = try core.decryptKey(armoredPrv: TestData.k0.prv, passphrase: TestData.k0.passphrase) + func testDecryptKeyWithCorrectPassPhrase() async throws { + let decryptKeyRes = try await core.decryptKey(armoredPrv: TestData.k0.prv, passphrase: TestData.k0.passphrase) XCTAssertNotNil(decryptKeyRes.decryptedKey) // make sure indeed decrypted - let parseKeyRes = try core.parseKeys(armoredOrBinary: decryptKeyRes.decryptedKey.data(using: .utf8)!) + let parseKeyRes = try await core.parseKeys(armoredOrBinary: decryptKeyRes.decryptedKey.data(using: .utf8)!) XCTAssertEqual(parseKeyRes.keyDetails[0].isFullyDecrypted, true) XCTAssertEqual(parseKeyRes.keyDetails[0].isFullyEncrypted, false) } - func testDecryptKeyWithWrongPassPhrase() { - XCTAssertThrowsError(try core.decryptKey(armoredPrv: TestData.k0.prv, passphrase: "wrong")) + func testDecryptKeyWithWrongPassPhrase() async { + do { + _ = try await core.decryptKey(armoredPrv: TestData.k0.prv, passphrase: "wrong") + XCTFail("Should have thrown above") + } catch { + Logger.logDebug("catched \(error)") + return + } + XCTFail("Should have thrown above") } func testComposeEmailPlain() async throws { @@ -187,7 +195,7 @@ final class FlowCryptCoreTests: XCTestCase { ) let mime = try await core.composeEmail(msg: msg, fmt: .encryptInline) let keys = [PrvKeyInfo(private: k.private!, longid: k.ids[0].longid, passphrase: passphrase, fingerprints: k.fingerprints)] - let decrypted = try core.parseDecryptMsg(encrypted: mime.mimeEncoded, keys: keys, msgPwd: nil, isEmail: true) + let decrypted = try await core.parseDecryptMsg(encrypted: mime.mimeEncoded, keys: keys, msgPwd: nil, isEmail: true) XCTAssertEqual(decrypted.text, text) XCTAssertEqual(decrypted.replyType, CoreRes.ReplyType.encrypted) XCTAssertEqual(decrypted.blocks.count, 1) @@ -198,9 +206,9 @@ final class FlowCryptCoreTests: XCTestCase { XCTAssertNotNil(b.content.range(of: text)) // original text contained within the formatted html block } - func testDecryptErrMismatch() throws { + func testDecryptErrMismatch() async throws { let key = PrvKeyInfo(private: TestData.k0.prv, longid: TestData.k0.longid, passphrase: TestData.k0.passphrase, fingerprints: TestData.k0.fingerprints) - let r = try core.parseDecryptMsg(encrypted: TestData.mismatchEncryptedMsg.data(using: .utf8)!, keys: [key], msgPwd: nil, isEmail: false) + let r = try await core.parseDecryptMsg(encrypted: TestData.mismatchEncryptedMsg.data(using: .utf8)!, keys: [key], msgPwd: nil, isEmail: false) let decrypted = r XCTAssertEqual(decrypted.text, "") XCTAssertEqual(decrypted.replyType, CoreRes.ReplyType.plain) // replies to errors should be plain @@ -240,12 +248,12 @@ final class FlowCryptCoreTests: XCTestCase { ] // When - let encrypted = try core.encryptFile( + let encrypted = try await core.encryptFile( pubKeys: [k.public], fileData: fileData, name: initialFileName ) - let decrypted = try core.decryptFile( + let decrypted = try await core.decryptFile( encrypted: encrypted.encryptedFile, keys: keys, msgPwd: nil @@ -282,7 +290,7 @@ final class FlowCryptCoreTests: XCTestCase { // When do { - _ = try self.core.decryptFile( + _ = try await core.decryptFile( encrypted: fileData, keys: keys, msgPwd: nil @@ -312,12 +320,12 @@ final class FlowCryptCoreTests: XCTestCase { // When do { - let encrypted = try core.encryptFile( + let encrypted = try await core.encryptFile( pubKeys: [k.public], fileData: fileData, name: initialFileName ) - _ = try self.core.decryptFile( + _ = try await core.decryptFile( encrypted: encrypted.encryptedFile, keys: [], msgPwd: nil @@ -354,12 +362,12 @@ final class FlowCryptCoreTests: XCTestCase { ] // When - let encrypted = try core.encryptFile( + let encrypted = try await core.encryptFile( pubKeys: [k.public], fileData: fileData, name: initialFileName ) - let decrypted = try self.core.decryptFile( + let decrypted = try await core.decryptFile( encrypted: encrypted.encryptedFile, keys: keys, msgPwd: nil @@ -370,12 +378,53 @@ final class FlowCryptCoreTests: XCTestCase { XCTAssertEqual(decrypted.content.count, fileData.count) } - func testException() throws { + func testException() async throws { do { - _ = try core.decryptKey(armoredPrv: "not really a key", passphrase: "whatnot") + _ = try await core.decryptKey(armoredPrv: "not really a key", passphrase: "whatnot") XCTFail("Should have thrown above") } catch let CoreError.exception(message) { XCTAssertNotNil(message.range(of: "Error: Misformed armored text")) } } + + // this test is only meaningful on a real device + // it passes on simulator even if implementation is broken + // maybe there's a way to run simulator with more cores? (on a mac that has them) which would simulate real device better + func testCoreResponseCorrectnessUnderConcurrency() async throws { + // given: a bunch of keys + let pp = "this particular pass phrase is long enough" + let k0 = try await core.generateKey(passphrase: pp, variant: KeyVariant.curve25519, userIds: [UserId(email: "k0@concurrent.test", name: "k0")]) + let k1 = try await core.generateKey(passphrase: pp, variant: KeyVariant.curve25519, userIds: [UserId(email: "k1@concurrent.test", name: "k1")]) + let k2 = try await core.generateKey(passphrase: pp, variant: KeyVariant.curve25519, userIds: [UserId(email: "k2@concurrent.test", name: "k2")]) + let k3 = try await core.generateKey(passphrase: pp, variant: KeyVariant.curve25519, userIds: [UserId(email: "k3@concurrent.test", name: "k3")]) + let k4 = try await core.generateKey(passphrase: pp, variant: KeyVariant.curve25519, userIds: [UserId(email: "k4@concurrent.test", name: "k4")]) + // when: keys are parsed concurrently + async let p0prv = try await core.parseKeys(armoredOrBinary: k0.key.private!.data()) + async let p0pub = try await core.parseKeys(armoredOrBinary: k0.key.public.data()) + async let p1prv = try await core.parseKeys(armoredOrBinary: k1.key.private!.data()) + async let p1pub = try await core.parseKeys(armoredOrBinary: k1.key.public.data()) + async let p2prv = try await core.parseKeys(armoredOrBinary: k2.key.private!.data()) + async let p2pub = try await core.parseKeys(armoredOrBinary: k2.key.public.data()) + async let p3prv = try await core.parseKeys(armoredOrBinary: k3.key.private!.data()) + async let p3pub = try await core.parseKeys(armoredOrBinary: k3.key.public.data()) + async let p4prv = try await core.parseKeys(armoredOrBinary: k4.key.private!.data()) + async let p4pub = try await core.parseKeys(armoredOrBinary: k4.key.public.data()) + let prvs = try await [p0prv, p1prv, p2prv, p3prv, p4prv] + let pubs = try await [p0pub, p1pub, p2pub, p3pub, p4pub] + // then: parse results are not mixed up + for (i, parsed) in prvs.enumerated() { + XCTAssertEqual( + parsed.keyDetails.first!.pgpUserEmails.first!, + "k\(i)@concurrent.test" + ) + XCTAssertNotNil(parsed.keyDetails.first?.private) + } + for (i, parsed) in pubs.enumerated() { + XCTAssertEqual( + parsed.keyDetails.first!.pgpUserEmails.first!, + "k\(i)@concurrent.test" + ) + XCTAssertNil(parsed.keyDetails.first?.private) + } + } } diff --git a/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift b/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift index db22e5ae3..904dd201d 100644 --- a/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift +++ b/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift @@ -20,14 +20,14 @@ class KeyMethodsTest: XCTestCase { sut = KeyMethods(decrypter: decrypter) } - func testEmptyParsingKey() { + func testEmptyParsingKey() async throws { let emptyKeys: [KeyDetails] = [] - let result = sut.filterByPassPhraseMatch(keys: emptyKeys, passPhrase: passPhrase) + let result = try await sut.filterByPassPhraseMatch(keys: emptyKeys, passPhrase: passPhrase) XCTAssertTrue(result.isEmpty) } - func testNoPrivateKey() { + func testPassPublicKeyWhenExpectingPrivateForPassPhraseMatch() async throws { // private part = nil let keys = [ KeyDetails( @@ -61,20 +61,23 @@ class KeyMethodsTest: XCTestCase { revoked: false ) ] - let result = sut.filterByPassPhraseMatch(keys: keys, passPhrase: passPhrase) - - XCTAssertTrue(result.isEmpty) + do { + try await sut.filterByPassPhraseMatch(keys: keys, passPhrase: passPhrase) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? KeyServiceError, KeyServiceError.expectedPrivateGotPublic) + } } - func testCantDecryptKey() { + func testCantDecryptKey() async throws { decrypter.result = .failure(.some) - let result = sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) + let result = try await sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) XCTAssertTrue(result.isEmpty) } - func testSuccessDecryption() { + func testSuccessDecryption() async throws { decrypter.result = .success(CoreRes.DecryptKey(decryptedKey: "some key")) - let result = sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) + let result = try await sut.filterByPassPhraseMatch(keys: validKeys, passPhrase: passPhrase) XCTAssertTrue(result.isNotEmpty) } } diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index 691f5349b..606882da7 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -37,292 +37,289 @@ class ComposeMessageServiceTests: XCTestCase { core: core ) - core.parseKeysResult = { _ in - CoreRes.ParseKeys(format: .armored, keyDetails: [self.validKeyDetails]) + core.parseKeysResult = { data in + guard !data.isEmpty else { + return CoreRes.ParseKeys(format: .unknown, keyDetails: []) + } + return CoreRes.ParseKeys(format: .armored, keyDetails: [self.validKeyDetails]) } } - func testValidateMessageInputWithEmptyRecipients() { - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "", - recipients: [], - subject: nil - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.emptyRecipient)) + func testValidateMessageInputWithEmptyRecipients() async throws { + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: [], + subject: nil + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptyRecipient) + } } - func testValidateMessageInputWithWhitespaceRecipients() { + func testValidateMessageInputWithWhitespaceRecipients() async { let recipients: [ComposeMessageRecipient] = [ ComposeMessageRecipient(email: " ", state: recipientIdleState), ComposeMessageRecipient(email: " ", state: recipientIdleState), ComposeMessageRecipient(email: "sdfff", state: recipientIdleState) ] - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "", - recipients: recipients, - subject: nil - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.emptyRecipient)) + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: nil + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptyRecipient) + } } - func testValidateMessageInputWithEmptySubject() { - func test() { - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.emptySubject)) + func testValidateMessageInputWithEmptySubject() async { + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: nil + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptySubject) + } + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: "" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptySubject) + } + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: " " + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptySubject) } - - var result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "", - recipients: recipients, - subject: nil - ), - email: "some@gmail.com", - signingPrv: nil - ) - - test() - - result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "", - recipients: recipients, - subject: "" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - test() - - result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "", - recipients: recipients, - subject: " " - ), - email: "some@gmail.com", - signingPrv: nil - ) } - func testValidateMessageInputWithEmptyMessage() { - func test() { - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.emptyMessage)) + func testValidateMessageInputWithEmptyMessage() async { + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: nil, + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptyMessage) + } + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptyMessage) + } + do { + try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: " ", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptyMessage) } - - var result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: nil, - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - test() - - result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - test() - - result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: " ", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - test() } - func testValidateMessageInputWithEmptyPublicKey() { + func testValidateMessageInputWithEmptyPublicKey() async { keyStorage.publicKeyResult = { nil } - - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "some message", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.missedPublicKey)) + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.missedPublicKey) + } } - func testValidateMessageInputWithAllEmptyRecipientPubKeys() { + func testValidateMessageInputWithAllEmptyRecipientPubKeys() async { keyStorage.publicKeyResult = { "public key" } - recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in [] } } - - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "some message", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.noPubRecipients)) + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.noPubRecipients) + } } - func testValidateMessageInputWithExpiredRecipientPubKey() { + func testValidateMessageInputWithExpiredRecipientPubKey() async { core.parseKeysResult = { _ in let keyDetails = KeyStorageMock.createFakeKeyDetails(expiration: Int(Date().timeIntervalSince1970 - 60)) return CoreRes.ParseKeys(format: .armored, keyDetails: [keyDetails]) } - keyStorage.publicKeyResult = { "public key" } - recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in ["pubKey"] } } - - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "some message", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.expiredKeyRecipients)) + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.expiredKeyRecipients) + } } - func testValidateMessageInputWithRevokedRecipientPubKey() { + func testValidateMessageInputWithRevokedRecipientPubKey() async { core.parseKeysResult = { _ in let keyDetails = KeyStorageMock.createFakeKeyDetails(expiration: nil, revoked: true) return CoreRes.ParseKeys(format: .armored, keyDetails: [keyDetails]) } - keyStorage.publicKeyResult = { "public key" } - recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in ["pubKey"] } } - - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "some message", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.revokedKeyRecipients)) + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.revokedKeyRecipients) + } } - func testValidateMessageInputWithValidAndInvalidRecipientPubKeys() { + func testValidateMessageInputWithValidAndInvalidRecipientPubKeys() async throws { core.parseKeysResult = { data in - let pubKey = data.toStr() - let isRevoked = pubKey == "revoked" - let expiration: Int? = pubKey == "expired" ? Int(Date().timeIntervalSince1970 - 60) : nil - let keyDetails = KeyStorageMock.createFakeKeyDetails(pub: pubKey, - expiration: expiration, - revoked: isRevoked) - return CoreRes.ParseKeys(format: .armored, keyDetails: [keyDetails]) + var allKeyDetails: [KeyDetails] = [] + let pubKeys = data.toStr() + .split(separator: "\n") + .map { String($0) } + for pubKey in pubKeys { + let isRevoked = pubKey == "revoked" + let expiration: Int? = pubKey == "expired" ? Int(Date().timeIntervalSince1970 - 60) : nil + allKeyDetails.append(KeyStorageMock.createFakeKeyDetails( + pub: pubKey, + expiration: expiration, + revoked: isRevoked + )) + } + return CoreRes.ParseKeys(format: .armored, keyDetails: allKeyDetails) } - keyStorage.publicKeyResult = { "public key" } - recipients.forEach { recipient in contactsService.retrievePubKeysResult = { _ in ["revoked", "expired", "valid"] } } - let message = "some message" let subject = "Some subject" let email = "some@gmail.com" let input = ComposeMessageInput(type: .idle) - let result = try? sut.validateMessage( + let result = try await sut.validateAndProduceSendableMsg( input: input, contextToSend: ComposeMessageContext( message: message, @@ -331,7 +328,7 @@ class ComposeMessageServiceTests: XCTestCase { ), email: email, signingPrv: nil - ).get() + ) let expected = SendableMsg( text: message, @@ -343,18 +340,18 @@ class ComposeMessageServiceTests: XCTestCase { replyToMimeMsg: nil, atts: [], pubKeys: [ + "public key", "valid", "valid", - "valid", - "public key" + "valid" ], signingPrv: nil) XCTAssertNotNil(result) - XCTAssertEqual(result!, expected) + XCTAssertEqual(result, expected) } - func testValidateMessageInputWithoutOneRecipientPubKey() { + func testValidateMessageInputWithoutOneRecipientPubKey() async throws { keyStorage.publicKeyResult = { "public key" } @@ -369,40 +366,38 @@ class ComposeMessageServiceTests: XCTestCase { } } - let result = sut.validateMessage( - input: ComposeMessageInput(type: .idle), - contextToSend: ComposeMessageContext( - message: "some message", - recipients: recipients, - subject: "Some subject" - ), - email: "some@gmail.com", - signingPrv: nil - ) - - var thrownError: Error? - XCTAssertThrowsError(try result.get()) { thrownError = $0 } - let error = expectComposeMessageError(for: thrownError) - XCTAssertEqual(error, .validationError(.noPubRecipients)) + do { + _ = try await sut.validateAndProduceSendableMsg( + input: ComposeMessageInput(type: .idle), + contextToSend: ComposeMessageContext( + message: "some message", + recipients: recipients, + subject: "Some subject" + ), + email: "some@gmail.com", + signingPrv: nil + ) + XCTFail("expected to throw above") + } catch { + XCTAssertEqual(error as? MessageValidationError, MessageValidationError.noPubRecipients) + } } - func testSuccessfulMessageValidation() { + func testSuccessfulMessageValidation() async throws { keyStorage.publicKeyResult = { "public key" } - recipients.enumerated().forEach { element, index in contactsService.retrievePubKeysResult = { recipient in ["pubKey"] } } - let message = "some message" let subject = "Some subject" let email = "some@gmail.com" let input = ComposeMessageInput(type: .idle) - let result = try? sut.validateMessage( + let result = try await sut.validateAndProduceSendableMsg( input: input, contextToSend: ComposeMessageContext( message: message, @@ -411,7 +406,7 @@ class ComposeMessageServiceTests: XCTestCase { ), email: email, signingPrv: nil - ).get() + ) let expected = SendableMsg( text: message, @@ -423,22 +418,14 @@ class ComposeMessageServiceTests: XCTestCase { replyToMimeMsg: nil, atts: [], pubKeys: [ + "public key", "pubKey", "pubKey", - "pubKey", - "public key" + "pubKey" ], signingPrv: nil) XCTAssertNotNil(result) - XCTAssertEqual(result!, expected) - } - - private func expectComposeMessageError(for thrownError: Error?) -> ComposeMessageError { - if let thrownError = thrownError as? ComposeMessageError { return thrownError - } else { - XCTFail() - return ComposeMessageError.validationError(.internalError("")) - } + XCTAssertEqual(result, expected) } } diff --git a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift index 091dbc784..6e5db9971 100644 --- a/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/Key Services/KeyServiceTests.swift @@ -21,15 +21,15 @@ final class KeyServiceTests: XCTestCase { wait(for: [expectation], timeout: 10) } - func testGetSigningKeyFirstEmail() throws { + func testGetSigningKeyFirstEmail() async throws { // arrange let userObject = UserObject(name: "Bill", email: "bill@test.com", imap: nil, smtp: nil) - guard let key1 = try Core.shared.parseKeys(armoredOrBinary: Self.privateKey1.data()).keyDetails.first else { + guard let key1 = try await Core.shared.parseKeys(armoredOrBinary: Self.privateKey1.data()).keyDetails.first else { XCTFail("key details expected") return } - guard let key2 = try Core.shared.parseKeys(armoredOrBinary: Self.privateKey2.data()).keyDetails.first else { + guard let key2 = try await Core.shared.parseKeys(armoredOrBinary: Self.privateKey2.data()).keyDetails.first else { XCTFail("key details expected") return } @@ -45,7 +45,7 @@ final class KeyServiceTests: XCTestCase { passPhraseService.passPhrases = [ PassPhrase( value: "this is a test phrase", - fingerprints: ["4D5BFAD925F6ED3A43002B21127071C29744D9AC"], + fingerprintsOfAssociatedKey: ["4D5BFAD925F6ED3A43002B21127071C29744D9AC"], date: nil ) ] @@ -57,18 +57,18 @@ final class KeyServiceTests: XCTestCase { ) // act - let result = try keyService.getSigningKey() + let result = try await keyService.getSigningKey() // assert XCTAssertEqual(result?.private, Self.privateKey2) XCTAssertEqual(result?.passphrase, "this is a test phrase") } - func testGetSigningKeyNotFirstEmail() throws { + func testGetSigningKeyNotFirstEmail() async throws { // arrange let userObject = UserObject(name: "Bill", email: "bill@test.com", imap: nil, smtp: nil) - guard let key = try Core.shared.parseKeys(armoredOrBinary: Self.privateKey1.data()).keyDetails.first else { + guard let key = try await Core.shared.parseKeys(armoredOrBinary: Self.privateKey1.data()).keyDetails.first else { XCTFail("key details expected") return } @@ -86,7 +86,7 @@ final class KeyServiceTests: XCTestCase { ) // act - let result = try keyService.getSigningKey() + let result = try await keyService.getSigningKey() // assert XCTAssertEqual(result?.private, Self.privateKey1) diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/InMemoryPassPhraseStorageTest.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/InMemoryPassPhraseStorageTest.swift index 07fdccc66..080419036 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/InMemoryPassPhraseStorageTest.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/InMemoryPassPhraseStorageTest.swift @@ -24,7 +24,7 @@ class InMemoryPassPhraseStorageTest: XCTestCase { } func testSavePassPhraseUpdatesDate() { - let pass = PassPhrase(value: "A", fingerprints: ["11","12"]) + let pass = PassPhrase(value: "A", fingerprintsOfAssociatedKey: ["11","12"]) sut.save(passPhrase: pass) passPhraseProvider.passPhrases.forEach { XCTAssertNotNil($0.date) @@ -32,7 +32,7 @@ class InMemoryPassPhraseStorageTest: XCTestCase { } func testUpdatePassPhraseUpdatesDate() { - let pass = PassPhrase(value: "A", fingerprints: ["11","12"]) + let pass = PassPhrase(value: "A", fingerprintsOfAssociatedKey: ["11","12"]) sut.update(passPhrase: pass) passPhraseProvider.passPhrases.forEach { XCTAssertNotNil($0.date) @@ -40,7 +40,7 @@ class InMemoryPassPhraseStorageTest: XCTestCase { } func testRemovePassPhrase() { - let pass = PassPhrase(value: "A", fingerprints: ["11","12"]) + let pass = PassPhrase(value: "A", fingerprintsOfAssociatedKey: ["11","12"]) sut.save(passPhrase: pass) sut.remove(passPhrase: pass) XCTAssertTrue(passPhraseProvider.passPhrases.isEmpty) @@ -49,17 +49,17 @@ class InMemoryPassPhraseStorageTest: XCTestCase { func testGetPassPhrases() { XCTAssertTrue(sut.getPassPhrases().isEmpty) - let pass = PassPhrase(value: "A", fingerprints: ["11","12"]) + let pass = PassPhrase(value: "A", fingerprintsOfAssociatedKey: ["11","12"]) sut.save(passPhrase: pass) XCTAssertTrue(sut.getPassPhrases().count == 1) - XCTAssertTrue(sut.getPassPhrases().contains(where: { $0.primaryFingerprint == "11" })) + XCTAssertTrue(sut.getPassPhrases().contains(where: { $0.primaryFingerprintOfAssociatedKey == "11" })) XCTAssertTrue(sut.getPassPhrases().filter { $0.date == nil }.isEmpty) } func testExpiredPassPhrases() { XCTAssertTrue(sut.getPassPhrases().isEmpty) - let pass = PassPhrase(value: "A", fingerprints: ["11","12"]) + let pass = PassPhrase(value: "A", fingerprintsOfAssociatedKey: ["11","12"]) sut.save(passPhrase: pass) sleep(3) XCTAssertTrue(sut.getPassPhrases().isEmpty) diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift index 8363ca5ef..7deb693bc 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageMock.swift @@ -28,8 +28,8 @@ class PassPhraseStorageMock: PassPhraseStorageType { var getPassPhrasesResult: () -> ([PassPhrase]) = { [ - PassPhrase(value: "a", fingerprints: ["11","12"]), - PassPhrase(value: "2", fingerprints: ["21","22"]) + PassPhrase(value: "a", fingerprintsOfAssociatedKey: ["11","12"]), + PassPhrase(value: "2", fingerprintsOfAssociatedKey: ["21","22"]) ] } func getPassPhrases() -> [PassPhrase] { diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift index 24841461b..0caf5b4bb 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift @@ -42,11 +42,11 @@ class PassPhraseStorageTests: XCTestCase { func testGetValidPassPhraseFromStorage() { let passPhrase1 = PassPhrase( value: "some", - fingerprints: ["11","12"] + fingerprintsOfAssociatedKey: ["11","12"] ) let passPhrase2 = PassPhrase( value: "some", - fingerprints: ["21","22"] + fingerprintsOfAssociatedKey: ["21","22"] ) encryptedStorage.getPassPhrasesResult = { [passPhrase1] } @@ -72,7 +72,7 @@ class PassPhraseStorageTests: XCTestCase { let savedDate = Date() let localPassPhrase = PassPhrase( value: "value", - fingerprints: ["f1"], + fingerprintsOfAssociatedKey: ["f1"], date: savedDate ) inMemoryStorage.getPassPhrasesResult = { [localPassPhrase] } @@ -87,11 +87,11 @@ class PassPhraseStorageTests: XCTestCase { func testBothStorageContainsValidPassPhrase() { let passPhrase1 = PassPhrase( value: "some", - fingerprints: ["A123"] + fingerprintsOfAssociatedKey: ["A123"] ) let passPhrase2 = PassPhrase( value: "some", - fingerprints: ["A123"] + fingerprintsOfAssociatedKey: ["A123"] ) encryptedStorage.getPassPhrasesResult = { [passPhrase1, passPhrase2] @@ -100,7 +100,7 @@ class PassPhraseStorageTests: XCTestCase { let savedDate = Date() let localPassPhrase = PassPhrase( value: "value", - fingerprints: ["123444"], + fingerprintsOfAssociatedKey: ["123444"], date: savedDate ) @@ -111,7 +111,7 @@ class PassPhraseStorageTests: XCTestCase { } func testSavePassPhraseInPersistenStorage() { - let passPhraseToSave = PassPhrase(value: "pass", fingerprints: ["fingerprint 1", "123333"]) + let passPhraseToSave = PassPhrase(value: "pass", fingerprintsOfAssociatedKey: ["fingerprint 1", "123333"]) let expectation = XCTestExpectation() expectation.expectedFulfillmentCount = 1 @@ -120,13 +120,13 @@ class PassPhraseStorageTests: XCTestCase { // encrypted storage contains pass phrase which should be saved locally encryptedStorage.getPassPhrasesResult = { [ - PassPhrase(value: "pass", fingerprints: ["fingerprint 1", "adfnhjfg"]) + PassPhrase(value: "pass", fingerprintsOfAssociatedKey: ["fingerprint 1", "adfnhjfg"]) ] } // encrypted storage should not contains pass phrase which user decide to save locally encryptedStorage.isRemovePassPhraseResult = { passPhraseToRemove in - if passPhraseToRemove.primaryFingerprint == "fingerprint 1" { + if passPhraseToRemove.primaryFingerprintOfAssociatedKey == "fingerprint 1" { expectation.fulfill() } } @@ -139,7 +139,7 @@ class PassPhraseStorageTests: XCTestCase { } func testSavePassPhraseInPersistentStorageWithoutAnyPassPhrases() { - let passPhraseToSave = PassPhrase(value: "pass", fingerprints: ["fingerprint 1", "123333"]) + let passPhraseToSave = PassPhrase(value: "pass", fingerprintsOfAssociatedKey: ["fingerprint 1", "123333"]) let expectation = XCTestExpectation() expectation.isInverted = true diff --git a/FlowCryptAppTests/LocalStorageTests.swift b/FlowCryptAppTests/LocalStorageTests.swift index bb275c69f..03a71e503 100644 --- a/FlowCryptAppTests/LocalStorageTests.swift +++ b/FlowCryptAppTests/LocalStorageTests.swift @@ -15,7 +15,7 @@ class LocalStorageTests: XCTestCase { override func setUp() { sut = LocalStorage() - let passPhrase = PassPhrase(value: "123", fingerprints: ["123"], date: nil) + let passPhrase = PassPhrase(value: "123", fingerprintsOfAssociatedKey: ["123"], date: nil) sut.passPhraseStorage.save(passPhrase: passPhrase) } diff --git a/FlowCryptAppTests/Mocks/PassPhraseServiceMock.swift b/FlowCryptAppTests/Mocks/PassPhraseServiceMock.swift index 944e5b7e7..7fd07fd4c 100644 --- a/FlowCryptAppTests/Mocks/PassPhraseServiceMock.swift +++ b/FlowCryptAppTests/Mocks/PassPhraseServiceMock.swift @@ -10,6 +10,9 @@ final class PassPhraseServiceMock: PassPhraseServiceType { + func savePassPhrasesInMemory(_ passPhrase: String, for privateKeys: [PrvKeyInfo]) { + } + var passPhrases: [PassPhrase] = [] func getPassPhrases() -> [PassPhrase] { diff --git a/FlowCryptCommon/Logger.swift b/FlowCryptCommon/Logger.swift index e20059230..3cee735bf 100644 --- a/FlowCryptCommon/Logger.swift +++ b/FlowCryptCommon/Logger.swift @@ -68,10 +68,10 @@ public struct Logger { var label: String { switch self { case .verbose: return "🏷" - case .info: return "â„šī¸" case .debug: return "âš™ī¸" - case .error: return "â—ī¸" - case .warning: return "đŸ”Ĩ" + case .info: return "â„šī¸" + case .warning: return "â—ī¸" + case .error: return "đŸ”Ĩ" } } } @@ -92,7 +92,7 @@ public struct Logger { private func log( _ level: Logger.Level, - _ message: @autoclosure () -> String, + _ message: String, file: String = #file, function: String = #function, line: UInt = #line @@ -129,7 +129,7 @@ public struct Logger { messageToPrint.append(" ") // "â„šī¸[App Start][GlobalRouter-proceed-56][11:25:02] Some message goes here" - messageToPrint.append(message()) + messageToPrint.append(message) debugPrint(messageToPrint) } diff --git a/appium/package.json b/appium/package.json index 9e8257cc7..dfe3a92db 100644 --- a/appium/package.json +++ b/appium/package.json @@ -20,7 +20,7 @@ "@babel/register": "^7.10.4", "@babel/traverse": "^7.10.4", "@babel/types": "^7.10.4", - "@wdio/allure-reporter": "7.0.0", + "@wdio/allure-reporter": "6.10.6", "@wdio/appium-service": "6.10.11", "@wdio/cli": "6.10.11", "@wdio/jasmine-framework": "6.10.11", @@ -36,7 +36,7 @@ "eslint-plugin-wdio": "^6.6.0", "node-fetch": "^2.6.1", "node-gyp": "^8.3.0", - "ts-node": "^10.4.0", + "ts-node": "^9.1.1", "typescript": "^4.1.3", "webdriverio": "6.10.11", "appium": "1.22.0"