diff --git a/FlowCrypt.xcodeproj/project.pbxproj b/FlowCrypt.xcodeproj/project.pbxproj index 4be1ef061..1481d12b8 100644 --- a/FlowCrypt.xcodeproj/project.pbxproj +++ b/FlowCrypt.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 215002A32690B1DD00980DDD /* client_configuraion_with_unknown_flag.json in Resources */ = {isa = PBXBuildFile; fileRef = 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */; }; 2155E9EF26E3628C008FB033 /* Refreshable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2155E9EE26E3628C008FB033 /* Refreshable.swift */; }; 215897E8267A553300423694 /* FilesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 215897E7267A553200423694 /* FilesManager.swift */; }; + 21594C9626F1DBA900BE654C /* data.txt in Resources */ = {isa = PBXBuildFile; fileRef = 21594C9526F1DBA900BE654C /* data.txt */; }; 21750D7D26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */; }; 21750D7F26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */; }; 2196A2202684B9BE001B9E00 /* URLExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2196A21F2684B9BE001B9E00 /* URLExtension.swift */; }; @@ -396,6 +397,7 @@ 215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion_with_unknown_flag.json; sourceTree = ""; }; 2155E9EE26E3628C008FB033 /* Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refreshable.swift; sourceTree = ""; }; 215897E7267A553200423694 /* FilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesManager.swift; sourceTree = ""; }; + 21594C9526F1DBA900BE654C /* data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = data.txt; sourceTree = ""; }; 21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupEKMKeyViewController.swift; sourceTree = ""; }; 21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupCreatePassphraseAbstractViewController.swift; sourceTree = ""; }; 2196A21F2684B9BE001B9E00 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = ""; }; @@ -1270,6 +1272,7 @@ 9F7E902D26A1AD4C0021C07F /* Models */, D254733324C597CD00DEE698 /* CoreTypesTest.swift */, A3DAD5FD22E4574B00F2C4CD /* FlowCryptCoreTests.swift */, + 21594C9526F1DBA900BE654C /* data.txt */, ); path = Core; sourceTree = ""; @@ -2140,6 +2143,7 @@ 9F976576267E18F90058419D /* client_configuraion_partly_empty.json in Resources */, 9F97657D267E18FE0058419D /* client_configuraion_empty.json in Resources */, 215002A32690B1DD00980DDD /* client_configuraion_with_unknown_flag.json in Resources */, + 21594C9626F1DBA900BE654C /* data.txt in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/FlowCrypt/Core/Core.swift b/FlowCrypt/Core/Core.swift index ed1b64410..90cfe8770 100644 --- a/FlowCrypt/Core/Core.swift +++ b/FlowCrypt/Core/Core.swift @@ -5,11 +5,24 @@ import FlowCryptCommon import JavaScriptCore -enum CoreError: Error { +enum CoreError: Error, Equatable { case exception(String) case notReady(String) + case format(String) + case keyMismatch(String) // wrong value passed into a function case value(String) + + init?(coreError: CoreRes.Error) { + guard let errorType = coreError.error.type else { + return nil + } + switch errorType { + case "format": self = .format(coreError.error.message) + case "key_mismatch": self = .keyMismatch(coreError.error.message) + default: return nil + } + } } protocol KeyDecrypter { @@ -36,6 +49,7 @@ final class Core: KeyDecrypter, CoreComposeMessageType { return try r.json.decodeJson(as: CoreRes.Version.self) } + // MARK: Keys func parseKeys(armoredOrBinary: Data) throws -> CoreRes.ParseKeys { let r = try call("parseKeys", jsonDict: [String: String](), data: armoredOrBinary) return try r.json.decodeJson(as: CoreRes.ParseKeys.self) @@ -50,6 +64,38 @@ final class Core: KeyDecrypter, CoreComposeMessageType { let r = try call("encryptKey", jsonDict: ["armored": armoredPrv, "passphrase": passphrase], data: nil) return try r.json.decodeJson(as: CoreRes.EncryptKey.self) } + + func generateKey(passphrase: String, variant: KeyVariant, userIds: [UserId]) throws -> CoreRes.GenerateKey { + let request: [String: Any] = ["passphrase": passphrase, "variant": String(variant.rawValue), "userIds": try userIds.map { try $0.toJsonEncodedDict() }] + let r = try call("generateKey", jsonDict: request, data: nil) + return try r.json.decodeJson(as: CoreRes.GenerateKey.self) + } + + // MARK: Files + public func decryptFile(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?) throws -> CoreRes.DecryptFile { + let json: [String : Any?]? = [ + "keys": try keys.map { try $0.toJsonEncodedDict() }, + "msgPwd": msgPwd + ] + let decrypted = try call("decryptFile", jsonDict: json, data: encrypted) + let meta = try decrypted.json.decodeJson(as: CoreRes.DecryptFileMeta.self) + + return CoreRes.DecryptFile(name: meta.name, content: decrypted.data) + } + + public func encryptFile(pubKeys: [String]?, fileData: Data, name: String) throws -> CoreRes.EncryptFile { + let json: [String: Any?]? = [ + "pubKeys": pubKeys, + "name": name + ] + + let encrypted = try call( + "encryptFile", + jsonDict: json, + data: fileData + ) + return CoreRes.EncryptFile(encryptedFile: encrypted.data) + } func parseDecryptMsg(encrypted: Data, keys: [PrvKeyInfo], msgPwd: String?, isEmail: Bool) throws -> CoreRes.ParseDecryptMsg { let json: [String : Any?]? = [ @@ -99,12 +145,6 @@ final class Core: KeyDecrypter, CoreComposeMessageType { return CoreRes.ComposeEmail(mimeEncoded: r.data) } - func generateKey(passphrase: String, variant: KeyVariant, userIds: [UserId]) throws -> CoreRes.GenerateKey { - let request: [String: Any] = ["passphrase": passphrase, "variant": String(variant.rawValue), "userIds": try userIds.map { try $0.toJsonEncodedDict() }] - let r = try call("generateKey", jsonDict: request, data: nil) - return try r.json.decodeJson(as: CoreRes.GenerateKey.self) - } - func zxcvbnStrengthBar(passPhrase: String) throws -> CoreRes.ZxcvbnStrengthBar { let r = try call("zxcvbnStrengthBar", jsonDict: ["value": passPhrase, "purpose": "passphrase"], data: nil) return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self) @@ -140,26 +180,13 @@ final class Core: KeyDecrypter, CoreComposeMessageType { } } - func blockUntilReadyOrThrow() throws { - // This will block the thread for up to 1000ms if the app was just started and Core method was called before JSContext is ready - // It should only affect the user if Core method was called within 500-800ms of starting the app - let start = DispatchTime.now() - while !ready { - if start.millisecondsSince > 1000 { // already waited for 1000 ms, give up - throw CoreError.notReady("App Core not ready yet") - } - usleep(50000) // 50ms - } - } - func gmailBackupSearch(for email: String) throws -> String { let response = try call("gmailBackupSearch", jsonDict: ["acctEmail": email], data: nil) let result = try response.json.decodeJson(as: GmailBackupSearchResponse.self) return result.query } - // private - + // MARK: Private calls private func call(_ endpoint: String, jsonDict: [String: Any?]?, data: Data?) throws -> RawRes { try call(endpoint, jsonData: try JSONSerialization.data(withJSONObject: jsonDict ?? [String: String]()), data: data ?? Data()) } @@ -177,13 +204,26 @@ final class Core: KeyDecrypter, CoreComposeMessageType { let separatorIndex = rawResponse.firstIndex(of: 10)! let resJsonData = Data(rawResponse[...(separatorIndex - 1)]) let error = try? resJsonData.decodeJson(as: CoreRes.Error.self) - if error != nil { - let errMsg = "------ js err -------\nCore \(endpoint):\n\(error!.error.message)\n\(error!.error.stack ?? "no stack")\n------- end js err -----" + if let error = error { + let errMsg = "------ js err -------\nCore \(endpoint):\n\(error.error.message)\n\(error.error.stack ?? "no stack")\n------- end js err -----" logger.logError(errMsg) - throw CoreError.exception(errMsg) + + throw CoreError.init(coreError: error) ?? CoreError.exception(errMsg) } return RawRes(json: resJsonData, data: Data(rawResponse[(separatorIndex + 1)...])) } + + private func blockUntilReadyOrThrow() throws { + // This will block the thread for up to 1000ms if the app was just started and Core method was called before JSContext is ready + // It should only affect the user if Core method was called within 500-800ms of starting the app + let start = DispatchTime.now() + while !ready { + if start.millisecondsSince > 1000 { // already waited for 1000 ms, give up + throw CoreError.notReady("App Core not ready yet") + } + usleep(50000) // 50ms + } + } private struct RawRes { let json: Data diff --git a/FlowCrypt/Core/CoreTypes.swift b/FlowCrypt/Core/CoreTypes.swift index ef42117cc..936aa187d 100644 --- a/FlowCrypt/Core/CoreTypes.swift +++ b/FlowCrypt/Core/CoreTypes.swift @@ -47,10 +47,24 @@ struct CoreRes { struct GenerateKey: Decodable { let key: KeyDetails } + + struct DecryptFile: Decodable { + let name: String + let content: Data + } + + struct EncryptFile: Decodable { + let encryptedFile: Data + } + + struct DecryptFileMeta: Decodable { + let name: String + } struct Error: Decodable { struct ErrorWithOptionalStack: Decodable { let message: String + let type: String? let stack: String? } diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index 95e60a6cc..a80a2b858 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -144,6 +144,120 @@ class FlowCryptCoreTests: XCTestCase { let e = decryptErrBlock.decryptErr! XCTAssertEqual(e.error.type, MsgBlock.DecryptErr.ErrorType.keyMismatch) } + + func testEncryptFile() 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 + let encrypted = try core.encryptFile( + pubKeys: [k.public], + fileData: fileData, + name: initialFileName + ) + let decrypted = try core.decryptFile( + encrypted: encrypted.encryptedFile, + keys: keys, + msgPwd: nil + ) + + // Then + XCTAssertTrue(decrypted.content == fileData) + XCTAssertTrue(decrypted.content.toStr() == fileData.toStr()) + XCTAssertTrue(decrypted.name == initialFileName) + } + + func testDecryptNotEncryptedFile() throws { + // Given + 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 { + _ = try self.core.decryptFile( + encrypted: fileData, + keys: keys, + msgPwd: nil + ) + XCTFail("Should have thrown above") + } catch let CoreError.format(message) { + // Then + XCTAssertNotNil(message.range(of: "Error: Error during parsing")) + } + } + + func testDecryptWithNoKeys() 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 + + // When + do { + let encrypted = try core.encryptFile( + pubKeys: [k.public], + fileData: fileData, + name: initialFileName + ) + _ = try self.core.decryptFile( + encrypted: encrypted.encryptedFile, + keys: [], + msgPwd: nil + ) + XCTFail("Should have thrown above") + } catch let CoreError.keyMismatch(message) { + // Then + XCTAssertNotNil(message.range(of: "Missing appropriate key")) + } + } func testException() throws { do { diff --git a/FlowCryptAppTests/Core/data.txt b/FlowCryptAppTests/Core/data.txt new file mode 100644 index 000000000..057364d56 --- /dev/null +++ b/FlowCryptAppTests/Core/data.txt @@ -0,0 +1 @@ +Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. Quisque rutrum. Aenean imperdiet. Etiam ultricies nisi vel augue. Curabitur ullamcorper ultricies nisi. Nam eget dui. Etiam rhoncus. Maecenas tempus, tellus eget condimentum rhoncus, sem quam semper libero, sit amet adipiscing sem neque sed ipsum. Nam quam nunc, blandit vel, luctus pulvinar, hendrerit id, lorem. Maecenas nec odio et ante tincidunt tempus. Donec vitae sapien ut libero venenatis faucibus. Nullam quis ante. Etiam sit amet orci eget eros faucibus tincidunt. Duis leo. Sed fringilla mauris sit amet nibh. Donec sodales sagittis magna.