Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/dotnet/runtime.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLayomi Akinrinade <layomia@gmail.com>2022-05-25 01:19:32 +0300
committerGitHub <noreply@github.com>2022-05-25 01:19:32 +0300
commitbfbb78354e536ac616f2f9dabf3db2b8fa8b9f64 (patch)
tree2cb29e98010fa2d9f3612aa980eba18283db1175 /src/mono/wasm/runtime
parentc5f949efa20bcb555c453037fa954fcc403f9490 (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.txt3
-rw-r--r--src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js4
-rw-r--r--src/mono/wasm/runtime/crypto-worker.ts211
-rw-r--r--src/mono/wasm/runtime/dotnet-crypto-worker.js170
-rw-r--r--src/mono/wasm/runtime/es6/dotnet.es6.lib.js4
-rw-r--r--src/mono/wasm/runtime/exports.ts5
-rw-r--r--src/mono/wasm/runtime/startup.ts3
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