From 3b48906b68c63227559d7beebbb35821db68d1f6 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Tue, 9 Nov 2021 12:19:14 +0200 Subject: [PATCH 01/15] issue #278 add verificationPubKeys param to decryptMessage --- .../Msg/MessageViewController.swift | 27 ++++++++++++---- .../Threads/ThreadDetailsViewController.swift | 21 ++++++++++-- FlowCrypt/Core/Core.swift | 9 ++++-- FlowCrypt/Extensions/Error+Extension.swift | 1 - FlowCrypt/Extensions/UIColorExtension.swift | 4 +-- .../DataManager/DataService.swift | 2 +- .../Message Gateway/GmailService+draft.swift | 2 +- .../Message Provider/MessageService.swift | 32 +++---------------- .../Migration/DBMigrationService.swift | 4 +-- .../ComposeMessageService.swift | 2 +- .../Models/PubKey.swift | 2 +- .../Models/Realm Models/PubKeyObject.swift | 2 +- .../Core/FlowCryptCoreTests.swift | 4 +-- .../Functionality/PGP/KeyMethodsTest.swift | 2 +- .../Services/ComposeMessageServiceTests.swift | 2 +- 15 files changed, 64 insertions(+), 52 deletions(-) diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index e752d2918..f5b1955a3 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -52,6 +52,7 @@ final class MessageViewController: TableNodeViewController { messageService: MessageService = MessageService(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, messageProvider: MessageProvider = MailProvider.shared.messageProvider, + contactsService: ContactsServiceType = ContactsService(), decorator: MessageViewDecorator = MessageViewDecorator(dateFormatter: DateFormatter()), trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), filesManager: FilesManagerType = FilesManager(), @@ -66,7 +67,8 @@ final class MessageViewController: TableNodeViewController { self.filesManager = filesManager self.serviceActor = ServiceActor( messageService: messageService, - messageProvider: messageProvider + messageProvider: messageProvider, + contactsService: contactsService ) super.init(node: TableNode()) @@ -117,7 +119,7 @@ extension MessageViewController { let matched = try await serviceActor.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) if matched { handleFetchProgress(state: .decrypt) - processedMessage = try await serviceActor.decryptAndProcessMessage(mime: rawMimeData) + processedMessage = try await serviceActor.decryptAndProcessMessage(mime: rawMimeData, sender: input.objMessage.sender) handleReceivedMessage() } else { handleWrongPassPhrase(for: rawMimeData, with: passPhrase) @@ -410,11 +412,14 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { private actor ServiceActor { private let messageService: MessageService private let messageProvider: MessageProvider + private let contactsService: ContactsServiceType init(messageService: MessageService, - messageProvider: MessageProvider) { + messageProvider: MessageProvider, + contactsService: ContactsServiceType) { self.messageService = messageService self.messageProvider = messageProvider + self.contactsService = contactsService } func fetchDecryptAndRenderMsg(message: Message, path: String, @@ -422,14 +427,24 @@ private actor ServiceActor { let rawMimeData = try await messageProvider.fetchMsg(message: message, folder: path, progressHandler: progressHandler) - return try await messageService.decryptAndProcessMessage(mime: rawMimeData) + + progressHandler?(.decrypt) + + return try await decryptAndProcessMessage(mime: rawMimeData, sender: message.sender) } func checkAndPotentiallySaveEnteredPassPhrase(_ passPhrase: String) async throws -> Bool { try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) } - func decryptAndProcessMessage(mime: Data) async throws -> ProcessedMessage { - try await messageService.decryptAndProcessMessage(mime: mime) + func decryptAndProcessMessage(mime: Data, sender: String?) async throws -> ProcessedMessage { + let pubKeys: [String] + if let sender = sender { + pubKeys = contactsService.retrievePubKeys(for: sender) + } else { + pubKeys = [] + } + + return try await messageService.decryptAndProcessMessage(mime: mime, verificationPubKeys: pubKeys) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index de0c3c9bf..49bb5a111 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -30,6 +30,7 @@ final class ThreadDetailsViewController: TableNodeViewController { case thread, message } + private let contactsService: ContactsServiceType private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider @@ -49,6 +50,7 @@ final class ThreadDetailsViewController: TableNodeViewController { ) init( + contactsService: ContactsServiceType = ContactsService(), messageService: MessageService = MessageService(), trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, @@ -57,15 +59,16 @@ final class ThreadDetailsViewController: TableNodeViewController { filesManager: FilesManagerType = FilesManager(), completion: @escaping MessageActionCompletion ) { - self.threadOperationsProvider = threadOperationsProvider + self.contactsService = contactsService self.messageService = messageService + self.threadOperationsProvider = threadOperationsProvider self.messageOperationsProvider = messageOperationsProvider self.trashFolderProvider = trashFolderProvider self.thread = thread self.filesManager = filesManager self.onComplete = completion self.input = thread.messages - .sorted(by: { $0 > $1 }) + .sorted(by: >) .map { Input(message: $0, isExpanded: false) } super.init(node: TableNode()) @@ -160,6 +163,7 @@ extension ThreadDetailsViewController { extension ThreadDetailsViewController { private func fetchDecryptAndRenderMsg(at indexPath: IndexPath) { let message = input[indexPath.section-1].rawMessage + let verificationPubKeys = fetchVerificationPubKeys(for: message.sender) logger.logInfo("Start loading message") handleFetchProgress(state: .fetch) @@ -169,6 +173,7 @@ extension ThreadDetailsViewController { let processedMessage = try await messageService.getAndProcessMessage( with: message, folder: thread.path, + verificationPubKeys: verificationPubKeys, progressHandler: { [weak self] in self?.handleFetchProgress(state: $0) } ) handleReceived(message: processedMessage, at: indexPath) @@ -249,7 +254,9 @@ extension ThreadDetailsViewController { do { let matched = try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) if matched { - let processedMessage = try await messageService.decryptAndProcessMessage(mime: rawMimeData) + let verificationPubKeys = fetchVerificationPubKeys(for: input[indexPath.section-1].rawMessage.sender) + let processedMessage = try await messageService.decryptAndProcessMessage(mime: rawMimeData, + verificationPubKeys: verificationPubKeys) handleReceived(message: processedMessage, at: indexPath) } else { handleWrongPassPhrase(for: rawMimeData, with: passPhrase, at: indexPath) @@ -270,6 +277,14 @@ extension ThreadDetailsViewController { updateSpinner(label: "decrypting_title".localized) } } + + private func fetchVerificationPubKeys(for sender: String?) -> [String] { + if let sender = sender { + return contactsService.retrievePubKeys(for: sender) + } else { + return [] + } + } } extension ThreadDetailsViewController: MessageActionsHandler { diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index 041ee537c..5b8f6773c 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -127,11 +127,16 @@ actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType { return CoreRes.EncryptFile(encryptedFile: encrypted.data) } - func parseDecryptMsg(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?, isEmail: Bool) async throws -> CoreRes.ParseDecryptMsg { + func parseDecryptMsg(encrypted: Data, + keys: [PrvKeyInfo], + msgPwd: String?, + isEmail: Bool, + verificationPubKeys: [String]) async throws -> CoreRes.ParseDecryptMsg { let json: [String : Any?]? = [ "keys": try keys.map { try $0.toJsonEncodedDict() }, "isEmail": isEmail, - "msgPwd": msgPwd + "msgPwd": msgPwd, + "verificationPubkeys": verificationPubKeys ] let parsed = try await call( "parseDecryptMsg", diff --git a/FlowCrypt/Extensions/Error+Extension.swift b/FlowCrypt/Extensions/Error+Extension.swift index ca56d9101..cc8c7f10b 100644 --- a/FlowCrypt/Extensions/Error+Extension.swift +++ b/FlowCrypt/Extensions/Error+Extension.swift @@ -5,7 +5,6 @@ // Created by Roma Sosnovsky on 04/11/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - import Foundation diff --git a/FlowCrypt/Extensions/UIColorExtension.swift b/FlowCrypt/Extensions/UIColorExtension.swift index 7cf9c47d6..10420bcbf 100644 --- a/FlowCrypt/Extensions/UIColorExtension.swift +++ b/FlowCrypt/Extensions/UIColorExtension.swift @@ -78,7 +78,7 @@ extension UIColor { } // Uncomment for FlowCryptUIApplication -//extension UIColor { +// extension UIColor { // static var main: UIColor { // .green // } @@ -110,4 +110,4 @@ extension UIColor { // static var blueColor: UIColor { // .blue // } -//} +// } diff --git a/FlowCrypt/Functionality/DataManager/DataService.swift b/FlowCrypt/Functionality/DataManager/DataService.swift index 0214dc71f..e83b8956b 100644 --- a/FlowCrypt/Functionality/DataManager/DataService.swift +++ b/FlowCrypt/Functionality/DataManager/DataService.swift @@ -127,7 +127,7 @@ extension DataService: DataServiceType { // MARK: - Migration extension DataService: DBMigration { /// Perform all kind of migrations - func performMigrationIfNeeded() async throws -> Void { + func performMigrationIfNeeded() async throws { try await migrationService.performMigrationIfNeeded() } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift index 6e591c9ce..f6dd93bf2 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Gateway/GmailService+draft.swift @@ -34,7 +34,7 @@ extension GmailService: DraftGateway { } func deleteDraft(with identifier: String) async { - await withCheckedContinuation{ (continuation: CheckedContinuation) in + await withCheckedContinuation { (continuation: CheckedContinuation) in let query = GTLRGmailQuery_UsersDraftsDelete.query(withUserId: .me, identifier: identifier) gmailService.executeQuery(query) { _, _, _ in continuation.resume() diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index cf8f9cb95..90f1517b8 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -97,33 +97,10 @@ final class MessageService { return matchingKeys.isNotEmpty } - func validateMessage(rawMimeData: Data, with passPhrase: String) async throws -> ProcessedMessage { - let keys = try await keyService.getPrvKeyInfo() - guard keys.isNotEmpty else { - throw MessageServiceError.emptyKeys - } - - let keysWithFilledPassPhrase = keys.map { $0.copy(with: passPhrase) } - let keysToSave = keys.filter { $0.passphrase == passPhrase } - - savePassPhrases(value: passPhrase, with: keysToSave) - - let decrypted = try await core.parseDecryptMsg( - encrypted: rawMimeData, - keys: keysWithFilledPassPhrase, - msgPwd: nil, - isEmail: true - ) - guard !self.hasMsgBlockThatNeedsPassPhrase(decrypted) else { - throw MessageServiceError.missingPassPhrase(rawMimeData) - } - - return try await processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) - } - func getAndProcessMessage( with input: Message, folder: String, + verificationPubKeys: [String], progressHandler: ((MessageFetchState) -> Void)? ) async throws -> ProcessedMessage { let rawMimeData = try await messageProvider.fetchMsg( @@ -131,10 +108,10 @@ final class MessageService { folder: folder, progressHandler: progressHandler ) - return try await decryptAndProcessMessage(mime: rawMimeData) + return try await decryptAndProcessMessage(mime: rawMimeData, verificationPubKeys: verificationPubKeys) } - func decryptAndProcessMessage(mime rawMimeData: Data) async throws -> ProcessedMessage { + func decryptAndProcessMessage(mime rawMimeData: Data, verificationPubKeys: [String]) async throws -> ProcessedMessage { let keys = try await keyService.getPrvKeyInfo() guard keys.isNotEmpty else { throw MessageServiceError.emptyKeys @@ -143,7 +120,8 @@ final class MessageService { encrypted: rawMimeData, keys: keys, msgPwd: nil, - isEmail: true + isEmail: true, + verificationPubKeys: verificationPubKeys ) guard !self.hasMsgBlockThatNeedsPassPhrase(decrypted) else { throw MessageServiceError.missingPassPhrase(rawMimeData) diff --git a/FlowCrypt/Functionality/Migration/DBMigrationService.swift b/FlowCrypt/Functionality/Migration/DBMigrationService.swift index 9ec09dcff..d43a7e420 100644 --- a/FlowCrypt/Functionality/Migration/DBMigrationService.swift +++ b/FlowCrypt/Functionality/Migration/DBMigrationService.swift @@ -11,7 +11,7 @@ import Foundation import RealmSwift protocol DBMigration { - func performMigrationIfNeeded() async throws -> Void + func performMigrationIfNeeded() async throws } struct DBMigrationService { @@ -29,7 +29,7 @@ struct DBMigrationService { // MARK: - DBMigration extension DBMigrationService: DBMigration { - func performMigrationIfNeeded() async throws -> Void { + func performMigrationIfNeeded() async throws { // self.performGmailApiMigration() } } diff --git a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift index 9d20da7f7..f32ca67f3 100644 --- a/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift +++ b/FlowCrypt/Functionality/Services/Compose Message Service/ComposeMessageService.swift @@ -155,7 +155,7 @@ final class ComposeMessageService { throw ComposeMessageError.gatewayError(error) } } - + // MARK: - Encrypt and Send func encryptAndSend(message: SendableMsg, threadId: String?, progressHandler: ((Float) -> Void)?) async throws { do { diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/PubKey.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/PubKey.swift index a5b6feca3..a8ee7373d 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/PubKey.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/PubKey.swift @@ -5,7 +5,7 @@ // Created by Roma Sosnovsky on 11/10/21 // Copyright © 2017-present FlowCrypt a. s. All rights reserved. // - + import Foundation struct PubKey { diff --git a/FlowCrypt/Models/Realm Models/PubKeyObject.swift b/FlowCrypt/Models/Realm Models/PubKeyObject.swift index a2d8ad069..c764a21c6 100644 --- a/FlowCrypt/Models/Realm Models/PubKeyObject.swift +++ b/FlowCrypt/Models/Realm Models/PubKeyObject.swift @@ -31,7 +31,7 @@ final class PubKeyObject: Object { fingerprints: [String] = [], created: Date? = nil) throws { self.init() - + self.armored = armored self.lastSig = lastSig self.lastChecked = lastChecked diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index 1b3b95758..a38d55be4 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -196,7 +196,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 await 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, verificationPubKeys: []) XCTAssertEqual(decrypted.text, text) XCTAssertEqual(decrypted.replyType, CoreRes.ReplyType.encrypted) XCTAssertEqual(decrypted.blocks.count, 1) @@ -209,7 +209,7 @@ final class FlowCryptCoreTests: XCTestCase { 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 await 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, verificationPubKeys: []) let decrypted = r XCTAssertEqual(decrypted.text, "") XCTAssertEqual(decrypted.replyType, CoreRes.ReplyType.plain) // replies to errors should be plain diff --git a/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift b/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift index 904dd201d..1c423e12e 100644 --- a/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift +++ b/FlowCryptAppTests/Functionality/PGP/KeyMethodsTest.swift @@ -62,7 +62,7 @@ class KeyMethodsTest: XCTestCase { ) ] do { - try await sut.filterByPassPhraseMatch(keys: keys, passPhrase: passPhrase) + _ = try await sut.filterByPassPhraseMatch(keys: keys, passPhrase: passPhrase) XCTFail("expected to throw above") } catch { XCTAssertEqual(error as? KeyServiceError, KeyServiceError.expectedPrivateGotPublic) diff --git a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift index 606882da7..fd546653e 100644 --- a/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift +++ b/FlowCryptAppTests/Functionality/Services/ComposeMessageServiceTests.swift @@ -166,7 +166,7 @@ class ComposeMessageServiceTests: XCTestCase { XCTAssertEqual(error as? MessageValidationError, MessageValidationError.emptyMessage) } do { - try await sut.validateAndProduceSendableMsg( + _ = try await sut.validateAndProduceSendableMsg( input: ComposeMessageInput(type: .idle), contextToSend: ComposeMessageContext( message: " ", From c19ee80f1d274402487d9ebffbcbf89d0b6b7435 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Wed, 10 Nov 2021 23:04:18 +0200 Subject: [PATCH 02/15] issue #278 add signature badge to messages --- .../Msg/MessageViewController.swift | 7 +- .../Threads/ThreadDetailsDecorator.swift | 4 + .../Threads/ThreadDetailsViewController.swift | 4 +- FlowCrypt/Core/CoreTypes.swift | 9 +- .../Message Provider/MessageService.swift | 59 +++++++++++- .../ContactsService.swift | 5 + .../LocalContactsProvider.swift | 11 +++ .../Models/RecipientWithSortedPubKeys.swift | 5 + .../Models/Realm Models/PubKeyObject.swift | 4 + .../Models/Realm Models/RecipientObject.swift | 6 ++ .../ThreadMessageSenderCellNode.swift | 96 +++++++++++++++++-- 11 files changed, 193 insertions(+), 17 deletions(-) diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index f5b1955a3..3cdbe9f50 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -241,9 +241,10 @@ extension MessageViewController { handler: { [weak self] _ in guard let self = self else { return } self.processedMessage = ProcessedMessage(rawMimeData: rawMimeData, - text: String(data: rawMimeData, encoding: .utf8) ?? "", - attachments: [], - messageType: .encrypted) + text: String(data: rawMimeData, encoding: .utf8) ?? "", + attachments: [], + messageType: .encrypted, + signature: .unknown) self.handleReceivedMessage() } ) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index 02a58cd9a..e916745d0 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -11,6 +11,7 @@ import UIKit extension ThreadMessageSenderCellNode.Input { init(threadMessage: ThreadDetailsViewController.Input) { + let signature = threadMessage.processedMessage?.signature.message ?? "" let sender = threadMessage.rawMessage.sender ?? "message_unknown_sender".localized let date = DateFormatter().formatDate(threadMessage.rawMessage.date) let isMessageRead = threadMessage.rawMessage.isMessageRead @@ -28,6 +29,9 @@ extension ThreadMessageSenderCellNode.Input { : .mainTextUnreadColor self.init( + signature: NSAttributedString.text(from: signature, style: .regular(12), color: .white), + signatureColor: threadMessage.processedMessage?.signature.color, + signatureIcon: threadMessage.processedMessage?.signature.icon, sender: NSAttributedString.text(from: sender, style: style, color: textColor), date: NSAttributedString.text(from: date, style: style, color: dateColor), isExpanded: threadMessage.isExpanded, diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 49bb5a111..538dd5399 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -196,8 +196,8 @@ extension ThreadDetailsViewController { animations: { self.node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) }, - completion: { _ in - self.node.scrollToRow(at: indexPath, at: .middle, animated: true) + completion: { [weak self] _ in + self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) }) } diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 169980b25..a72f7f9d4 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -144,7 +144,7 @@ struct SendableMsg: Equatable { struct MsgBlock: Decodable { static func blockParseErr(with content: String) -> MsgBlock { - MsgBlock(type: .blockParseErr, content: content, decryptErr: nil, keyDetails: nil, attMeta: nil) + MsgBlock(type: .blockParseErr, content: content, decryptErr: nil, keyDetails: nil, attMeta: nil, verifyRes: nil) } let type: BlockType @@ -152,7 +152,7 @@ struct MsgBlock: Decodable { let decryptErr: DecryptErr? // always present in decryptErr BlockType let keyDetails: KeyDetails? // always present in publicKey BlockType let attMeta: AttMeta? // always present in plainAtt, encryptedAtt, decryptedAtt, encryptedAttLink - // let verifyRes: VerifyRes?, + let verifyRes: VerifyRes? // let signature: String? // possibly not neded in Swift @@ -192,6 +192,11 @@ struct MsgBlock: Decodable { let length: Int } + struct VerifyRes: Decodable { + let match: Bool? + let signer: String? + } + enum BlockType: String, Decodable { case plainHtml // all content blocks, regardless if encrypted or not, formatted as a plainHtml (todo - rename this one day to formattedHtml) case publicKey diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 90f1517b8..94220b72d 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -8,6 +8,7 @@ import Foundation import FlowCryptCommon +import UIKit // MARK: - MessageAttachment struct MessageAttachment: FileType { @@ -30,10 +31,48 @@ struct ProcessedMessage { case error(MsgBlock.DecryptErr.ErrorType), encrypted, plain } + enum MessageSignature { + case valid(String), invalid, unknown + + var message: String { + switch self { + case .valid(let email): + return "Signed by \(email)" + case .invalid: + return "Bad signature - cannot trust authenticity of this message" + case .unknown: + return "Signature not verified - unknown sender public key" + } + } + + var icon: String { + switch self { + case .valid: + return "lock" + case .invalid: + return "exclamationmark.triangle" + case .unknown: + return "xmark" + } + } + + var color: UIColor { + switch self { + case .valid: + return .main + case .invalid: + return .warningColor + case .unknown: + return .errorColor + } + } + } + let rawMimeData: Data let text: String let attachments: [MessageAttachment] let messageType: MessageType + let signature: MessageSignature } extension ProcessedMessage { @@ -42,7 +81,8 @@ extension ProcessedMessage { rawMimeData: Data(), text: "loading_title".localized + "...", attachments: [], - messageType: .plain + messageType: .plain, + signature: .unknown ) } @@ -65,6 +105,7 @@ final class MessageService { private let keyService: KeyServiceType private let keyMethods: KeyMethodsType private let passPhraseService: PassPhraseServiceType + private let contactsService: ContactsServiceType private let core: Core private let logger: Logger @@ -73,7 +114,8 @@ final class MessageService { keyService: KeyServiceType = KeyService(), core: Core = Core.shared, passPhraseService: PassPhraseServiceType = PassPhraseService(), - keyMethods: KeyMethodsType = KeyMethods() + keyMethods: KeyMethodsType = KeyMethods(), + contactsService: ContactsServiceType = ContactsService() ) { self.messageProvider = messageProvider self.keyService = keyService @@ -81,6 +123,7 @@ final class MessageService { self.passPhraseService = passPhraseService self.logger = Logger.nested(in: Self.self, with: "MessageService") self.keyMethods = keyMethods + self.contactsService = contactsService } func checkAndPotentiallySaveEnteredPassPhrase(_ passPhrase: String) async throws -> Bool { @@ -128,6 +171,7 @@ final class MessageService { } let processedMessage = try await processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) + switch processedMessage.messageType { case .error(let errorType): switch errorType { @@ -156,13 +200,21 @@ final class MessageService { ) let messageType: ProcessedMessage.MessageType let text: String + let signature: ProcessedMessage.MessageSignature if let decryptErrBlock = decryptErrBlocks.first { let rawMsg = decryptErrBlock.content let err = decryptErrBlock.decryptErr?.error text = "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)" messageType = .error(err?.type ?? .other) + signature = .unknown } else { + if let longid = decrypted.blocks.first?.verifyRes?.signer, + let contact = await contactsService.findBy(longId: longid) { + signature = contact.isValid(longid: longid) ? .valid(contact.email) : .invalid + } else { + signature = .unknown + } text = decrypted.text messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain } @@ -171,7 +223,8 @@ final class MessageService { rawMimeData: rawMimeData, text: text, attachments: attachments, - messageType: messageType + messageType: messageType, + signature: signature ) } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift index c71dfb9e9..568558880 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift @@ -19,6 +19,7 @@ protocol ContactsServiceType: PublicKeyProvider, ContactsProviderType { protocol ContactsProviderType { func searchContact(with email: String) async throws -> RecipientWithSortedPubKeys func searchContacts(query: String) -> [String] + func findBy(longId: String) async -> RecipientWithSortedPubKeys? } protocol PublicKeyProvider { @@ -58,6 +59,10 @@ extension ContactsService: ContactsProviderType { func searchContacts(query: String) -> [String] { localContactsProvider.searchEmails(query: query) } + + func findBy(longId: String) async -> RecipientWithSortedPubKeys? { + await localContactsProvider.findBy(longid: longId) + } } extension ContactsService: PublicKeyProvider { diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index 246ed9df0..b467e1fb9 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -13,6 +13,7 @@ protocol LocalContactsProviderType: PublicKeyProvider { func updateLastUsedDate(for email: String) func searchRecipient(with email: String) async throws -> RecipientWithSortedPubKeys? func searchEmails(query: String) -> [String] + func findBy(longid: String) async -> RecipientWithSortedPubKeys? func save(recipient: RecipientWithSortedPubKeys) func remove(recipient: RecipientWithSortedPubKeys) func updateKeys(for recipient: RecipientWithSortedPubKeys) @@ -46,6 +47,16 @@ extension LocalContactsProvider: LocalContactsProviderType { .map { $0.armored } ?? [] } + func findBy(longid: String) async -> RecipientWithSortedPubKeys? { + if let object = localContactsCache.realm + .objects(RecipientObject.self) + .first(where: { $0.contains(longid: longid) }) { + return try? await parseRecipient(from: object.freeze()) + } + + return nil + } + func save(recipient: RecipientWithSortedPubKeys) { localContactsCache.save(RecipientObject(recipient)) } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift index bb6e3e377..afb8b485b 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift @@ -49,6 +49,10 @@ extension RecipientWithSortedPubKeys { var keyState: PubKeyState { pubKeys.first?.keyState ?? .empty } var activePubKeys: [PubKey] { pubKeys.filter { $0.keyState == .active } } + func isValid(longid: String) -> Bool { + activePubKeys.contains(where: { $0.longids.contains(longid) }) + } + private var sortedPubKeys: [PubKey] { _pubKeys .sorted(by: { key1, key2 in @@ -64,6 +68,7 @@ extension RecipientWithSortedPubKeys { return expire1 > expire2 }) } + } extension RecipientWithSortedPubKeys: Equatable { diff --git a/FlowCrypt/Models/Realm Models/PubKeyObject.swift b/FlowCrypt/Models/Realm Models/PubKeyObject.swift index c764a21c6..6d9b87a48 100644 --- a/FlowCrypt/Models/Realm Models/PubKeyObject.swift +++ b/FlowCrypt/Models/Realm Models/PubKeyObject.swift @@ -79,4 +79,8 @@ extension PubKeyObject { fingerprints.append(objectsIn: key.fingerprints) self.fingerprints = fingerprints } + + func contains(longid: String) -> Bool { + longids.contains(longid) + } } diff --git a/FlowCrypt/Models/Realm Models/RecipientObject.swift b/FlowCrypt/Models/Realm Models/RecipientObject.swift index d189df144..ea70e56fd 100644 --- a/FlowCrypt/Models/Realm Models/RecipientObject.swift +++ b/FlowCrypt/Models/Realm Models/RecipientObject.swift @@ -53,6 +53,12 @@ extension RecipientObject { } } +extension RecipientObject { + func contains(longid: String) -> Bool { + pubKeys.first(where: { $0.contains(longid: longid) }) != nil + } +} + extension RecipientObject: CachedObject { // Contacts can be shared between accounts // https://github.com/FlowCrypt/flowcrypt-ios/issues/269 diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index d1fe2749c..a992363c8 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -11,15 +11,25 @@ import UIKit public final class ThreadMessageSenderCellNode: CellNode { public struct Input { + public let signature: NSAttributedString? + public let signatureColor: UIColor? + public let signatureIcon: String? + public let sender: NSAttributedString public let date: NSAttributedString? public let isExpanded: Bool public let buttonColor: UIColor - public init(sender: NSAttributedString, + public init(signature: NSAttributedString, + signatureColor: UIColor?, + signatureIcon: String?, + sender: NSAttributedString, date: NSAttributedString, isExpanded: Bool, buttonColor: UIColor) { + self.signature = signature + self.signatureColor = signatureColor + self.signatureIcon = signatureIcon self.sender = sender self.date = date self.isExpanded = isExpanded @@ -40,8 +50,18 @@ public final class ThreadMessageSenderCellNode: CellNode { } } + private lazy var signatureNode: ThreadMessageSignatureNode = { + let input = ThreadMessageSignatureNode.Input( + signature: input.signature, + color: input.signatureColor, + icon: input.signatureIcon + ) + return ThreadMessageSignatureNode(input: input) + }() + private let senderNode = ASTextNode2() private let dateNode = ASTextNode2() + public private(set) var replyNode = ASButtonNode() public private(set) var expandNode = ASImageNode() @@ -84,23 +104,31 @@ public final class ThreadMessageSenderCellNode: CellNode { replyNode.style.preferredSize = CGSize(width: 44, height: 44) expandNode.style.preferredSize = CGSize(width: 18, height: 44) + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1.0 + let infoNode = ASStackLayoutSpec( direction: .vertical, spacing: 4, - justifyContent: .start, + justifyContent: .spaceBetween, alignItems: .start, children: [senderNode, dateNode] ) - infoNode.style.flexGrow = 1 - infoNode.style.flexShrink = 1 + let senderSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .start, + children: [infoNode, spacer, replyNode, expandNode] + ) let contentSpec = ASStackLayoutSpec( - direction: .horizontal, + direction: .vertical, spacing: 4, justifyContent: .spaceBetween, - alignItems: .center, - children: [infoNode, replyNode, expandNode] + alignItems: .start, + children: [signatureNode, senderSpec] ) return ASInsetLayoutSpec( @@ -109,3 +137,57 @@ public final class ThreadMessageSenderCellNode: CellNode { ) } } + +private final class ThreadMessageSignatureNode: ASDisplayNode { + public struct Input { + public let signature: NSAttributedString? + public let color: UIColor? + public let icon: String? + + public init(signature: NSAttributedString?, + color: UIColor?, + icon: String?) { + self.signature = signature + self.color = color + self.icon = icon + } + } + + private lazy var iconNode: ASImageNode? = { + guard let icon = input.icon else { return nil } + let imageNode = ASImageNode() + let configuration = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium) + imageNode.image = UIImage(systemName: icon, withConfiguration: configuration) + imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.white) + return imageNode + }() + + private let signatureNode = ASTextNode2() + private let input: ThreadMessageSignatureNode.Input + + init(input: ThreadMessageSignatureNode.Input) { + self.input = input + super.init() + + automaticallyManagesSubnodes = true + + signatureNode.attributedText = input.signature + backgroundColor = input.color + cornerRadius = 4 + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let contentSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 2, + justifyContent: .spaceBetween, + alignItems: .center, + children: [iconNode, signatureNode].compactMap { $0 } + ) + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4), + child: contentSpec + ) + } +} From a1901d3a0e5af269ab539ea29a1e35d2d7b9b669 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 11 Nov 2021 14:50:41 +0200 Subject: [PATCH 03/15] issue #278 hide signature for plain messages --- .../Msg/MessageViewController.swift | 4 +- .../Threads/ThreadDetailsDecorator.swift | 9 +++- .../Threads/ThreadDetailsViewController.swift | 4 +- .../Message Provider/MessageService.swift | 27 ++++++++--- .../Mocks/ContactsServiceMock.swift | 1 + .../ThreadMessageSenderCellNode.swift | 47 +++++++++++++------ 6 files changed, 67 insertions(+), 25 deletions(-) diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index 3cdbe9f50..3d8e2d3ca 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -446,6 +446,8 @@ private actor ServiceActor { pubKeys = [] } - return try await messageService.decryptAndProcessMessage(mime: mime, verificationPubKeys: pubKeys) + return try await messageService.decryptAndProcessMessage(mime: mime, + sender: sender, + verificationPubKeys: pubKeys) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index e916745d0..a23166361 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -11,7 +11,12 @@ import UIKit extension ThreadMessageSenderCellNode.Input { init(threadMessage: ThreadDetailsViewController.Input) { - let signature = threadMessage.processedMessage?.signature.message ?? "" + let signature: NSAttributedString? + if let processedMessage = threadMessage.processedMessage, processedMessage.messageType != .plain { + signature = NSAttributedString.text(from: processedMessage.signature.message, style: .regular(12), color: .white) + } else { + signature = nil + } let sender = threadMessage.rawMessage.sender ?? "message_unknown_sender".localized let date = DateFormatter().formatDate(threadMessage.rawMessage.date) let isMessageRead = threadMessage.rawMessage.isMessageRead @@ -29,7 +34,7 @@ extension ThreadMessageSenderCellNode.Input { : .mainTextUnreadColor self.init( - signature: NSAttributedString.text(from: signature, style: .regular(12), color: .white), + signature: signature, signatureColor: threadMessage.processedMessage?.signature.color, signatureIcon: threadMessage.processedMessage?.signature.icon, sender: NSAttributedString.text(from: sender, style: style, color: textColor), diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 538dd5399..84c408aee 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -254,8 +254,10 @@ extension ThreadDetailsViewController { do { let matched = try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) if matched { - let verificationPubKeys = fetchVerificationPubKeys(for: input[indexPath.section-1].rawMessage.sender) + let sender = input[indexPath.section-1].rawMessage.sender + let verificationPubKeys = fetchVerificationPubKeys(for: sender) let processedMessage = try await messageService.decryptAndProcessMessage(mime: rawMimeData, + sender: sender, verificationPubKeys: verificationPubKeys) handleReceived(message: processedMessage, at: indexPath) } else { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 94220b72d..f6774629d 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -27,7 +27,7 @@ enum MessageFetchState { // MARK: - ProcessedMessage struct ProcessedMessage { - enum MessageType { + enum MessageType: Hashable { case error(MsgBlock.DecryptErr.ErrorType), encrypted, plain } @@ -151,10 +151,14 @@ final class MessageService { folder: folder, progressHandler: progressHandler ) - return try await decryptAndProcessMessage(mime: rawMimeData, verificationPubKeys: verificationPubKeys) + return try await decryptAndProcessMessage(mime: rawMimeData, + sender: input.sender, + verificationPubKeys: verificationPubKeys) } - func decryptAndProcessMessage(mime rawMimeData: Data, verificationPubKeys: [String]) async throws -> ProcessedMessage { + func decryptAndProcessMessage(mime rawMimeData: Data, + sender: String?, + verificationPubKeys: [String]) async throws -> ProcessedMessage { let keys = try await keyService.getPrvKeyInfo() guard keys.isNotEmpty else { throw MessageServiceError.emptyKeys @@ -170,7 +174,10 @@ final class MessageService { throw MessageServiceError.missingPassPhrase(rawMimeData) } - let processedMessage = try await processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) + let processedMessage = try await processMessage(rawMimeData: rawMimeData, + sender: sender, + with: decrypted, + keys: keys) switch processedMessage.messageType { case .error(let errorType): @@ -189,6 +196,7 @@ final class MessageService { private func processMessage( rawMimeData: Data, + sender: String?, with decrypted: CoreRes.ParseDecryptMsg, keys: [PrvKeyInfo] ) async throws -> ProcessedMessage { @@ -209,9 +217,14 @@ final class MessageService { messageType = .error(err?.type ?? .other) signature = .unknown } else { - if let longid = decrypted.blocks.first?.verifyRes?.signer, - let contact = await contactsService.findBy(longId: longid) { - signature = contact.isValid(longid: longid) ? .valid(contact.email) : .invalid + if let longid = decrypted.blocks.first?.verifyRes?.signer { + if let contact = await contactsService.findBy(longId: longid) { + signature = contact.isValid(longid: longid) ? .valid(contact.email) : .invalid + } else if let email = sender, let contact = try? await contactsService.searchContact(with: email) { + signature = contact.isValid(longid: longid) ? .valid(contact.email) : .invalid + } else { + signature = .unknown + } } else { signature = .unknown } diff --git a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift index 7a899f349..d1bc39b2b 100644 --- a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift +++ b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift @@ -27,6 +27,7 @@ final class ContactsServiceMock: ContactsServiceType { } } func searchContacts(query: String) -> [String] { [] } + func findBy(longId: String) async -> RecipientWithSortedPubKeys? { nil } func removePubKey(with fingerprint: String, for email: String) {} } diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index a992363c8..90ca7f72c 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -19,14 +19,16 @@ public final class ThreadMessageSenderCellNode: CellNode { public let date: NSAttributedString? public let isExpanded: Bool public let buttonColor: UIColor + public let nodeInsets: UIEdgeInsets - public init(signature: NSAttributedString, + public init(signature: NSAttributedString?, signatureColor: UIColor?, signatureIcon: String?, sender: NSAttributedString, date: NSAttributedString, isExpanded: Bool, - buttonColor: UIColor) { + buttonColor: UIColor, + nodeInsets: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 12)) { self.signature = signature self.signatureColor = signatureColor self.signatureIcon = signatureIcon @@ -34,6 +36,7 @@ public final class ThreadMessageSenderCellNode: CellNode { self.date = date self.isExpanded = isExpanded self.buttonColor = buttonColor + self.nodeInsets = nodeInsets } var replyImage: UIImage? { @@ -43,6 +46,7 @@ public final class ThreadMessageSenderCellNode: CellNode { let systemName = isExpanded ? "chevron.up" : "chevron.down" return createButtonImage(systemName: systemName) } + var shouldShowSignature: Bool { signature != nil } private func createButtonImage(systemName: String, pointSize: CGFloat = 18) -> UIImage? { let configuration = UIImage.SymbolConfiguration(pointSize: pointSize) @@ -104,9 +108,6 @@ public final class ThreadMessageSenderCellNode: CellNode { replyNode.style.preferredSize = CGSize(width: 44, height: 44) expandNode.style.preferredSize = CGSize(width: 18, height: 44) - let spacer = ASLayoutSpec() - spacer.style.flexGrow = 1.0 - let infoNode = ASStackLayoutSpec( direction: .vertical, spacing: 4, @@ -114,25 +115,43 @@ public final class ThreadMessageSenderCellNode: CellNode { alignItems: .start, children: [senderNode, dateNode] ) + infoNode.style.flexGrow = 1 + infoNode.style.flexShrink = 1 let senderSpec = ASStackLayoutSpec( direction: .horizontal, spacing: 4, justifyContent: .spaceBetween, alignItems: .start, - children: [infoNode, spacer, replyNode, expandNode] + children: [infoNode, replyNode, expandNode] ) - let contentSpec = ASStackLayoutSpec( - direction: .vertical, - spacing: 4, - justifyContent: .spaceBetween, - alignItems: .start, - children: [signatureNode, senderSpec] - ) + let contentSpec: ASStackLayoutSpec + if input.shouldShowSignature { + let spacer = ASLayoutSpec() + spacer.style.flexGrow = 1.0 + + let signatureSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .start, + children: [signatureNode, spacer] + ) + + contentSpec = ASStackLayoutSpec( + direction: .vertical, + spacing: 4, + justifyContent: .spaceBetween, + alignItems: .stretch, + children: [signatureSpec, senderSpec] + ) + } else { + contentSpec = senderSpec + } return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 12), + insets: input.nodeInsets, child: contentSpec ) } From 8c8c7d6890408fdd734ea2a2e0f36448ec14dd10 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 11 Nov 2021 17:12:25 +0200 Subject: [PATCH 04/15] issue #278 verify signature when processing message --- .../Message Provider/MessageService.swift | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index f6774629d..27b05befc 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -217,19 +217,10 @@ final class MessageService { messageType = .error(err?.type ?? .other) signature = .unknown } else { - if let longid = decrypted.blocks.first?.verifyRes?.signer { - if let contact = await contactsService.findBy(longId: longid) { - signature = contact.isValid(longid: longid) ? .valid(contact.email) : .invalid - } else if let email = sender, let contact = try? await contactsService.searchContact(with: email) { - signature = contact.isValid(longid: longid) ? .valid(contact.email) : .invalid - } else { - signature = .unknown - } - } else { - signature = .unknown - } text = decrypted.text messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain + signature = await verifySignature(longid: decrypted.blocks.first?.verifyRes?.signer, + email: sender) } return ProcessedMessage( @@ -241,6 +232,22 @@ final class MessageService { ) } + private func verifySignature(longid: String?, email: String?) async -> ProcessedMessage.MessageSignature { + guard let longid = longid else { return .unknown } + + if let contact = await contactsService.findBy(longId: longid) { + return check(longid: longid, for: contact) + } else if let email = email, let contact = try? await contactsService.searchContact(with: email) { + return check(longid: longid, for: contact) + } + + return .unknown + } + + private func check(longid: String, for recipient: RecipientWithSortedPubKeys) -> ProcessedMessage.MessageSignature { + recipient.isValid(longid: longid) ? .valid(recipient.email) : .invalid + } + private func getAttachments( blocks: [MsgBlock], keys: [PrvKeyInfo] From 5786189a8f4be19a408b0efb5eb3a26e3bffda93 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Thu, 11 Nov 2021 22:28:55 +0200 Subject: [PATCH 05/15] issue #278 add new states to MessageSignature --- .../Msg/MessageViewController.swift | 23 +--- .../Threads/ThreadDetailsViewController.swift | 17 +-- FlowCrypt/Core/CoreTypes.swift | 1 + .../Mail Provider/Imap/Imap+session.swift | 2 +- .../Message Provider/MessageService.swift | 102 +++++++++++------- .../UserMailSessionProvider.swift | 6 +- .../Models/RecipientWithSortedPubKeys.swift | 4 +- 7 files changed, 77 insertions(+), 78 deletions(-) diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index 3d8e2d3ca..d5ec9e0c6 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -52,7 +52,6 @@ final class MessageViewController: TableNodeViewController { messageService: MessageService = MessageService(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, messageProvider: MessageProvider = MailProvider.shared.messageProvider, - contactsService: ContactsServiceType = ContactsService(), decorator: MessageViewDecorator = MessageViewDecorator(dateFormatter: DateFormatter()), trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), filesManager: FilesManagerType = FilesManager(), @@ -67,8 +66,7 @@ final class MessageViewController: TableNodeViewController { self.filesManager = filesManager self.serviceActor = ServiceActor( messageService: messageService, - messageProvider: messageProvider, - contactsService: contactsService + messageProvider: messageProvider ) super.init(node: TableNode()) @@ -244,7 +242,7 @@ extension MessageViewController { text: String(data: rawMimeData, encoding: .utf8) ?? "", attachments: [], messageType: .encrypted, - signature: .unknown) + signature: .missingPubkey("error_key_mismatch".localized)) self.handleReceivedMessage() } ) @@ -413,14 +411,11 @@ extension MessageViewController: ASTableDelegate, ASTableDataSource { private actor ServiceActor { private let messageService: MessageService private let messageProvider: MessageProvider - private let contactsService: ContactsServiceType init(messageService: MessageService, - messageProvider: MessageProvider, - contactsService: ContactsServiceType) { + messageProvider: MessageProvider) { self.messageService = messageService self.messageProvider = messageProvider - self.contactsService = contactsService } func fetchDecryptAndRenderMsg(message: Message, path: String, @@ -439,15 +434,7 @@ private actor ServiceActor { } func decryptAndProcessMessage(mime: Data, sender: String?) async throws -> ProcessedMessage { - let pubKeys: [String] - if let sender = sender { - pubKeys = contactsService.retrievePubKeys(for: sender) - } else { - pubKeys = [] - } - - return try await messageService.decryptAndProcessMessage(mime: mime, - sender: sender, - verificationPubKeys: pubKeys) + try await messageService.decryptAndProcessMessage(mime: mime, + sender: sender) } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 84c408aee..aadbab6ed 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -30,7 +30,6 @@ final class ThreadDetailsViewController: TableNodeViewController { case thread, message } - private let contactsService: ContactsServiceType private let messageService: MessageService private let messageOperationsProvider: MessageOperationsProvider private let threadOperationsProvider: MessagesThreadOperationsProvider @@ -50,7 +49,6 @@ final class ThreadDetailsViewController: TableNodeViewController { ) init( - contactsService: ContactsServiceType = ContactsService(), messageService: MessageService = MessageService(), trashFolderProvider: TrashFolderProviderType = TrashFolderProvider(), messageOperationsProvider: MessageOperationsProvider = MailProvider.shared.messageOperationsProvider, @@ -59,7 +57,6 @@ final class ThreadDetailsViewController: TableNodeViewController { filesManager: FilesManagerType = FilesManager(), completion: @escaping MessageActionCompletion ) { - self.contactsService = contactsService self.messageService = messageService self.threadOperationsProvider = threadOperationsProvider self.messageOperationsProvider = messageOperationsProvider @@ -163,7 +160,6 @@ extension ThreadDetailsViewController { extension ThreadDetailsViewController { private func fetchDecryptAndRenderMsg(at indexPath: IndexPath) { let message = input[indexPath.section-1].rawMessage - let verificationPubKeys = fetchVerificationPubKeys(for: message.sender) logger.logInfo("Start loading message") handleFetchProgress(state: .fetch) @@ -173,7 +169,6 @@ extension ThreadDetailsViewController { let processedMessage = try await messageService.getAndProcessMessage( with: message, folder: thread.path, - verificationPubKeys: verificationPubKeys, progressHandler: { [weak self] in self?.handleFetchProgress(state: $0) } ) handleReceived(message: processedMessage, at: indexPath) @@ -255,10 +250,8 @@ extension ThreadDetailsViewController { let matched = try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) if matched { let sender = input[indexPath.section-1].rawMessage.sender - let verificationPubKeys = fetchVerificationPubKeys(for: sender) let processedMessage = try await messageService.decryptAndProcessMessage(mime: rawMimeData, - sender: sender, - verificationPubKeys: verificationPubKeys) + sender: sender) handleReceived(message: processedMessage, at: indexPath) } else { handleWrongPassPhrase(for: rawMimeData, with: passPhrase, at: indexPath) @@ -279,14 +272,6 @@ extension ThreadDetailsViewController { updateSpinner(label: "decrypting_title".localized) } } - - private func fetchVerificationPubKeys(for sender: String?) -> [String] { - if let sender = sender { - return contactsService.retrievePubKeys(for: sender) - } else { - return [] - } - } } extension ThreadDetailsViewController: MessageActionsHandler { diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index a72f7f9d4..17d5e944c 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -195,6 +195,7 @@ struct MsgBlock: Decodable { struct VerifyRes: Decodable { let match: Bool? let signer: String? + let error: String? } enum BlockType: String, Decodable { diff --git a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift index a030ffd02..ccc00e212 100644 --- a/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift +++ b/FlowCrypt/Functionality/Mail Provider/Imap/Imap+session.swift @@ -40,7 +40,7 @@ extension Imap { } } - func connectImap(session: IMAPSession) async throws -> Void { + func connectImap(session: IMAPSession) async throws { return try await withCheckedThrowingContinuation { continuation in MCOIMAPSession(session: session) .log() diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 27b05befc..3c5187625 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -32,37 +32,41 @@ struct ProcessedMessage { } enum MessageSignature { - case valid(String), invalid, unknown + case good, unsigned, error(String), missingPubkey(String), bad var message: String { switch self { - case .valid(let email): - return "Signed by \(email)" - case .invalid: - return "Bad signature - cannot trust authenticity of this message" - case .unknown: - return "Signature not verified - unknown sender public key" + case .good: + return "signed" + case .unsigned: + return "not signed" + case .error(let message): + return "cannot verify signature: \(message)" + case .missingPubkey(let longid): + return "cannot verify signature: no Public Key \(longid)" + case .bad: + return "bad signature" } } var icon: String { switch self { - case .valid: + case .good: return "lock" - case .invalid: + case .error, .missingPubkey: return "exclamationmark.triangle" - case .unknown: + case .unsigned, .bad: return "xmark" } } var color: UIColor { switch self { - case .valid: + case .good: return .main - case .invalid: + case .error, .missingPubkey: return .warningColor - case .unknown: + case .unsigned, .bad: return .errorColor } } @@ -82,7 +86,7 @@ extension ProcessedMessage { text: "loading_title".localized + "...", attachments: [], messageType: .plain, - signature: .unknown + signature: .unsigned ) } @@ -143,7 +147,6 @@ final class MessageService { func getAndProcessMessage( with input: Message, folder: String, - verificationPubKeys: [String], progressHandler: ((MessageFetchState) -> Void)? ) async throws -> ProcessedMessage { let rawMimeData = try await messageProvider.fetchMsg( @@ -152,17 +155,16 @@ final class MessageService { progressHandler: progressHandler ) return try await decryptAndProcessMessage(mime: rawMimeData, - sender: input.sender, - verificationPubKeys: verificationPubKeys) + sender: input.sender) } func decryptAndProcessMessage(mime rawMimeData: Data, - sender: String?, - verificationPubKeys: [String]) async throws -> ProcessedMessage { + sender: String?) async throws -> ProcessedMessage { let keys = try await keyService.getPrvKeyInfo() guard keys.isNotEmpty else { throw MessageServiceError.emptyKeys } + let verificationPubKeys = fetchVerificationPubKeys(for: sender) let decrypted = try await core.parseDecryptMsg( encrypted: rawMimeData, keys: keys, @@ -215,12 +217,14 @@ final class MessageService { let err = decryptErrBlock.decryptErr?.error text = "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)" messageType = .error(err?.type ?? .other) - signature = .unknown + signature = .error(rawMsg) } else { text = decrypted.text messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain - signature = await verifySignature(longid: decrypted.blocks.first?.verifyRes?.signer, - email: sender) + signature = await evaluateSignatureVerificationResult( + signature: decrypted.blocks.first?.verifyRes, + sender: sender + ) } return ProcessedMessage( @@ -232,22 +236,6 @@ final class MessageService { ) } - private func verifySignature(longid: String?, email: String?) async -> ProcessedMessage.MessageSignature { - guard let longid = longid else { return .unknown } - - if let contact = await contactsService.findBy(longId: longid) { - return check(longid: longid, for: contact) - } else if let email = email, let contact = try? await contactsService.searchContact(with: email) { - return check(longid: longid, for: contact) - } - - return .unknown - } - - private func check(longid: String, for recipient: RecipientWithSortedPubKeys) -> ProcessedMessage.MessageSignature { - recipient.isValid(longid: longid) ? .valid(recipient.email) : .invalid - } - private func getAttachments( blocks: [MsgBlock], keys: [PrvKeyInfo] @@ -288,6 +276,44 @@ final class MessageService { } } +// MARK: - Message verification +extension MessageService { + private func fetchVerificationPubKeys(for sender: String?) -> [String] { + if let sender = sender { + return contactsService.retrievePubKeys(for: sender) + } else { + return [] + } + } + + private func evaluateSignatureVerificationResult( + signature: MsgBlock.VerifyRes?, + sender: String? + ) async -> ProcessedMessage.MessageSignature { + guard let signature = signature else { return .unsigned } + + if let error = signature.error { + return .error(error) + } + + guard let signer = signature.signer else { return .unsigned } + + var pubKey: PubKey? + + if let contact = await contactsService.findBy(longId: signer) { + pubKey = contact.pubKey(with: signer) + } else if let email = sender, let contact = try? await contactsService.searchContact(with: email) { + pubKey = contact.pubKey(with: signer) + } + + guard pubKey != nil && signature.match != nil else { return .missingPubkey(signer) } + + guard signature.match == true else { return .bad } + + return .good + } +} + private extension MessageAttachment { init(block: MsgBlock) { self.name = block.attMeta?.name ?? "Attachment" diff --git a/FlowCrypt/Functionality/Mail Provider/UsersMailSession Provider/UserMailSessionProvider.swift b/FlowCrypt/Functionality/Mail Provider/UsersMailSession Provider/UserMailSessionProvider.swift index 70cddf873..a67f7cbdc 100644 --- a/FlowCrypt/Functionality/Mail Provider/UsersMailSession Provider/UserMailSessionProvider.swift +++ b/FlowCrypt/Functionality/Mail Provider/UsersMailSession Provider/UserMailSessionProvider.swift @@ -7,13 +7,13 @@ // protocol UsersMailSessionProvider { - func renewSession() async throws -> Void + func renewSession() async throws } // MARK: - GmailService extension GmailService: UsersMailSessionProvider { @discardableResult - func renewSession() async throws -> Void { + func renewSession() async throws { try await userService.renewSession() } } @@ -21,7 +21,7 @@ extension GmailService: UsersMailSessionProvider { // MARK: - Imap extension Imap: UsersMailSessionProvider { @discardableResult - func renewSession() async throws -> Void { + func renewSession() async throws { try await self.setupSession() } } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift index afb8b485b..06cb6ce85 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift @@ -49,8 +49,8 @@ extension RecipientWithSortedPubKeys { var keyState: PubKeyState { pubKeys.first?.keyState ?? .empty } var activePubKeys: [PubKey] { pubKeys.filter { $0.keyState == .active } } - func isValid(longid: String) -> Bool { - activePubKeys.contains(where: { $0.longids.contains(longid) }) + func pubKey(with longid: String) -> PubKey? { + activePubKeys.first(where: { $0.longids.contains(longid) }) } private var sortedPubKeys: [PubKey] { From e304d431539c2d5bb7634672ca01ae110fae9e0d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 12 Nov 2021 12:31:04 +0200 Subject: [PATCH 06/15] issue #278 update signature verification --- .../Message Provider/MessageService.swift | 40 ++++++++----------- .../LocalContactsProvider.swift | 3 +- .../Models/RecipientWithSortedPubKeys.swift | 2 +- .../ThreadMessageSenderCellNode.swift | 4 +- 4 files changed, 20 insertions(+), 29 deletions(-) diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 3c5187625..fbbf28d6a 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -164,7 +164,7 @@ final class MessageService { guard keys.isNotEmpty else { throw MessageServiceError.emptyKeys } - let verificationPubKeys = fetchVerificationPubKeys(for: sender) + let verificationPubKeys = try await fetchVerificationPubKeys(for: sender) let decrypted = try await core.parseDecryptMsg( encrypted: rawMimeData, keys: keys, @@ -217,13 +217,12 @@ final class MessageService { let err = decryptErrBlock.decryptErr?.error text = "Could not decrypt:\n\(err?.type.rawValue ?? "UNKNOWN"): \(err?.message ?? "??")\n\n\n\(rawMsg)" messageType = .error(err?.type ?? .other) - signature = .error(rawMsg) + signature = .error("processing error") } else { text = decrypted.text messageType = decrypted.replyType == CoreRes.ReplyType.encrypted ? .encrypted : .plain signature = await evaluateSignatureVerificationResult( - signature: decrypted.blocks.first?.verifyRes, - sender: sender + signature: decrypted.blocks.first?.verifyRes ) } @@ -278,35 +277,28 @@ final class MessageService { // MARK: - Message verification extension MessageService { - private func fetchVerificationPubKeys(for sender: String?) -> [String] { - if let sender = sender { - return contactsService.retrievePubKeys(for: sender) - } else { - return [] - } + private func fetchVerificationPubKeys(for sender: String?) async throws -> [String] { + guard let sender = sender else { return [] } + + let pubKeys = contactsService.retrievePubKeys(for: sender) + if pubKeys.isNotEmpty { return pubKeys } + + guard let contact = try? await contactsService.searchContact(with: sender) + else { return [] } + + return contact.activePubKeys.map(\.armored) } private func evaluateSignatureVerificationResult( - signature: MsgBlock.VerifyRes?, - sender: String? + signature: MsgBlock.VerifyRes? ) async -> ProcessedMessage.MessageSignature { guard let signature = signature else { return .unsigned } - if let error = signature.error { - return .error(error) - } + if let error = signature.error { return .error(error) } guard let signer = signature.signer else { return .unsigned } - var pubKey: PubKey? - - if let contact = await contactsService.findBy(longId: signer) { - pubKey = contact.pubKey(with: signer) - } else if let email = sender, let contact = try? await contactsService.searchContact(with: email) { - pubKey = contact.pubKey(with: signer) - } - - guard pubKey != nil && signature.match != nil else { return .missingPubkey(signer) } + guard signature.match != nil else { return .missingPubkey(signer) } guard signature.match == true else { return .bad } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index b467e1fb9..698db588e 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -43,8 +43,7 @@ extension LocalContactsProvider: LocalContactsProviderType { } func retrievePubKeys(for email: String) -> [String] { - find(with: email)?.pubKeys - .map { $0.armored } ?? [] + find(with: email)?.pubKeys.map(\.armored) ?? [] } func findBy(longid: String) async -> RecipientWithSortedPubKeys? { diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift index 06cb6ce85..51c0f46e9 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift @@ -68,7 +68,7 @@ extension RecipientWithSortedPubKeys { return expire1 > expire2 }) } - + } extension RecipientWithSortedPubKeys: Equatable { diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index 90ca7f72c..a0b1cac78 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -127,7 +127,7 @@ public final class ThreadMessageSenderCellNode: CellNode { ) let contentSpec: ASStackLayoutSpec - if input.shouldShowSignature { + if input.isExpanded && input.shouldShowSignature { let spacer = ASLayoutSpec() spacer.style.flexGrow = 1.0 @@ -144,7 +144,7 @@ public final class ThreadMessageSenderCellNode: CellNode { spacing: 4, justifyContent: .spaceBetween, alignItems: .stretch, - children: [signatureSpec, senderSpec] + children: [senderSpec, signatureSpec] ) } else { contentSpec = senderSpec From 742db134fe3bc42a7f0bfa660bcf5b2fcaf3e627 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 12 Nov 2021 13:34:44 +0200 Subject: [PATCH 07/15] issue #278 add 'encrypted' badge to messages --- FlowCrypt.xcodeproj/project.pbxproj | 4 + .../Threads/ThreadDetailsDecorator.swift | 11 +-- .../Message Provider/MessageService.swift | 11 +-- .../Resources/en.lproj/Localizable.strings | 7 ++ .../ThreadMessageSenderCellNode.swift | 84 +++++-------------- FlowCryptUI/Nodes/BadgeNode.swift | 64 ++++++++++++++ 6 files changed, 108 insertions(+), 73 deletions(-) create mode 100644 FlowCryptUI/Nodes/BadgeNode.swift diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index dfe0c4c0d..cc2bb5be5 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ 51C0C1F2271987DB000C9738 /* Toast in Frameworks */ = {isa = PBXBuildFile; productRef = 51C0C1F1271987DB000C9738 /* Toast */; }; 51DA5BD62721AB07001C4359 /* PubKeyState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD52721AB07001C4359 /* PubKeyState.swift */; }; 51DA5BDA2722C82E001C4359 /* RecipientTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DA5BD92722C82E001C4359 /* RecipientTests.swift */; }; + 51DAD9BD273E7DD20076CBA7 /* BadgeNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */; }; 51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DE2FED2714DA0400916222 /* ContactKeyCellNode.swift */; }; 51E13F12270F92BA00F287CA /* IDZSwiftCommonCrypto in Frameworks */ = {isa = PBXBuildFile; productRef = 51E13F11270F92BA00F287CA /* IDZSwiftCommonCrypto */; }; 51E13F15270F92F200F287CA /* BigInt in Frameworks */ = {isa = PBXBuildFile; productRef = 51E13F14270F92F200F287CA /* BigInt */; }; @@ -493,6 +494,7 @@ 51B4AE5227144E590001F33B /* PubKey.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKey.swift; sourceTree = ""; }; 51DA5BD52721AB07001C4359 /* PubKeyState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PubKeyState.swift; sourceTree = ""; }; 51DA5BD92722C82E001C4359 /* RecipientTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientTests.swift; sourceTree = ""; }; + 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BadgeNode.swift; sourceTree = ""; }; 51DE2FED2714DA0400916222 /* ContactKeyCellNode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactKeyCellNode.swift; sourceTree = ""; }; 51E1673C270DAFF900D27C52 /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 51E4F0B427348E310017DABB /* Error+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Error+Extension.swift"; sourceTree = ""; }; @@ -2008,6 +2010,7 @@ 9F4453C1236B9273005D7D05 /* TextFieldNode.swift */, D24FAFAA2520BFAE00BF46C5 /* CheckBoxNode.swift */, 50531BE32629B9A80039BAE9 /* AttachmentNode.swift */, + 51DAD9BC273E7DD20076CBA7 /* BadgeNode.swift */, ); path = Nodes; sourceTree = ""; @@ -2745,6 +2748,7 @@ 5133B6742716E5EA00C95463 /* LabelCellNode.swift in Sources */, D211CE7123FC35AC00D1CE38 /* TextFieldCellNode.swift in Sources */, 9FA19890253C841F008C9CF2 /* TableViewController.swift in Sources */, + 51DAD9BD273E7DD20076CBA7 /* BadgeNode.swift in Sources */, D2CCD1FA247C50DA00D21F9C /* UIColorExtension.swift in Sources */, 51DE2FEE2714DA0400916222 /* ContactKeyCellNode.swift in Sources */, D2A9CA432426210200E1D898 /* SetupTitleNode.swift in Sources */, diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift index a23166361..afdc7d4b0 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsDecorator.swift @@ -11,11 +11,11 @@ import UIKit extension ThreadMessageSenderCellNode.Input { init(threadMessage: ThreadDetailsViewController.Input) { - let signature: NSAttributedString? - if let processedMessage = threadMessage.processedMessage, processedMessage.messageType != .plain { - signature = NSAttributedString.text(from: processedMessage.signature.message, style: .regular(12), color: .white) + let signature: String + if let processedMessage = threadMessage.processedMessage, processedMessage.messageType == .encrypted { + signature = processedMessage.signature.message } else { - signature = nil + signature = "message_not_signed".localized } let sender = threadMessage.rawMessage.sender ?? "message_unknown_sender".localized let date = DateFormatter().formatDate(threadMessage.rawMessage.date) @@ -34,12 +34,13 @@ extension ThreadMessageSenderCellNode.Input { : .mainTextUnreadColor self.init( - signature: signature, + signature: NSAttributedString.text(from: signature, style: .regular(12), color: .white), signatureColor: threadMessage.processedMessage?.signature.color, signatureIcon: threadMessage.processedMessage?.signature.icon, sender: NSAttributedString.text(from: sender, style: style, color: textColor), date: NSAttributedString.text(from: date, style: style, color: dateColor), isExpanded: threadMessage.isExpanded, + isEncrypted: threadMessage.processedMessage?.messageType == .encrypted, buttonColor: .messageButtonColor ) } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index fbbf28d6a..478fc8ee4 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -37,15 +37,16 @@ struct ProcessedMessage { var message: String { switch self { case .good: - return "signed" + return "message_signed".localized case .unsigned: - return "not signed" + return "message_not_signed".localized case .error(let message): - return "cannot verify signature: \(message)" + return "message_signature_verify_error".localizeWithArguments(message) case .missingPubkey(let longid): - return "cannot verify signature: no Public Key \(longid)" + let message = "message_missing_pubkey".localizeWithArguments(longid) + return "message_signature_verify_error".localizeWithArguments(message) case .bad: - return "bad signature" + return "message_bad_signature".localized } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 15be9c74b..58ae9008e 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -29,6 +29,13 @@ "message_attachment_saved_successfully_message" = "Your attachment was saved in Files. Would you like to open it?"; "message_attachment_saved_with_error" = "Attachment could not be saved."; "message_open_anyway" = "Open anyway"; +"message_encrypted" = "encrypted"; +"message_not_encrypted" = "not encrypted"; +"message_signed" = "signed"; +"message_not_signed" = "not signed"; +"message_missing_pubkey" = "no public key %@"; +"message_signature_verify_error" = "cannot verify signature: %@"; +"message_bad_signature" = "bad_signature"; // REPLY "reply_title" = "Your reply"; // not used diff --git a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift index a0b1cac78..09cdf70d0 100644 --- a/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift +++ b/FlowCryptUI/Cell Nodes/ThreadMessageSenderCellNode.swift @@ -18,6 +18,7 @@ public final class ThreadMessageSenderCellNode: CellNode { public let sender: NSAttributedString public let date: NSAttributedString? public let isExpanded: Bool + public let isEncrypted: Bool public let buttonColor: UIColor public let nodeInsets: UIEdgeInsets @@ -27,6 +28,7 @@ public final class ThreadMessageSenderCellNode: CellNode { sender: NSAttributedString, date: NSAttributedString, isExpanded: Bool, + isEncrypted: Bool, buttonColor: UIColor, nodeInsets: UIEdgeInsets = UIEdgeInsets(top: 16, left: 16, bottom: 16, right: 12)) { self.signature = signature @@ -35,6 +37,7 @@ public final class ThreadMessageSenderCellNode: CellNode { self.sender = sender self.date = date self.isExpanded = isExpanded + self.isEncrypted = isEncrypted self.buttonColor = buttonColor self.nodeInsets = nodeInsets } @@ -46,7 +49,6 @@ public final class ThreadMessageSenderCellNode: CellNode { let systemName = isExpanded ? "chevron.up" : "chevron.down" return createButtonImage(systemName: systemName) } - var shouldShowSignature: Bool { signature != nil } private func createButtonImage(systemName: String, pointSize: CGFloat = 18) -> UIImage? { let configuration = UIImage.SymbolConfiguration(pointSize: pointSize) @@ -54,13 +56,23 @@ public final class ThreadMessageSenderCellNode: CellNode { } } - private lazy var signatureNode: ThreadMessageSignatureNode = { - let input = ThreadMessageSignatureNode.Input( - signature: input.signature, - color: input.signatureColor, - icon: input.signatureIcon + private lazy var encryptionNode: BadgeNode = { + let text = input.isEncrypted ? "message_encrypted".localized : "message_not_encrypted".localized + let input = BadgeNode.Input( + icon: input.isEncrypted ? "lock" : "lock.open", + text: NSAttributedString.text(from: text, style: .regular(12), color: .white), + color: input.isEncrypted ? .main : .warningColor ) - return ThreadMessageSignatureNode(input: input) + return BadgeNode(input: input) + }() + + private lazy var signatureNode: BadgeNode = { + let input = BadgeNode.Input( + icon: input.signatureIcon, + text: input.signature, + color: input.signatureColor + ) + return BadgeNode(input: input) }() private let senderNode = ASTextNode2() @@ -127,7 +139,7 @@ public final class ThreadMessageSenderCellNode: CellNode { ) let contentSpec: ASStackLayoutSpec - if input.isExpanded && input.shouldShowSignature { + if input.isExpanded { let spacer = ASLayoutSpec() spacer.style.flexGrow = 1.0 @@ -136,7 +148,7 @@ public final class ThreadMessageSenderCellNode: CellNode { spacing: 4, justifyContent: .spaceBetween, alignItems: .start, - children: [signatureNode, spacer] + children: [encryptionNode, signatureNode, spacer] ) contentSpec = ASStackLayoutSpec( @@ -156,57 +168,3 @@ public final class ThreadMessageSenderCellNode: CellNode { ) } } - -private final class ThreadMessageSignatureNode: ASDisplayNode { - public struct Input { - public let signature: NSAttributedString? - public let color: UIColor? - public let icon: String? - - public init(signature: NSAttributedString?, - color: UIColor?, - icon: String?) { - self.signature = signature - self.color = color - self.icon = icon - } - } - - private lazy var iconNode: ASImageNode? = { - guard let icon = input.icon else { return nil } - let imageNode = ASImageNode() - let configuration = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium) - imageNode.image = UIImage(systemName: icon, withConfiguration: configuration) - imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.white) - return imageNode - }() - - private let signatureNode = ASTextNode2() - private let input: ThreadMessageSignatureNode.Input - - init(input: ThreadMessageSignatureNode.Input) { - self.input = input - super.init() - - automaticallyManagesSubnodes = true - - signatureNode.attributedText = input.signature - backgroundColor = input.color - cornerRadius = 4 - } - - public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { - let contentSpec = ASStackLayoutSpec( - direction: .horizontal, - spacing: 2, - justifyContent: .spaceBetween, - alignItems: .center, - children: [iconNode, signatureNode].compactMap { $0 } - ) - - return ASInsetLayoutSpec( - insets: UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4), - child: contentSpec - ) - } -} diff --git a/FlowCryptUI/Nodes/BadgeNode.swift b/FlowCryptUI/Nodes/BadgeNode.swift new file mode 100644 index 000000000..8dc587a4e --- /dev/null +++ b/FlowCryptUI/Nodes/BadgeNode.swift @@ -0,0 +1,64 @@ +// +// BadgeNode.swift +// FlowCryptUI +// +// Created by Roma Sosnovsky on 12/11/21 +// Copyright © 2017-present FlowCrypt a. s. All rights reserved. +// + + +import AsyncDisplayKit + +public final class BadgeNode: ASDisplayNode { + public struct Input { + public let icon: String? + public let text: NSAttributedString? + public let color: UIColor? + + public init(icon: String?, + text: NSAttributedString?, + color: UIColor?) { + self.icon = icon + self.text = text + self.color = color + } + } + + private lazy var iconNode: ASImageNode? = { + guard let icon = input.icon else { return nil } + let imageNode = ASImageNode() + let configuration = UIImage.SymbolConfiguration(pointSize: 10, weight: .medium) + imageNode.image = UIImage(systemName: icon, withConfiguration: configuration) + imageNode.imageModificationBlock = ASImageNodeTintColorModificationBlock(.white) + return imageNode + }() + + private let textNode = ASTextNode2() + private let input: BadgeNode.Input + + init(input: BadgeNode.Input) { + self.input = input + super.init() + + automaticallyManagesSubnodes = true + + textNode.attributedText = input.text + backgroundColor = input.color + cornerRadius = 4 + } + + public override func layoutSpecThatFits(_: ASSizeRange) -> ASLayoutSpec { + let contentSpec = ASStackLayoutSpec( + direction: .horizontal, + spacing: 2, + justifyContent: .spaceBetween, + alignItems: .center, + children: [iconNode, textNode].compactMap { $0 } + ) + + return ASInsetLayoutSpec( + insets: UIEdgeInsets(top: 2, left: 4, bottom: 2, right: 4), + child: contentSpec + ) + } +} From e7ac65eef8063d7faecec66aab30166ccb5e3110 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Fri, 12 Nov 2021 14:04:22 +0200 Subject: [PATCH 08/15] issue #278 return all keys in fetchVerificationPubKeys --- FlowCrypt/Core/CoreTypes.swift | 2 ++ .../Mail Provider/Message Provider/MessageService.swift | 2 +- .../Models/RecipientWithSortedPubKeys.swift | 4 ---- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index 17d5e944c..10ae43877 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -196,6 +196,8 @@ struct MsgBlock: Decodable { let match: Bool? let signer: String? let error: String? + let mixed: Bool? + let partial: Bool? } enum BlockType: String, Decodable { diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 478fc8ee4..eebe4d968 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -287,7 +287,7 @@ extension MessageService { guard let contact = try? await contactsService.searchContact(with: sender) else { return [] } - return contact.activePubKeys.map(\.armored) + return contact.pubKeys.map(\.armored) } private func evaluateSignatureVerificationResult( diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift index 51c0f46e9..c511a8e9a 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/Models/RecipientWithSortedPubKeys.swift @@ -49,10 +49,6 @@ extension RecipientWithSortedPubKeys { var keyState: PubKeyState { pubKeys.first?.keyState ?? .empty } var activePubKeys: [PubKey] { pubKeys.filter { $0.keyState == .active } } - func pubKey(with longid: String) -> PubKey? { - activePubKeys.first(where: { $0.longids.contains(longid) }) - } - private var sortedPubKeys: [PubKey] { _pubKeys .sorted(by: { key1, key2 in From bd13d3de6fc46bc8e93bb1bc4400cff13b09974a Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Sat, 13 Nov 2021 23:18:28 +0200 Subject: [PATCH 09/15] issue #278 add signature verification retry --- .../Msg/MessageViewController.swift | 6 +++-- .../Threads/ThreadDetailsViewController.swift | 27 ++++++++++++++++--- .../Message Provider/MessageService.swift | 25 +++++++++++------ .../Resources/en.lproj/Localizable.strings | 3 ++- 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift index d5ec9e0c6..aff594ec2 100644 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ b/FlowCrypt/Controllers/Msg/MessageViewController.swift @@ -434,7 +434,9 @@ private actor ServiceActor { } func decryptAndProcessMessage(mime: Data, sender: String?) async throws -> ProcessedMessage { - try await messageService.decryptAndProcessMessage(mime: mime, - sender: sender) + let message = try await messageService.decryptAndProcessMessage(mime: mime, + sender: sender, + onlyLocalKeys: true) + return message } } diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index aadbab6ed..ec5261f18 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -166,11 +166,18 @@ extension ThreadDetailsViewController { Task { do { - let processedMessage = try await messageService.getAndProcessMessage( + var processedMessage = try await messageService.getAndProcessMessage( with: message, folder: thread.path, + onlyLocalKeys: true, progressHandler: { [weak self] in self?.handleFetchProgress(state: $0) } ) + if processedMessage.signature != .good { + processedMessage.signature = .pending + tryToFetchSenderPubKeys(message: message, + folder: thread.path, + indexPath: indexPath) + } handleReceived(message: processedMessage, at: indexPath) } catch { handleError(error, at: indexPath) @@ -250,8 +257,10 @@ extension ThreadDetailsViewController { let matched = try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) if matched { let sender = input[indexPath.section-1].rawMessage.sender - let processedMessage = try await messageService.decryptAndProcessMessage(mime: rawMimeData, - sender: sender) + let processedMessage = try await messageService.decryptAndProcessMessage( + mime: rawMimeData, + sender: sender, + onlyLocalKeys: false) handleReceived(message: processedMessage, at: indexPath) } else { handleWrongPassPhrase(for: rawMimeData, with: passPhrase, at: indexPath) @@ -262,6 +271,18 @@ extension ThreadDetailsViewController { } } + private func tryToFetchSenderPubKeys(message: Message, folder: String, indexPath: IndexPath) { + Task { + let processedMessage = try await messageService.getAndProcessMessage( + with: message, + folder: thread.path, + onlyLocalKeys: false, + progressHandler: { _ in } + ) + handleReceived(message: processedMessage, at: indexPath) + } + } + private func handleFetchProgress(state: MessageFetchState) { switch state { case .fetch: diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index eebe4d968..d48351aa1 100644 --- a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift +++ b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift @@ -31,8 +31,8 @@ struct ProcessedMessage { case error(MsgBlock.DecryptErr.ErrorType), encrypted, plain } - enum MessageSignature { - case good, unsigned, error(String), missingPubkey(String), bad + enum MessageSignature: Hashable { + case good, unsigned, error(String), missingPubkey(String), bad, pending var message: String { switch self { @@ -47,6 +47,8 @@ struct ProcessedMessage { return "message_signature_verify_error".localizeWithArguments(message) case .bad: return "message_bad_signature".localized + case .pending: + return "message_signature_pending".localized } } @@ -58,6 +60,8 @@ struct ProcessedMessage { return "exclamationmark.triangle" case .unsigned, .bad: return "xmark" + case .pending: + return "clock" } } @@ -69,6 +73,8 @@ struct ProcessedMessage { return .warningColor case .unsigned, .bad: return .errorColor + case .pending: + return .lightGray } } } @@ -77,7 +83,7 @@ struct ProcessedMessage { let text: String let attachments: [MessageAttachment] let messageType: MessageType - let signature: MessageSignature + var signature: MessageSignature } extension ProcessedMessage { @@ -148,6 +154,7 @@ final class MessageService { func getAndProcessMessage( with input: Message, folder: String, + onlyLocalKeys: Bool, progressHandler: ((MessageFetchState) -> Void)? ) async throws -> ProcessedMessage { let rawMimeData = try await messageProvider.fetchMsg( @@ -156,16 +163,18 @@ final class MessageService { progressHandler: progressHandler ) return try await decryptAndProcessMessage(mime: rawMimeData, - sender: input.sender) + sender: input.sender, + onlyLocalKeys: onlyLocalKeys) } func decryptAndProcessMessage(mime rawMimeData: Data, - sender: String?) async throws -> ProcessedMessage { + sender: String?, + onlyLocalKeys: Bool) async throws -> ProcessedMessage { let keys = try await keyService.getPrvKeyInfo() guard keys.isNotEmpty else { throw MessageServiceError.emptyKeys } - let verificationPubKeys = try await fetchVerificationPubKeys(for: sender) + let verificationPubKeys = try await fetchVerificationPubKeys(for: sender, onlyLocal: onlyLocalKeys) let decrypted = try await core.parseDecryptMsg( encrypted: rawMimeData, keys: keys, @@ -278,11 +287,11 @@ final class MessageService { // MARK: - Message verification extension MessageService { - private func fetchVerificationPubKeys(for sender: String?) async throws -> [String] { + private func fetchVerificationPubKeys(for sender: String?, onlyLocal: Bool) async throws -> [String] { guard let sender = sender else { return [] } let pubKeys = contactsService.retrievePubKeys(for: sender) - if pubKeys.isNotEmpty { return pubKeys } + if pubKeys.isNotEmpty || onlyLocal { return pubKeys } guard let contact = try? await contactsService.searchContact(with: sender) else { return [] } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 58ae9008e..07c24cc08 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -35,7 +35,8 @@ "message_not_signed" = "not signed"; "message_missing_pubkey" = "no public key %@"; "message_signature_verify_error" = "cannot verify signature: %@"; -"message_bad_signature" = "bad_signature"; +"message_bad_signature" = "bad signature"; +"message_signature_pending" = "verifying signature..."; // REPLY "reply_title" = "Your reply"; // not used From e1653fe45fb4631925a00b64064c468c71db717d Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 15 Nov 2021 12:47:55 +0200 Subject: [PATCH 10/15] issue #278 separate remote pub keys fetching --- .../Threads/ThreadDetailsViewController.swift | 81 +++++++++---------- .../LocalContactsProvider.swift | 2 +- .../Realm Models/RecipientRealmObject.swift | 2 +- 3 files changed, 41 insertions(+), 44 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index ec5261f18..4c07270da 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -189,18 +189,25 @@ extension ThreadDetailsViewController { hideSpinner() let messageIndex = indexPath.section - 1 + let shouldAnimate = input[messageIndex].processedMessage == nil + input[messageIndex].processedMessage = processedMessage - input[messageIndex].isExpanded = !input[messageIndex].isExpanded - markAsRead(at: messageIndex) - UIView.animate( - withDuration: 0.2, - animations: { - self.node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) - }, - completion: { [weak self] _ in - self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) - }) + if shouldAnimate { + input[messageIndex].isExpanded = true + markAsRead(at: messageIndex) + + UIView.animate( + withDuration: 0.2, + animations: { + self.node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) + }, + completion: { [weak self] _ in + self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) + }) + } else { + node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) + } } private func handleError(_ error: Error, at indexPath: IndexPath) { @@ -309,54 +316,44 @@ extension ThreadDetailsViewController: MessageActionsHandler { func permanentlyDelete() { logger.logInfo("permanently delete") - Task { - do { - showSpinner() - try await threadOperationsProvider.delete(thread: thread) - handleSuccessfulMessage(action: .permanentlyDelete) - } catch { - handleMessageAction(error: error) - } - } + handle(action: .permanentlyDelete) } func moveToTrash(with trashPath: String) { logger.logInfo("move to trash \(trashPath)") - Task { - do { - showSpinner() - try await threadOperationsProvider.moveThreadToTrash(thread: thread) - handleSuccessfulMessage(action: .moveToTrash) - } catch { - handleMessageAction(error: error) - } - } + handle(action: .moveToTrash) } func handleArchiveTap() { - Task { - do { - showSpinner() - try await threadOperationsProvider.archive(thread: thread, in: currentFolderPath) - handleSuccessfulMessage(action: .archive) - } catch { - handleMessageAction(error: error) - } - } + handle(action: .archive) } func handleMarkUnreadTap() { let messages = input.filter { $0.isExpanded }.map(\.rawMessage) - guard messages.isNotEmpty else { - return - } + guard messages.isNotEmpty else { return } + + handle(action: .markAsRead(false)) + } + func handle(action: MessageAction) { Task { do { showSpinner() - try await threadOperationsProvider.mark(thread: thread, asRead: false, in: currentFolderPath) - handleSuccessfulMessage(action: .markAsRead(false)) + + switch action { + case .archive: + try await threadOperationsProvider.archive(thread: thread, in: currentFolderPath) + case .markAsRead(let isRead): + guard !isRead else { return } + try await threadOperationsProvider.mark(thread: thread, asRead: false, in: currentFolderPath) + case .moveToTrash: + try await threadOperationsProvider.moveThreadToTrash(thread: thread) + case .permanentlyDelete: + try await threadOperationsProvider.delete(thread: thread) + } + + handleSuccessfulMessage(action: action) } catch { handleMessageAction(error: error) } diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index 6fd66aac2..747e96d4c 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -48,7 +48,7 @@ extension LocalContactsProvider: LocalContactsProviderType { func findBy(longid: String) async -> RecipientWithSortedPubKeys? { if let object = localContactsCache.realm - .objects(RecipientObject.self) + .objects(RecipientRealmObject.self) .first(where: { $0.contains(longid: longid) }) { return try? await parseRecipient(from: object.freeze()) } diff --git a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift index bf4f97fc1..b45697e5c 100644 --- a/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift +++ b/FlowCrypt/Models/Realm Models/RecipientRealmObject.swift @@ -43,7 +43,7 @@ extension RecipientRealmObject { } } -extension RecipientObject { +extension RecipientRealmObject { func contains(longid: String) -> Bool { pubKeys.first(where: { $0.contains(longid: longid) }) != nil } From 0929c0869cda812c7209d39ba270f7e5d1284def Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 15 Nov 2021 13:04:23 +0200 Subject: [PATCH 11/15] issue #278 improve message processing --- .../Threads/ThreadDetailsViewController.swift | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 4c07270da..4b4ae9647 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -189,12 +189,12 @@ extension ThreadDetailsViewController { hideSpinner() let messageIndex = indexPath.section - 1 - let shouldAnimate = input[messageIndex].processedMessage == nil + let isAlreadyProcessed = input[messageIndex].processedMessage != nil - input[messageIndex].processedMessage = processedMessage - - if shouldAnimate { + if !isAlreadyProcessed { + input[messageIndex].processedMessage = processedMessage input[messageIndex].isExpanded = true + markAsRead(at: messageIndex) UIView.animate( @@ -206,6 +206,7 @@ extension ThreadDetailsViewController { self?.node.scrollToRow(at: indexPath, at: .middle, animated: true) }) } else { + input[messageIndex].processedMessage?.signature = processedMessage.signature node.reloadSections(IndexSet(integer: indexPath.section), with: .fade) } } From 69bcd84121453d6175d60ae56e02ea8ba80dc009 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 15 Nov 2021 16:44:49 +0200 Subject: [PATCH 12/15] issue #278 handle verify retry errors --- .../Threads/ThreadDetailsViewController.swift | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 4b4ae9647..42a9989e3 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -172,11 +172,11 @@ extension ThreadDetailsViewController { onlyLocalKeys: true, progressHandler: { [weak self] in self?.handleFetchProgress(state: $0) } ) - if processedMessage.signature != .good { + if case .missingPubkey = processedMessage.signature { processedMessage.signature = .pending - tryToFetchSenderPubKeys(message: message, - folder: thread.path, - indexPath: indexPath) + retryVerifyingSignatureWithRemotelyFetchedKeys(message: message, + folder: thread.path, + indexPath: indexPath) } handleReceived(message: processedMessage, at: indexPath) } catch { @@ -279,15 +279,22 @@ extension ThreadDetailsViewController { } } - private func tryToFetchSenderPubKeys(message: Message, folder: String, indexPath: IndexPath) { + private func retryVerifyingSignatureWithRemotelyFetchedKeys(message: Message, + folder: String, + indexPath: IndexPath) { Task { - let processedMessage = try await messageService.getAndProcessMessage( - with: message, - folder: thread.path, - onlyLocalKeys: false, - progressHandler: { _ in } - ) - handleReceived(message: processedMessage, at: indexPath) + do { + let processedMessage = try await messageService.getAndProcessMessage( + with: message, + folder: thread.path, + onlyLocalKeys: false, + progressHandler: { _ in } + ) + handleReceived(message: processedMessage, at: indexPath) + } catch { + let message = "Failed to verify signature due to: \(error)" + input[indexPath.section-1].processedMessage?.signature = .error(message) + } } } From 91bfca4865697bec318b2616391e0b197242bd54 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 15 Nov 2021 22:17:04 +0200 Subject: [PATCH 13/15] issue #278 add signature error localization --- FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift | 2 +- FlowCrypt/Resources/en.lproj/Localizable.strings | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift index 123e33f16..9031db9c4 100644 --- a/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift +++ b/FlowCrypt/Controllers/Threads/ThreadDetailsViewController.swift @@ -292,7 +292,7 @@ extension ThreadDetailsViewController { ) handleReceived(message: processedMessage, at: indexPath) } catch { - let message = "Failed to verify signature due to: \(error)" + let message = "message_signature_fail_reason".localizeWithArguments(error.errorMessage) input[indexPath.section-1].processedMessage?.signature = .error(message) } } diff --git a/FlowCrypt/Resources/en.lproj/Localizable.strings b/FlowCrypt/Resources/en.lproj/Localizable.strings index 47a005ebf..41d2cd18e 100644 --- a/FlowCrypt/Resources/en.lproj/Localizable.strings +++ b/FlowCrypt/Resources/en.lproj/Localizable.strings @@ -37,6 +37,7 @@ "message_signature_verify_error" = "cannot verify signature: %@"; "message_bad_signature" = "bad signature"; "message_signature_pending" = "verifying signature..."; +"message_signature_fail_reason" = "Failed to verify signature due to: %@"; // REPLY "reply_title" = "Your reply"; // not used From 8ceee7819d8c17dcff23e5b9167d162533a444aa Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 15 Nov 2021 22:51:57 +0200 Subject: [PATCH 14/15] issue #278 delete MessageViewController --- .../Msg/MessageViewController.swift | 442 ------------------ 1 file changed, 442 deletions(-) delete mode 100644 FlowCrypt/Controllers/Msg/MessageViewController.swift diff --git a/FlowCrypt/Controllers/Msg/MessageViewController.swift b/FlowCrypt/Controllers/Msg/MessageViewController.swift deleted file mode 100644 index aff594ec2..000000000 --- a/FlowCrypt/Controllers/Msg/MessageViewController.swift +++ /dev/null @@ -1,442 +0,0 @@ -// -// © 2017-2019 FlowCrypt Limited. All rights reserved. -// - -import AsyncDisplayKit -import FlowCryptCommon -import FlowCryptUI - -/** - * View controller to render an email message (sender, subject, message body, attachments) - * Also contains buttons to archive, move to trash, move to inbox, mark as unread, and reply - */ -final class MessageViewController: TableNodeViewController { - struct Input { - var objMessage: Message - var bodyMessage: Data? - var path = "" - } - - enum Sections: Int, CaseIterable { - case main, attributes - } - - enum Parts: Int, CaseIterable { - case sender, subject, text - - var indexPath: IndexPath { - IndexPath(row: rawValue, section: 0) - } - } - - private let onCompletion: MessageActionCompletion - - private var input: MessageViewController.Input - private let decorator: MessageViewDecorator - private let messageOperationsProvider: MessageOperationsProvider - private let filesManager: FilesManagerType - private let serviceActor: ServiceActor - private var processedMessage: ProcessedMessage = .empty - - let trashFolderProvider: TrashFolderProviderType - var currentFolderPath: String { - input.path - } - - private lazy var attachmentManager = AttachmentManager( - controller: self, - filesManager: filesManager - ) - - 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(), - input: MessageViewController.Input, - completion: @escaping MessageActionCompletion - ) { - self.messageOperationsProvider = messageOperationsProvider - self.input = input - self.decorator = decorator - self.trashFolderProvider = trashFolderProvider - self.onCompletion = completion - self.filesManager = filesManager - self.serviceActor = ServiceActor( - messageService: messageService, - messageProvider: messageProvider - ) - - super.init(node: TableNode()) - } - - @available(*, unavailable) - required init?(coder _: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func viewDidLoad() { - super.viewDidLoad() - setupUI() - setupNavigationBar() - fetchDecryptAndRenderMsg() - } - - private func setupUI() { - node.do { - $0.delegate = self - $0.dataSource = self - $0.view.keyboardDismissMode = .interactive - } - } -} - -// MARK: - Message -extension MessageViewController { - private func fetchDecryptAndRenderMsg() { - handleFetchProgress(state: .fetch) - - Task { - do { - processedMessage = try await serviceActor.fetchDecryptAndRenderMsg( - message: input.objMessage, - path: input.path, - progressHandler: { [weak self] in self?.handleFetchProgress(state: $0)}) - handleReceivedMessage() - } catch { - handleError(error) - } - } - } - - private func handlePassPhraseEntry(rawMimeData: Data, with passPhrase: String) { - Task { - do { - let matched = try await serviceActor.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) - if matched { - handleFetchProgress(state: .decrypt) - processedMessage = try await serviceActor.decryptAndProcessMessage(mime: rawMimeData, sender: input.objMessage.sender) - handleReceivedMessage() - } else { - handleWrongPassPhrase(for: rawMimeData, with: passPhrase) - } - } catch { - handleError(error) - } - } - } - - private func handleFetchProgress(state: MessageFetchState) { - switch state { - case .fetch: - showSpinner("loading_title".localized, isUserInteractionEnabled: true) - case .download(let progress): - updateSpinner(label: "downloading_title".localized, progress: progress) - case .decrypt: - updateSpinner(label: "decrypting_title".localized) - } - } - - private func handleReceivedMessage() { - hideSpinner() - node.reloadData() - asyncMarkAsReadIfNotAlreadyMarked() - } - - private func asyncMarkAsReadIfNotAlreadyMarked() { - Task { - do { - try await messageOperationsProvider.markAsRead(message: input.objMessage, folder: input.path) - input.objMessage = self.input.objMessage.markAsRead(true) - } catch { - showToast("Could not mark message as read: \(error)") - } - } - } - - private func handleOpSuccess(operation: MessageAction) { - hideSpinner() - operation.text.flatMap { showToast($0) } - - navigationController?.popViewController(animated: true) { [weak self] in - guard let self = self else { return } - self.onCompletion(operation, .init(message: self.input.objMessage)) - } - } -} - -// MARK: - Error Handling - -extension MessageViewController { - private func handleError(_ error: Error) { - hideSpinner() - - switch error as? MessageServiceError { - case let .missingPassPhrase(rawMimeData): - handleMissingPassPhrase(for: rawMimeData) - case let .wrongPassPhrase(rawMimeData, passPhrase): - handleWrongPassPhrase(for: rawMimeData, with: passPhrase) - case let .keyMismatch(rawMimeData): - handleKeyMismatch(for: rawMimeData) - - default: - // TODO: - Ticket - Improve error handling for MessageViewController - if let someError = error as NSError?, someError.code == Imap.Err.fetch.rawValue { - // todo - the missing msg should be removed from the list in inbox view - // reproduce: 1) load inbox 2) move msg to trash on another email client 3) open trashed message in inbox - showToast("Message not found in folder: \(input.path)") - } else { - // todo - this should be a retry / cancel alert - showAlert(error: error, message: "message_failed_open".localized + "\n\n\(error)") - } - navigationController?.popViewController(animated: true) - } - } - - 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?.handlePassPhraseEntry(rawMimeData: rawMimeData, with: passPhrase) - }) - - present(alert, animated: true, completion: nil) - } - - private func handleWrongPassPhrase(for rawMimeData: Data, with phrase: String) { - let alert = AlertsFactory.makeWrongPassPhraseAlert( - onCancel: { [weak self] in - self?.navigationController?.popViewController(animated: true) - }, - onCompletion: { [weak self] passPhrase in - self?.handlePassPhraseEntry(rawMimeData: rawMimeData, with: passPhrase) - }) - present(alert, animated: true, completion: nil) - } - - private func handleKeyMismatch(for rawMimeData: Data) { - let alert = UIAlertController( - title: "error_key_mismatch".localized, - message: nil, preferredStyle: .alert - ) - alert.addAction( - UIAlertAction( - title: "go_back".localized, - style: .cancel, - handler: { [weak self] _ in - self?.navigationController?.popViewController(animated: true) - } - ) - ) - alert.addAction( - UIAlertAction( - title: "message_open_anyway".localized, - style: .default, - handler: { [weak self] _ in - guard let self = self else { return } - self.processedMessage = ProcessedMessage(rawMimeData: rawMimeData, - text: String(data: rawMimeData, encoding: .utf8) ?? "", - attachments: [], - messageType: .encrypted, - signature: .missingPubkey("error_key_mismatch".localized)) - self.handleReceivedMessage() - } - ) - ) - present(alert, animated: true, completion: nil) - } - - private func handleOpErr(operation: MessageAction) { - hideSpinner() - operation.error.flatMap { showToast($0) } - } -} - -// MARK: - Handle Actions - -extension MessageViewController: MessageActionsHandler { - - func handleMarkUnreadTap() { - Task { - do { - try await messageOperationsProvider.markAsUnread(message: input.objMessage, folder: input.path) - onCompletion(MessageAction.markAsRead(false), .init(message: self.input.objMessage)) - navigationController?.popViewController(animated: true) - } catch { - showToast("Could not mark message as unread: \(error)") - } - } - } - - func handleArchiveTap() { - Task { - do { - showSpinner() - try await messageOperationsProvider.archiveMessage(message: input.objMessage, folderPath: input.path) - handleOpSuccess(operation: .archive) - } catch { - handleOpErr(operation: .archive) - } - } - } - - func permanentlyDelete() { - Task { - do { - try await messageOperationsProvider.delete(message: self.input.objMessage, form: self.input.path) - handleOpSuccess(operation: .permanentlyDelete) - } catch { - handleOpErr(operation: .archive) - } - } - } - - func moveToTrash(with trashPath: String) { - Task { - do { - try await messageOperationsProvider.moveMessageToTrash( - message: input.objMessage, - trashPath: trashPath, - from: input.path - ) - handleOpSuccess(operation: .moveToTrash) - } catch { - handleOpErr(operation: .moveToTrash) - } - } - } - - private func handleReplyTap() { - guard let email = DataService.shared.email else { return } - - let replyInfo = ComposeMessageInput.ReplyInfo( - recipient: input.objMessage.sender, - subject: input.objMessage.subject, - mime: processedMessage.rawMimeData, - sentDate: input.objMessage.date, - message: processedMessage.text, - threadId: input.objMessage.threadId - ) - - let composeInput = ComposeMessageInput(type: .reply(replyInfo)) - navigationController?.pushViewController( - ComposeViewController(email: email, input: composeInput), - animated: true - ) - } -} - -// MARK: - NavigationChildController - -extension MessageViewController: NavigationChildController { - func handleBackButtonTap() { - onCompletion(MessageAction.markAsRead(true), .init(message: input.objMessage)) - navigationController?.popViewController(animated: true) - } -} - -// MARK: - ASTableDelegate, ASTableDataSource - -extension MessageViewController: ASTableDelegate, ASTableDataSource { - func numberOfSections(in tableNode: ASTableNode) -> Int { - Sections.allCases.count - } - - func tableNode(_: ASTableNode, numberOfRowsInSection section: Int) -> Int { - guard let section = Sections(rawValue: section) else { - return 0 - } - switch section { - case .main: - return Parts.allCases.count - case .attributes: - return processedMessage.attachments.count - } - } - - func tableNode(_ tableNode: ASTableNode, nodeBlockForRowAt indexPath: IndexPath) -> ASCellNodeBlock { - return { [weak self] in - guard let self = self, let section = Sections(rawValue: indexPath.section) else { return ASCellNode() } - - switch section { - case .main: - return self.mainSectionNode(for: indexPath.row) - case .attributes: - return self.attachmentNode(for: indexPath.row) - } - } - } - - private func mainSectionNode(for index: Int) -> ASCellNode { - guard let part = Parts(rawValue: index) else { return ASCellNode() } - - let senderTitle = decorator.attributed( - title: input.objMessage.sender ?? "(unknown sender)" - ) - let subject = decorator.attributed( - subject: input.objMessage.subject ?? "(no subject)" - ) - let time = decorator.attributed( - date: input.objMessage.date - ) - - switch part { - case .sender: - return MessageSenderNode(senderTitle) { [weak self] in - self?.handleReplyTap() - } - case .subject: - return MessageSubjectAndTimeNode(subject, time: time) - case .text: - return MessageTextSubjectNode(self.processedMessage.attributedMessage) - } - } - - private func attachmentNode(for index: Int) -> ASCellNode { - let attachment = processedMessage.attachments[index] - return AttachmentNode( - input: .init( - msgAttachment: attachment - ), - onDownloadTap: { [weak self] in self?.attachmentManager.open(attachment) } - ) - } -} - -// 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, - progressHandler: ((MessageFetchState) -> Void)?) async throws -> ProcessedMessage { - let rawMimeData = try await messageProvider.fetchMsg(message: message, - folder: path, - progressHandler: progressHandler) - - progressHandler?(.decrypt) - - return try await decryptAndProcessMessage(mime: rawMimeData, sender: message.sender) - } - - func checkAndPotentiallySaveEnteredPassPhrase(_ passPhrase: String) async throws -> Bool { - try await messageService.checkAndPotentiallySaveEnteredPassPhrase(passPhrase) - } - - func decryptAndProcessMessage(mime: Data, sender: String?) async throws -> ProcessedMessage { - let message = try await messageService.decryptAndProcessMessage(mime: mime, - sender: sender, - onlyLocalKeys: true) - return message - } -} From 5af99693db19ff44215040d5ec906f61d634d8e4 Mon Sep 17 00:00:00 2001 From: Roma Sosnovsky Date: Mon, 15 Nov 2021 23:06:54 +0200 Subject: [PATCH 15/15] issue #278 remove unused code --- .../Local Pub Key Services/ContactsService.swift | 5 ----- .../LocalContactsProvider.swift | 11 ----------- FlowCryptAppTests/Mocks/ContactsServiceMock.swift | 1 - 3 files changed, 17 deletions(-) diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift index 2e553c338..0a9b6045a 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/ContactsService.swift @@ -19,7 +19,6 @@ protocol ContactsServiceType: PublicKeyProvider, ContactsProviderType { protocol ContactsProviderType { func searchContact(with email: String) async throws -> RecipientWithSortedPubKeys func searchContacts(query: String) -> [String] - func findBy(longId: String) async -> RecipientWithSortedPubKeys? } protocol PublicKeyProvider { @@ -59,10 +58,6 @@ extension ContactsService: ContactsProviderType { func searchContacts(query: String) -> [String] { localContactsProvider.searchEmails(query: query) } - - func findBy(longId: String) async -> RecipientWithSortedPubKeys? { - await localContactsProvider.findBy(longid: longId) - } } extension ContactsService: PublicKeyProvider { diff --git a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift index 747e96d4c..a4c7061fc 100644 --- a/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift +++ b/FlowCrypt/Functionality/Services/Local Pub Key Services/LocalContactsProvider.swift @@ -13,7 +13,6 @@ protocol LocalContactsProviderType: PublicKeyProvider { func updateLastUsedDate(for email: String) func searchRecipient(with email: String) async throws -> RecipientWithSortedPubKeys? func searchEmails(query: String) -> [String] - func findBy(longid: String) async -> RecipientWithSortedPubKeys? func save(recipient: RecipientWithSortedPubKeys) func remove(recipient: RecipientWithSortedPubKeys) func updateKeys(for recipient: RecipientWithSortedPubKeys) @@ -46,16 +45,6 @@ extension LocalContactsProvider: LocalContactsProviderType { find(with: email)?.pubKeys.map(\.armored) ?? [] } - func findBy(longid: String) async -> RecipientWithSortedPubKeys? { - if let object = localContactsCache.realm - .objects(RecipientRealmObject.self) - .first(where: { $0.contains(longid: longid) }) { - return try? await parseRecipient(from: object.freeze()) - } - - return nil - } - func save(recipient: RecipientWithSortedPubKeys) { localContactsCache.save(RecipientRealmObject(recipient)) } diff --git a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift index d1bc39b2b..7a899f349 100644 --- a/FlowCryptAppTests/Mocks/ContactsServiceMock.swift +++ b/FlowCryptAppTests/Mocks/ContactsServiceMock.swift @@ -27,7 +27,6 @@ final class ContactsServiceMock: ContactsServiceType { } } func searchContacts(query: String) -> [String] { [] } - func findBy(longId: String) async -> RecipientWithSortedPubKeys? { nil } func removePubKey(with fingerprint: String, for email: String) {} }