diff --git a/eng/SignCheckExclusionsFile.txt b/eng/SignCheckExclusionsFile.txt
index b45e2daaceb8d4..f41753338155eb 100644
--- a/eng/SignCheckExclusionsFile.txt
+++ b/eng/SignCheckExclusionsFile.txt
@@ -12,4 +12,4 @@
*apphosttemplateapphostexe.exe;;Template, DO-NOT-SIGN, https://github.com/dotnet/core-setup/pull/7549
*comhosttemplatecomhostdll.dll;;Template, DO-NOT-SIGN, https://github.com/dotnet/core-setup/pull/7549
*staticapphosttemplateapphostexe.exe;;Template, DO-NOT-SIGN, https://github.com/dotnet/core-setup/pull/7549
-*dotnet.js;;Workaround, https://github.com/dotnet/core-eng/issues/9933
+*dotnet.js;;Workaround, https://github.com/dotnet/core-eng/issues/9933
\ No newline at end of file
diff --git a/eng/liveBuilds.targets b/eng/liveBuilds.targets
index 77e9a90b97d30c..d38e44645672b7 100644
--- a/eng/liveBuilds.targets
+++ b/eng/liveBuilds.targets
@@ -180,6 +180,7 @@
+
@@ -210,6 +211,7 @@
+
diff --git a/src/libraries/Common/src/Interop/Browser/Interop.Libraries.cs b/src/libraries/Common/src/Interop/Browser/Interop.Libraries.cs
new file mode 100644
index 00000000000000..b28d723f0bfc9d
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Browser/Interop.Libraries.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+internal static partial class Interop
+{
+ internal static partial class Libraries
+ {
+ // Shims
+ internal const string SystemNative = "libSystem.Native";
+ internal const string CryptoNative = "libSystem.Security.Cryptography.Native.Browser";
+ }
+}
diff --git a/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SimpleDigestHash.cs b/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SimpleDigestHash.cs
new file mode 100644
index 00000000000000..1304b45735b7ef
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SimpleDigestHash.cs
@@ -0,0 +1,32 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.Diagnostics;
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class BrowserCrypto
+ {
+ // These values are also defined in the pal_crypto_webworker header file, and utilized in the dotnet-crypto-worker in the wasm runtime.
+ internal enum SimpleDigest
+ {
+ Sha1,
+ Sha256,
+ Sha384,
+ Sha512,
+ };
+
+ [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_CanUseSimpleDigestHash")]
+ internal static partial int CanUseSimpleDigestHash();
+
+ [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_SimpleDigestHash")]
+ internal static unsafe partial int SimpleDigestHash(
+ SimpleDigest hash,
+ byte* input_buffer,
+ int input_len,
+ byte* output_buffer,
+ int output_len);
+ }
+}
diff --git a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
index 19a6e4ed3f8ad9..53d2ee092b0396 100644
--- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
+++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
@@ -1,4 +1,4 @@
-
+
true
$(DefineConstants);INTERNAL_ASYMMETRIC_IMPLEMENTATIONS
@@ -532,12 +532,15 @@
-
+
+
+
@@ -563,7 +566,8 @@
-
+
+
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HashProviderDispenser.Browser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HashProviderDispenser.Browser.cs
index d49be47509f26d..031d28ffea2ef4 100644
--- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HashProviderDispenser.Browser.cs
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HashProviderDispenser.Browser.cs
@@ -1,17 +1,14 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System;
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-using System.Security.Cryptography;
-using Microsoft.Win32.SafeHandles;
using Internal.Cryptography;
namespace System.Security.Cryptography
{
internal static partial class HashProviderDispenser
{
+ internal static readonly bool CanUseSubtleCryptoImpl = Interop.BrowserCrypto.CanUseSimpleDigestHash() == 1;
+
public static HashProvider CreateHashProvider(string hashAlgorithmId)
{
switch (hashAlgorithmId)
@@ -20,7 +17,9 @@ public static HashProvider CreateHashProvider(string hashAlgorithmId)
case HashAlgorithmNames.SHA256:
case HashAlgorithmNames.SHA384:
case HashAlgorithmNames.SHA512:
- return new SHAHashProvider(hashAlgorithmId);
+ return CanUseSubtleCryptoImpl
+ ? new SHANativeHashProvider(hashAlgorithmId)
+ : new SHAManagedHashProvider(hashAlgorithmId);
}
throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmId));
}
@@ -38,7 +37,7 @@ public static unsafe int MacData(
public static int HashData(string hashAlgorithmId, ReadOnlySpan source, Span destination)
{
- HashProvider provider = HashProviderDispenser.CreateHashProvider(hashAlgorithmId);
+ HashProvider provider = CreateHashProvider(hashAlgorithmId);
provider.AppendHashData(source);
return provider.FinalizeHashAndReset(destination);
}
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Managed.cs
similarity index 99%
rename from src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.cs
rename to src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Managed.cs
index 5a72d4de54b071..574cdf8790b8af 100644
--- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.cs
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Managed.cs
@@ -1,4 +1,4 @@
-// Licensed to the .NET Foundation under one or more agreements.
+// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO;
@@ -9,13 +9,13 @@
namespace System.Security.Cryptography
{
- internal sealed class SHAHashProvider : HashProvider
+ internal sealed class SHAManagedHashProvider : HashProvider
{
private int hashSizeInBytes;
private SHAManagedImplementationBase impl;
private MemoryStream? buffer;
- public SHAHashProvider(string hashAlgorithmId)
+ public SHAManagedHashProvider(string hashAlgorithmId)
{
switch (hashAlgorithmId)
{
diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs
new file mode 100644
index 00000000000000..c037761aafb541
--- /dev/null
+++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs
@@ -0,0 +1,96 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+using System.IO;
+using System.Diagnostics;
+using System.Security.Cryptography;
+
+using SimpleDigest = Interop.BrowserCrypto.SimpleDigest;
+
+namespace Internal.Cryptography
+{
+ internal sealed class SHANativeHashProvider : HashProvider
+ {
+ private readonly int _hashSizeInBytes;
+ private readonly SimpleDigest _impl;
+ private MemoryStream? _buffer;
+
+ public SHANativeHashProvider(string hashAlgorithmId)
+ {
+ Debug.Assert(HashProviderDispenser.CanUseSubtleCryptoImpl);
+
+ switch (hashAlgorithmId)
+ {
+ case HashAlgorithmNames.SHA1:
+ _impl = SimpleDigest.Sha1;
+ _hashSizeInBytes = 20;
+ break;
+ case HashAlgorithmNames.SHA256:
+ _impl = SimpleDigest.Sha256;
+ _hashSizeInBytes = 32;
+ break;
+ case HashAlgorithmNames.SHA384:
+ _impl = SimpleDigest.Sha384;
+ _hashSizeInBytes = 48;
+ break;
+ case HashAlgorithmNames.SHA512:
+ _impl = SimpleDigest.Sha512;
+ _hashSizeInBytes = 64;
+ break;
+ default:
+ throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, hashAlgorithmId));
+ }
+ }
+
+ public override void AppendHashData(ReadOnlySpan data)
+ {
+ _buffer ??= new MemoryStream(1000);
+ _buffer.Write(data);
+ }
+
+ public override int FinalizeHashAndReset(Span destination)
+ {
+ GetCurrentHash(destination);
+ _buffer = null;
+
+ return _hashSizeInBytes;
+ }
+
+ public override int GetCurrentHash(Span destination)
+ {
+ Debug.Assert(destination.Length >= _hashSizeInBytes);
+
+ byte[] srcArray = Array.Empty();
+ int srcLength = 0;
+ if (_buffer != null)
+ {
+ srcArray = _buffer.GetBuffer();
+ srcLength = (int)_buffer.Length;
+ }
+
+ unsafe
+ {
+ fixed (byte* src = srcArray)
+ fixed (byte* dest = destination)
+ {
+ int res = Interop.BrowserCrypto.SimpleDigestHash(_impl, src, srcLength, dest, destination.Length);
+ Debug.Assert(res != 0);
+ }
+ }
+
+ return _hashSizeInBytes;
+ }
+
+ public override int HashSizeInBytes => _hashSizeInBytes;
+
+ public override void Dispose(bool disposing)
+ {
+ }
+
+ public override void Reset()
+ {
+ _buffer = null;
+ }
+ }
+}
diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
index 6fa63793c4d82b..f8e1b4697b8046 100644
--- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
+++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj
@@ -6,6 +6,10 @@
true
true
+
+ WasmTestOnBrowser
+ $(WasmXHarnessArgs) --web-server-use-cop
+
$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))
diff --git a/src/mono/wasm/build/WasmApp.Native.targets b/src/mono/wasm/build/WasmApp.Native.targets
index b92320d735befa..a224ecb9b0fef7 100644
--- a/src/mono/wasm/build/WasmApp.Native.targets
+++ b/src/mono/wasm/build/WasmApp.Native.targets
@@ -271,6 +271,7 @@
<_WasmPInvokeModules Include="libSystem.Native" />
<_WasmPInvokeModules Include="libSystem.IO.Compression.Native" />
<_WasmPInvokeModules Include="libSystem.Globalization.Native" />
+ <_WasmPInvokeModules Include="libSystem.Security.Cryptography.Native.Browser" />
true
<_HasDotnetJsWorker Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.worker.js'">true
+ <_HasDotnetJsCryptoWorker Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet-crypto-worker.js'">true
<_HasDotnetJsSymbols Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js.symbols'">true
<_HasDotnetJs Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js'">true
@@ -270,6 +271,7 @@
+
diff --git a/src/mono/wasm/runtime/CMakeLists.txt b/src/mono/wasm/runtime/CMakeLists.txt
index dac8d63e719d35..9962dbff59c2b5 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 79116860e1ce8b..ec39de8e376575 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 00000000000000..ea7bd9e6fce5a7
--- /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 00000000000000..c6416492a7157e
--- /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 f886800ab508f0..47b59063b7b951 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 ae7efdfab333e3..8a77c785c9ca8a 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 3a34af2244f447..5c74ef0eb4b562 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 {
await requirePromise;
}
+ init_crypto();
+
if (moduleExt.configSrc) {
try {
// sets MONO.config implicitly
diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj
index e439c173cc61fd..267bf886396a63 100644
--- a/src/mono/wasm/wasm.proj
+++ b/src/mono/wasm/wasm.proj
@@ -24,6 +24,7 @@
<_EmccCompileRspPath>$(NativeBinDir)src\emcc-compile.rsp
<_EmccLinkRspPath>$(NativeBinDir)src\emcc-link.rsp
false
+ $(RepoRoot)\src\native\libs\System.Security.Cryptography.Native.Browser
@@ -47,6 +48,7 @@
+
@@ -224,6 +226,7 @@
@@ -270,6 +273,7 @@
$(NativeBinDir)dotnet.d.ts;
$(NativeBinDir)package.json;
$(NativeBinDir)dotnet.wasm;
+ $(NativeBinDir)\src\dotnet-crypto-worker.js;
$(NativeBinDir)dotnet.timezones.blat"
DestinationFolder="$(MicrosoftNetCoreAppRuntimePackNativeDir)"
SkipUnchangedFiles="true" />
diff --git a/src/native/libs/CMakeLists.txt b/src/native/libs/CMakeLists.txt
index c15ca54cb10001..577a6dee6b714f 100644
--- a/src/native/libs/CMakeLists.txt
+++ b/src/native/libs/CMakeLists.txt
@@ -149,7 +149,7 @@ if (CLR_CMAKE_TARGET_UNIX OR CLR_CMAKE_TARGET_BROWSER)
add_subdirectory(System.Native)
if (CLR_CMAKE_TARGET_BROWSER)
- # skip for now
+ add_subdirectory(System.Security.Cryptography.Native.Browser)
elseif (CLR_CMAKE_TARGET_MACCATALYST)
add_subdirectory(System.Net.Security.Native)
# System.Security.Cryptography.Native is intentionally disabled on iOS
diff --git a/src/native/libs/System.Security.Cryptography.Native.Browser/CMakeLists.txt b/src/native/libs/System.Security.Cryptography.Native.Browser/CMakeLists.txt
new file mode 100644
index 00000000000000..c411aa9ee9cd66
--- /dev/null
+++ b/src/native/libs/System.Security.Cryptography.Native.Browser/CMakeLists.txt
@@ -0,0 +1,14 @@
+project(System.Security.Cryptography.Native.Browser C)
+
+set (NATIVE_SOURCES
+ pal_crypto_webworker.c
+)
+
+add_library (System.Security.Cryptography.Native.Browser-Static
+ STATIC
+ ${NATIVE_SOURCES}
+)
+
+set_target_properties(System.Security.Cryptography.Native.Browser-Static PROPERTIES OUTPUT_NAME System.Security.Cryptography.Native.Browser CLEAN_DIRECT_OUTPUT 1)
+
+install (TARGETS System.Security.Cryptography.Native.Browser-Static DESTINATION ${STATIC_LIB_DESTINATION})
diff --git a/src/native/libs/System.Security.Cryptography.Native.Browser/pal_browser.h b/src/native/libs/System.Security.Cryptography.Native.Browser/pal_browser.h
new file mode 100644
index 00000000000000..775fe634536e26
--- /dev/null
+++ b/src/native/libs/System.Security.Cryptography.Native.Browser/pal_browser.h
@@ -0,0 +1,18 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma once
+
+#include
+
+#ifndef __EMSCRIPTEN__
+#error Cryptography Native Browser is designed to be compiled with Emscripten.
+#endif // __EMSCRIPTEN__
+
+#ifndef PALEXPORT
+#ifdef TARGET_UNIX
+#define PALEXPORT __attribute__ ((__visibility__ ("default")))
+#else
+#define PALEXPORT __declspec(dllexport)
+#endif
+#endif // PALEXPORT
diff --git a/src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.c b/src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.c
new file mode 100644
index 00000000000000..5f4da5a98627a9
--- /dev/null
+++ b/src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.c
@@ -0,0 +1,30 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#include "pal_browser.h"
+#include "pal_crypto_webworker.h"
+
+// Forward declarations
+extern int32_t dotnet_browser_simple_digest_hash(
+ enum simple_digest ver,
+ uint8_t* input_buffer,
+ int32_t input_len,
+ uint8_t* output_buffer,
+ int32_t output_len);
+
+extern int32_t dotnet_browser_can_use_simple_digest_hash(void);
+
+int32_t SystemCryptoNativeBrowser_SimpleDigestHash(
+ enum simple_digest ver,
+ uint8_t* input_buffer,
+ int32_t input_len,
+ uint8_t* output_buffer,
+ int32_t output_len)
+{
+ return dotnet_browser_simple_digest_hash(ver, input_buffer, input_len, output_buffer, output_len);
+}
+
+int32_t SystemCryptoNativeBrowser_CanUseSimpleDigestHash(void)
+{
+ return dotnet_browser_can_use_simple_digest_hash();
+}
diff --git a/src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.h b/src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.h
new file mode 100644
index 00000000000000..fe8b4d2762bf2f
--- /dev/null
+++ b/src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.h
@@ -0,0 +1,25 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+#pragma once
+
+#include
+
+// These values are also defined in the System.Security.Cryptography library's
+// browser-crypto implementation, and utilized in the dotnet-crypto-worker in the wasm runtime.
+enum simple_digest
+{
+ sd_sha_1,
+ sd_sha_256,
+ sd_sha_384,
+ sd_sha_512,
+};
+
+PALEXPORT int32_t SystemCryptoNativeBrowser_SimpleDigestHash(
+ enum simple_digest ver,
+ uint8_t* input_buffer,
+ int32_t input_len,
+ uint8_t* output_buffer,
+ int32_t output_len);
+
+PALEXPORT int32_t SystemCryptoNativeBrowser_CanUseSimpleDigestHash(void);
diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
index 722d5246cc9c60..6c11e076aba594 100644
--- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
+++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs
@@ -156,7 +156,7 @@ protected string RunAndTestWasmApp(BuildArgs buildArgs,
{
RunHost.V8 => ("wasm test", "--js-file=test-main.js --engine=V8 -v trace"),
RunHost.NodeJS => ("wasm test", "--js-file=test-main.js --engine=NodeJS -v trace"),
- _ => ("wasm test-browser", $"-v trace -b {host}")
+ _ => ("wasm test-browser", $"-v trace -b {host} --web-server-use-cop")
};
string testLogPath = Path.Combine(_logPath, host.ToString());
@@ -509,7 +509,8 @@ protected static void AssertBasicAppBundle(string bundleDir, string projectName,
"dotnet.timezones.blat",
"dotnet.wasm",
"mono-config.json",
- "dotnet.js"
+ "dotnet.js",
+ "dotnet-crypto-worker.js"
});
AssertFilesExist(bundleDir, new[] { "run-v8.sh" }, expectToExist: hasV8Script);
diff --git a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeLibraryTests.cs b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeLibraryTests.cs
index c659ce90739e83..e3e013b55f4454 100644
--- a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeLibraryTests.cs
+++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeLibraryTests.cs
@@ -93,5 +93,58 @@ public static int Main()
Assert.Contains("Size: 26462 Height: 599, Width: 499", output);
}
+
+ [ConditionalTheory(typeof(BuildTestBase), nameof(IsUsingWorkloads))]
+ [BuildAndRun(aot: false)]
+ [BuildAndRun(aot: true)]
+ public void ProjectUsingBrowserNativeCrypto(BuildArgs buildArgs, RunHost host, string id)
+ {
+ string projectName = $"AppUsingBrowserNativeCrypto";
+ buildArgs = buildArgs with { ProjectName = projectName };
+ buildArgs = ExpandBuildArgs(buildArgs);
+
+ string programText = @"
+using System;
+using System.Security.Cryptography;
+
+public class Test
+{
+ public static int Main()
+ {
+ using (SHA256 mySHA256 = SHA256.Create())
+ {
+ byte[] data = { (byte)'H', (byte)'e', (byte)'l', (byte)'l', (byte)'o' };
+ byte[] hashed = mySHA256.ComputeHash(data);
+ string asStr = string.Join(' ', hashed);
+ Console.WriteLine(""Hashed: "" + asStr);
+ return 0;
+ }
+ }
+}";
+
+ BuildProject(buildArgs,
+ id: id,
+ new BuildProjectOptions(
+ InitProject: () => File.WriteAllText(Path.Combine(_projectDir!, "Program.cs"), programText),
+ DotnetWasmFromRuntimePack: !buildArgs.AOT && buildArgs.Config != "Release"));
+
+ string output = RunAndTestWasmApp(buildArgs, buildDir: _projectDir, expectedExitCode: 0,
+ test: output => {},
+ host: host, id: id);
+
+ Assert.Contains(
+ "Hashed: 24 95 141 179 34 113 254 37 245 97 166 252 147 139 46 38 67 6 236 48 78 218 81 128 7 209 118 72 38 56 25 105",
+ output);
+
+ string cryptoInitMsg = "MONO_WASM: Initializing Crypto WebWorker";
+ if (host == RunHost.V8 || host == RunHost.NodeJS)
+ {
+ Assert.DoesNotContain(cryptoInitMsg, output);
+ }
+ else
+ {
+ Assert.Contains(cryptoInitMsg, output);
+ }
+ }
}
}