From 4c780ef19149ef30781a7070e09cc695998828b8 Mon Sep 17 00:00:00 2001 From: ykyivskyi-gd Date: Wed, 15 Sep 2021 14:57:07 +0300 Subject: [PATCH 1/2] Added decrypt and encrypt file methods to Core; Added tests for decrypt and encrypt file methods of Core; --- FlowCrypt.xcodeproj/project.pbxproj | 4 + FlowCrypt/Core/Core.swift | 88 ++++++++++---- FlowCrypt/Core/CoreTypes.swift | 14 +++ .../Core/FlowCryptCoreTests.swift | 112 ++++++++++++++++++ FlowCryptAppTests/Core/data.txt | 1 + 5 files changed, 195 insertions(+), 24 deletions(-) create mode 100644 FlowCryptAppTests/Core/data.txt 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..f6bf5febd 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -144,6 +144,118 @@ 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 + ) + } 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 + ) + } 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. From 7b0f9f2cfa3eb333a6e1d4cc0ca2bfc517258312 Mon Sep 17 00:00:00 2001 From: ykyivskyi-gd Date: Thu, 16 Sep 2021 11:50:05 +0300 Subject: [PATCH 2/2] Failing success flow in decrypt/encrypt file tests; --- FlowCryptAppTests/Core/FlowCryptCoreTests.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift index f6bf5febd..a80a2b858 100644 --- a/FlowCryptAppTests/Core/FlowCryptCoreTests.swift +++ b/FlowCryptAppTests/Core/FlowCryptCoreTests.swift @@ -217,6 +217,7 @@ class FlowCryptCoreTests: XCTestCase { keys: keys, msgPwd: nil ) + XCTFail("Should have thrown above") } catch let CoreError.format(message) { // Then XCTAssertNotNil(message.range(of: "Error: Error during parsing")) @@ -251,6 +252,7 @@ class FlowCryptCoreTests: XCTestCase { keys: [], msgPwd: nil ) + XCTFail("Should have thrown above") } catch let CoreError.keyMismatch(message) { // Then XCTAssertNotNil(message.range(of: "Missing appropriate key"))