From 7ade2b47bb18c4ef3a28e393e9b1d7824c33d997 Mon Sep 17 00:00:00 2001 From: Pavel Savara Date: Sat, 6 Aug 2022 06:50:40 +0200 Subject: [PATCH] [wasm] rewrite crypto-worker to typescript (#73073) * rewrite of crypto-worker to typescript * returning some noisy console logs in hope that they are serializing access and preventing the deadlock. This is RC1 stop-gap. --- src/mono/wasm/runtime/crypto-worker.ts | 34 +- .../runtime/workers/dotnet-crypto-worker.js | 394 ------------------ .../runtime/workers/dotnet-crypto-worker.ts | 388 +++++++++++++++++ 3 files changed, 410 insertions(+), 406 deletions(-) delete mode 100644 src/mono/wasm/runtime/workers/dotnet-crypto-worker.js create mode 100644 src/mono/wasm/runtime/workers/dotnet-crypto-worker.ts diff --git a/src/mono/wasm/runtime/crypto-worker.ts b/src/mono/wasm/runtime/crypto-worker.ts index d43ccdc93b0a6..adde81a9ec9bc 100644 --- a/src/mono/wasm/runtime/crypto-worker.ts +++ b/src/mono/wasm/runtime/crypto-worker.ts @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -import { Module } from "./imports"; +import { Module, runtimeHelpers } from "./imports"; import { mono_assert } from "./types"; class OperationFailedError extends Error { } @@ -112,11 +112,13 @@ export function init_crypto(): void { channel: chan, worker: worker, }; - worker.postMessage({ + const messageData: InitCryptoMessageData = { + config: JSON.stringify(runtimeHelpers.config), comm_buf: chan.get_comm_buffer(), msg_buf: chan.get_msg_buffer(), msg_char_len: chan.get_msg_len() - }); + }; + worker.postMessage(messageData); worker.onerror = event => { console.warn(`MONO_WASM: Error in Crypto WebWorker. Cryptography digest calls will fallback to managed implementation. Error: ${event.message}`); mono_wasm_crypto = null; @@ -202,9 +204,10 @@ class LibraryChannel { public send_msg(msg: string): string { try { - // const state = Atomics.load(this.comm, this.STATE_IDX); - // if (state !== this.STATE_IDLE) console.debug(`MONO_WASM_ENCRYPT_DECRYPT: send_msg, waiting for idle now, ${state}`); - this.wait_for_state(pstate => pstate == this.STATE_IDLE, "waiting"); + let state = Atomics.load(this.comm, this.STATE_IDX); + // FIXME: this console write is possibly serializing the access and prevents a deadlock + if (state !== this.STATE_IDLE) console.debug(`MONO_WASM_ENCRYPT_DECRYPT: send_msg, waiting for idle now, ${state}`); + state = this.wait_for_state(pstate => pstate == this.STATE_IDLE, "waiting"); this.send_request(msg); return this.read_response(); @@ -213,13 +216,14 @@ class LibraryChannel { throw err; } finally { - // const state = Atomics.load(this.comm, this.STATE_IDX); - // if (state !== this.STATE_IDLE) console.debug(`MONO_WASM_ENCRYPT_DECRYPT: state at end of send_msg: ${state}`); + const state = Atomics.load(this.comm, this.STATE_IDX); + // FIXME: this console write is possibly serializing the access and prevents a deadlock + if (state !== this.STATE_IDLE) console.debug(`MONO_WASM_ENCRYPT_DECRYPT: state at end of send_msg: ${state}`); } } public shutdown(): void { - // console.debug("MONO_WASM_ENCRYPT_DECRYPT: Shutting down crypto"); + console.debug("MONO_WASM_ENCRYPT_DECRYPT: Shutting down crypto"); const state = Atomics.load(this.comm, this.STATE_IDX); if (state !== this.STATE_IDLE) throw new Error(`OWNER: Invalid sync communication channel state: ${state}`); @@ -230,15 +234,14 @@ class LibraryChannel { Atomics.notify(this.comm, this.STATE_IDX); } - // eslint-disable-next-line @typescript-eslint/no-unused-vars private reset(reason: string): void { - // console.debug(`MONO_WASM_ENCRYPT_DECRYPT: reset: ${reason}`); + console.debug(`MONO_WASM_ENCRYPT_DECRYPT: reset: ${reason}`); const state = Atomics.load(this.comm, this.STATE_IDX); if (state === this.STATE_SHUTDOWN) return; if (state === this.STATE_RESET || state === this.STATE_IDLE) { - // console.debug(`MONO_WASM_ENCRYPT_DECRYPT: state is already RESET or idle: ${state}`); + console.debug(`MONO_WASM_ENCRYPT_DECRYPT: state is already RESET or idle: ${state}`); return; } @@ -386,3 +389,10 @@ class LibraryChannel { return new LibraryChannel(msg_char_len); } } + +export type InitCryptoMessageData = { + config: string,// serialized to avoid passing non-clonable objects + comm_buf: SharedArrayBuffer, + msg_buf: SharedArrayBuffer, + msg_char_len: number +} diff --git a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js deleted file mode 100644 index 7e000aa5649ea..0000000000000 --- a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js +++ /dev/null @@ -1,394 +0,0 @@ -// @ts-check -/// -/// -/// -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -import { setup_proxy_console } from "../debug"; - -class FailedOrStoppedLoopError extends Error { } -class ArgumentsError extends Error { } -class WorkerFailedError extends Error { } - -var ChannelWorker = { - _impl: class { - // LOCK states - get LOCK_UNLOCKED() { return 0; } // 0 means the lock is unlocked - get LOCK_OWNED() { return 2; } // 2 means the ChannelWorker owns the lock - - // BEGIN ChannelOwner contract - shared constants. - get STATE_IDX() { return 0; } - get MSG_SIZE_IDX() { return 1; } - get LOCK_IDX() { return 2; } - - // Communication states. - get STATE_SHUTDOWN() { return -1; } // Shutdown - get STATE_IDLE() { return 0; } - get STATE_REQ() { return 1; } - get STATE_RESP() { return 2; } - get STATE_REQ_P() { return 3; } // Request has multiple parts - get STATE_RESP_P() { return 4; } // Response has multiple parts - get STATE_AWAIT() { return 5; } // Awaiting the next part - get STATE_REQ_FAILED() { return 6; } // The Request failed - get STATE_RESET() { return 7; } // Reset to a known state - // END ChannelOwner contract - shared constants. - - constructor(comm_buf, msg_buf, msg_char_len) { - this.comm = new Int32Array(comm_buf); - this.msg = new Uint16Array(msg_buf); - this.msg_char_len = msg_char_len; - } - - async run_message_loop(async_op) { - for (; ;) { - try { - // Wait for signal to perform operation - let state; - do { - this._wait(this.STATE_IDLE); - state = Atomics.load(this.comm, this.STATE_IDX); - } while (state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_SHUTDOWN && state !== this.STATE_REQ_FAILED && state !== this.STATE_RESET); - - this._throw_if_reset_or_shutdown(); - - // Read in request - var req = this._read_request(); - var resp = {}; - try { - // Perform async action based on request - resp.result = await async_op(req); - } - catch (err) { - resp.error_type = typeof err; - resp.error = _stringify_err(err); - console.error(`MONO_WASM: Request error: ${resp.error}. req was: ${req}`); - } - - // Send response - this._send_response(JSON.stringify(resp)); - } catch (err) { - if (err instanceof FailedOrStoppedLoopError) { - const state = Atomics.load(this.comm, this.STATE_IDX); - if (state === this.STATE_SHUTDOWN) - break; - if (state === this.STATE_RESET) - console.debug("MONO_WASM: caller failed, resetting worker"); - } else { - console.error(`MONO_WASM: Worker failed to handle the request: ${_stringify_err(err)}`); - this._change_state_locked(this.STATE_REQ_FAILED); - Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); - - console.debug("MONO_WASM: set state to failed, now waiting to get RESET"); - Atomics.wait(this.comm, this.STATE_IDX, this.STATE_REQ_FAILED); - const state = Atomics.load(this.comm, this.STATE_IDX); - if (state !== this.STATE_RESET) { - throw new WorkerFailedError(`expected to RESET, but got ${state}`); - } - } - - Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); - Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); - this._change_state_locked(this.STATE_IDLE); - } - - const state = Atomics.load(this.comm, this.STATE_IDX); - const lock_state = Atomics.load(this.comm, this.LOCK_IDX); - - if (state !== this.STATE_IDLE && state !== this.STATE_REQ && state !== this.STATE_REQ_P) - console.error(`MONO_WASM: -- state is not idle at the top of the loop: ${state}, and lock_state: ${lock_state}`); - if (lock_state !== this.LOCK_UNLOCKED && state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_IDLE) - console.error(`MONO_WASM: -- lock is not unlocked at the top of the loop: ${lock_state}, and state: ${state}`); - } - - Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); - this._change_state_locked(this.STATE_SHUTDOWN); - console.debug("MONO_WASM: ******* run_message_loop ending"); - } - - _read_request() { - var request = ""; - for (; ;) { - this._acquire_lock(); - try { - this._throw_if_reset_or_shutdown(); - - // Get the current state and message size - var state = Atomics.load(this.comm, this.STATE_IDX); - var size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); - - // Append the latest part of the message. - request += this._read_from_msg(0, size_to_read); - - // The request is complete. - if (state === this.STATE_REQ) { - break; - } - - // Shutdown the worker. - this._throw_if_reset_or_shutdown(); - - // Reset the size and transition to await state. - Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); - this._change_state_locked(this.STATE_AWAIT); - } finally { - this._release_lock(); - } - - this._wait(this.STATE_AWAIT); - } - - return request; - } - - _read_from_msg(begin, end) { - return String.fromCharCode.apply(null, this.msg.slice(begin, end)); - } - - _send_response(msg) { - if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_REQ) - throw new WorkerFailedError("WORKER: Invalid sync communication channel state."); - - var state; // State machine variable - const msg_len = msg.length; - var msg_written = 0; - - for (; ;) { - this._acquire_lock(); - - try { - // Write the message and return how much was written. - var wrote = this._write_to_msg(msg, msg_written, msg_len); - msg_written += wrote; - - // Indicate how much was written to the this.msg buffer. - Atomics.store(this.comm, this.MSG_SIZE_IDX, wrote); - - // Indicate if this was the whole message or part of it. - state = msg_written === msg_len ? this.STATE_RESP : this.STATE_RESP_P; - - // Update the state - this._change_state_locked(state); - } finally { - this._release_lock(); - } - - // Wait for the transition to know the main thread has - // received the response by moving onto a new state. - this._wait(state); - - // Done sending response. - if (state === this.STATE_RESP) - break; - } - } - - _write_to_msg(input, start, input_len) { - var mi = 0; - var ii = start; - while (mi < this.msg_char_len && ii < input_len) { - this.msg[mi] = input.charCodeAt(ii); - ii++; // Next character - mi++; // Next buffer index - } - return ii - start; - } - - _change_state_locked(newState) { - Atomics.store(this.comm, this.STATE_IDX, newState); - } - - _acquire_lock() { - for (; ;) { - const lockState = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED); - this._throw_if_reset_or_shutdown(); - - if (lockState === this.LOCK_UNLOCKED) - return; - } - } - - _release_lock() { - const result = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_OWNED, this.LOCK_UNLOCKED); - if (result !== this.LOCK_OWNED) { - throw new WorkerFailedError("CRYPTO: ChannelWorker tried to release a lock that wasn't acquired: " + result); - } - } - - _wait(expected_state) { - Atomics.wait(this.comm, this.STATE_IDX, expected_state); - this._throw_if_reset_or_shutdown(); - } - - _throw_if_reset_or_shutdown() { - const state = Atomics.load(this.comm, this.STATE_IDX); - if (state === this.STATE_RESET || state === this.STATE_SHUTDOWN) - throw new FailedOrStoppedLoopError(); - } - }, - - create: function (comm_buf, msg_buf, msg_char_len) { - return new this._impl(comm_buf, msg_buf, msg_char_len); - } -}; - -async function call_digest(type, data) { - const digest_type = get_hash_name(type); - - // The 'crypto' API is not available in non-browser - // environments (for example, v8 server). - const digest = await crypto.subtle.digest(digest_type, data); - return Array.from(new Uint8Array(digest)); -} - -async function sign(type, key, data) { - const hash_name = get_hash_name(type); - - if (key.length === 0) { - // crypto.subtle.importKey will raise an error for an empty key. - // To prevent an error, reset it to a key with just a `0x00` byte. This is equivalent - // since HMAC keys get zero-extended up to the block size of the algorithm. - key = new Uint8Array([0]); - } - - const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: hash_name }, false /* extractable */, ["sign"]); - const signResult = await crypto.subtle.sign("HMAC", cryptoKey, data); - return Array.from(new Uint8Array(signResult)); -} - -async function derive_bits(password, salt, iterations, hashAlgorithm, lengthInBytes) { - const hash_name = get_hash_name(hashAlgorithm); - - const passwordKey = await importKey(password, "PBKDF2", ["deriveBits"]); - const result = await crypto.subtle.deriveBits( - { - name: "PBKDF2", - salt: salt, - iterations: iterations, - hash: hash_name - }, - passwordKey, - lengthInBytes * 8 // deriveBits takes number of bits - ); - - return Array.from(new Uint8Array(result)); -} - -function get_hash_name(type) { - switch (type) { - case 0: return "SHA-1"; - case 1: return "SHA-256"; - case 2: return "SHA-384"; - case 3: return "SHA-512"; - default: - throw new ArgumentsError("CRYPTO: Unknown digest: " + type); - } -} - -const AesBlockSizeBytes = 16; // 128 bits - -async function encrypt_decrypt(isEncrypting, key, iv, data) { - const algorithmName = "AES-CBC"; - const keyUsage = isEncrypting ? ["encrypt"] : ["encrypt", "decrypt"]; - const cryptoKey = await importKey(new Uint8Array(key), algorithmName, keyUsage); - const algorithm = { - name: algorithmName, - iv: new Uint8Array(iv) - }; - - const result = await (isEncrypting ? - crypto.subtle.encrypt( - algorithm, - cryptoKey, - new Uint8Array(data)) : - decrypt( - algorithm, - cryptoKey, - data)); - - let resultByteArray = new Uint8Array(result); - if (isEncrypting) { - // trim off the last block, which is always a padding block. - resultByteArray = resultByteArray.slice(0, resultByteArray.length - AesBlockSizeBytes); - } - return Array.from(resultByteArray); -} - -async function decrypt(algorithm, cryptoKey, data) { - // crypto.subtle AES-CBC will only allow a PaddingMode of PKCS7, but we need to use - // PaddingMode None. To simulate this, we only decrypt full blocks of data, with an extra full - // padding block of 0x10 (16) bytes appended to data. crypto.subtle will see that padding block and return - // the fully decrypted message. To create the encrypted padding block, we encrypt an empty array using the - // last block of the cipher text as the IV. This will create a full block of padding bytes. - - const paddingBlockIV = new Uint8Array(data).slice(data.length - AesBlockSizeBytes); - const empty = new Uint8Array(); - const encryptedPaddingBlockResult = await crypto.subtle.encrypt( - { - name: algorithm.name, - iv: paddingBlockIV - }, - cryptoKey, - empty - ); - - const encryptedPaddingBlock = new Uint8Array(encryptedPaddingBlockResult); - for (let i = 0; i < encryptedPaddingBlock.length; i++) { - data.push(encryptedPaddingBlock[i]); - } - - return await crypto.subtle.decrypt( - algorithm, - cryptoKey, - new Uint8Array(data)); -} - -function importKey(key, algorithmName, keyUsage) { - return crypto.subtle.importKey( - "raw", - key, - { - name: algorithmName - }, - false /* extractable */, - keyUsage); -} - -// Operation to perform. -async function handle_req_async(msg) { - const req = JSON.parse(msg); - - if (req.func === "digest") { - return await call_digest(req.type, new Uint8Array(req.data)); - } - else if (req.func === "sign") { - return await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data)); - } - else if (req.func === "encrypt_decrypt") { - return await encrypt_decrypt(req.isEncrypting, req.key, req.iv, req.data); - } - else if (req.func === "derive_bits") { - return await derive_bits(new Uint8Array(req.password), new Uint8Array(req.salt), req.iterations, req.hashAlgorithm, req.lengthInBytes); - } - else { - throw new ArgumentsError("CRYPTO: Unknown request: " + req.func); - } -} - -function _stringify_err(err) { - return (err instanceof Error && err.stack !== undefined) ? err.stack : err; -} - -var s_channel; - -setup_proxy_console("crypto-worker", console, self.location.origin); - -// Initialize WebWorker -onmessage = function (p) { - var data = p; - if (p.data !== undefined) { - data = p.data; - } - s_channel = ChannelWorker.create(data.comm_buf, data.msg_buf, data.msg_char_len); - s_channel.run_message_loop(handle_req_async); -}; diff --git a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.ts b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.ts new file mode 100644 index 0000000000000..6b4dcde69c12d --- /dev/null +++ b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.ts @@ -0,0 +1,388 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +import { setup_proxy_console } from "../debug"; +import type { InitCryptoMessageData } from "../crypto-worker"; +import type { MonoConfig } from "../types"; + +class FailedOrStoppedLoopError extends Error { } +class ArgumentsError extends Error { } +class WorkerFailedError extends Error { } + +class ChannelWorker { + // LOCK states + get LOCK_UNLOCKED() { return 0; } // 0 means the lock is unlocked + get LOCK_OWNED() { return 2; } // 2 means the ChannelWorker owns the lock + + // BEGIN ChannelOwner contract - shared constants. + get STATE_IDX() { return 0; } + get MSG_SIZE_IDX() { return 1; } + get LOCK_IDX() { return 2; } + + // Communication states. + get STATE_SHUTDOWN() { return -1; } // Shutdown + get STATE_IDLE() { return 0; } + get STATE_REQ() { return 1; } + get STATE_RESP() { return 2; } + get STATE_REQ_P() { return 3; } // Request has multiple parts + get STATE_RESP_P() { return 4; } // Response has multiple parts + get STATE_AWAIT() { return 5; } // Awaiting the next part + get STATE_REQ_FAILED() { return 6; } // The Request failed + get STATE_RESET() { return 7; } // Reset to a known state + // END ChannelOwner contract - shared constants. + + private comm: Int32Array; + private msg: Uint16Array; + private msg_char_len: number; + + constructor(comm_buf: SharedArrayBuffer, msg_buf: SharedArrayBuffer, msg_char_len: number) { + this.comm = new Int32Array(comm_buf); + this.msg = new Uint16Array(msg_buf); + this.msg_char_len = msg_char_len; + } + + async run_message_loop(async_op: (jsonRequest: string) => Promise) { + for (; ;) { + try { + // Wait for signal to perform operation + let state; + do { + this._wait(this.STATE_IDLE); + state = Atomics.load(this.comm, this.STATE_IDX); + } while (state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_SHUTDOWN && state !== this.STATE_REQ_FAILED && state !== this.STATE_RESET); + + this._throw_if_reset_or_shutdown(); + + // Read in request + const request_json = this._read_request(); + const response: any = {}; + try { + // Perform async action based on request + response.result = await async_op(request_json); + } + catch (err) { + response.error_type = typeof err; + response.error = _stringify_err(err); + console.error(`MONO_WASM: Request error: ${response.error}. req was: ${request_json}`); + } + + // Send response + this._send_response(JSON.stringify(response)); + } catch (err) { + if (err instanceof FailedOrStoppedLoopError) { + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state === this.STATE_SHUTDOWN) + break; + if (state === this.STATE_RESET) + console.debug("MONO_WASM: caller failed, resetting worker"); + } else { + console.error(`MONO_WASM: Worker failed to handle the request: ${_stringify_err(err)}`); + this._change_state_locked(this.STATE_REQ_FAILED); + Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); + + console.debug("MONO_WASM: set state to failed, now waiting to get RESET"); + Atomics.wait(this.comm, this.STATE_IDX, this.STATE_REQ_FAILED); + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state !== this.STATE_RESET) { + throw new WorkerFailedError(`expected to RESET, but got ${state}`); + } + } + + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + Atomics.store(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED); + this._change_state_locked(this.STATE_IDLE); + } + + const state = Atomics.load(this.comm, this.STATE_IDX); + const lock_state = Atomics.load(this.comm, this.LOCK_IDX); + + if (state !== this.STATE_IDLE && state !== this.STATE_REQ && state !== this.STATE_REQ_P) + console.error(`MONO_WASM: -- state is not idle at the top of the loop: ${state}, and lock_state: ${lock_state}`); + if (lock_state !== this.LOCK_UNLOCKED && state !== this.STATE_REQ && state !== this.STATE_REQ_P && state !== this.STATE_IDLE) + console.error(`MONO_WASM: -- lock is not unlocked at the top of the loop: ${lock_state}, and state: ${state}`); + } + + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + this._change_state_locked(this.STATE_SHUTDOWN); + console.debug("MONO_WASM: ******* run_message_loop ending"); + } + + _read_request(): string { + let request = ""; + for (; ;) { + this._acquire_lock(); + try { + this._throw_if_reset_or_shutdown(); + + // Get the current state and message size + const state = Atomics.load(this.comm, this.STATE_IDX); + const size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); + + const view = this.msg.subarray(0, size_to_read); + const part = String.fromCharCode(...view); + // Append the latest part of the message. + request += part; + + // The request is complete. + if (state === this.STATE_REQ) { + break; + } + + // Shutdown the worker. + this._throw_if_reset_or_shutdown(); + + // Reset the size and transition to await state. + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + this._change_state_locked(this.STATE_AWAIT); + } finally { + this._release_lock(); + } + + this._wait(this.STATE_AWAIT); + } + + return request; + } + + _send_response(msg: string) { + if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_REQ) + throw new WorkerFailedError("WORKER: Invalid sync communication channel state."); + + let state; // State machine variable + const msg_len = msg.length; + let msg_written = 0; + + for (; ;) { + this._acquire_lock(); + + try { + // Write the message and return how much was written. + const wrote = this._write_to_msg(msg, msg_written, msg_len); + msg_written += wrote; + + // Indicate how much was written to the this.msg buffer. + Atomics.store(this.comm, this.MSG_SIZE_IDX, wrote); + + // Indicate if this was the whole message or part of it. + state = msg_written === msg_len ? this.STATE_RESP : this.STATE_RESP_P; + + // Update the state + this._change_state_locked(state); + } finally { + this._release_lock(); + } + + // Wait for the transition to know the main thread has + // received the response by moving onto a new state. + this._wait(state); + + // Done sending response. + if (state === this.STATE_RESP) + break; + } + } + + _write_to_msg(input: string, start: number, input_len: number) { + let mi = 0; + let ii = start; + while (mi < this.msg_char_len && ii < input_len) { + this.msg[mi] = input.charCodeAt(ii); + ii++; // Next character + mi++; // Next buffer index + } + return ii - start; + } + + _change_state_locked(newState: number) { + Atomics.store(this.comm, this.STATE_IDX, newState); + } + + _acquire_lock() { + for (; ;) { + const lockState = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_UNLOCKED, this.LOCK_OWNED); + this._throw_if_reset_or_shutdown(); + + if (lockState === this.LOCK_UNLOCKED) + return; + } + } + + _release_lock() { + const result = Atomics.compareExchange(this.comm, this.LOCK_IDX, this.LOCK_OWNED, this.LOCK_UNLOCKED); + if (result !== this.LOCK_OWNED) { + throw new WorkerFailedError("CRYPTO: ChannelWorker tried to release a lock that wasn't acquired: " + result); + } + } + + _wait(expected_state: number) { + Atomics.wait(this.comm, this.STATE_IDX, expected_state); + this._throw_if_reset_or_shutdown(); + } + + _throw_if_reset_or_shutdown() { + const state = Atomics.load(this.comm, this.STATE_IDX); + if (state === this.STATE_RESET || state === this.STATE_SHUTDOWN) + throw new FailedOrStoppedLoopError(); + } +} + +async function call_digest(type: number, data: Uint8Array) { + const digest_type = get_hash_name(type); + + // The 'crypto' API is not available in non-browser + // environments (for example, v8 server). + const digest = await crypto.subtle.digest(digest_type, data); + return Array.from(new Uint8Array(digest)); +} + +async function sign(type: number, key: Uint8Array, data: Uint8Array) { + const hash_name = get_hash_name(type); + + if (key.length === 0) { + // crypto.subtle.importKey will raise an error for an empty key. + // To prevent an error, reset it to a key with just a `0x00` byte. This is equivalent + // since HMAC keys get zero-extended up to the block size of the algorithm. + key = new Uint8Array([0]); + } + + const cryptoKey = await crypto.subtle.importKey("raw", key, { name: "HMAC", hash: hash_name }, false /* extractable */, ["sign"]); + const signResult = await crypto.subtle.sign("HMAC", cryptoKey, data); + return Array.from(new Uint8Array(signResult)); +} + +async function derive_bits(password: Uint8Array, salt: Uint8Array, iterations: number, hashAlgorithm: number, lengthInBytes: number) { + const hash_name = get_hash_name(hashAlgorithm); + + const passwordKey = await importKey(password, "PBKDF2", ["deriveBits"]); + const result = await crypto.subtle.deriveBits( + { + name: "PBKDF2", + salt: salt, + iterations: iterations, + hash: hash_name + }, + passwordKey, + lengthInBytes * 8 // deriveBits takes number of bits + ); + + return Array.from(new Uint8Array(result)); +} + +function get_hash_name(type: number) { + switch (type) { + case 0: return "SHA-1"; + case 1: return "SHA-256"; + case 2: return "SHA-384"; + case 3: return "SHA-512"; + default: + throw new ArgumentsError("CRYPTO: Unknown digest: " + type); + } +} + +const AesBlockSizeBytes = 16; // 128 bits + +async function encrypt_decrypt(isEncrypting: boolean, key: number[], iv: number[], data: number[]) { + const algorithmName = "AES-CBC"; + const keyUsage: KeyUsage[] = isEncrypting ? ["encrypt"] : ["encrypt", "decrypt"]; + const cryptoKey = await importKey(new Uint8Array(key), algorithmName, keyUsage); + const algorithm = { + name: algorithmName, + iv: new Uint8Array(iv) + }; + + const result = await (isEncrypting ? + crypto.subtle.encrypt( + algorithm, + cryptoKey, + new Uint8Array(data)) : + decrypt( + algorithm, + cryptoKey, + data)); + + let resultByteArray = new Uint8Array(result); + if (isEncrypting) { + // trim off the last block, which is always a padding block. + resultByteArray = resultByteArray.slice(0, resultByteArray.length - AesBlockSizeBytes); + } + return Array.from(resultByteArray); +} + +async function decrypt(algorithm: Algorithm, cryptoKey: CryptoKey, data: number[]) { + // crypto.subtle AES-CBC will only allow a PaddingMode of PKCS7, but we need to use + // PaddingMode None. To simulate this, we only decrypt full blocks of data, with an extra full + // padding block of 0x10 (16) bytes appended to data. crypto.subtle will see that padding block and return + // the fully decrypted message. To create the encrypted padding block, we encrypt an empty array using the + // last block of the cipher text as the IV. This will create a full block of padding bytes. + + const paddingBlockIV = new Uint8Array(data).slice(data.length - AesBlockSizeBytes); + const empty = new Uint8Array(); + const encryptedPaddingBlockResult = await crypto.subtle.encrypt( + { + name: algorithm.name, + iv: paddingBlockIV + }, + cryptoKey, + empty + ); + + const encryptedPaddingBlock = new Uint8Array(encryptedPaddingBlockResult); + for (let i = 0; i < encryptedPaddingBlock.length; i++) { + data.push(encryptedPaddingBlock[i]); + } + + return await crypto.subtle.decrypt( + algorithm, + cryptoKey, + new Uint8Array(data)); +} + +function importKey(key: ArrayBuffer, algorithmName: string, keyUsage: KeyUsage[]) { + return crypto.subtle.importKey( + "raw", + key, + { + name: algorithmName + }, + false /* extractable */, + keyUsage); +} + +// Operation to perform. +async function handle_req_async(jsonRequest: string): Promise { + const req = JSON.parse(jsonRequest); + + if (req.func === "digest") { + return await call_digest(req.type, new Uint8Array(req.data)); + } + else if (req.func === "sign") { + return await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data)); + } + else if (req.func === "encrypt_decrypt") { + return await encrypt_decrypt(req.isEncrypting, req.key, req.iv, req.data); + } + else if (req.func === "derive_bits") { + return await derive_bits(new Uint8Array(req.password), new Uint8Array(req.salt), req.iterations, req.hashAlgorithm, req.lengthInBytes); + } + else { + throw new ArgumentsError("CRYPTO: Unknown request: " + req.func); + } +} + +function _stringify_err(err: any) { + return (err instanceof Error && err.stack !== undefined) ? err.stack : err; +} + +let s_channel; +let config: MonoConfig = null; + +// Initialize WebWorker +self.addEventListener("message", (event: MessageEvent) => { + const data = event.data as InitCryptoMessageData; + config = data && data.config ? JSON.parse(data.config) : {}; + if (config.diagnosticTracing) { + setup_proxy_console("crypto-worker", console, self.location.origin); + } + s_channel = new ChannelWorker(data.comm_buf, data.msg_buf, data.msg_char_len); + s_channel.run_message_loop(handle_req_async); +});