diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index f26339d20..3a2dfb906 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -1318,8 +1318,8 @@ isa = PBXGroup; children = ( 9F4163EC266574CB00106194 /* BackupServiceMock.swift */, - 9F6F3C3B26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift */, 9F6F3C6926ADFBEB005BD9C6 /* MessageGatewayMock.swift */, + 9F6F3C3B26ADFBC7005BD9C6 /* CoreComposeMessageMock.swift */, 9F6F3C7526ADFC37005BD9C6 /* KeyStorageMock.swift */, 9F6F3C7C26ADFC60005BD9C6 /* ContactsServiceMock.swift */, ); diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index d8ea9e7fb..044d958a5 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -11,6 +11,10 @@ enum CoreError: Error, Equatable { case notReady(String) case format(String) case keyMismatch(String) + case noMDC(String) + case badMDC(String) + case needPassphrase(String) + case wrongPassphrase(String) // wrong value passed into a function case value(String) @@ -21,6 +25,10 @@ enum CoreError: Error, Equatable { switch errorType { case "format": self = .format(coreError.error.message) case "key_mismatch": self = .keyMismatch(coreError.error.message) + case "no_mdc": self = .noMDC(coreError.error.message) + case "bad_mdc": self = .badMDC(coreError.error.message) + case "need_passphrase": self = .needPassphrase(coreError.error.message) + case "wrong_passphrase": self = .wrongPassphrase(coreError.error.message) default: return nil } } diff --git a/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift b/FlowCrypt/Functionality/Mail Provider/Message Provider/MessageService.swift index 4d4c2b345..8d46a326b 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 Promises +import FlowCryptCommon // MARK: - MessageAttachment struct MessageAttachment: FileType { @@ -47,6 +48,10 @@ enum MessageServiceError: Error { } final class MessageService { + private enum Constants { + static let encryptedAttachmentExtension = "pgp" + } + private let messageProvider: MessageProvider private let keyService: KeyServiceType private let passPhraseService: PassPhraseServiceType @@ -90,7 +95,7 @@ final class MessageService { reject(MessageServiceError.wrongPassPhrase(rawMimeData, passPhrase)) } else { self.savePassPhrases(value: passPhrase, with: keys) - let processedMessage = self.processMessage(rawMimeData: rawMimeData, with: decrypted) + let processedMessage = try self.processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) resolve(processedMessage) } } @@ -125,18 +130,30 @@ final class MessageService { isEmail: true ) - let processedMessage = self.processMessage(rawMimeData: rawMimeData, with: decrypted) + let processedMessage = try self.processMessage(rawMimeData: rawMimeData, with: decrypted, keys: keys) resolve(processedMessage) } } - private func processMessage(rawMimeData: Data, with decrypted: CoreRes.ParseDecryptMsg) -> ProcessedMessage { + private func processMessage(rawMimeData: Data, with decrypted: CoreRes.ParseDecryptMsg, keys: [PrvKeyInfo]) throws -> ProcessedMessage { let decryptErrBlocks = decrypted.blocks .filter { $0.decryptErr != nil } - let attachments = decrypted.blocks + let attachments = try decrypted.blocks .filter(\.isAttachmentBlock) - .map(MessageAttachment.init) + .compactMap { block -> MessageAttachment? in + guard let attMeta = block.attMeta else { return nil } + var name = attMeta.name + let size = attMeta.length + var data = attMeta.data + + if block.type == .encryptedAtt { + data = (try core.decryptFile(encrypted: data, keys: keys, msgPwd: nil).content) + name = name.deletingPathExtension + } + + return MessageAttachment(name: name, size: size, data: data) + } let messageType: ProcessedMessage.MessageType let text: String diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index ec5d72756..6b82aafb3 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -128,6 +128,43 @@ class FlowCryptCoreTests: XCTestCase { XCTAssertNotNil(mime.range(of: "Subject: \(msg.subject)")) // has mime Subject header XCTAssertNil(mime.range(of: "In-Reply-To")) // Not a reply } + + func testComposeEmailInlineWithAttachment() throws { + + let initialFileName = "data.txt" + let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)) + .path(forResource: "data", ofType: "txt")!) + let fileData = try! Data(contentsOf: urlPath, options: .dataReadingMapped) + + let attachment = SendableMsg.Attachment( + name: initialFileName, type: "text/plain", + base64: fileData.base64EncodedString() + ) + + let msg = SendableMsg( + text: "this is the message", + to: ["email@hello.com"], cc: [], bcc: [], + from: "sender@hello.com", + subject: "subj", replyToMimeMsg: nil, + atts: [attachment], pubKeys: nil + ) + let expectation = XCTestExpectation() + + var mime: String = "" + core.composeEmail(msg: msg, fmt: .encryptInline, pubKeys: [TestData.k0.pub, TestData.k1.pub]) + .sinkFuture( + receiveValue: { composeEmailRes in + mime = String(data: composeEmailRes.mimeEncoded, encoding: .utf8)! + expectation.fulfill() + }, receiveError: {_ in } + ) + .store(in: &cancellable) + wait(for: [expectation], timeout: 3) + XCTAssertNil(mime.range(of: msg.text)) // text encrypted + XCTAssertNotNil(mime.range(of: "Content-Type: application/pgp-encrypted")) // encrypted + XCTAssertNotNil(mime.range(of: "name=\(attachment.name)")) // attachment + XCTAssertNotNil(mime.range(of: "Subject: \(msg.subject)")) // has mime Subject header + } func testEndToEnd() throws { let passphrase = "some pass phrase test" @@ -290,7 +327,51 @@ class FlowCryptCoreTests: XCTestCase { XCTAssertNotNil(message.range(of: "Missing appropriate key")) } } - + + func testDecryptEncryptedFile() throws { + // Given + let initialFileName = "data.txt" + let urlPath = URL(fileURLWithPath: Bundle(for: type(of: self)) + .path(forResource: "data", ofType: "txt")!) + let fileData = try! Data(contentsOf: urlPath, options: .dataReadingMapped) + + let passphrase = "some pass phrase test" + let email = "e2e@domain.com" + let generateKeyRes = try core.generateKey( + passphrase: passphrase, + variant: KeyVariant.curve25519, + userIds: [UserId(email: email, name: "End to end")] + ) + let k = generateKeyRes.key + let keys = [ + PrvKeyInfo( + private: k.private!, + longid: k.ids[0].longid, + passphrase: passphrase, + fingerprints: k.fingerprints + ) + ] + + // When + do { + let encrypted = try core.encryptFile( + pubKeys: [k.public], + fileData: fileData, + name: initialFileName + ) + let decrypted = try self.core.decryptFile( + encrypted: encrypted.encryptedFile, + keys: keys, + msgPwd: nil + ) + // Then + XCTAssertEqual(decrypted.name, initialFileName) + XCTAssertEqual(decrypted.content.count, fileData.count) + } catch { + XCTFail("Core file decryption should not fail") + } + } + func testException() throws { do { _ = try core.decryptKey(armoredPrv: "not really a key", passphrase: "whatnot") diff --git a/FlowCryptCommon/Extensions/StringExtensions.swift b/FlowCryptCommon/Extensions/StringExtensions.swift index 52f448087..af201d9d9 100644 --- a/FlowCryptCommon/Extensions/StringExtensions.swift +++ b/FlowCryptCommon/Extensions/StringExtensions.swift @@ -45,6 +45,10 @@ public extension String { } } } + + var deletingPathExtension: String { + return NSString(string: self).deletingPathExtension as String + } } public extension NSAttributedString {