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: 2 additions & 2 deletions Core/source/assets/flowcrypt-ios-begin.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ const window = { navigator, crypto, setTimeout, clearTimeout };
const self = { window, navigator, crypto };
global.window = window;

var engine_host_cb_value_formatter = function(val) {
coreHost.handleCallback(val.json, val.data);
var engine_host_cb_value_formatter = function(callbackId, val) {
coreHost.handleCallback(callbackId, val.json, val.data);
};

(function() {
Expand Down
10 changes: 5 additions & 5 deletions Core/source/entrypoint-bare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,17 @@ declare const global: any;

const endpoints = new Endpoints();

global.handleRequestFromHost = (endpointName: string, request: string, data: Uint8Array, cb: (response: EndpointRes) => void): void => {
global.handleRequestFromHost = (endpointName: string, callbackId: string, request: string, data: Uint8Array, cb: (key: string, response: EndpointRes) => void): void => {
try {
const handler = endpoints[endpointName];
if (!handler) {
cb(fmtErr(new Error(`Unknown endpoint: ${endpointName}`)));
cb(callbackId, fmtErr(new Error(`Unknown endpoint: ${endpointName}`)));
} else {
handler(JSON.parse(request), [data])
.then(res => cb(res))
.catch(err => cb(fmtErr(err)));
.then(res => cb(callbackId, res))
.catch(err => cb(callbackId, fmtErr(err)));
}
} catch (err) {
cb(fmtErr(err));
cb(callbackId, fmtErr(err));
}
};
135 changes: 59 additions & 76 deletions FlowCrypt/Core/Core.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,18 @@ protocol KeyParser {
func parseKeys(armoredOrBinary: Data) async throws -> CoreRes.ParseKeys
}

final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
actor Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
static let shared = Core()

private typealias CallbackResult = (String, [UInt8])

private var jsEndpointListener: JSValue?
private var cb_catcher: JSValue?
private var cb_last_value: (String, [UInt8])?
private var vm = JSVirtualMachine()!
private var context: JSContext?
private dynamic var started = false
private dynamic var ready = false

private let queue = DispatchQueue(label: "com.flowcrypt.core", qos: .default) // todo - try also with .userInitiated

private var callbackResults: [String: CallbackResult] = [:]
private var ready = false

private lazy var logger = Logger.nested(in: Self.self, with: "Js")

Expand Down Expand Up @@ -188,32 +188,25 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
return try r.json.decodeJson(as: CoreRes.ZxcvbnStrengthBar.self)
}

func startInBackgroundIfNotAlreadyRunning(_ completion: @escaping (() -> Void)) {
if self.ready {
completion()
}
if !started {
started = true
DispatchQueue.global(qos: .default).async { [weak self] in
guard let self = self else { return }
let trace = Trace(id: "Start in background")
let jsFile = Bundle(for: Core.self).path(forResource: "flowcrypt-ios-prod.js.txt", ofType: nil)!
let jsFileSrc = try? String(contentsOfFile: jsFile)
self.context = JSContext(virtualMachine: self.vm)!
self.context?.setObject(CoreHost(), forKeyedSubscript: "coreHost" as (NSCopying & NSObjectProtocol))
self.context!.exceptionHandler = { [weak self] _, exception in
guard let exception = exception else { return }
self?.logger.logWarning("\(exception)")
}
self.context!.evaluateScript("const APP_VERSION = 'iOS 0.2';")
self.context!.evaluateScript(jsFileSrc)
self.jsEndpointListener = self.context!.objectForKeyedSubscript("handleRequestFromHost")
self.cb_catcher = self.context!.objectForKeyedSubscript("engine_host_cb_value_formatter")
self.ready = true
self.logger.logInfo("JsContext took \(trace.finish()) to start")
completion()
}
func startIfNotAlreadyRunning() async {
guard !ready else { return }

let trace = Trace(id: "Start in background")
let jsFile = Bundle(for: Self.self).path(forResource: "flowcrypt-ios-prod.js.txt", ofType: nil)!
let jsFileSrc = try? String(contentsOfFile: jsFile)
context = JSContext(virtualMachine: vm)!
context?.setObject(CoreHost(), forKeyedSubscript: "coreHost" as (NSCopying & NSObjectProtocol))
context!.exceptionHandler = { _, exception in
guard let exception = exception else { return }
let logger = Logger.nested(in: Self.self, with: "Js")
logger.logWarning("\(exception)")
}
context!.evaluateScript("const APP_VERSION = 'iOS 0.2';")
context!.evaluateScript(jsFileSrc)
jsEndpointListener = context!.objectForKeyedSubscript("handleRequestFromHost")
cb_catcher = context!.objectForKeyedSubscript("engine_host_cb_value_formatter")
ready = true
logger.logInfo("JsContext took \(trace.finish()) to start")
}

func gmailBackupSearch(for email: String) async throws -> String {
Expand All @@ -222,8 +215,8 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
return result.query
}

func handleCallbackResult(json: String, data: [UInt8]) {
cb_last_value = (json, data)
func handleCallbackResult(callbackId: String, json: String, data: [UInt8]) {
callbackResults[callbackId] = (json, data)
}

// MARK: Private calls
Expand All @@ -236,55 +229,45 @@ final class Core: KeyDecrypter, KeyParser, CoreComposeMessageType {
}

private func call(_ endpoint: String, jsonData: Data, data: Data) async throws -> RawRes {
try await sleepUntilReadyOrThrow()
return try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<RawRes, Error>) in
// tom - todo - currently there is only one callback storage variable "cb_last_value"
// for all JavaScript calls, and so we have to synchronize
// all calls into JavaScript to happen serially, else
// the return values would be undefined when used concurrently
// see https://github.com/FlowCrypt/flowcrypt-ios/issues/852
// A possible solution would be to only synchronize returning o fthe callbac values into some dict. But I'm unsure if JavaScript is otherwise safe to call concurrently, so for now we'll do the safer thing.
queue.async {
self.cb_last_value = nil
self.jsEndpointListener!.call(withArguments: [endpoint, String(data: jsonData, encoding: .utf8)!, Array<UInt8>(data), self.cb_catcher!])
guard
let resJsonData = self.cb_last_value?.0.data(using: .utf8),
let rawResponse = self.cb_last_value?.1
else {
self.logger.logError("could not see callback response, got cb_last_value: \(String(describing: self.cb_last_value))")
continuation.resume(throwing: CoreError.format("JavaScript callback response not available"))
return
}
let error = try? resJsonData.decodeJson(as: CoreRes.Error.self)
if let error = error {
let errMsg = "------ js err -------\nCore \(endpoint):\n\(error.error.message)\n\(error.error.stack ?? "no stack")\n------- end js err -----"
self.logger.logError(errMsg)
continuation.resume(throwing: CoreError(coreError: error))
return
}
continuation.resume(returning: RawRes(json: resJsonData, data: Data(rawResponse)))
}
guard ready else {
throw CoreError.exception("Core is not ready yet. Most likeyly startIfNotAlreadyRunning wasn't called first")
}
}

private func sleepUntilReadyOrThrow() async throws {
// This will block the task 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")
}
await Task.sleep(50 * 1_000_000) // 50ms

let callbackId = NSUUID().uuidString
jsEndpointListener!.call(withArguments: [
endpoint,
callbackId,
String(data: jsonData, encoding: .utf8)!,
Array<UInt8>(data), cb_catcher!
])

while callbackResults[callbackId] == nil {
await Task.sleep(1_000_000) // 1ms
}
}

private struct RawRes {
let json: Data
let data: Data
guard
let result = callbackResults.removeValue(forKey: callbackId),
let resJsonData = result.0.data(using: .utf8)
else {
throw CoreError.format("JavaScript callback response not available")
}

let error = try? resJsonData.decodeJson(as: CoreRes.Error.self)
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(coreError: error)
}

return RawRes(json: resJsonData, data: Data(result.1))
}
}

private struct RawRes {
let json: Data
let data: Data
}

private struct GmailBackupSearchResponse: Decodable {
let query: String
}
8 changes: 5 additions & 3 deletions FlowCrypt/Core/CoreHost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import SwiftyRSA // for rsa
func setTimeout(_ callback: JSValue, _ ms: Double) -> String
func clearTimeout(_ identifier: String)

func handleCallback(_ string: String, _ data: [UInt8])
func handleCallback(_ endpointKey: String, _ string: String, _ data: [UInt8])
}

var timers = [String: Timer]()
Expand Down Expand Up @@ -119,8 +119,10 @@ final class CoreHost: NSObject, CoreHostExports {
return uuid
}

func handleCallback(_ string: String, _ data: [UInt8]) {
Core.shared.handleCallbackResult(json: string, data: data)
func handleCallback(_ callbackId: String, _ string: String, _ data: [UInt8]) {
Task {
await Core.shared.handleCallbackResult(callbackId: callbackId, json: string, data: data)
}
}

@objc func callJsCb(_ timer: Timer) {
Expand Down
12 changes: 4 additions & 8 deletions FlowCrypt/Functionality/Services/AppStartup.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ struct AppStartup {

Task {
do {
try awaitPromise(setupCore())
await setupCore()
try setupMigrationIfNeeded()
try setupSession()
try await getUserOrgRulesIfNeeded()
Expand All @@ -37,13 +37,9 @@ struct AppStartup {
}
}

private func setupCore() -> Promise<Void> {
Promise { resolve, _ in
logger.logInfo("Setup Core")
Core.shared.startInBackgroundIfNotAlreadyRunning {
resolve(())
}
}
private func setupCore() async {
logger.logInfo("Setup Core")
await Core.shared.startIfNotAlreadyRunning()
}

private func setupMigrationIfNeeded() throws {
Expand Down
16 changes: 8 additions & 8 deletions FlowCrypt/Resources/flowcrypt-ios-prod.js.txt
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ const window = { navigator, crypto, setTimeout, clearTimeout };
const self = { window, navigator, crypto };
global.window = window;

var engine_host_cb_value_formatter = function(val) {
coreHost.handleCallback(val.json, val.data);
var engine_host_cb_value_formatter = function(callbackId, val) {
coreHost.handleCallback(callbackId, val.json, val.data);
};

(function() {
Expand Down Expand Up @@ -86405,20 +86405,20 @@ Object.defineProperty(exports, "__esModule", { value: true });
const endpoints_1 = __webpack_require__(2);
const format_output_1 = __webpack_require__(3);
const endpoints = new endpoints_1.Endpoints();
global.handleRequestFromHost = (endpointName, request, data, cb) => {
global.handleRequestFromHost = (endpointName, callbackId, request, data, cb) => {
try {
const handler = endpoints[endpointName];
if (!handler) {
cb((0, format_output_1.fmtErr)(new Error(`Unknown endpoint: ${endpointName}`)));
cb(callbackId, (0, format_output_1.fmtErr)(new Error(`Unknown endpoint: ${endpointName}`)));
}
else {
handler(JSON.parse(request), [data])
.then(res => cb(res))
.catch(err => cb((0, format_output_1.fmtErr)(err)));
.then(res => cb(callbackId, res))
.catch(err => cb(callbackId, (0, format_output_1.fmtErr)(err)));
}
}
catch (err) {
cb((0, format_output_1.fmtErr)(err));
cb(callbackId, (0, format_output_1.fmtErr)(err));
}
};

Expand Down Expand Up @@ -111770,7 +111770,7 @@ elliptic.eddsa = __webpack_require__(170);
/* 144 */
/***/ (function(module) {

module.exports = JSON.parse("{\"_from\":\"elliptic@^6.5.3\",\"_id\":\"elliptic@6.5.4\",\"_inBundle\":false,\"_integrity\":\"sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==\",\"_location\":\"/elliptic\",\"_phantomChildren\":{},\"_requested\":{\"type\":\"range\",\"registry\":true,\"raw\":\"elliptic@^6.5.3\",\"name\":\"elliptic\",\"escapedName\":\"elliptic\",\"rawSpec\":\"^6.5.3\",\"saveSpec\":null,\"fetchSpec\":\"^6.5.3\"},\"_requiredBy\":[\"/browserify-sign\",\"/create-ecdh\"],\"_resolved\":\"https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz\",\"_shasum\":\"da37cebd31e79a1367e941b592ed1fbebd58abbb\",\"_spec\":\"elliptic@^6.5.3\",\"_where\":\"/home/ivan/pro/flowcrypt/flowcrypt-ios/Core/node_modules/browserify-sign\",\"author\":{\"name\":\"Fedor Indutny\",\"email\":\"fedor@indutny.com\"},\"bugs\":{\"url\":\"https://github.com/indutny/elliptic/issues\"},\"bundleDependencies\":false,\"dependencies\":{\"bn.js\":\"^4.11.9\",\"brorand\":\"^1.1.0\",\"hash.js\":\"^1.0.0\",\"hmac-drbg\":\"^1.0.1\",\"inherits\":\"^2.0.4\",\"minimalistic-assert\":\"^1.0.1\",\"minimalistic-crypto-utils\":\"^1.0.1\"},\"deprecated\":false,\"description\":\"EC cryptography\",\"devDependencies\":{\"brfs\":\"^2.0.2\",\"coveralls\":\"^3.1.0\",\"eslint\":\"^7.6.0\",\"grunt\":\"^1.2.1\",\"grunt-browserify\":\"^5.3.0\",\"grunt-cli\":\"^1.3.2\",\"grunt-contrib-connect\":\"^3.0.0\",\"grunt-contrib-copy\":\"^1.0.0\",\"grunt-contrib-uglify\":\"^5.0.0\",\"grunt-mocha-istanbul\":\"^5.0.2\",\"grunt-saucelabs\":\"^9.0.1\",\"istanbul\":\"^0.4.5\",\"mocha\":\"^8.0.1\"},\"files\":[\"lib\"],\"homepage\":\"https://github.com/indutny/elliptic\",\"keywords\":[\"EC\",\"Elliptic\",\"curve\",\"Cryptography\"],\"license\":\"MIT\",\"main\":\"lib/elliptic.js\",\"name\":\"elliptic\",\"repository\":{\"type\":\"git\",\"url\":\"git+ssh://git@github.com/indutny/elliptic.git\"},\"scripts\":{\"lint\":\"eslint lib test\",\"lint:fix\":\"npm run lint -- --fix\",\"test\":\"npm run lint && npm run unit\",\"unit\":\"istanbul test _mocha --reporter=spec test/index.js\",\"version\":\"grunt dist && git add dist/\"},\"version\":\"6.5.4\"}");
module.exports = JSON.parse("{\"_args\":[[\"elliptic@6.5.4\",\"/Users/ivan/Documents/Projects/FlowCrypt/flowcrypt-ios/Core\"]],\"_development\":true,\"_from\":\"elliptic@6.5.4\",\"_id\":\"elliptic@6.5.4\",\"_inBundle\":false,\"_integrity\":\"sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==\",\"_location\":\"/elliptic\",\"_phantomChildren\":{},\"_requested\":{\"type\":\"version\",\"registry\":true,\"raw\":\"elliptic@6.5.4\",\"name\":\"elliptic\",\"escapedName\":\"elliptic\",\"rawSpec\":\"6.5.4\",\"saveSpec\":null,\"fetchSpec\":\"6.5.4\"},\"_requiredBy\":[\"/browserify-sign\",\"/create-ecdh\"],\"_resolved\":\"https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz\",\"_spec\":\"6.5.4\",\"_where\":\"/Users/ivan/Documents/Projects/FlowCrypt/flowcrypt-ios/Core\",\"author\":{\"name\":\"Fedor Indutny\",\"email\":\"fedor@indutny.com\"},\"bugs\":{\"url\":\"https://github.com/indutny/elliptic/issues\"},\"dependencies\":{\"bn.js\":\"^4.11.9\",\"brorand\":\"^1.1.0\",\"hash.js\":\"^1.0.0\",\"hmac-drbg\":\"^1.0.1\",\"inherits\":\"^2.0.4\",\"minimalistic-assert\":\"^1.0.1\",\"minimalistic-crypto-utils\":\"^1.0.1\"},\"description\":\"EC cryptography\",\"devDependencies\":{\"brfs\":\"^2.0.2\",\"coveralls\":\"^3.1.0\",\"eslint\":\"^7.6.0\",\"grunt\":\"^1.2.1\",\"grunt-browserify\":\"^5.3.0\",\"grunt-cli\":\"^1.3.2\",\"grunt-contrib-connect\":\"^3.0.0\",\"grunt-contrib-copy\":\"^1.0.0\",\"grunt-contrib-uglify\":\"^5.0.0\",\"grunt-mocha-istanbul\":\"^5.0.2\",\"grunt-saucelabs\":\"^9.0.1\",\"istanbul\":\"^0.4.5\",\"mocha\":\"^8.0.1\"},\"files\":[\"lib\"],\"homepage\":\"https://github.com/indutny/elliptic\",\"keywords\":[\"EC\",\"Elliptic\",\"curve\",\"Cryptography\"],\"license\":\"MIT\",\"main\":\"lib/elliptic.js\",\"name\":\"elliptic\",\"repository\":{\"type\":\"git\",\"url\":\"git+ssh://git@github.com/indutny/elliptic.git\"},\"scripts\":{\"lint\":\"eslint lib test\",\"lint:fix\":\"npm run lint -- --fix\",\"test\":\"npm run lint && npm run unit\",\"unit\":\"istanbul test _mocha --reporter=spec test/index.js\",\"version\":\"grunt dist && git add dist/\"},\"version\":\"6.5.4\"}");

/***/ }),
/* 145 */
Expand Down
8 changes: 4 additions & 4 deletions FlowCryptAppTests/Core/FlowCryptCoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ final class FlowCryptCoreTests: XCTestCase {

override func setUp() {
let expectation = XCTestExpectation()
core.startInBackgroundIfNotAlreadyRunning {
Task {
await core.startIfNotAlreadyRunning()
expectation.fulfill()
}
wait(for: [expectation], timeout: 20)
Expand Down Expand Up @@ -387,9 +388,8 @@ final class FlowCryptCoreTests: XCTestCase {
}
}

// this test is only meaningful on a real device
// it passes on simulator even if implementation is broken
// maybe there's a way to run simulator with more cores? (on a mac that has them) which would simulate real device better
// This test always passes, even wrongly, on simulators running on a mac with 2 or fewer cores.
// Behaves meaningfully on real iPhone or simulator on a mac with many cores
func testCoreResponseCorrectnessUnderConcurrency() async throws {
// given: a bunch of keys
let pp = "this particular pass phrase is long enough"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ final class KeyServiceTests: XCTestCase {
super.setUp()

let expectation = XCTestExpectation()
Core.shared.startInBackgroundIfNotAlreadyRunning {
Task {
await Core.shared.startIfNotAlreadyRunning()
expectation.fulfill()
}
wait(for: [expectation], timeout: 10)
Expand Down