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 | |
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')
25 files changed, 713 insertions, 18 deletions
diff --git a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props index e98978abda4..6704af1c528 100644 --- a/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props +++ b/src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props @@ -95,6 +95,7 @@ <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.Apple.dylib" IsNative="true" /> <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.Android.a" IsNative="true" /> <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.Android.so" IsNative="true" /> + <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.Browser.a" IsNative="true" /> <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.OpenSsl.a" IsNative="true" /> <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.OpenSsl.dylib" IsNative="true" /> <PlatformManifestFileEntry Include="libSystem.Security.Cryptography.Native.OpenSsl.so" IsNative="true" /> @@ -210,6 +211,7 @@ <PlatformManifestFileEntry Include="libmono-wasm-eh-js.a" IsNative="true" /> <PlatformManifestFileEntry Include="libmono-wasm-eh-wasm.a" IsNative="true" /> <PlatformManifestFileEntry Include="System.Private.Runtime.InteropServices.Javascript.dll" /> + <PlatformManifestFileEntry Include="dotnet-crypto-worker.js" IsNative="true" /> <PlatformManifestFileEntry Include="dotnet.js" IsNative="true" /> <PlatformManifestFileEntry Include="dotnet.worker.js" IsNative="true" /> <PlatformManifestFileEntry Include="dotnet.js.symbols" IsNative="true" /> 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 00000000000..b28d723f0bf --- /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 00000000000..1304b45735b --- /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 19a6e4ed3f8..53d2ee092b0 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 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <DefineConstants>$(DefineConstants);INTERNAL_ASYMMETRIC_IMPLEMENTATIONS</DefineConstants> @@ -532,12 +532,15 @@ </Compile> </ItemGroup> <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'Browser'"> - <Compile Include="$(CommonPath)Interop\Unix\Interop.Libraries.cs" - Link="Common\Interop\Unix\Interop.Libraries.cs" /> + <!-- GetRandomBytes is identical on Unix and Browser --> <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.GetRandomBytes.cs" Link="Common\Interop\Unix\System.Native\Interop.GetRandomBytes.cs" /> + <Compile Include="$(CommonPath)Interop\Browser\Interop.Libraries.cs" + Link="Common\Interop\Browser\Interop.Libraries.cs" /> <Compile Include="$(CommonPath)System\Sha1ForNonSecretPurposes.cs" Link="Common\System\Sha1ForNonSecretPurposes.cs" /> + <Compile Include="$(CommonPath)Interop\Browser\System.Security.Cryptography.Native.Browser\Interop.SimpleDigestHash.cs" + Link="Common\Interop\Browser\System.Security.Cryptography.Native.Browser\Interop.SimpleDigestHash.cs" /> <Compile Include="System\Security\Cryptography\AesCcm.NotSupported.cs" /> <Compile Include="System\Security\Cryptography\AesGcm.NotSupported.cs" /> <Compile Include="System\Security\Cryptography\AesImplementation.NotSupported.cs" /> @@ -563,7 +566,8 @@ <Compile Include="System\Security\Cryptography\RC2Implementation.NotSupported.cs" /> <Compile Include="System\Security\Cryptography\RSACryptoServiceProvider.NotSupported.cs" /> <Compile Include="System\Security\Cryptography\RSA.Create.NotSupported.cs" /> - <Compile Include="System\Security\Cryptography\SHAHashProvider.Browser.cs" /> + <Compile Include="System\Security\Cryptography\SHAHashProvider.Browser.Managed.cs" /> + <Compile Include="System\Security\Cryptography\SHAHashProvider.Browser.Native.cs" /> <Compile Include="System\Security\Cryptography\TripleDESCryptoServiceProvider.NotSupported.cs" /> <Compile Include="System\Security\Cryptography\TripleDesImplementation.NotSupported.cs" /> <Compile Include="System\Security\Cryptography\X509Certificates\CertificatePal.NotSupported.cs" /> 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 d49be47509f..031d28ffea2 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 @@ namespace System.Security.Cryptography 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 @@ namespace System.Security.Cryptography public static int HashData(string hashAlgorithmId, ReadOnlySpan<byte> source, Span<byte> 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 index 5a72d4de54b..574cdf8790b 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 @@ using static System.Numerics.BitOperations; 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 00000000000..c037761aafb --- /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<byte> data) + { + _buffer ??= new MemoryStream(1000); + _buffer.Write(data); + } + + public override int FinalizeHashAndReset(Span<byte> destination) + { + GetCurrentHash(destination); + _buffer = null; + + return _hashSizeInBytes; + } + + public override int GetCurrentHash(Span<byte> destination) + { + Debug.Assert(destination.Length >= _hashSizeInBytes); + + byte[] srcArray = Array.Empty<byte>(); + 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 6fa63793c4d..f8e1b4697b8 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 @@ <AllowUnsafeBlocks>true</AllowUnsafeBlocks> <EnableLibraryImportGenerator>true</EnableLibraryImportGenerator> </PropertyGroup> + <PropertyGroup Condition="'$(TargetOS)' == 'Browser'"> + <Scenario>WasmTestOnBrowser</Scenario> + <WasmXHarnessArgs>$(WasmXHarnessArgs) --web-server-use-cop</WasmXHarnessArgs> + </PropertyGroup> <!-- DesignTimeBuild requires all the TargetFramework Derived Properties to not be present in the first property group. --> <PropertyGroup> <TargetPlatformIdentifier>$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)'))</TargetPlatformIdentifier> diff --git a/src/mono/wasm/build/WasmApp.Native.targets b/src/mono/wasm/build/WasmApp.Native.targets index b92320d735b..a224ecb9b0f 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" /> </ItemGroup> <PInvokeTableGenerator diff --git a/src/mono/wasm/build/WasmApp.targets b/src/mono/wasm/build/WasmApp.targets index bc598307276..c1c7c84fd90 100644 --- a/src/mono/wasm/build/WasmApp.targets +++ b/src/mono/wasm/build/WasmApp.targets @@ -261,6 +261,7 @@ <_HasDotnetWasm Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.wasm'">true</_HasDotnetWasm> <_HasDotnetJsWorker Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.worker.js'">true</_HasDotnetJsWorker> + <_HasDotnetJsCryptoWorker Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet-crypto-worker.js'">true</_HasDotnetJsCryptoWorker> <_HasDotnetJsSymbols Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js.symbols'">true</_HasDotnetJsSymbols> <_HasDotnetJs Condition="'%(WasmNativeAsset.FileName)%(WasmNativeAsset.Extension)' == 'dotnet.js'">true</_HasDotnetJs> </PropertyGroup> @@ -270,6 +271,7 @@ <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.wasm" Condition="'$(_HasDotnetWasm)' != 'true'" /> <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.js" Condition="'$(_HasDotnetJs)' != 'true'" /> <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.worker.js" Condition="'$(_HasDotnetJsWorker)' != 'true' and Exists('$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.worker.js')" /> + <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet-crypto-worker.js" Condition="'$(_HasDotnetJsCryptoWorker)' != 'true' and Exists('$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet-crypto-worker.js')" /> <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.js.symbols" Condition="'$(WasmEmitSymbolMap)' == 'true' and '$(_HasDotnetJs)' != 'true' and Exists('$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.js.symbols')" /> 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 diff --git a/src/mono/wasm/wasm.proj b/src/mono/wasm/wasm.proj index e439c173cc6..267bf886396 100644 --- a/src/mono/wasm/wasm.proj +++ b/src/mono/wasm/wasm.proj @@ -24,6 +24,7 @@ <_EmccCompileRspPath>$(NativeBinDir)src\emcc-compile.rsp</_EmccCompileRspPath> <_EmccLinkRspPath>$(NativeBinDir)src\emcc-link.rsp</_EmccLinkRspPath> <WasmNativeStrip Condition="'$(ContinuousIntegrationBuild)' == 'true'">false</WasmNativeStrip> + <SystemCryptoNativeDir>$(RepoRoot)\src\native\libs\System.Security.Cryptography.Native.Browser</SystemCryptoNativeDir> </PropertyGroup> <Target Name="CheckEnv"> @@ -47,6 +48,7 @@ <WasmPInvokeModule Include="libSystem.Native" /> <WasmPInvokeModule Include="libSystem.IO.Compression.Native" /> <WasmPInvokeModule Include="libSystem.Globalization.Native" /> + <WasmPInvokeModule Include="libSystem.Security.Cryptography.Native.Browser" /> <WasmPInvokeAssembly Include="@(LibrariesRuntimeFiles)" Condition="'%(Extension)' == '.dll' and '%(IsNative)' != 'true'" /> </ItemGroup> @@ -224,6 +226,7 @@ <Copy SourceFiles="runtime/driver.c; runtime/pinvoke.c; runtime/corebindings.c; + runtime/dotnet-crypto-worker.js; $(SharedNativeRoot)libs\System.Native\pal_random.lib.js;" DestinationFolder="$(NativeBinDir)src" SkipUnchangedFiles="true" /> @@ -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 c15ca54cb10..577a6dee6b7 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 00000000000..c411aa9ee9c --- /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 00000000000..775fe634536 --- /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 <emscripten.h> + +#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 00000000000..5f4da5a9862 --- /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 00000000000..fe8b4d2762b --- /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 <stdint.h> + +// 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 722d5246cc9..6c11e076aba 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/BuildTestBase.cs @@ -156,7 +156,7 @@ namespace Wasm.Build.Tests { 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 @@ namespace Wasm.Build.Tests "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 c659ce90739..e3e013b55f4 100644 --- a/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeLibraryTests.cs +++ b/src/tests/BuildWasmApps/Wasm.Build.Tests/NativeLibraryTests.cs @@ -93,5 +93,58 @@ public class Test 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); + } + } } } |