Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions FlowCrypt.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -396,6 +397,7 @@
215002A22690B1DD00980DDD /* client_configuraion_with_unknown_flag.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = client_configuraion_with_unknown_flag.json; sourceTree = "<group>"; };
2155E9EE26E3628C008FB033 /* Refreshable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Refreshable.swift; sourceTree = "<group>"; };
215897E7267A553200423694 /* FilesManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesManager.swift; sourceTree = "<group>"; };
21594C9526F1DBA900BE654C /* data.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = data.txt; sourceTree = "<group>"; };
21750D7C26C6AFA6007E6A6F /* SetupEKMKeyViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupEKMKeyViewController.swift; sourceTree = "<group>"; };
21750D7E26C6C1E3007E6A6F /* SetupCreatePassphraseAbstractViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SetupCreatePassphraseAbstractViewController.swift; sourceTree = "<group>"; };
2196A21F2684B9BE001B9E00 /* URLExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLExtension.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1270,6 +1272,7 @@
9F7E902D26A1AD4C0021C07F /* Models */,
D254733324C597CD00DEE698 /* CoreTypesTest.swift */,
A3DAD5FD22E4574B00F2C4CD /* FlowCryptCoreTests.swift */,
21594C9526F1DBA900BE654C /* data.txt */,
);
path = Core;
sourceTree = "<group>";
Expand Down Expand Up @@ -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;
};
Expand Down
88 changes: 64 additions & 24 deletions FlowCrypt/Core/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand All @@ -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?]? = [
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
}
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions FlowCrypt/Core/CoreTypes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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?
}

Expand Down
114 changes: 114 additions & 0 deletions FlowCryptAppTests/Core/FlowCryptCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions FlowCryptAppTests/Core/data.txt
Original file line number Diff line number Diff line change
@@ -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.