Use crypto.subtle for HMAC on Browser WASM (#70745)
authorEric Erhardt <eric.erhardt@microsoft.com>
Tue, 21 Jun 2022 21:27:16 +0000 (16:27 -0500)
committerGitHub <noreply@github.com>
Tue, 21 Jun 2022 21:27:16 +0000 (16:27 -0500)
* 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

18 files changed:
src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.Sign.cs [new file with mode: 0644]
src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SimpleDigestHash.cs
src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HMACHashProvider.Browser.Native.cs [new file with mode: 0644]
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/HashProviderDispenser.Browser.cs
src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/SHAHashProvider.Browser.Native.cs
src/libraries/System.Security.Cryptography/tests/HmacMD5Tests.cs
src/libraries/System.Security.Cryptography/tests/HmacSha1Tests.cs
src/libraries/System.Security.Cryptography/tests/HmacSha256Tests.cs
src/libraries/System.Security.Cryptography/tests/HmacSha384Tests.cs
src/libraries/System.Security.Cryptography/tests/HmacSha512Tests.cs
src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js
src/mono/wasm/runtime/crypto-worker.ts
src/mono/wasm/runtime/es6/dotnet.es6.lib.js
src/mono/wasm/runtime/exports.ts
src/mono/wasm/runtime/workers/dotnet-crypto-worker.js
src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.c
src/native/libs/System.Security.Cryptography.Native.Browser/pal_crypto_webworker.h

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 (file)
index 0000000..2100b3f
--- /dev/null
@@ -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);
+    }
+}
index 1304b45..d664276 100644 (file)
@@ -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(
index dfb550a..669191a 100644 (file)
              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="$(CommonPath)Interop\Browser\System.Security.Cryptography.Native.Browser\Interop.Sign.cs"
+             Link="Common\Interop\Browser\System.Security.Cryptography.Native.Browser\Interop.Sign.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" />
     <Compile Include="System\Security\Cryptography\ECDsa.Create.NotSupported.cs" />
     <Compile Include="System\Security\Cryptography\HashProviderDispenser.Browser.cs" />
     <Compile Include="System\Security\Cryptography\HMACHashProvider.Browser.Managed.cs" />
+    <Compile Include="System\Security\Cryptography\HMACHashProvider.Browser.Native.cs" />
     <Compile Include="System\Security\Cryptography\LiteHash.Browser.cs" />
     <Compile Include="System\Security\Cryptography\OidLookup.NoFallback.cs" />
     <Compile Include="System\Security\Cryptography\OpenSsl.NotSupported.cs" />
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 (file)
index 0000000..819ae0e
--- /dev/null
@@ -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<byte> key)
+        {
+            Debug.Assert(HashProviderDispenser.CanUseSubtleCryptoImpl);
+
+            (_hashAlgorithm, _hashSizeInBytes) = SHANativeHashProvider.HashAlgorithmToPal(hashAlgorithmId);
+            _key = key.ToArray();
+        }
+
+        public override void AppendHashData(ReadOnlySpan<byte> data)
+        {
+            _buffer ??= new MemoryStream(1000);
+            _buffer.Write(data);
+        }
+
+        public override int FinalizeHashAndReset(Span<byte> destination)
+        {
+            int written = GetCurrentHash(destination);
+            _buffer = null;
+
+            return written;
+        }
+
+        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* 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<byte> key, ReadOnlySpan<byte> data, Span<byte> 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;
+        }
+    }
+}
index 1326aeb..3b4f42b 100644 (file)
@@ -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<byte> source,
                 Span<byte> 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<byte> source, Span<byte> 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));
         }
index e7748e6..205f46e 100644 (file)
@@ -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
             {
index d658411..d657f96 100644 (file)
@@ -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:
index 4ae956c..9b5aa3e 100644 (file)
@@ -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]);
index e0f4348..dea0798 100644 (file)
@@ -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:
index deb2396..d026442 100644 (file)
@@ -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:
index 8c9f027..44aec88 100644 (file)
@@ -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:
index ceeb9d3..d8492ff 100644 (file)
@@ -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! --
index f17a455..92171e7 100644 (file)
@@ -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"
index 5866ffe..3cabbf8 100644 (file)
@@ -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! --
index 439665b..16b7e88 100644 (file)
@@ -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 = {
index 329e6a4..5e27dd5 100644 (file)
@@ -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;
     }
index 5f4da5a..60b665d 100644 (file)
@@ -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();
 }
index fe8b4d2..c0b598e 100644 (file)
@@ -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);