diff options
author | Layomi Akinrinade <layomia@gmail.com> | 2022-05-25 01:19:32 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-05-25 01:19:32 +0300 |
commit | bfbb78354e536ac616f2f9dabf3db2b8fa8b9f64 (patch) | |
tree | 2cb29e98010fa2d9f3612aa980eba18283db1175 /src/mono/wasm/runtime | |
parent | c5f949efa20bcb555c453037fa954fcc403f9490 (diff) |
Use SubtleCrypto API on browser DOM scenarios (#65966)
* Use SubtleCrypto API on browser DOM scenarios
* Add sync over async implementation
* Address misc feedback and make fixes
* Address pinvoke errors
* [Attempt] Correct execution of native digest API call at wasm layer
* [Fix up] Correct execution of native digest API call at wasm layer
* Update src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
* Address feedback and clean up
* Re-implement the crypto worker in ts
* Address feedback
* Revert "Re-implement the crypto worker in ts"
This reverts commit 6a743909605fb5b1194cae6bf571c2e6ff059409.
* * moved stuff around and renamed it
* initialization bit later
* Add code to handle errors in worker (particularly on init)
* Clean up
* Add crypto dll to wasm native project
* Add e2e test
* Adjust test to reflect lack of SharedArrayBuffer for Chrome in test harness
* Enable Chrome test and validate hashed value in tests
* fix merge to track assert being renamed to mono_assert
Co-authored-by: Eric StJohn <ericstj@microsoft.com>
Co-authored-by: pavelsavara <pavel.savara@gmail.com>
Co-authored-by: Ankit Jain <radical@gmail.com>
Diffstat (limited to 'src/mono/wasm/runtime')
-rw-r--r-- | src/mono/wasm/runtime/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 4 | ||||
-rw-r--r-- | src/mono/wasm/runtime/crypto-worker.ts | 211 | ||||
-rw-r--r-- | src/mono/wasm/runtime/dotnet-crypto-worker.js | 170 | ||||
-rw-r--r-- | src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 4 | ||||
-rw-r--r-- | src/mono/wasm/runtime/exports.ts | 5 | ||||
-rw-r--r-- | src/mono/wasm/runtime/startup.ts | 3 |
7 files changed, 399 insertions, 1 deletions
diff --git a/src/mono/wasm/runtime/CMakeLists.txt b/src/mono/wasm/runtime/CMakeLists.txt index dac8d63e719..9962dbff59c 100644 --- a/src/mono/wasm/runtime/CMakeLists.txt +++ b/src/mono/wasm/runtime/CMakeLists.txt @@ -26,7 +26,8 @@ target_link_libraries(dotnet ${MONO_ARTIFACTS_DIR}/libmono-wasm-eh-js.a ${MONO_ARTIFACTS_DIR}/libmono-profiler-aot.a ${NATIVE_BIN_DIR}/libSystem.Native.a - ${NATIVE_BIN_DIR}/libSystem.IO.Compression.Native.a) + ${NATIVE_BIN_DIR}/libSystem.IO.Compression.Native.a + ${NATIVE_BIN_DIR}/libSystem.Security.Cryptography.Native.Browser.a) set_target_properties(dotnet PROPERTIES LINK_DEPENDS "${NATIVE_BIN_DIR}/src/emcc-default.rsp;${NATIVE_BIN_DIR}/src/cjs/dotnet.cjs.pre.js;${NATIVE_BIN_DIR}/src/cjs/runtime.cjs.iffe.js;${NATIVE_BIN_DIR}/src/cjs/dotnet.cjs.lib.js;${NATIVE_BIN_DIR}/src/pal_random.lib.js;${NATIVE_BIN_DIR}/src/cjs/dotnet.cjs.post.js;${NATIVE_BIN_DIR}/src/cjs/dotnet.cjs.extpost.js;" diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 79116860e1c..ec39de8e376 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -67,6 +67,10 @@ const linked_functions = [ // pal_icushim_static.c "mono_wasm_load_icu_data", "mono_wasm_get_icudt_name", + + // pal_crypto_webworker.c + "dotnet_browser_simple_digest_hash", + "dotnet_browser_can_use_simple_digest_hash", ]; // -- this javascript file is evaluated by emcc during compilation! -- diff --git a/src/mono/wasm/runtime/crypto-worker.ts b/src/mono/wasm/runtime/crypto-worker.ts new file mode 100644 index 00000000000..ea7bd9e6fce --- /dev/null +++ b/src/mono/wasm/runtime/crypto-worker.ts @@ -0,0 +1,211 @@ +// 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 { mono_assert } from "./types"; + +let mono_wasm_crypto: { + channel: LibraryChannel + worker: Worker +} | null = null; + +export function dotnet_browser_can_use_simple_digest_hash(): number { + return mono_wasm_crypto === null ? 0 : 1; +} + +export function dotnet_browser_simple_digest_hash(ver: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number { + mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized"); + + const msg = { + func: "digest", + type: ver, + data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len)) + }; + + const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg)); + const digest = JSON.parse(response); + if (digest.length > output_len) { + console.info("call_digest: about to throw!"); + throw "DIGEST HASH: Digest length exceeds output length: " + digest.length + " > " + output_len; + } + + Module.HEAPU8.set(digest, output_buffer); + return 1; +} + +export function init_crypto(): void { + if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined" + && typeof SharedArrayBuffer !== "undefined" + && typeof Worker !== "undefined" + ) { + console.debug("MONO_WASM: Initializing Crypto WebWorker"); + + const chan = LibraryChannel.create(1024); // 1024 is the buffer size in char units. + const worker = new Worker("dotnet-crypto-worker.js"); + mono_wasm_crypto = { + channel: chan, + worker: worker, + }; + worker.postMessage({ + comm_buf: chan.get_comm_buffer(), + msg_buf: chan.get_msg_buffer(), + msg_char_len: chan.get_msg_len() + }); + 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; + }; + } +} + +class LibraryChannel { + private msg_char_len: number; + private comm_buf: SharedArrayBuffer; + private msg_buf: SharedArrayBuffer; + private comm: Int32Array; + private msg: Uint16Array; + + // Index constants for the communication buffer. + private get STATE_IDX(): number { return 0; } + private get MSG_SIZE_IDX(): number { return 1; } + private get COMM_LAST_IDX(): number { return this.MSG_SIZE_IDX; } + + // Communication states. + private get STATE_SHUTDOWN(): number { return -1; } // Shutdown + private get STATE_IDLE(): number { return 0; } + private get STATE_REQ(): number { return 1; } + private get STATE_RESP(): number { return 2; } + private get STATE_REQ_P(): number { return 3; } // Request has multiple parts + private get STATE_RESP_P(): number { return 4; } // Response has multiple parts + private get STATE_AWAIT(): number { return 5; } // Awaiting the next part + + private constructor(msg_char_len: number) { + this.msg_char_len = msg_char_len; + + const int_bytes = 4; + const comm_byte_len = int_bytes * (this.COMM_LAST_IDX + 1); + this.comm_buf = new SharedArrayBuffer(comm_byte_len); + + // JavaScript character encoding is UTF-16. + const char_bytes = 2; + const msg_byte_len = char_bytes * this.msg_char_len; + this.msg_buf = new SharedArrayBuffer(msg_byte_len); + + // Create the local arrays to use. + this.comm = new Int32Array(this.comm_buf); + this.msg = new Uint16Array(this.msg_buf); + } + + public get_msg_len(): number { return this.msg_char_len; } + public get_msg_buffer(): SharedArrayBuffer { return this.msg_buf; } + public get_comm_buffer(): SharedArrayBuffer { return this.comm_buf; } + + public send_msg(msg: string): string { + if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_IDLE) { + throw "OWNER: Invalid sync communication channel state. " + Atomics.load(this.comm, this.STATE_IDX); + } + this.send_request(msg); + return this.read_response(); + } + + public shutdown(): void { + if (Atomics.load(this.comm, this.STATE_IDX) !== this.STATE_IDLE) { + throw "OWNER: Invalid sync communication channel state. " + Atomics.load(this.comm, this.STATE_IDX); + } + + // Notify webworker + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + Atomics.store(this.comm, this.STATE_IDX, this.STATE_SHUTDOWN); + Atomics.notify(this.comm, this.STATE_IDX); + } + + private send_request(msg: string): void { + let state; + const msg_len = msg.length; + let msg_written = 0; + + for (; ;) { + // 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_REQ : this.STATE_REQ_P; + + // Notify webworker + Atomics.store(this.comm, this.STATE_IDX, state); + Atomics.notify(this.comm, this.STATE_IDX); + + // The send message is complete. + if (state === this.STATE_REQ) + break; + + // Wait for the worker to be ready for the next part. + // - Atomics.wait() is not permissible on the main thread. + do { + state = Atomics.load(this.comm, this.STATE_IDX); + } while (state !== this.STATE_AWAIT); + } + } + + private write_to_msg(input: string, start: number, input_len: number): 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; + } + + private read_response(): string { + let state; + let response = ""; + for (; ;) { + // Wait for webworker response. + // - Atomics.wait() is not permissible on the main thread. + do { + state = Atomics.load(this.comm, this.STATE_IDX); + } while (state !== this.STATE_RESP && state !== this.STATE_RESP_P); + + const size_to_read = Atomics.load(this.comm, this.MSG_SIZE_IDX); + + // Append the latest part of the message. + response += this.read_from_msg(0, size_to_read); + + // The response is complete. + if (state === this.STATE_RESP) { + break; + } + + // Reset the size and transition to await state. + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + Atomics.store(this.comm, this.STATE_IDX, this.STATE_AWAIT); + Atomics.notify(this.comm, this.STATE_IDX); + } + + // Reset the communication channel's state and let the + // webworker know we are done. + Atomics.store(this.comm, this.STATE_IDX, this.STATE_IDLE); + Atomics.notify(this.comm, this.STATE_IDX); + + return response; + } + + private read_from_msg(begin: number, end: number): string { + const slicedMessage: number[] = []; + this.msg.slice(begin, end).forEach((value, index) => slicedMessage[index] = value); + return String.fromCharCode.apply(null, slicedMessage); + } + + public static create(msg_char_len: number): LibraryChannel { + if (msg_char_len === undefined) { + msg_char_len = 1024; // Default size is arbitrary but is in 'char' units (i.e. UTF-16 code points). + } + return new LibraryChannel(msg_char_len); + } +} diff --git a/src/mono/wasm/runtime/dotnet-crypto-worker.js b/src/mono/wasm/runtime/dotnet-crypto-worker.js new file mode 100644 index 00000000000..c6416492a71 --- /dev/null +++ b/src/mono/wasm/runtime/dotnet-crypto-worker.js @@ -0,0 +1,170 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +var ChannelWorker = { + _impl: class { + // BEGIN ChannelOwner contract - shared constants. + get STATE_IDX() { return 0; } + get MSG_SIZE_IDX() { return 1; } + + // 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 + // 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 await_request(async_call) { + for (;;) { + // Wait for signal to perform operation + Atomics.wait(this.comm, this.STATE_IDX, this.STATE_IDLE); + + // Read in request + var req = this._read_request(); + if (req === this.STATE_SHUTDOWN) + break; + + var resp = null; + try { + // Perform async action based on request + resp = await async_call(req); + } + catch (err) { + console.log("Request error: " + err); + resp = JSON.stringify(err); + } + + // Send response + this._send_response(resp); + } + } + + _read_request() { + var request = ""; + for (;;) { + // 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. + if (state === this.STATE_SHUTDOWN) + return this.STATE_SHUTDOWN; + + // Reset the size and transition to await state. + Atomics.store(this.comm, this.MSG_SIZE_IDX, 0); + Atomics.store(this.comm, this.STATE_IDX, this.STATE_AWAIT); + Atomics.wait(this.comm, this.STATE_IDX, 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 "WORKER: Invalid sync communication channel state."; + + var state; // State machine variable + const msg_len = msg.length; + var msg_written = 0; + + for (;;) { + // 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 + Atomics.store(this.comm, this.STATE_IDX, state); + + // Wait for the transition to know the main thread has + // received the response by moving onto a new state. + Atomics.wait(this.comm, this.STATE_IDX, 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; + } + }, + + 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) { + var digest_type = ""; + switch(type) { + case 0: digest_type = "SHA-1"; break; + case 1: digest_type = "SHA-256"; break; + case 2: digest_type = "SHA-384"; break; + case 3: digest_type = "SHA-512"; break; + default: + throw "CRYPTO: Unknown digest: " + type; + } + + // The 'crypto' API is not available in non-browser + // environments (for example, v8 server). + var digest = await crypto.subtle.digest(digest_type, data); + return Array.from(new Uint8Array(digest)); +} + +// Operation to perform. +async function async_call(msg) { + const req = JSON.parse(msg); + + if (req.func === "digest") { + var digestArr = await call_digest(req.type, new Uint8Array(req.data)); + return JSON.stringify(digestArr); + } else { + throw "CRYPTO: Unknown request: " + req.func; + } +} + +var s_channel; + +// 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.await_request(async_call); +}; diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index f886800ab50..47b59063b7b 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -104,6 +104,10 @@ const linked_functions = [ // pal_icushim_static.c "mono_wasm_load_icu_data", "mono_wasm_get_icudt_name", + + // pal_crypto_webworker.c + "dotnet_browser_simple_digest_hash", + "dotnet_browser_can_use_simple_digest_hash", ]; // -- this javascript file is evaluated by emcc during compilation! -- diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index ae7efdfab33..8a77c785c9c 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -68,6 +68,7 @@ import { fetch_like, readAsync_like } from "./polyfills"; import { EmscriptenModule } from "./types/emscripten"; import { mono_run_main, mono_run_main_and_exit } from "./run"; import { diagnostics } from "./diagnostics"; +import { dotnet_browser_can_use_simple_digest_hash, dotnet_browser_simple_digest_hash } from "./crypto-worker"; const MONO = { // current "public" MONO API @@ -365,6 +366,10 @@ export const __linker_exports: any = { // also keep in sync with pal_icushim_static.c mono_wasm_load_icu_data, mono_wasm_get_icudt_name, + + // pal_crypto_webworker.c + dotnet_browser_simple_digest_hash, + dotnet_browser_can_use_simple_digest_hash, }; const INTERNAL: any = { diff --git a/src/mono/wasm/runtime/startup.ts b/src/mono/wasm/runtime/startup.ts index 3a34af2244f..5c74ef0eb4b 100644 --- a/src/mono/wasm/runtime/startup.ts +++ b/src/mono/wasm/runtime/startup.ts @@ -15,6 +15,7 @@ import { VoidPtr, CharPtr } from "./types/emscripten"; import { DotnetPublicAPI } from "./exports"; import { mono_on_abort } from "./run"; import { mono_wasm_new_root } from "./roots"; +import { init_crypto } from "./crypto-worker"; export let runtime_is_initialized_resolve: Function; export let runtime_is_initialized_reject: Function; @@ -119,6 +120,8 @@ async function mono_wasm_pre_init(): Promise<void> { await requirePromise; } + init_crypto(); + if (moduleExt.configSrc) { try { // sets MONO.config implicitly |