diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 839d836d0..acd14dfb8 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 2C141B2F274578C20038A3F8 /* KeyInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C141B2E274578C20038A3F8 /* KeyInfo.swift */; }; 2C2A3B4B2719EE6100B7F27B /* KeyServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */; }; 2C2A3B4D2719EF7300B7F27B /* PassPhraseServiceMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */; }; + 2C339B07275CB136005DEA79 /* FatalErrorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C339B06275CB136005DEA79 /* FatalErrorViewController.swift */; }; 2C4E60F72757D91A00DE5770 /* EncryptedStorageMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C4E60F62757D91A00DE5770 /* EncryptedStorageMock.swift */; }; 2C60AB0C272564D40040D7F2 /* InvalidStorageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */; }; 2CAF25322756C37E005C7C7C /* AppContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2CAF25312756C37E005C7C7C /* AppContext.swift */; }; @@ -301,7 +302,6 @@ D2717752242567EB00BDA9A9 /* KeyTextCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5ADEDCC123A43C6800EC495E /* KeyTextCellNode.swift */; }; D2717753242568A600BDA9A9 /* NavigationBarItemsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD22A19230FD781005067A6 /* NavigationBarItemsView.swift */; }; D2717754242568A600BDA9A9 /* NavigationBarActionButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FD22A1E230FEFC6005067A6 /* NavigationBarActionButton.swift */; }; - D274724124F97C5C006BA6EF /* EncryptedCacheService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274724024F97C5C006BA6EF /* EncryptedCacheService.swift */; }; D274724424FD1932006BA6EF /* FolderRealmObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D274724324FD1932006BA6EF /* FolderRealmObject.swift */; }; D27B911924EFE79F002DF0A1 /* LocalContactsProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B911824EFE79F002DF0A1 /* LocalContactsProvider.swift */; }; D27B911D24EFE806002DF0A1 /* RecipientRealmObject.swift in Sources */ = {isa = PBXBuildFile; fileRef = D27B911C24EFE806002DF0A1 /* RecipientRealmObject.swift */; }; @@ -451,6 +451,7 @@ 2C141B2E274578C20038A3F8 /* KeyInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyInfo.swift; sourceTree = ""; }; 2C2A3B4A2719EE6100B7F27B /* KeyServiceTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyServiceTests.swift; sourceTree = ""; }; 2C2A3B4C2719EF7300B7F27B /* PassPhraseServiceMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PassPhraseServiceMock.swift; sourceTree = ""; }; + 2C339B06275CB136005DEA79 /* FatalErrorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FatalErrorViewController.swift; sourceTree = ""; }; 2C4E60F62757D91A00DE5770 /* EncryptedStorageMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedStorageMock.swift; sourceTree = ""; }; 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InvalidStorageViewController.swift; sourceTree = ""; }; 2CAF25312756C37E005C7C7C /* AppContext.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppContext.swift; sourceTree = ""; }; @@ -732,7 +733,6 @@ D26F132624509EB6009175BA /* RecipientEmailsCellNodeInput.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientEmailsCellNodeInput.swift; sourceTree = ""; }; D27177482424D73000BDA9A9 /* InboxViewDecorator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InboxViewDecorator.swift; sourceTree = ""; }; D271774D24255F1100BDA9A9 /* UIColorExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIColorExtensions.swift; sourceTree = ""; }; - D274724024F97C5C006BA6EF /* EncryptedCacheService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncryptedCacheService.swift; sourceTree = ""; }; D274724324FD1932006BA6EF /* FolderRealmObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderRealmObject.swift; sourceTree = ""; }; D27B911824EFE79F002DF0A1 /* LocalContactsProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalContactsProvider.swift; sourceTree = ""; }; D27B911C24EFE806002DF0A1 /* RecipientRealmObject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientRealmObject.swift; sourceTree = ""; }; @@ -1127,7 +1127,6 @@ 9F41FA1C25372C2D003B970D /* Backup Services */, 21489B81267CC3BC00BDE4AC /* Client Configuration Service */, 9F6F3BEB26ADF5DE005BD9C6 /* Compose Message Service */, - D274724024F97C5C006BA6EF /* EncryptedCacheService.swift */, D227C0E4250538190070F805 /* Folders Services */, C132B9CA1EC2DE6400763715 /* GeneralConstants.swift */, 9F31AB9F232C071700CF87EA /* GlobalRouter.swift */, @@ -1959,6 +1958,7 @@ isa = PBXGroup; children = ( D24F4C2123E2359B00C5EEE4 /* BootstrapViewController.swift */, + 2C339B06275CB136005DEA79 /* FatalErrorViewController.swift */, 2C60AB0B272564D40040D7F2 /* InvalidStorageViewController.swift */, ); path = Bootstrap; @@ -2537,7 +2537,6 @@ 9F5F504326FA6C7500294FA2 /* EnterpriseServerApiMock.swift in Sources */, 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, - 9F6F3C7626ADFC37005BD9C6 /* KeyStorageMock.swift in Sources */, 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */, 9FC41171268118A7004C0A69 /* PassPhraseStorageTests.swift in Sources */, ); @@ -2551,6 +2550,7 @@ D2A9CA3D242619EC00E1D898 /* SignInViewDecorator.swift in Sources */, 9F4300CC2571045B00791CFB /* InboxViewControllerContainerDecorator.swift in Sources */, 9F4163E6266520B600106194 /* CommonNodesInputs.swift in Sources */, + 2C339B07275CB136005DEA79 /* FatalErrorViewController.swift in Sources */, 9F883912271F242900669B56 /* ThreadDetailsDecorator.swift in Sources */, 04B472951ECE29F600B8266F /* MyMenuViewController.swift in Sources */, 51B4AE5327144E590001F33B /* PubKey.swift in Sources */, @@ -2667,7 +2667,6 @@ 9FC4112E2595EA8B001180A8 /* Gmail+Search.swift in Sources */, 5A948DC5239EF2F4006284D7 /* LegalViewController.swift in Sources */, 5133B6722716321F00C95463 /* ContactKeyDetailDecorator.swift in Sources */, - D274724124F97C5C006BA6EF /* EncryptedCacheService.swift in Sources */, A3B7C31923F576BA0022D628 /* AppStartup.swift in Sources */, 9F31AB8E23298BCF00CF87EA /* Imap+folders.swift in Sources */, D2891AC224C59EFA008918E3 /* KeyService.swift in Sources */, diff --git a/FlowCrypt/Controllers/Bootstrap/FatalErrorViewController.swift b/FlowCrypt/Controllers/Bootstrap/FatalErrorViewController.swift new file mode 100644 index 000000000..1535fea65 --- /dev/null +++ b/FlowCrypt/Controllers/Bootstrap/FatalErrorViewController.swift @@ -0,0 +1,96 @@ +// +// FatalErrorViewController.swift +// FlowCrypt +// +// Created by  Ivan Ushakov on 05.12.2021 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + +import FlowCryptUI +import AsyncDisplayKit + +final class FatalErrorViewController: TableNodeViewController { + private enum Parts: Int, CaseIterable { + case screenTitle + case title + case description + } + + private let error: Error + + init(error: Error) { + self.error = error + super.init(node: TableNode()) + } + + @available(*, unavailable) + required init?(coder _: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .backgroundColor + + node.delegate = self + node.dataSource = self + node.bounces = false + node.reloadData() + } +} + +extension FatalErrorViewController: ASTableDelegate, ASTableDataSource { + func tableNode(_: ASTableNode, numberOfRowsInSection _: Int) -> Int { + Parts.allCases.count + } + + func tableNode(_ node: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { + return { [weak self] in + guard let self = self, let part = Parts(rawValue: indexPath.row) else { + return ASCellNode() + } + + let insets = UIEdgeInsets(top: 8, left: 16, bottom: 8, right: 16) + switch part { + case .screenTitle: + return SetupTitleNode( + SetupTitleNode.Input( + title: "fatal_error_screen_title".localized + .attributed( + .bold(18), + color: .mainTextColor, + alignment: .center + ), + insets: insets, + backgroundColor: .backgroundColor + ) + ) + case .title: + return SetupTitleNode( + SetupTitleNode.Input( + title: "fatal_error_screen_text".localized + .attributed( + .regular(16), + color: .mainTextColor, + alignment: .center + ), + insets: insets, + backgroundColor: .backgroundColor + ) + ) + case .description: + return SetupTitleNode( + SetupTitleNode.Input( + title: self.error.localizedDescription.attributed( + .regular(16), + color: .mainTextColor, + alignment: .center + ), + insets: insets, + backgroundColor: .backgroundColor + ) + ) + } + } + } +} diff --git a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift index 3439f317b..2117ce76d 100644 --- a/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift +++ b/FlowCrypt/Controllers/CheckMailAuth/CheckMailAuthViewController.swift @@ -85,7 +85,12 @@ extension CheckMailAuthViewController { case 2: return ButtonCellNode(input: .signInAgain) { [weak self] in guard let self = self else { return } - self.appContext.globalRouter.signIn(appContext: self.appContext, route: .gmailLogin(self)) + Task { + await self.appContext.globalRouter.signIn( + appContext: self.appContext, + route: .gmailLogin(self) + ) + } } default: return ASCellNode() diff --git a/FlowCrypt/Controllers/Compose/ComposeViewController.swift b/FlowCrypt/Controllers/Compose/ComposeViewController.swift index fadcd4594..7cb835ec1 100644 --- a/FlowCrypt/Controllers/Compose/ComposeViewController.swift +++ b/FlowCrypt/Controllers/Compose/ComposeViewController.swift @@ -446,7 +446,7 @@ extension ComposeViewController { } let matchingKeys = try await self.keyMethods.filterByPassPhraseMatch(keys: allKeys, passPhrase: passPhrase) // save passphrase for all matching keys - appContext.passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) + try appContext.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 diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift index 670c6605c..2ca435464 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactDetailViewController.swift @@ -91,7 +91,11 @@ extension ContactDetailViewController { recipient.remove(pubKey: keyToRemove) if let fingerprint = keyToRemove.fingerprint, fingerprint.isNotEmpty { - contactsProvider.removePubKey(with: fingerprint, for: recipient.email) + do { + try contactsProvider.removePubKey(with: fingerprint, for: recipient.email) + } catch { + showToast("contact_detail_remove_public_key_error".localizeWithArguments(error.localizedDescription)) + } } node.deleteRows(at: [indexPathToRemove], with: .left) } diff --git a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift index 5a51f1aee..ddd652ca5 100644 --- a/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift +++ b/FlowCrypt/Controllers/Settings/Contacts/Contacts List/ContactsListViewController.swift @@ -69,10 +69,9 @@ extension ContactsListViewController { self.recipients = try await localContactsProvider.getAllRecipients() await self.node.reloadData() } catch { - self.showToast("Failed to load recipients: \(error.localizedDescription)") + self.showToast("contacts_screen_load_error".localizeWithArguments(error.localizedDescription)) } } - } } @@ -136,8 +135,12 @@ extension ContactsListViewController { recipientToRemove = recipients[indexPath.row] } - localContactsProvider.remove(recipient: recipientToRemove) - recipients.remove(at: indexPathToRemove.row) - node.deleteRows(at: [indexPathToRemove], with: .left) + do { + try localContactsProvider.remove(recipient: recipientToRemove) + recipients.remove(at: indexPathToRemove.row) + node.deleteRows(at: [indexPathToRemove], with: .left) + } catch { + showToast("contacts_screen_remove_error".localizeWithArguments(error.localizedDescription)) + } } } diff --git a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift index 69f750e67..8dbe56ab4 100644 --- a/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupBackupsViewController.swift @@ -140,10 +140,10 @@ extension SetupBackupsViewController { if storageMethod == .memory { for backup in matchingKeyBackups { let pp = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: backup.fingerprints) - appContext.passPhraseService.savePassPhrase(with: pp, storageMethod: storageMethod) + try appContext.passPhraseService.savePassPhrase(with: pp, storageMethod: storageMethod) } } - appContext.encryptedStorage.putKeypairs( + try appContext.encryptedStorage.putKeypairs( keyDetails: Array(matchingKeyBackups), passPhrase: storageMethod == .persistent ? passPhrase : nil, source: .backup, diff --git a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift index 035a5ddc5..1c5caf1df 100644 --- a/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupEKMKeyViewController.swift @@ -92,7 +92,7 @@ extension SetupEKMKeyViewController { passphrase: passPhrase ) let parsedKey = try await self.core.parseKeys(armoredOrBinary: encryptedPrv.encryptedKey.data()) - appContext.encryptedStorage.putKeypairs( + try appContext.encryptedStorage.putKeypairs( keyDetails: parsedKey.keyDetails, passPhrase: self.storageMethod == .persistent ? passPhrase : nil, source: .ekm, @@ -102,7 +102,7 @@ extension SetupEKMKeyViewController { } if self.storageMethod == .memory { let passPhrase = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: allFingerprints.unique()) - appContext.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: self.storageMethod) + try appContext.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: self.storageMethod) } } } diff --git a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift index 95282f441..90b46fa9a 100644 --- a/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupGenerateKeyViewController.swift @@ -112,7 +112,7 @@ private actor Service { ) try await backupService.backupToInbox(keys: [encryptedPrv.key], for: user) - appContext.encryptedStorage.putKeypairs( + try appContext.encryptedStorage.putKeypairs( keyDetails: [encryptedPrv.key], passPhrase: storageMethod == .persistent ? passPhrase: nil, source: .generated, @@ -124,7 +124,7 @@ private actor Service { value: passPhrase, fingerprintsOfAssociatedKey: encryptedPrv.key.fingerprints ) - appContext.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: .memory) + try appContext.passPhraseService.savePassPhrase(with: passPhrase, storageMethod: .memory) } await submitKeyToAttesterAndShowAlertOnFailure( diff --git a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift index ec10549f8..b4aa60d0b 100644 --- a/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift +++ b/FlowCrypt/Controllers/Setup/SetupManuallyEnterPassPhraseViewController.swift @@ -228,20 +228,20 @@ extension SetupManuallyEnterPassPhraseViewController { return } let keyDetails = try await appContext.keyService.getPrvKeyDetails() - importKeys(with: keyDetails, and: passPhrase) + try importKeys(with: keyDetails, and: passPhrase) } - private func importKeys(with existedKeys: [KeyDetails], and passPhrase: String) { + private func importKeys(with existedKeys: [KeyDetails], and passPhrase: String) throws { let keysToUpdate = Array(Set(existedKeys).intersection(fetchedKeys)) let newKeysToAdd = Array(Set(fetchedKeys).subtracting(existedKeys)) - appContext.encryptedStorage.putKeypairs( + try appContext.encryptedStorage.putKeypairs( keyDetails: newKeysToAdd, passPhrase: passPhrase, source: .imported, for: email ) - appContext.encryptedStorage.putKeypairs( + try appContext.encryptedStorage.putKeypairs( keyDetails: keysToUpdate, passPhrase: passPhrase, source: .imported, @@ -249,20 +249,20 @@ extension SetupManuallyEnterPassPhraseViewController { ) if storageMethod == .memory { - keysToUpdate + try keysToUpdate .map { PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { - appContext.passPhraseService.updatePassPhrase(with: $0, storageMethod: storageMethod) + try appContext.passPhraseService.updatePassPhrase(with: $0, storageMethod: storageMethod) } - newKeysToAdd + try newKeysToAdd .map { PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: $0.fingerprints) } .forEach { - appContext.passPhraseService.savePassPhrase(with: $0, storageMethod: storageMethod) + try appContext.passPhraseService.savePassPhrase(with: $0, storageMethod: storageMethod) } } diff --git a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift index e947e535b..bb5809ff6 100644 --- a/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift +++ b/FlowCrypt/Controllers/SetupImap/SetupImapViewController.swift @@ -529,7 +529,9 @@ extension SetupImapViewController { private func handleSuccessfulConnection() { hideSpinner() - globalRouter.signIn(appContext: self.appContext, route: .other(.session(user))) + Task { + await globalRouter.signIn(appContext: self.appContext, route: .other(.session(user))) + } } private func checkCurrentUser() throws { diff --git a/FlowCrypt/Controllers/SignIn/SignInViewController.swift b/FlowCrypt/Controllers/SignIn/SignInViewController.swift index 92e5fd551..9ceba711f 100644 --- a/FlowCrypt/Controllers/SignIn/SignInViewController.swift +++ b/FlowCrypt/Controllers/SignIn/SignInViewController.swift @@ -119,7 +119,9 @@ extension SignInViewController: ASTableDelegate, ASTableDataSource { extension SignInViewController { private func signInWithGmail() { - globalRouter.signIn(appContext: appContext, route: .gmailLogin(self)) + Task { + await globalRouter.signIn(appContext: appContext, route: .gmailLogin(self)) + } } private func signInWithOutlook() { diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index c72b59f90..3a8be3e16 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -222,7 +222,7 @@ extension ThreadDetailsViewController { input[index].rawMessage = updatedMessage node.reloadSections(IndexSet(integer: index), with: .fade) } catch { - showToast("Could not mark message as read: \(error)") + showToast("message_mark_read_error".localizeWithArguments(error.localizedDescription)) } } } diff --git a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift index 91417d242..ad7a90967 100644 --- a/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift +++ b/FlowCrypt/Functionality/DataManager/Encrypted Storage/EncryptedStorage.swift @@ -10,16 +10,15 @@ import FlowCryptCommon import RealmSwift import UIKit -// swiftlint:disable force_try protocol EncryptedStorageType { var storage: Realm { get } var activeUser: User? { get } func getAllUsers() -> [User] - func saveActiveUser(with user: User) + func saveActiveUser(with user: User) throws func doesAnyKeypairExist(for email: String) -> Bool - func putKeypairs(keyDetails: [KeyDetails], passPhrase: String?, source: KeySource, for email: String) + func putKeypairs(keyDetails: [KeyDetails], passPhrase: String?, source: KeySource, for email: String) throws func getKeypairs(by email: String) -> [KeyInfo] func validate() throws @@ -159,28 +158,20 @@ extension EncryptedStorage { // MARK: - Keys extension EncryptedStorage { - func putKeypairs(keyDetails: [KeyDetails], passPhrase: String?, source: KeySource, for email: String) { + func putKeypairs(keyDetails: [KeyDetails], passPhrase: String?, source: KeySource, for email: String) throws { guard let user = getUserObject(for: email) else { logger.logError("Can't find user with given email to update keys. User should be already saved") return } - try! storage.write { + + try storage.write { for key in keyDetails { - storage.add(try! KeyInfoRealmObject(key, passphrase: passPhrase, source: source, user: user), update: .all) + let object = try KeyInfoRealmObject(key, passphrase: passPhrase, source: source, user: user) + storage.add(object, update: .all) } } } - func updateKeys(with primaryFingerprint: String, passphrase: String?) { - let keys = storage.objects(KeyInfoRealmObject.self).where { - $0.primaryFingerprint == primaryFingerprint - } - - try! storage.write { - keys.map { $0.passphrase = passphrase } - } - } - func getKeypairs(by email: String) -> [KeyInfo] { return storage.objects(KeyInfoRealmObject.self).where({ $0.account == email @@ -199,20 +190,30 @@ extension EncryptedStorage { $0.email == email }.first } + + private func updateKeys(with primaryFingerprint: String, passphrase: String?) throws { + let keys = storage.objects(KeyInfoRealmObject.self).where { + $0.primaryFingerprint == primaryFingerprint + } + + try storage.write { + keys.map { $0.passphrase = passphrase } + } + } } // MARK: - PassPhrase extension EncryptedStorage: PassPhraseStorageType { - func save(passPhrase: PassPhrase) { - updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: passPhrase.value) + func save(passPhrase: PassPhrase) throws { + try updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: passPhrase.value) } - func update(passPhrase: PassPhrase) { - updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: passPhrase.value) + func update(passPhrase: PassPhrase) throws { + try updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: passPhrase.value) } - func remove(passPhrase: PassPhrase) { - updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: nil) + func remove(passPhrase: PassPhrase) throws { + try updateKeys(with: passPhrase.primaryFingerprintOfAssociatedKey, passphrase: nil) } func getPassPhrases() -> [PassPhrase] { @@ -234,8 +235,8 @@ extension EncryptedStorage { storage.objects(UserRealmObject.self).map(User.init) } - func saveActiveUser(with user: User) { - try! storage.write { + func saveActiveUser(with user: User) throws { + try storage.write { // Mark all users as inactive storage.objects(UserRealmObject.self).forEach { $0.isActive = false diff --git a/FlowCrypt/Functionality/DataManager/SessionService.swift b/FlowCrypt/Functionality/DataManager/SessionService.swift index f4d6183cb..0a96cb123 100644 --- a/FlowCrypt/Functionality/DataManager/SessionService.swift +++ b/FlowCrypt/Functionality/DataManager/SessionService.swift @@ -10,10 +10,10 @@ import FlowCryptCommon import Foundation protocol SessionServiceType { - func startSessionFor(session: SessionType) - func switchActiveSessionFor(user: User) -> SessionType? - func startActiveSessionForNextUser() -> SessionType? - func cleanupSessions() + func startSessionFor(session: SessionType) throws + func switchActiveSessionFor(user: User) throws -> SessionType? + func startActiveSessionForNextUser() throws -> SessionType? + func cleanupSessions() throws func cleanup() } @@ -50,7 +50,7 @@ final class SessionService { extension SessionService: SessionServiceType { /// start session for a user, this method will log out current user if user was saved, save and start session for a new user - func startSessionFor(session: SessionType) { + func startSessionFor(session: SessionType) throws { switch session { case let .google(email, name, token): let user = User.googleUser( @@ -58,14 +58,14 @@ extension SessionService: SessionServiceType { email: email, token: token ) - encryptedStorage.saveActiveUser(with: user) + try encryptedStorage.saveActiveUser(with: user) case let .session(user): imap.setupSession() - encryptedStorage.saveActiveUser(with: user) + try encryptedStorage.saveActiveUser(with: user) } } - func startActiveSessionForNextUser() -> SessionType? { + func startActiveSessionForNextUser() throws -> SessionType? { guard let currentUser = dataService.currentUser else { return nil } @@ -75,12 +75,12 @@ extension SessionService: SessionServiceType { return nil } - let session = switchActiveSession(for: nextUser) + let session = try switchActiveSession(for: nextUser) return session } - func switchActiveSessionFor(user: User) -> SessionType? { + func switchActiveSessionFor(user: User) throws -> SessionType? { let currentUser = encryptedStorage .getAllUsers() .first(where: { $0.email == user.email }) @@ -90,11 +90,11 @@ extension SessionService: SessionServiceType { return nil } - return switchActiveSession(for: currentUser) + return try switchActiveSession(for: currentUser) } // todo - rename to "logOutUsersThatDontHaveAnyKeysSetUp" - func cleanupSessions() { + func cleanupSessions() throws { logger.logInfo("Clean up sessions") for user in encryptedStorage.getAllUsers() { if !encryptedStorage.doesAnyKeypairExist(for: user.email) { @@ -104,12 +104,12 @@ extension SessionService: SessionServiceType { } let users = encryptedStorage.getAllUsers() if !users.contains(where: { $0.isActive }), let user = users.first(where: { encryptedStorage.doesAnyKeypairExist(for: $0.email ) }) { - switchActiveSession(for: user) + try switchActiveSession(for: user) } } @discardableResult - private func switchActiveSession(for user: User) -> SessionType? { + private func switchActiveSession(for user: User) throws -> SessionType? { logger.logInfo("Try to switch session for \(user.email)") let sessionType: SessionType @@ -123,7 +123,7 @@ extension SessionService: SessionServiceType { return nil } - startSessionFor(session: sessionType) + try startSessionFor(session: sessionType) return sessionType } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 36af58f77..6809f9ba7 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -137,7 +137,7 @@ final class MessageService { keys: keysWithoutPassPhrases, passPhrase: passPhrase ) - passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) + try passPhraseService.savePassPhrasesInMemory(passPhrase, for: matchingKeys) return matchingKeys.isNotEmpty } diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift index 0141079fb..b3d21ba28 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/ClientConfigurationService.swift @@ -37,7 +37,7 @@ extension ClientConfigurationService: ClientConfigurationServiceType { // } do { let raw = try await server.getClientConfiguration(for: user.email) - local.save(for: user, raw: raw) + try local.save(for: user, raw: raw) return ClientConfiguration(raw: raw) } catch { guard let raw = local.load(for: user.email) else { diff --git a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift index f8dae16b7..2e8e6a66b 100644 --- a/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift +++ b/FlowCrypt/Functionality/Services/Client Configuration Service/LocalClientConfiguration.swift @@ -8,32 +8,46 @@ import Foundation import RealmSwift -import IDZSwiftCommonCrypto protocol LocalClientConfigurationType { func load(for user: String) -> RawClientConfiguration? - func remove(for user: String) - func save(for user: User, raw: RawClientConfiguration) + func remove(for user: String) throws + func save(for user: User, raw: RawClientConfiguration) throws } -struct LocalClientConfiguration { - let cache: EncryptedCacheService +final class LocalClientConfiguration { + private let encryptedStorage: EncryptedStorageType + + private var storage: Realm { + encryptedStorage.storage + } + init(encryptedStorage: EncryptedStorageType) { - self.cache = EncryptedCacheService(encryptedStorage: encryptedStorage) + self.encryptedStorage = encryptedStorage } } extension LocalClientConfiguration: LocalClientConfigurationType { func load(for userEmail: String) -> RawClientConfiguration? { - guard let foundLocal = cache.getAll(for: userEmail).first else { return nil } - return RawClientConfiguration(foundLocal) + return storage.objects(ClientConfigurationRealmObject.self).where { + $0.userEmail == userEmail + }.first.flatMap(RawClientConfiguration.init) } - func remove(for userEmail: String) { - cache.removeAll(for: userEmail) + func remove(for userEmail: String) throws { + let objects = storage.objects(ClientConfigurationRealmObject.self).where { + $0.userEmail == userEmail + } + + try storage.write { + storage.delete(objects) + } } - func save(for user: User, raw: RawClientConfiguration) { - cache.save(ClientConfigurationRealmObject(configuration: raw, user: user)) + func save(for user: User, raw: RawClientConfiguration) throws { + let object = ClientConfigurationRealmObject(configuration: raw, user: user) + try storage.write { + storage.add(object, update: .modified) + } } } diff --git a/FlowCrypt/Functionality/Services/EncryptedCacheService.swift b/FlowCrypt/Functionality/Services/EncryptedCacheService.swift deleted file mode 100644 index 38451ddda..000000000 --- a/FlowCrypt/Functionality/Services/EncryptedCacheService.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// CacheService.swift -// FlowCrypt -// -// Created by Anton Kharchevskyi on 28/08/2020. -// Copyright © 2017-present FlowCrypt a. s. All rights reserved. -// - -import Foundation -import RealmSwift - -// MARK: - CachedRealmObject -protocol CachedRealmObject: Object { - associatedtype Identifier: Equatable - var identifier: Identifier { get } - var activeUser: UserRealmObject? { get } -} - -// MARK: - Cache -final class EncryptedCacheService { - let encryptedStorage: EncryptedStorageType - var realm: Realm { encryptedStorage.storage } - - init(encryptedStorage: EncryptedStorageType) { - self.encryptedStorage = encryptedStorage - } - - func save(_ object: T) { - // todo - should throw instead, don't "try?" - try? realm.write { - realm.add(object, update: .modified) - } - } - - func remove(object: T, with identifier: T.Identifier) { - guard let objectToDelete = realm - .objects(T.self) - .first(where: { $0.identifier == identifier }) - else { return } - - // todo - should throw instead, don't "try?" - try? realm.write { - realm.delete(objectToDelete) - } - } - - func remove(objects: [T]) { - // todo - should throw instead, don't "try?" - try? realm.write { - realm.delete(objects) - } - } - - func removeAll(for userEmail: String) { - let allObjects = getAll(for: userEmail) - remove(objects: allObjects) - } - - func getAll(for userEmail: String) -> [T] { - return Array(realm.objects(T.self)) - .filter { $0.activeUser?.email == userEmail } - } -} diff --git a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift index 03ce97e65..e8c274a26 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/FoldersService.swift @@ -53,18 +53,22 @@ final class FoldersService: FoldersServiceType { let fetchedFolders = try await self.remoteFoldersProvider.fetchFolders() return try await withCheckedThrowingContinuation { continuation in DispatchQueue.main.async { - // TODO: - Ticket? - instead of removing all folders remove only - // those folders which are in DB and not in remoteFolders - self.localFoldersProvider.removeFolders(for: user.email) + do { + // TODO: - Ticket? - instead of removing all folders remove only + // those folders which are in DB and not in remoteFolders + try self.localFoldersProvider.removeFolders(for: user.email) - // save to Realm - self.localFoldersProvider.save(folders: fetchedFolders, for: user) + // save to Realm + try self.localFoldersProvider.save(folders: fetchedFolders, for: user) - // save trash folder path - self.saveTrashFolderPath(with: fetchedFolders.map(\.path)) + // save trash folder path + self.saveTrashFolderPath(with: fetchedFolders.map(\.path)) - // return folders - continuation.resume(returning: fetchedFolders.map(FolderViewModel.init)) + // return folders + continuation.resume(returning: fetchedFolders.map(FolderViewModel.init)) + } catch { + continuation.resume(throwing: error) + } } } } diff --git a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift index b24264c29..5d7f3284d 100644 --- a/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift +++ b/FlowCrypt/Functionality/Services/Folders Services/LocalFoldersProvider.swift @@ -11,27 +11,45 @@ import RealmSwift protocol LocalFoldersProviderType { func fetchFolders(for userEmail: String) -> [FolderViewModel] - func removeFolders(for userEmail: String) - func save(folders: [Folder], for user: User) + func removeFolders(for userEmail: String) throws + func save(folders: [Folder], for user: User) throws } -struct LocalFoldersProvider: LocalFoldersProviderType { - private let folderCache: EncryptedCacheService +final class LocalFoldersProvider { + private let encryptedStorage: EncryptedStorageType + + private var storage: Realm { + encryptedStorage.storage + } init(encryptedStorage: EncryptedStorageType) { - self.folderCache = EncryptedCacheService(encryptedStorage: encryptedStorage) + self.encryptedStorage = encryptedStorage } +} +extension LocalFoldersProvider: LocalFoldersProviderType { func fetchFolders(for userEmail: String) -> [FolderViewModel] { - return folderCache.getAll(for: userEmail).compactMap(FolderViewModel.init) + storage.objects(FolderRealmObject.self).where { + $0.user.email == userEmail + }.compactMap(FolderViewModel.init) } - func save(folders: [Folder], for user: User) { - folders.map { FolderRealmObject(folder: $0, user: user) } - .forEach(folderCache.save) + func save(folders: [Folder], for user: User) throws { + let objects = folders.map { FolderRealmObject(folder: $0, user: user) } + try storage.write { + objects.forEach { + storage.add($0, update: .modified) + } + } } - func removeFolders(for userEmail: String) { - folderCache.removeAll(for: userEmail) + func removeFolders(for userEmail: String) throws { + let objects = storage.objects(FolderRealmObject.self).where { + $0.user.email == userEmail + } + + try storage.write { + storage.delete(objects) + } } } diff --git a/FlowCrypt/Functionality/Services/GlobalRouter.swift b/FlowCrypt/Functionality/Services/GlobalRouter.swift index 0419095a5..0fe280d4e 100644 --- a/FlowCrypt/Functionality/Services/GlobalRouter.swift +++ b/FlowCrypt/Functionality/Services/GlobalRouter.swift @@ -12,7 +12,7 @@ import UIKit @MainActor protocol GlobalRouterType { func proceed() - func signIn(appContext: AppContext, route: GlobalRoutingType) + func signIn(appContext: AppContext, route: GlobalRoutingType) async func askForContactsPermission(for route: GlobalRoutingType, appContext: AppContext) async throws func switchActive(user: User, appContext: AppContext) func signOut(appContext: AppContext) @@ -58,41 +58,44 @@ extension GlobalRouter: GlobalRouterType { } } - func signIn(appContext: AppContext, route: GlobalRoutingType) { + func signIn(appContext: AppContext, route: GlobalRoutingType) async { logger.logInfo("Sign in with \(route)") - - switch route { - case .gmailLogin(let viewController): - Task { - do { - let googleService = GoogleUserService( - currentUserEmail: appContext.dataService.currentUser?.email, - appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate - ) - let session = try await googleService.signIn( - in: viewController, - scopes: GeneralConstants.Gmail.mailScope - ) - appContext.userAccountService.startSessionFor(session: session) - self.proceed(with: appContext.withSession(session)) - } catch { - self.handleGmailError(appContext: appContext, error, in: viewController) - } + do { + switch route { + case .gmailLogin(let viewController): + let googleService = GoogleUserService( + currentUserEmail: appContext.dataService.currentUser?.email, + appDelegateGoogleSessionContainer: UIApplication.shared.delegate as? AppDelegate + ) + let session = try await googleService.signIn( + in: viewController, + scopes: GeneralConstants.Gmail.mailScope + ) + try appContext.userAccountService.startSessionFor(session: session) + proceed(with: appContext.withSession(session)) + case .other(let session): + try appContext.userAccountService.startSessionFor(session: session) + proceed(with: appContext.withSession(session)) } - case .other(let session): - appContext.userAccountService.startSessionFor(session: session) - proceed(with: appContext.withSession(session)) + } catch { + logger.logError("Failed to sign in due to \(error.localizedDescription)") + handle(error: error, appContext: appContext) } } func signOut(appContext: AppContext) { - if let session = appContext.userAccountService.startActiveSessionForNextUser() { - logger.logInfo("Start session for another email user \(session)") - proceed(with: appContext.withSession(session)) - } else { - logger.logInfo("Sign out") - appContext.userAccountService.cleanup() - proceed() + do { + if let session = try appContext.userAccountService.startActiveSessionForNextUser() { + logger.logInfo("Start session for another email user \(session)") + proceed(with: appContext.withSession(session)) + } else { + logger.logInfo("Sign out") + appContext.userAccountService.cleanup() + proceed() + } + } catch { + logger.logError("Failed to sign out due to \(error.localizedDescription)") + handle(error: error, appContext: appContext) } } @@ -110,7 +113,7 @@ extension GlobalRouter: GlobalRouterType { in: viewController, scopes: GeneralConstants.Gmail.contactsScope ) - appContext.userAccountService.startSessionFor(session: session) + try appContext.userAccountService.startSessionFor(session: session) // todo? - no need to update context itself with new session? } catch { logger.logInfo("Contacts scope failed with error \(error.errorMessage)") @@ -123,11 +126,16 @@ extension GlobalRouter: GlobalRouterType { func switchActive(user: User, appContext: AppContext) { logger.logInfo("Switching active user \(user)") - guard let session = appContext.userAccountService.switchActiveSessionFor(user: user) else { - logger.logWarning("Can't switch active user with \(user.email)") - return + do { + guard let session = try appContext.userAccountService.switchActiveSessionFor(user: user) else { + logger.logWarning("Can't switch active user with \(user.email)") + return + } + proceed(with: appContext.withSession(session)) + } catch { + logger.logError("Failed to switch active user due to \(error.localizedDescription)") + handle(error: error, appContext: appContext) } - proceed(with: appContext.withSession(session)) } @MainActor @@ -149,13 +157,15 @@ extension GlobalRouter: GlobalRouterType { } @MainActor - private func handleGmailError(appContext: AppContext, _ error: Error, in viewController: UIViewController) { - logger.logInfo("gmail login failed with error \(error.errorMessage)") + private func handle(error: Error, appContext: AppContext) { if let gmailUserError = error as? GoogleUserServiceError, case .userNotAllowedAllNeededScopes = gmailUserError { - let navigationController = viewController.navigationController + logger.logInfo("gmail login failed with error \(gmailUserError.errorMessage)") + let navigationController = keyWindow.rootViewController?.navigationController let checkAuthViewController = CheckMailAuthViewController(appContext: appContext) navigationController?.pushViewController(checkAuthViewController, animated: true) + } else { + keyWindow.rootViewController = FatalErrorViewController(error: error) } } } diff --git a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift index 3ba7dd543..d4d5fe526 100644 --- a/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift +++ b/FlowCrypt/Functionality/Services/Local Private Key Services/PassPhraseService.swift @@ -54,9 +54,9 @@ extension PassPhrase { // MARK: - Pass Phrase Storage protocol PassPhraseStorageType { - func save(passPhrase: PassPhrase) - func update(passPhrase: PassPhrase) - func remove(passPhrase: PassPhrase) + func save(passPhrase: PassPhrase) throws + func update(passPhrase: PassPhrase) throws + func remove(passPhrase: PassPhrase) throws func getPassPhrases() -> [PassPhrase] } @@ -64,9 +64,9 @@ protocol PassPhraseStorageType { // MARK: - PassPhraseService 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]) + func savePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) throws + func updatePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) throws + func savePassPhrasesInMemory(_ passPhrase: String, for privateKeys: [PrvKeyInfo]) throws } final class PassPhraseService: PassPhraseServiceType { @@ -83,27 +83,27 @@ final class PassPhraseService: PassPhraseServiceType { self.inMemoryStorage = localStorage } - func savePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) { + func savePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) throws { logger.logInfo("\(storageMethod): saving passphrase for key \(passPhrase.primaryFingerprintOfAssociatedKey)") switch storageMethod { case .persistent: - encryptedStorage.save(passPhrase: passPhrase) + try encryptedStorage.save(passPhrase: passPhrase) case .memory: 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) + try encryptedStorage.remove(passPhrase: passPhrase) } - inMemoryStorage.save(passPhrase: passPhrase) + try inMemoryStorage.save(passPhrase: passPhrase) } } - func updatePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) { + func updatePassPhrase(with passPhrase: PassPhrase, storageMethod: StorageMethod) throws { logger.logInfo("\(storageMethod): updating passphrase for key \(passPhrase.primaryFingerprintOfAssociatedKey)") switch storageMethod { case .persistent: - encryptedStorage.update(passPhrase: passPhrase) + try encryptedStorage.update(passPhrase: passPhrase) case .memory: - inMemoryStorage.save(passPhrase: passPhrase) + try inMemoryStorage.save(passPhrase: passPhrase) } } @@ -111,10 +111,10 @@ final class PassPhraseService: PassPhraseServiceType { encryptedStorage.getPassPhrases() + inMemoryStorage.getPassPhrases() } - func savePassPhrasesInMemory(_ passPhrase: String, for privateKeys: [PrvKeyInfo]) { + func savePassPhrasesInMemory(_ passPhrase: String, for privateKeys: [PrvKeyInfo]) throws { for privateKey in privateKeys { let pp = PassPhrase(value: passPhrase, fingerprintsOfAssociatedKey: privateKey.fingerprints) - savePassPhrase(with: pp, storageMethod: StorageMethod.memory) + try 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 808a0342b..462ca1140 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift @@ -23,7 +23,7 @@ protocol ContactsProviderType { protocol PublicKeyProvider { func retrievePubKeys(for email: String) -> [String] - func removePubKey(with fingerprint: String, for email: String) + func removePubKey(with fingerprint: String, for email: String) throws } // MARK: - PROVIDER @@ -46,12 +46,12 @@ extension ContactsService: ContactsProviderType { let contact = try await localContactsProvider.searchRecipient(with: email) guard let contact = contact else { let recipient = try await pubLookup.lookup(email: email) - localContactsProvider.save(recipient: recipient) + try localContactsProvider.save(recipient: recipient) return recipient } let recipient = try await pubLookup.lookup(email: email) - localContactsProvider.updateKeys(for: recipient) + try localContactsProvider.updateKeys(for: recipient) return contact } @@ -62,12 +62,10 @@ extension ContactsService: ContactsProviderType { extension ContactsService: PublicKeyProvider { func retrievePubKeys(for email: String) -> [String] { - let publicKeys = localContactsProvider.retrievePubKeys(for: email) - localContactsProvider.updateLastUsedDate(for: email) - return publicKeys + return localContactsProvider.retrievePubKeys(for: email) } - func removePubKey(with fingerprint: String, for email: String) { - localContactsProvider.removePubKey(with: fingerprint, for: email) + func removePubKey(with fingerprint: String, for email: String) throws { + try localContactsProvider.removePubKey(with: fingerprint, for: email) } } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index 27601648f..690d57ac0 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -8,66 +8,77 @@ import Foundation import RealmSwift +import FlowCryptCommon protocol LocalContactsProviderType: PublicKeyProvider { - func updateLastUsedDate(for email: String) 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 save(recipient: RecipientWithSortedPubKeys) throws + func remove(recipient: RecipientWithSortedPubKeys) throws + func updateKeys(for recipient: RecipientWithSortedPubKeys) throws func getAllRecipients() async throws -> [RecipientWithSortedPubKeys] } -struct LocalContactsProvider { - private let localContactsCache: EncryptedCacheService - let core: Core +final class LocalContactsProvider { + private let encryptedStorage: EncryptedStorageType + private let core: Core + + private lazy var logger = Logger.nested(Self.self) + + private var storage: Realm { + encryptedStorage.storage + } init( encryptedStorage: EncryptedStorageType, core: Core = .shared ) { - self.localContactsCache = EncryptedCacheService(encryptedStorage: encryptedStorage) + self.encryptedStorage = encryptedStorage self.core = core } } extension LocalContactsProvider: LocalContactsProviderType { - func updateLastUsedDate(for email: String) { - let recipient = find(with: email) + func retrievePubKeys(for email: String) -> [String] { + guard let object = find(with: email) else { return [] } - try? localContactsCache.realm.write { - recipient?.lastUsed = Date() + do { + try storage.write { + object.lastUsed = Date() + } + } catch { + logger.logError("fail to update last used property \(error)") } - } - func retrievePubKeys(for email: String) -> [String] { - find(with: email)?.pubKeys.map(\.armored) ?? [] + return object.pubKeys.map(\.armored) } - func save(recipient: RecipientWithSortedPubKeys) { - localContactsCache.save(RecipientRealmObject(recipient)) + func save(recipient: RecipientWithSortedPubKeys) throws { + try save(RecipientRealmObject(recipient)) } - func remove(recipient: RecipientWithSortedPubKeys) { - localContactsCache.remove( - object: RecipientRealmObject(recipient), - with: recipient.email - ) + func remove(recipient: RecipientWithSortedPubKeys) throws { + guard let object = find(with: recipient.email) else { + return + } + + try storage.write { + storage.delete(object) + } } - func updateKeys(for recipient: RecipientWithSortedPubKeys) { + func updateKeys(for recipient: RecipientWithSortedPubKeys) throws { guard let recipientObject = find(with: recipient.email) else { - localContactsCache.save(RecipientRealmObject(recipient)) + try save(RecipientRealmObject(recipient)) return } - recipient.pubKeys + try recipient.pubKeys .forEach { pubKey in if let index = recipientObject.pubKeys.firstIndex(where: { $0.primaryFingerprint == pubKey.fingerprint }) { - update(pubKey: pubKey, for: recipientObject, at: index) + try update(pubKey: pubKey, for: recipientObject, at: index) } else { - add(pubKey: pubKey, to: recipientObject) + try add(pubKey: pubKey, to: recipientObject) } } } @@ -78,14 +89,13 @@ extension LocalContactsProvider: LocalContactsProviderType { } func searchEmails(query: String) -> [String] { - localContactsCache.realm - .objects(RecipientRealmObject.self) + storage.objects(RecipientRealmObject.self) .filter("email contains[c] %@", query) .map(\.email) } func getAllRecipients() async throws -> [RecipientWithSortedPubKeys] { - let objects: [Recipient] = localContactsCache.realm.objects(RecipientRealmObject.self) + let objects: [Recipient] = storage.objects(RecipientRealmObject.self) .map(Recipient.init) var recipients: [RecipientWithSortedPubKeys] = [] for object in objects { @@ -94,13 +104,13 @@ extension LocalContactsProvider: LocalContactsProviderType { return recipients.sorted(by: { $0.email > $1.email }) } - func removePubKey(with fingerprint: String, for email: String) { - find(with: email)? + func removePubKey(with fingerprint: String, for email: String) throws { + try find(with: email)? .pubKeys .filter { $0.primaryFingerprint == fingerprint } .forEach { key in - try? localContactsCache.realm.write { - localContactsCache.realm.delete(key) + try storage.write { + storage.delete(key) } } } @@ -108,10 +118,13 @@ extension LocalContactsProvider: LocalContactsProviderType { extension LocalContactsProvider { private func find(with email: String) -> RecipientRealmObject? { - localContactsCache.realm.object( - ofType: RecipientRealmObject.self, - forPrimaryKey: email - ) + storage.object(ofType: RecipientRealmObject.self, forPrimaryKey: email) + } + + private func save(_ object: RecipientRealmObject) throws { + try storage.write { + storage.add(object, update: .modified) + } } private func parseRecipient(from recipient: Recipient) async throws -> RecipientWithSortedPubKeys { @@ -122,20 +135,23 @@ extension LocalContactsProvider { return RecipientWithSortedPubKeys(recipient, keyDetails: parsed.keyDetails) } - private func add(pubKey: PubKey, to recipient: RecipientRealmObject) { - guard let pubKeyObject = try? PubKeyRealmObject(pubKey) else { return } - try? localContactsCache.realm.write { + private func add(pubKey: PubKey, to recipient: RecipientRealmObject) throws { + let pubKeyObject = try PubKeyRealmObject(pubKey) + try storage.write { recipient.pubKeys.append(pubKeyObject) } } - private func update(pubKey: PubKey, for recipient: RecipientRealmObject, at index: Int) { - guard let existingKeyLastSig = recipient.pubKeys[index].lastSig, - let updateKeyLastSig = pubKey.lastSig, - updateKeyLastSig > existingKeyLastSig - else { return } + private func update(pubKey: PubKey, for recipient: RecipientRealmObject, at index: Int) throws { + guard + let existingKeyLastSig = recipient.pubKeys[index].lastSig, + let updateKeyLastSig = pubKey.lastSig, + updateKeyLastSig > existingKeyLastSig + else { + return + } - try? localContactsCache.realm.write { + try storage.write { recipient.pubKeys[index].update(from: pubKey) } } diff --git a/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift b/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift index 55467bb37..1b4b23c48 100644 --- a/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/ClientConfigurationRealmObject.swift @@ -57,9 +57,3 @@ extension ClientConfigurationRealmObject { ) } } - -extension ClientConfigurationRealmObject: CachedRealmObject { - var identifier: String { userEmail ?? "" } - - var activeUser: UserRealmObject? { user } -} diff --git a/FlowCrypt/Models/Realm Models/FolderRealmObject.swift b/FlowCrypt/Models/Realm Models/FolderRealmObject.swift index a3da8d873..3ef12be67 100644 --- a/FlowCrypt/Models/Realm Models/FolderRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/FolderRealmObject.swift @@ -27,9 +27,3 @@ extension FolderRealmObject { self.user = UserRealmObject(user) } } - -extension FolderRealmObject: CachedRealmObject { - var identifier: String { name } - - var activeUser: UserRealmObject? { user } -} diff --git a/FlowCrypt/Models/Realm Models/KeyInfoRealmObject.swift b/FlowCrypt/Models/Realm Models/KeyInfoRealmObject.swift index ee19560cb..86d1eae62 100644 --- a/FlowCrypt/Models/Realm Models/KeyInfoRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/KeyInfoRealmObject.swift @@ -70,7 +70,8 @@ extension KeyInfoRealmObject { extension KeyInfoRealmObject { /// associated user email - var account: String? { - user?.email + var account: String { + guard let email = user?.email else { fatalError() } + return email } } diff --git a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift index 51986345f..99bca1721 100644 --- a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift @@ -50,11 +50,3 @@ extension RecipientRealmObject { pubKeys.first(where: { $0.contains(longid: longid) }) != nil } } - -extension RecipientRealmObject: CachedRealmObject { - // Contacts can be shared between accounts - // https://github.com/FlowCrypt/flowcrypt-ios/issues/269 - var activeUser: UserRealmObject? { nil } - - var identifier: String { email } -} diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 84e893f69..59907de12 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -46,6 +46,7 @@ "message_signature_pending" = "verifying signature..."; "message_signature_fail_reason" = "Failed to verify signature due to: %@"; "message_decrypt_error" = "decrypt error"; +"message_mark_read_error" = "Could not mark message as read: %@"; // ERROR "error_fetch_folders" = "Could not fetch folders"; @@ -160,8 +161,11 @@ // Contacts "contacts_screen_title" = "Contacts"; +"contacts_screen_load_error" = "Failed to load recipients: %@"; +"contacts_screen_remove_error" = "Failed to remove contact: %@"; "contact_detail_screen_title" = "Public Keys"; "contact_detail_copy" = "Public Key copied to clipboard"; +"contact_detail_remove_public_key_error" = "Failed to remove public due to: %@"; "contact_key_detail_screen_title" = "Public Key"; // Contacts keys @@ -273,3 +277,7 @@ "invalid_storage_reset_button" = "Reset"; "invalid_storage_reset_error" = "Couldn't remove storage. Please reinstall application"; "invalid_storage_failed_to_initialize" = "Failed to initialize storage. Please restart your device. If it doesn't help, please reinstall application"; + +// Fatal error view controller +"fatal_error_screen_title" = "Error"; +"fatal_error_screen_text" = "Not possible to continue due to fatal error. Please try to restart application. If it doesn't help, please reinstall application"; diff --git a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift index c2def833a..3f6e6516d 100644 --- a/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift +++ b/FlowCryptAppTests/Functionality/Services/PassPhraseStorageTests/PassPhraseStorageTests.swift @@ -107,7 +107,7 @@ class PassPhraseStorageTests: XCTestCase { XCTAssertTrue(result.count == 3) } - func testSavePassPhraseInPersistenStorage() { + func testSavePassPhraseInPersistenStorage() throws { let passPhraseToSave = PassPhrase(value: "pass", fingerprintsOfAssociatedKey: ["fingerprint 1", "123333"]) let expectation = XCTestExpectation() @@ -128,14 +128,14 @@ class PassPhraseStorageTests: XCTestCase { } } - sut.savePassPhrase(with: passPhraseToSave, storageMethod: .persistent) + try sut.savePassPhrase(with: passPhraseToSave, storageMethod: .persistent) XCTAssertFalse(inMemoryStorage.saveResult != nil ) wait(for: [expectation], timeout: 0.1, enforceOrder: false) } - func testSavePassPhraseInPersistentStorageWithoutAnyPassPhrases() { + func testSavePassPhraseInPersistentStorageWithoutAnyPassPhrases() throws { let passPhraseToSave = PassPhrase(value: "pass", fingerprintsOfAssociatedKey: ["fingerprint 1", "123333"]) let expectation = XCTestExpectation() @@ -148,7 +148,7 @@ class PassPhraseStorageTests: XCTestCase { expectation.fulfill() } - sut.savePassPhrase(with: passPhraseToSave, storageMethod: .persistent) + try sut.savePassPhrase(with: passPhraseToSave, storageMethod: .persistent) XCTAssertFalse(inMemoryStorage.saveResult != nil ) diff --git a/FlowCryptAppTests/LocalStorageTests.swift b/FlowCryptAppTests/LocalStorageTests.swift index 03a71e503..59e95cb7c 100644 --- a/FlowCryptAppTests/LocalStorageTests.swift +++ b/FlowCryptAppTests/LocalStorageTests.swift @@ -12,11 +12,12 @@ import XCTest class LocalStorageTests: XCTestCase { var sut: LocalStorage! - override func setUp() { + override func setUpWithError() throws { + try super.setUpWithError() sut = LocalStorage() let passPhrase = PassPhrase(value: "123", fingerprintsOfAssociatedKey: ["123"], date: nil) - sut.passPhraseStorage.save(passPhrase: passPhrase) + try sut.passPhraseStorage.save(passPhrase: passPhrase) } var trashKey: String {