From: Eric Erhardt Date: Tue, 21 Jun 2022 21:27:16 +0000 (-0500) Subject: Use crypto.subtle for HMAC on Browser WASM (#70745) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055536~8408 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=eed18afa59b00f780461e0f9bd871fbd4b843b7c;p=platform%2Fupstream%2Fdotnet%2Fruntime.git Use crypto.subtle for HMAC on Browser WASM (#70745) * Use crypto.subtle for HMAC on Browser WASM Implement the browser "native" portion for HMAC on Browser WASM. I also made a few refactoring / simplifications where necessary. Contributes to #40074 --- diff --git a/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.Sign.cs b/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.Sign.cs new file mode 100644 index 0000000..2100b3f --- /dev/null +++ b/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.Sign.cs @@ -0,0 +1,22 @@ +// 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 + { + [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_Sign")] + internal static unsafe partial int Sign( + SimpleDigest hashAlgorithm, + byte* key_buffer, + int key_len, + byte* input_buffer, + int input_len, + byte* output_buffer, + int output_len); + } +} 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 index 1304b45..d664276 100644 --- 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 @@ -18,8 +18,8 @@ internal static partial class Interop Sha512, }; - [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_CanUseSimpleDigestHash")] - internal static partial int CanUseSimpleDigestHash(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_CanUseSubtleCryptoImpl")] + internal static partial int CanUseSubtleCryptoImpl(); [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_SimpleDigestHash")] internal static unsafe partial int SimpleDigestHash( 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 dfb550a..669191a 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -543,6 +543,8 @@ Link="Common\System\Sha1ForNonSecretPurposes.cs" /> + @@ -559,6 +561,7 @@ + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs new file mode 100644 index 0000000..819ae0e --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs @@ -0,0 +1,99 @@ +// 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 System.Security.Cryptography +{ + internal sealed class HMACNativeHashProvider : HashProvider + { + private readonly int _hashSizeInBytes; + private readonly SimpleDigest _hashAlgorithm; + private readonly byte[] _key; + private MemoryStream? _buffer; + + public HMACNativeHashProvider(string hashAlgorithmId, ReadOnlySpan key) + { + Debug.Assert(HashProviderDispenser.CanUseSubtleCryptoImpl); + + (_hashAlgorithm, _hashSizeInBytes) = SHANativeHashProvider.HashAlgorithmToPal(hashAlgorithmId); + _key = key.ToArray(); + } + + public override void AppendHashData(ReadOnlySpan data) + { + _buffer ??= new MemoryStream(1000); + _buffer.Write(data); + } + + public override int FinalizeHashAndReset(Span destination) + { + int written = GetCurrentHash(destination); + _buffer = null; + + return written; + } + + 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* key = _key) + fixed (byte* src = srcArray) + fixed (byte* dest = destination) + { + int res = Interop.BrowserCrypto.Sign(_hashAlgorithm, key, _key.Length, src, srcLength, dest, destination.Length); + Debug.Assert(res != 0); + } + } + + return _hashSizeInBytes; + } + + public static unsafe int MacDataOneShot(string hashAlgorithmId, ReadOnlySpan key, ReadOnlySpan data, Span destination) + { + (SimpleDigest hashName, int hashSizeInBytes) = SHANativeHashProvider.HashAlgorithmToPal(hashAlgorithmId); + Debug.Assert(destination.Length >= hashSizeInBytes); + + fixed (byte* k = key) + fixed (byte* src = data) + fixed (byte* dest = destination) + { + int res = Interop.BrowserCrypto.Sign(hashName, k, key.Length, src, data.Length, dest, destination.Length); + Debug.Assert(res != 0); + } + + return hashSizeInBytes; + } + + public override int HashSizeInBytes => _hashSizeInBytes; + + public override void Dispose(bool disposing) + { + if (disposing) + { + CryptographicOperations.ZeroMemory(_key); + } + } + + public override void Reset() + { + _buffer = null; + } + } +} 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 1326aeb..3b4f42b 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,13 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Internal.Cryptography; - namespace System.Security.Cryptography { internal static partial class HashProviderDispenser { - internal static readonly bool CanUseSubtleCryptoImpl = Interop.BrowserCrypto.CanUseSimpleDigestHash() == 1; + internal static readonly bool CanUseSubtleCryptoImpl = Interop.BrowserCrypto.CanUseSubtleCryptoImpl() == 1; public static HashProvider CreateHashProvider(string hashAlgorithmId) { @@ -32,9 +30,16 @@ namespace System.Security.Cryptography ReadOnlySpan source, Span destination) { - HashProvider provider = CreateMacProvider(hashAlgorithmId, key); - provider.AppendHashData(source); - return provider.FinalizeHashAndReset(destination); + if (CanUseSubtleCryptoImpl) + { + return HMACNativeHashProvider.MacDataOneShot(hashAlgorithmId, key, source, destination); + } + else + { + using HashProvider provider = CreateMacProvider(hashAlgorithmId, key); + provider.AppendHashData(source); + return provider.FinalizeHashAndReset(destination); + } } public static int HashData(string hashAlgorithmId, ReadOnlySpan source, Span destination) @@ -60,7 +65,9 @@ namespace System.Security.Cryptography case HashAlgorithmNames.SHA256: case HashAlgorithmNames.SHA384: case HashAlgorithmNames.SHA512: - return new HMACManagedHashProvider(hashAlgorithmId, key); + return CanUseSubtleCryptoImpl + ? new HMACNativeHashProvider(hashAlgorithmId, key) + : new HMACManagedHashProvider(hashAlgorithmId, key); } throw new CryptographicException(SR.Format(SR.Cryptography_UnknownHashAlgorithm, 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 index e7748e6..205f46e 100644 --- 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 @@ -8,7 +8,7 @@ using System.Security.Cryptography; using SimpleDigest = Interop.BrowserCrypto.SimpleDigest; -namespace Internal.Cryptography +namespace System.Security.Cryptography { internal sealed class SHANativeHashProvider : HashProvider { @@ -87,7 +87,7 @@ namespace Internal.Cryptography _buffer = null; } - private static (SimpleDigest, int) HashAlgorithmToPal(string hashAlgorithmId) + internal static (SimpleDigest HashName, int HashSizeInBytes) HashAlgorithmToPal(string hashAlgorithmId) { return hashAlgorithmId switch { diff --git a/src/libraries/System.Security.Cryptography/tests/HmacMD5Tests.cs b/src/libraries/System.Security.Cryptography/tests/HmacMD5Tests.cs index d658411..d657f96 100644 --- a/src/libraries/System.Security.Cryptography/tests/HmacMD5Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HmacMD5Tests.cs @@ -138,6 +138,16 @@ namespace System.Security.Cryptography.Tests } [Fact] + public void HMacMD5_EmptyKey() + { + VerifyRepeating( + input: "Crypto is fun!", + 1, + hexKey: "", + output: "7554A8C4641CBA36BE2AC20CACEA1136"); + } + + [Fact] public void HmacMD5_Stream_MultipleOf4096() { // Verfied with: diff --git a/src/libraries/System.Security.Cryptography/tests/HmacSha1Tests.cs b/src/libraries/System.Security.Cryptography/tests/HmacSha1Tests.cs index 4ae956c..9b5aa3e 100644 --- a/src/libraries/System.Security.Cryptography/tests/HmacSha1Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HmacSha1Tests.cs @@ -113,6 +113,16 @@ namespace System.Security.Cryptography.Tests } [Fact] + public void HmacSha1_EmptyKey() + { + VerifyRepeating( + input: "Crypto is fun!", + 1, + hexKey: "", + output: "C979AD8DE8CC546CF82D948226FDD8024599F6CE"); + } + + [Fact] public void HmacSha1_Rfc2202_1() { VerifyHmac(1, s_testMacs2202[1]); diff --git a/src/libraries/System.Security.Cryptography/tests/HmacSha256Tests.cs b/src/libraries/System.Security.Cryptography/tests/HmacSha256Tests.cs index e0f4348..dea07982 100644 --- a/src/libraries/System.Security.Cryptography/tests/HmacSha256Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HmacSha256Tests.cs @@ -125,6 +125,16 @@ namespace System.Security.Cryptography.Tests } [Fact] + public void HmacSha256_EmptyKey() + { + VerifyRepeating( + input: "Crypto is fun!", + 1, + hexKey: "", + output: "DE26DD5A23A91021F61EACF8A8DD324AB5637977486A10D701C4DFA4AE33CB4F"); + } + + [Fact] public void HmacSha256_Stream_MultipleOf4096() { // Verfied with: diff --git a/src/libraries/System.Security.Cryptography/tests/HmacSha384Tests.cs b/src/libraries/System.Security.Cryptography/tests/HmacSha384Tests.cs index deb2396..d026442 100644 --- a/src/libraries/System.Security.Cryptography/tests/HmacSha384Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HmacSha384Tests.cs @@ -138,6 +138,16 @@ namespace System.Security.Cryptography.Tests } [Fact] + public void HmacSha384_EmptyKey() + { + VerifyRepeating( + input: "Crypto is fun!", + 1, + hexKey: "", + output: "CFEB81812C8DB4EDB385FCC7CB81E4D715685741AAB1E470FB0B395A414F89867E510E4A2BA2F1F11D7005849FA0DF11"); + } + + [Fact] public void HmacSha384_Stream_MultipleOf4096() { // Verfied with: diff --git a/src/libraries/System.Security.Cryptography/tests/HmacSha512Tests.cs b/src/libraries/System.Security.Cryptography/tests/HmacSha512Tests.cs index 8c9f027..44aec88 100644 --- a/src/libraries/System.Security.Cryptography/tests/HmacSha512Tests.cs +++ b/src/libraries/System.Security.Cryptography/tests/HmacSha512Tests.cs @@ -138,6 +138,16 @@ namespace System.Security.Cryptography.Tests } [Fact] + public void HmacSha512_EmptyKey() + { + VerifyRepeating( + input: "Crypto is fun!", + 1, + hexKey: "", + output: "0C75CCE182743282AAB081BA12AA6C9DEA44852E567063B4EEBD7B33F940B6C8BC16958F9A23401E6FAA00483962A2A8FC7DE9D8B7A14EDD55B49419A211BC37"); + } + + [Fact] public void HmacSha512_Stream_MultipleOf4096() { // Verfied with: diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index ceeb9d3..d8492ff 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -70,8 +70,9 @@ const linked_functions = [ "mono_wasm_get_icudt_name", // pal_crypto_webworker.c + "dotnet_browser_can_use_subtle_crypto_impl", "dotnet_browser_simple_digest_hash", - "dotnet_browser_can_use_simple_digest_hash", + "dotnet_browser_sign", ]; // -- 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 index f17a455..92171e7 100644 --- a/src/mono/wasm/runtime/crypto-worker.ts +++ b/src/mono/wasm/runtime/crypto-worker.ts @@ -9,7 +9,7 @@ let mono_wasm_crypto: { worker: Worker } | null = null; -export function dotnet_browser_can_use_simple_digest_hash(): number { +export function dotnet_browser_can_use_subtle_crypto_impl(): number { return mono_wasm_crypto === null ? 0 : 1; } @@ -33,6 +33,27 @@ export function dotnet_browser_simple_digest_hash(ver: number, input_buffer: num return 1; } +export function dotnet_browser_sign(hashAlgorithm: number, key_buffer: number, key_len: 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: "sign", + type: hashAlgorithm, + key: Array.from(Module.HEAPU8.subarray(key_buffer, key_buffer + key_len)), + data: Array.from(Module.HEAPU8.subarray(input_buffer, input_buffer + input_len)) + }; + + const response = mono_wasm_crypto.channel.send_msg(JSON.stringify(msg)); + const signResult = JSON.parse(response); + if (signResult.length > output_len) { + console.info("dotnet_browser_sign: about to throw!"); + throw "SIGN HASH: Sign length exceeds output length: " + signResult.length + " > " + output_len; + } + + Module.HEAPU8.set(signResult, output_buffer); + return 1; +} + export function init_crypto(): void { if (typeof globalThis.crypto !== "undefined" && typeof globalThis.crypto.subtle !== "undefined" && typeof SharedArrayBuffer !== "undefined" diff --git a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js index 5866ffe..3cabbf8 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -107,8 +107,9 @@ const linked_functions = [ "mono_wasm_get_icudt_name", // pal_crypto_webworker.c + "dotnet_browser_can_use_subtle_crypto_impl", "dotnet_browser_simple_digest_hash", - "dotnet_browser_can_use_simple_digest_hash", + "dotnet_browser_sign", ]; // -- 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 439665b..16b7e88 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -69,7 +69,11 @@ 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"; +import { + dotnet_browser_can_use_subtle_crypto_impl, + dotnet_browser_simple_digest_hash, + dotnet_browser_sign +} from "./crypto-worker"; const MONO = { // current "public" MONO API @@ -370,8 +374,9 @@ export const __linker_exports: any = { mono_wasm_get_icudt_name, // pal_crypto_webworker.c + dotnet_browser_can_use_subtle_crypto_impl, dotnet_browser_simple_digest_hash, - dotnet_browser_can_use_simple_digest_hash, + dotnet_browser_sign }; const INTERNAL: any = { diff --git a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js index 329e6a4..5e27dd5 100644 --- a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js +++ b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js @@ -163,29 +163,51 @@ var ChannelWorker = { }; 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; - } + const digest_type = get_hash_name(type); // The 'crypto' API is not available in non-browser // environments (for example, v8 server). - var digest = await crypto.subtle.digest(digest_type, data); + const digest = await crypto.subtle.digest(digest_type, data); return Array.from(new Uint8Array(digest)); } +async function sign(type, key, data) { + const hash_name = get_hash_name(type); + + if (key.length === 0) { + // crypto.subtle.importKey will raise an error for an empty key. + // To prevent an error, reset it to a key with just a `0x00` byte. This is equivalent + // since HMAC keys get zero-extended up to the block size of the algorithm. + key = new Uint8Array([0]); + } + + const cryptoKey = await crypto.subtle.importKey("raw", key, {name: "HMAC", hash: hash_name}, false /* extractable */, ["sign"]); + const signResult = await crypto.subtle.sign("HMAC", cryptoKey, data); + return Array.from(new Uint8Array(signResult)); +} + +function get_hash_name(type) { + switch(type) { + case 0: return "SHA-1"; + case 1: return "SHA-256"; + case 2: return "SHA-384"; + case 3: return "SHA-512"; + default: + throw "CRYPTO: Unknown digest: " + type; + } +} + // 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)); + const digestArr = await call_digest(req.type, new Uint8Array(req.data)); return JSON.stringify(digestArr); + } + else if (req.func === "sign") { + const signResult = await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data)); + return JSON.stringify(signResult); } else { throw "CRYPTO: Unknown request: " + req.func; } 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 index 5f4da5a..60b665d 100644 --- 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 @@ -12,7 +12,16 @@ extern int32_t dotnet_browser_simple_digest_hash( uint8_t* output_buffer, int32_t output_len); -extern int32_t dotnet_browser_can_use_simple_digest_hash(void); +extern int32_t dotnet_browser_sign( + enum simple_digest hashAlgorithm, + uint8_t* key_buffer, + int32_t key_len, + uint8_t* input_buffer, + int32_t input_len, + uint8_t* output_buffer, + int32_t output_len); + +extern int32_t dotnet_browser_can_use_subtle_crypto_impl(void); int32_t SystemCryptoNativeBrowser_SimpleDigestHash( enum simple_digest ver, @@ -24,7 +33,19 @@ int32_t SystemCryptoNativeBrowser_SimpleDigestHash( return dotnet_browser_simple_digest_hash(ver, input_buffer, input_len, output_buffer, output_len); } -int32_t SystemCryptoNativeBrowser_CanUseSimpleDigestHash(void) +int32_t SystemCryptoNativeBrowser_Sign( + enum simple_digest hashAlgorithm, + uint8_t* key_buffer, + int32_t key_len, + uint8_t* input_buffer, + int32_t input_len, + uint8_t* output_buffer, + int32_t output_len) +{ + return dotnet_browser_sign(hashAlgorithm, key_buffer, key_len, input_buffer, input_len, output_buffer, output_len); +} + +int32_t SystemCryptoNativeBrowser_CanUseSubtleCryptoImpl(void) { - return dotnet_browser_can_use_simple_digest_hash(); + return dotnet_browser_can_use_subtle_crypto_impl(); } 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 index fe8b4d2..c0b598e 100644 --- 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 @@ -22,4 +22,13 @@ PALEXPORT int32_t SystemCryptoNativeBrowser_SimpleDigestHash( uint8_t* output_buffer, int32_t output_len); -PALEXPORT int32_t SystemCryptoNativeBrowser_CanUseSimpleDigestHash(void); +PALEXPORT int32_t SystemCryptoNativeBrowser_Sign( + enum simple_digest ver, + uint8_t* key_buffer, + int32_t key_len, + uint8_t* input_buffer, + int32_t input_len, + uint8_t* output_buffer, + int32_t output_len); + +PALEXPORT int32_t SystemCryptoNativeBrowser_CanUseSubtleCryptoImpl(void);