From 4222e699371ed72ac1fe702e5cfb44a01f3847d8 Mon Sep 17 00:00:00 2001 From: Eric Erhardt Date: Tue, 5 Jul 2022 18:46:09 -0600 Subject: Use crypto.subtle for AES on Browser WASM (#71501) * Use crypto.subtle for AES on Browser WASM Implement the browser "native" portion for AES on Browser WASM. There are two issues to solve .NET's Aes API on crypto.subtle: 1. The .NET API supports streaming while crypto.subtle only supports "one shot" APIs. 2. The .NET API supports multiple padding modes while crypto.subtle only supports PKCS7. To solve these issues, we use the following approach: 1. We only invoke crypto.subtle with complete AES "blocks" of data. This allows us to make assumptions about the padding behavior. 2. To implement streaming, remember the last block of the previous cipher text to use as the IV for the next stream of data. 3. When encrypting, since we have a complete block of data and crypto.subtle uses PKCS7 padding, strip off the last block of cipher text which will always be a full block of padding. 4. When decrypting do the inverse of encrypting - append an encrypted block of padding to the cipher text so crypto.subtle will return the full message as plain text. Other changes: - Make a few refactoring / simplifications where necessary. - SubtleCrypto doesn't support 192 bit AES keys, so no longer support AES-192 on Browser. Contributes to #40074 * Use an empty array to create encrypted padding block. --- .../Interop.Sign.cs | 22 --- .../Interop.SimpleDigestHash.cs | 32 ---- .../Interop.SubtleCrypto.cs | 56 +++++++ .../AlgorithmImplementations/AES/AesCipherTests.cs | 3 + .../AES/AesContractTests.cs | 6 +- .../src/System.Security.Cryptography.csproj | 10 +- .../src/System/Security/Cryptography/Aes.cs | 3 +- .../Cryptography/AesImplementation.Browser.cs | 22 ++- .../Cryptography/AesImplementation.NonBrowser.cs | 10 ++ .../Cryptography/AesManagedTransform.Browser.cs | 12 +- .../AesSubtleCryptoTransform.Browser.cs | 169 +++++++++++++++++++++ .../HMACHashProvider.Browser.Native.cs | 2 +- .../Cryptography/HashProviderDispenser.Browser.cs | 10 +- .../Cryptography/SHAHashProvider.Browser.Native.cs | 2 +- .../tests/AesTests.Browser.cs | 15 ++ src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js | 1 + src/mono/wasm/runtime/crypto-worker.ts | 29 ++++ src/mono/wasm/runtime/es6/dotnet.es6.lib.js | 1 + src/mono/wasm/runtime/exports.ts | 4 +- .../wasm/runtime/workers/dotnet-crypto-worker.js | 78 +++++++++- .../pal_crypto_webworker.c | 25 +++ .../pal_crypto_webworker.h | 11 ++ 22 files changed, 437 insertions(+), 86 deletions(-) delete mode 100644 src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.Sign.cs delete mode 100644 src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SimpleDigestHash.cs create mode 100644 src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SubtleCrypto.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.NonBrowser.cs create mode 100644 src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs (limited to 'src') 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 deleted file mode 100644 index 2100b3f0530..00000000000 --- a/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.Sign.cs +++ /dev/null @@ -1,22 +0,0 @@ -// 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 deleted file mode 100644 index d664276a278..00000000000 --- a/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SimpleDigestHash.cs +++ /dev/null @@ -1,32 +0,0 @@ -// 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_CanUseSubtleCryptoImpl")] - internal static partial int CanUseSubtleCryptoImpl(); - - [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/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SubtleCrypto.cs b/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SubtleCrypto.cs new file mode 100644 index 00000000000..f0330945eda --- /dev/null +++ b/src/libraries/Common/src/Interop/Browser/System.Security.Cryptography.Native.Browser/Interop.SubtleCrypto.cs @@ -0,0 +1,56 @@ +// 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, + }; + + internal static readonly bool CanUseSubtleCrypto = CanUseSubtleCryptoImpl() == 1; + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_CanUseSubtleCryptoImpl")] + private static partial int CanUseSubtleCryptoImpl(); + + [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); + + [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); + + [LibraryImport(Libraries.CryptoNative, EntryPoint = "SystemCryptoNativeBrowser_EncryptDecrypt")] + internal static unsafe partial int EncryptDecrypt( + int encrypting, + byte* key_buffer, + int key_len, + byte* iv_buffer, + int iv_len, + byte* input_buffer, + int input_len, + byte* output_buffer, + int output_len); + } +} diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.cs index 27ee7aec3c8..8bf77c7e73b 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesCipherTests.cs @@ -34,6 +34,7 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "AES-192 is not supported on Browser")] public static void RandomKeyRoundtrip_192() { using (Aes aes = AesFactory.Create()) @@ -485,6 +486,7 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "AES-192 is not supported on Browser")] public static void VerifyKnownTransform_CBC192_NoPadding() { TestAesTransformDirectKey( @@ -525,6 +527,7 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "AES-192 is not supported on Browser")] public static void VerifyKnownTransform_CBC192_NoPadding_2() { TestAesTransformDirectKey( diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesContractTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesContractTests.cs index f2a63a2cbb2..dc8940d6b38 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesContractTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/AesContractTests.cs @@ -55,7 +55,10 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests Assert.Equal(128, keySizeLimits.MinSize); Assert.Equal(256, keySizeLimits.MaxSize); - Assert.Equal(64, keySizeLimits.SkipSize); + + // Browser's SubtleCrypto doesn't support AES-192 + int expectedKeySkipSize = PlatformDetection.IsBrowser ? 128 : 64; + Assert.Equal(expectedKeySkipSize, keySizeLimits.SkipSize); } } @@ -214,6 +217,7 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests } [Fact] + [SkipOnPlatform(TestPlatforms.Browser, "AES-192 is not supported on Browser")] public static void VerifyKeyGeneration_192() { using (Aes aes = AesFactory.Create()) 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 7821a8e03a5..cd48ee4e222 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -541,14 +541,13 @@ Link="Common\Interop\Browser\Interop.Libraries.cs" /> - - + + @@ -583,6 +582,9 @@ + + + diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Aes.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Aes.cs index 57658ac9ad6..b28378d4362 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Aes.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/Aes.cs @@ -11,7 +11,7 @@ namespace System.Security.Cryptography protected Aes() { LegalBlockSizesValue = s_legalBlockSizes.CloneKeySizesArray(); - LegalKeySizesValue = s_legalKeySizes.CloneKeySizesArray(); + LegalKeySizesValue = AesImplementation.s_legalKeySizes.CloneKeySizesArray(); BlockSizeValue = 128; FeedbackSizeValue = 8; @@ -31,6 +31,5 @@ namespace System.Security.Cryptography } private static readonly KeySizes[] s_legalBlockSizes = { new KeySizes(128, 128, 0) }; - private static readonly KeySizes[] s_legalKeySizes = { new KeySizes(128, 256, 64) }; } } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Browser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Browser.cs index 5e3f9c4eb0e..3523b919f71 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Browser.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Browser.cs @@ -2,12 +2,16 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics; -using Internal.Cryptography; namespace System.Security.Cryptography { internal sealed partial class AesImplementation { + internal const int BlockSizeBytes = 16; // 128 bits + + // SubtleCrypto doesn't support AES-192. http://crbug.com/533699 + internal static readonly KeySizes[] s_legalKeySizes = { new KeySizes(128, 256, 128) }; + private static UniversalCryptoTransform CreateTransformCore( CipherMode cipherMode, PaddingMode paddingMode, @@ -19,11 +23,17 @@ namespace System.Security.Cryptography bool encrypting) { ValidateCipherMode(cipherMode); + if (iv is null) + throw new CryptographicException(SR.Cryptography_MissingIV); - Debug.Assert(blockSize == AesManagedTransform.BlockSizeBytes); + Debug.Assert(blockSize == BlockSizeBytes); Debug.Assert(paddingSize == blockSize); - return UniversalCryptoTransform.Create(paddingMode, new AesManagedTransform(key, iv, encrypting), encrypting); + BasicSymmetricCipher cipher = Interop.BrowserCrypto.CanUseSubtleCrypto ? + new AesSubtleCryptoTransform(key, iv, encrypting) : + new AesManagedTransform(key, iv, encrypting); + + return UniversalCryptoTransform.Create(paddingMode, cipher, encrypting); } private static ILiteSymmetricCipher CreateLiteCipher( @@ -37,10 +47,12 @@ namespace System.Security.Cryptography { ValidateCipherMode(cipherMode); - Debug.Assert(blockSize == AesManagedTransform.BlockSizeBytes); + Debug.Assert(blockSize == BlockSizeBytes); Debug.Assert(paddingSize == blockSize); - return new AesManagedTransform(key, iv, encrypting); + return Interop.BrowserCrypto.CanUseSubtleCrypto ? + new AesSubtleCryptoTransform(key, iv, encrypting) : + new AesManagedTransform(key, iv, encrypting); } private static void ValidateCipherMode(CipherMode cipherMode) diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.NonBrowser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.NonBrowser.cs new file mode 100644 index 00000000000..78b74ac82ab --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.NonBrowser.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace System.Security.Cryptography +{ + internal sealed partial class AesImplementation + { + internal static readonly KeySizes[] s_legalKeySizes = { new KeySizes(128, 256, 64) }; + } +} diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesManagedTransform.Browser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesManagedTransform.Browser.cs index 6354214dffb..0ae66acc18e 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesManagedTransform.Browser.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesManagedTransform.Browser.cs @@ -9,7 +9,7 @@ namespace System.Security.Cryptography { internal sealed class AesManagedTransform : BasicSymmetricCipher, ILiteSymmetricCipher { - public const int BlockSizeBytes = 16; // 128 bits + private const int BlockSizeBytes = AesImplementation.BlockSizeBytes; private const int BlockSizeInts = BlockSizeBytes / 4; private readonly bool _encrypting; @@ -30,13 +30,7 @@ namespace System.Security.Cryptography : base(iv: null, BlockSizeBytes, BlockSizeBytes) { Debug.Assert(BitConverter.IsLittleEndian, "The logic of casting Span to Span below assumes little endian"); - - if (iv.IsEmpty) - throw new CryptographicException(SR.Cryptography_MissingIV); - - // we only support the standard AES block size - if (iv.Length != BlockSizeBytes) - throw new CryptographicException(SR.Cryptography_InvalidIVSize); + Debug.Assert(iv.Length == BlockSizeBytes); _encrypting = encrypting; _Nr = GetNumberOfRounds(key); @@ -331,7 +325,7 @@ namespace System.Security.Cryptography return (BlockSizeBytes > key.Length ? BlockSizeBytes : key.Length) switch { 16 => 10, // 128 bits - 24 => 12, // 192 bits + // 24 => 12, // 192 bits is not supported by SubtleCrypto, so the managed implementation doesn't support it either 32 => 14, // 256 bits _ => throw new CryptographicException(SR.Cryptography_InvalidKeySize) }; diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs new file mode 100644 index 00000000000..5e0cfc68641 --- /dev/null +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesSubtleCryptoTransform.Browser.cs @@ -0,0 +1,169 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + internal sealed class AesSubtleCryptoTransform : BasicSymmetricCipher, ILiteSymmetricCipher + { + private const int BlockSizeBytes = AesImplementation.BlockSizeBytes; + + private readonly bool _encrypting; + + private readonly byte[] _key; + private byte[]? _lastBlockBuffer; + + public AesSubtleCryptoTransform(byte[] key, + byte[] iv, + bool encrypting) + : base(iv, BlockSizeBytes, BlockSizeBytes) + { + _encrypting = encrypting; + + // iv is guaranteed to be cloned before this method, but not key + _key = key.CloneByteArray(); + } + + public AesSubtleCryptoTransform(ReadOnlySpan key, + ReadOnlySpan iv, + bool encrypting) + : base(iv.ToArray(), BlockSizeBytes, BlockSizeBytes) + { + _encrypting = encrypting; + + _key = key.ToArray(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + // We need to always zeroize the following fields because they contain sensitive data + CryptographicOperations.ZeroMemory(_key); + CryptographicOperations.ZeroMemory(_lastBlockBuffer); + } + + base.Dispose(disposing); + } + + public override int Transform(ReadOnlySpan input, Span output) => + Transform(input, output, isFinal: false); + + public override int TransformFinal(ReadOnlySpan input, Span output) + { + int bytesWritten = Transform(input, output, isFinal: true); + Reset(); + return bytesWritten; + } + + private int Transform(ReadOnlySpan input, Span output, bool isFinal) + { + Debug.Assert(output.Length >= input.Length); + Debug.Assert(input.Length % BlockSizeInBytes == 0); + + if (input.IsEmpty) + { + return 0; + } + + // Note: SubtleCrypto always uses PKCS7 padding. + + // In order to implement streaming on top of SubtleCrypto's "one shot" API, we have to do the following: + // 1. Remember the last block of cipher text to pass as the "IV" of the next block. + // 2. When encrypting a complete block, PKCS7 padding will always add one block of '0x10' padding bytes. We + // need to strip this padding block off in between Transform calls. This is done by Interop.BrowserCrypto.EncryptDecrypt. + // 3. When decrypting, we need to do the inverse: append an encrypted block of '0x10' padding bytes, so + // SubtleCrypto will decrypt input as a complete message. This is done by Interop.BrowserCrypto.EncryptDecrypt. + + return _encrypting ? + EncryptBlock(input, output, isFinal) : + DecryptBlock(input, output, isFinal); + } + + private int EncryptBlock(ReadOnlySpan input, Span output, bool isFinal) + { + int bytesWritten = EncryptDecrypt(input, output); + + if (!isFinal) + { + SaveLastBlock(output.Slice(0, bytesWritten)); + } + + return bytesWritten; + } + + private int DecryptBlock(ReadOnlySpan input, Span output, bool isFinal) + { + Span lastInputBlockCopy = stackalloc byte[BlockSizeBytes]; + if (!isFinal) + { + // Save the lastInputBlock in a temp buffer first, in case input and output are overlapped + // and decrypting to the output overwrites the input. + ReadOnlySpan lastInputBlock = input.Slice(input.Length - BlockSizeBytes); + lastInputBlock.CopyTo(lastInputBlockCopy); + } + + int numBytesWritten = EncryptDecrypt(input, output); + + if (!isFinal) + { + SaveLastBlock(lastInputBlockCopy); + } + + return numBytesWritten; + } + + private void SaveLastBlock(ReadOnlySpan buffer) + { + Debug.Assert(buffer.Length > 0 && buffer.Length % BlockSizeBytes == 0); + + ReadOnlySpan lastBlock = buffer.Slice(buffer.Length - BlockSizeBytes); + if (_lastBlockBuffer is null) + { + _lastBlockBuffer = lastBlock.ToArray(); + } + else + { + Debug.Assert(_lastBlockBuffer.Length == BlockSizeBytes); + lastBlock.CopyTo(_lastBlockBuffer); + } + } + + private unsafe int EncryptDecrypt(ReadOnlySpan input, Span output) + { + byte[] iv = _lastBlockBuffer ?? IV!; + + fixed (byte* pKey = _key) + fixed (byte* pIV = iv) + fixed (byte* pInput = input) + fixed (byte* pOutput = output) + { + int bytesWritten = Interop.BrowserCrypto.EncryptDecrypt( + _encrypting ? 1 : 0, + pKey, _key.Length, + pIV, iv.Length, + pInput, input.Length, + pOutput, output.Length); + + if (bytesWritten < 0) + throw new Exception(SR.Unknown_Error); + + return bytesWritten; + } + } + + // + // resets the state of the transform + // + + void ILiteSymmetricCipher.Reset(ReadOnlySpan iv) => throw new NotImplementedException(); // never invoked + + private void Reset() + { + CryptographicOperations.ZeroMemory(_lastBlockBuffer); + _lastBlockBuffer = null; + } + } +} 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 index 819ae0eaba1..2b040191232 100644 --- 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 @@ -19,7 +19,7 @@ namespace System.Security.Cryptography public HMACNativeHashProvider(string hashAlgorithmId, ReadOnlySpan key) { - Debug.Assert(HashProviderDispenser.CanUseSubtleCryptoImpl); + Debug.Assert(Interop.BrowserCrypto.CanUseSubtleCrypto); (_hashAlgorithm, _hashSizeInBytes) = SHANativeHashProvider.HashAlgorithmToPal(hashAlgorithmId); _key = key.ToArray(); 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 3b4f42b786d..8c604bb32b1 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 @@ -5,8 +5,6 @@ namespace System.Security.Cryptography { internal static partial class HashProviderDispenser { - internal static readonly bool CanUseSubtleCryptoImpl = Interop.BrowserCrypto.CanUseSubtleCryptoImpl() == 1; - public static HashProvider CreateHashProvider(string hashAlgorithmId) { switch (hashAlgorithmId) @@ -15,7 +13,7 @@ namespace System.Security.Cryptography case HashAlgorithmNames.SHA256: case HashAlgorithmNames.SHA384: case HashAlgorithmNames.SHA512: - return CanUseSubtleCryptoImpl + return Interop.BrowserCrypto.CanUseSubtleCrypto ? new SHANativeHashProvider(hashAlgorithmId) : new SHAManagedHashProvider(hashAlgorithmId); } @@ -30,7 +28,7 @@ namespace System.Security.Cryptography ReadOnlySpan source, Span destination) { - if (CanUseSubtleCryptoImpl) + if (Interop.BrowserCrypto.CanUseSubtleCrypto) { return HMACNativeHashProvider.MacDataOneShot(hashAlgorithmId, key, source, destination); } @@ -44,7 +42,7 @@ namespace System.Security.Cryptography public static int HashData(string hashAlgorithmId, ReadOnlySpan source, Span destination) { - if (CanUseSubtleCryptoImpl) + if (Interop.BrowserCrypto.CanUseSubtleCrypto) { return SHANativeHashProvider.HashOneShot(hashAlgorithmId, source, destination); } @@ -65,7 +63,7 @@ namespace System.Security.Cryptography case HashAlgorithmNames.SHA256: case HashAlgorithmNames.SHA384: case HashAlgorithmNames.SHA512: - return CanUseSubtleCryptoImpl + return Interop.BrowserCrypto.CanUseSubtleCrypto ? new HMACNativeHashProvider(hashAlgorithmId, key) : new HMACManagedHashProvider(hashAlgorithmId, key); } 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 205f46e1cd0..6c5f70489af 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 @@ -18,7 +18,7 @@ namespace System.Security.Cryptography public SHANativeHashProvider(string hashAlgorithmId) { - Debug.Assert(HashProviderDispenser.CanUseSubtleCryptoImpl); + Debug.Assert(Interop.BrowserCrypto.CanUseSubtleCrypto); (_impl, _hashSizeInBytes) = HashAlgorithmToPal(hashAlgorithmId); } diff --git a/src/libraries/System.Security.Cryptography/tests/AesTests.Browser.cs b/src/libraries/System.Security.Cryptography/tests/AesTests.Browser.cs index 32c253a1234..5e8eafbaa38 100644 --- a/src/libraries/System.Security.Cryptography/tests/AesTests.Browser.cs +++ b/src/libraries/System.Security.Cryptography/tests/AesTests.Browser.cs @@ -43,5 +43,20 @@ namespace System.Security.Cryptography.Tests Assert.Throws(() => aes.CreateDecryptor(s_iv, s_iv)); } } + + // Browser's SubtleCrypto doesn't support AES-192 + [Fact] + public static void Aes_InvalidKeySize_192_Browser() + { + byte[] key192 = new byte[192 / 8]; + using (Aes aes = Aes.Create()) + { + Assert.False(aes.ValidKeySize(192)); + Assert.Throws(() => aes.Key = key192); + Assert.Throws(() => aes.KeySize = 192); + Assert.Throws(() => aes.CreateEncryptor(key192, s_iv)); + Assert.Throws(() => aes.CreateDecryptor(key192, s_iv)); + } + } } } diff --git a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js index 35da8f4d649..cd78c6a2240 100644 --- a/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js +++ b/src/mono/wasm/runtime/cjs/dotnet.cjs.lib.js @@ -90,6 +90,7 @@ const linked_functions = [ "dotnet_browser_can_use_subtle_crypto_impl", "dotnet_browser_simple_digest_hash", "dotnet_browser_sign", + "dotnet_browser_encrypt_decrypt", /// mono-threads-wasm.c #if USE_PTHREADS diff --git a/src/mono/wasm/runtime/crypto-worker.ts b/src/mono/wasm/runtime/crypto-worker.ts index 92171e76562..674f1700d22 100644 --- a/src/mono/wasm/runtime/crypto-worker.ts +++ b/src/mono/wasm/runtime/crypto-worker.ts @@ -54,6 +54,35 @@ export function dotnet_browser_sign(hashAlgorithm: number, key_buffer: number, k return 1; } +const AesBlockSizeBytes = 16; // 128 bits + +export function dotnet_browser_encrypt_decrypt(isEncrypting: boolean, key_buffer: number, key_len: number, iv_buffer: number, iv_len: number, input_buffer: number, input_len: number, output_buffer: number, output_len: number): number { + mono_assert(!!mono_wasm_crypto, "subtle crypto not initialized"); + + if (input_len <= 0 || input_len % AesBlockSizeBytes !== 0) { + throw "ENCRYPT DECRYPT: data was not a full block: " + input_len; + } + + const msg = { + func: "encrypt_decrypt", + isEncrypting: isEncrypting, + key: Array.from(Module.HEAPU8.subarray(key_buffer, key_buffer + key_len)), + iv: Array.from(Module.HEAPU8.subarray(iv_buffer, iv_buffer + iv_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 result = JSON.parse(response); + + if (result.length > output_len) { + console.info("dotnet_browser_encrypt_decrypt: about to throw!"); + throw "ENCRYPT DECRYPT: Encrypt/Decrypt length exceeds output length: " + result.length + " > " + output_len; + } + + Module.HEAPU8.set(result, output_buffer); + return result.length; +} + 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 c5851007de1..a559c8f42de 100644 --- a/src/mono/wasm/runtime/es6/dotnet.es6.lib.js +++ b/src/mono/wasm/runtime/es6/dotnet.es6.lib.js @@ -127,6 +127,7 @@ const linked_functions = [ "dotnet_browser_can_use_subtle_crypto_impl", "dotnet_browser_simple_digest_hash", "dotnet_browser_sign", + "dotnet_browser_encrypt_decrypt", /// mono-threads-wasm.c #if USE_PTHREADS diff --git a/src/mono/wasm/runtime/exports.ts b/src/mono/wasm/runtime/exports.ts index d91a67adcfe..436e0ba347a 100644 --- a/src/mono/wasm/runtime/exports.ts +++ b/src/mono/wasm/runtime/exports.ts @@ -71,7 +71,8 @@ import { diagnostics } from "./diagnostics"; import { dotnet_browser_can_use_subtle_crypto_impl, dotnet_browser_simple_digest_hash, - dotnet_browser_sign + dotnet_browser_sign, + dotnet_browser_encrypt_decrypt } from "./crypto-worker"; import { mono_wasm_cancel_promise_ref } from "./cancelable-promise"; import { mono_wasm_web_socket_open_ref, mono_wasm_web_socket_send, mono_wasm_web_socket_receive, mono_wasm_web_socket_close_ref, mono_wasm_web_socket_abort } from "./web-socket"; @@ -406,6 +407,7 @@ export const __linker_exports: any = { dotnet_browser_can_use_subtle_crypto_impl, dotnet_browser_simple_digest_hash, dotnet_browser_sign, + dotnet_browser_encrypt_decrypt, // threading exports, if threading is enabled ...mono_wasm_threads_exports, diff --git a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js index 5e27dd59b5f..f6d66c82da6 100644 --- a/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js +++ b/src/mono/wasm/runtime/workers/dotnet-crypto-worker.js @@ -187,7 +187,7 @@ async function sign(type, key, data) { } function get_hash_name(type) { - switch(type) { + switch (type) { case 0: return "SHA-1"; case 1: return "SHA-256"; case 2: return "SHA-384"; @@ -197,6 +197,75 @@ function get_hash_name(type) { } } +const AesBlockSizeBytes = 16; // 128 bits + +async function encrypt_decrypt(isEncrypting, key, iv, data) { + const algorithmName = "AES-CBC"; + const keyUsage = isEncrypting ? ["encrypt"] : ["encrypt", "decrypt"]; + const cryptoKey = await importKey(key, algorithmName, keyUsage); + const algorithm = { + name: algorithmName, + iv: new Uint8Array(iv) + }; + + const result = await (isEncrypting ? + crypto.subtle.encrypt( + algorithm, + cryptoKey, + new Uint8Array(data)) : + decrypt( + algorithm, + cryptoKey, + data)); + + let resultByteArray = new Uint8Array(result); + if (isEncrypting) { + // trim off the last block, which is always a padding block. + resultByteArray = resultByteArray.slice(0, resultByteArray.length - AesBlockSizeBytes); + } + return Array.from(resultByteArray); +} + +async function decrypt(algorithm, cryptoKey, data) { + // crypto.subtle AES-CBC will only allow a PaddingMode of PKCS7, but we need to use + // PaddingMode None. To simulate this, we only decrypt full blocks of data, with an extra full + // padding block of 0x10 (16) bytes appended to data. crypto.subtle will see that padding block and return + // the fully decrypted message. To create the encrypted padding block, we encrypt an empty array using the + // last block of the cipher text as the IV. This will create a full block of padding bytes. + + const paddingBlockIV = new Uint8Array(data).slice(data.length - AesBlockSizeBytes); + const empty = new Uint8Array(); + const encryptedPaddingBlockResult = await crypto.subtle.encrypt( + { + name: algorithm.name, + iv: paddingBlockIV + }, + cryptoKey, + empty + ); + + const encryptedPaddingBlock = new Uint8Array(encryptedPaddingBlockResult); + for (var i = 0; i < encryptedPaddingBlock.length; i++) { + data.push(encryptedPaddingBlock[i]); + } + + return await crypto.subtle.decrypt( + algorithm, + cryptoKey, + new Uint8Array(data)); +} + +function importKey(key, algorithmName, keyUsage) { + return crypto.subtle.importKey( + "raw", + new Uint8Array(key), + { + name: algorithmName + }, + false /* extractable */, + keyUsage); +} + // Operation to perform. async function async_call(msg) { const req = JSON.parse(msg); @@ -208,7 +277,12 @@ async function async_call(msg) { else if (req.func === "sign") { const signResult = await sign(req.type, new Uint8Array(req.key), new Uint8Array(req.data)); return JSON.stringify(signResult); - } else { + } + else if (req.func === "encrypt_decrypt") { + const signResult = await encrypt_decrypt(req.isEncrypting, req.key, req.iv, 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 60b665dced1..0c78013ac34 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 @@ -21,6 +21,17 @@ extern int32_t dotnet_browser_sign( uint8_t* output_buffer, int32_t output_len); +extern int32_t dotnet_browser_encrypt_decrypt( + int32_t encrypting, + uint8_t* key_buffer, + int32_t key_len, + uint8_t* iv_buffer, + int32_t iv_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( @@ -45,6 +56,20 @@ int32_t SystemCryptoNativeBrowser_Sign( return dotnet_browser_sign(hashAlgorithm, key_buffer, key_len, input_buffer, input_len, output_buffer, output_len); } +int32_t SystemCryptoNativeBrowser_EncryptDecrypt( + int32_t encrypting, + uint8_t* key_buffer, + int32_t key_len, + uint8_t* iv_buffer, + int32_t iv_len, + uint8_t* input_buffer, + int32_t input_len, + uint8_t* output_buffer, + int32_t output_len) +{ + return dotnet_browser_encrypt_decrypt(encrypting, key_buffer, key_len, iv_buffer, iv_len, input_buffer, input_len, output_buffer, output_len); +} + int32_t SystemCryptoNativeBrowser_CanUseSubtleCryptoImpl(void) { 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 c0b598edb0d..2166a4a2177 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 @@ -31,4 +31,15 @@ PALEXPORT int32_t SystemCryptoNativeBrowser_Sign( uint8_t* output_buffer, int32_t output_len); +PALEXPORT int32_t SystemCryptoNativeBrowser_EncryptDecrypt( + int32_t encrypting, + uint8_t* key_buffer, + int32_t key_len, + uint8_t* iv_buffer, + int32_t iv_len, + uint8_t* input_buffer, + int32_t input_len, + uint8_t* output_buffer, + int32_t output_len); + PALEXPORT int32_t SystemCryptoNativeBrowser_CanUseSubtleCryptoImpl(void); -- cgit v1.2.3