diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/AEADBCryptHandles.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/AEADBCryptHandles.cs new file mode 100644 index 00000000000000..a05c5ae962d4a3 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/AEADBCryptHandles.cs @@ -0,0 +1,49 @@ +// 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.Security.Cryptography; +using System.Threading; +using Internal.NativeCrypto; + +namespace Internal.Cryptography +{ + internal static class AeadBCryptHandles + { + private static SafeAlgorithmHandle? s_aesCcm; + private static SafeAlgorithmHandle? s_aesGcm; + private static SafeAlgorithmHandle? s_chaCha20Poly1305; + + internal static SafeAlgorithmHandle AesCcm => GetCachedAlgorithmHandle(ref s_aesCcm, Cng.BCRYPT_AES_ALGORITHM, Cng.BCRYPT_CHAIN_MODE_CCM); + internal static SafeAlgorithmHandle AesGcm => GetCachedAlgorithmHandle(ref s_aesGcm, Cng.BCRYPT_AES_ALGORITHM, Cng.BCRYPT_CHAIN_MODE_GCM); + + internal static bool IsChaCha20Poly1305Supported { get; } = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 20142); + internal static SafeAlgorithmHandle ChaCha20Poly1305 => GetCachedAlgorithmHandle(ref s_chaCha20Poly1305, Cng.BCRYPT_CHACHA20_POLY1305_ALGORITHM); + + private static SafeAlgorithmHandle GetCachedAlgorithmHandle(ref SafeAlgorithmHandle? handle, string algId, string? chainingMode = null) + { + // Do we already have a handle to this algorithm? + SafeAlgorithmHandle? existingHandle = Volatile.Read(ref handle); + if (existingHandle != null) { return existingHandle; } + + // No cached handle exists; create a new handle. It's ok if multiple threads call + // this concurrently. Only one handle will "win" and the rest will be destroyed. + SafeAlgorithmHandle newHandle = Cng.BCryptOpenAlgorithmProvider(algId, null, Cng.OpenAlgorithmProviderFlags.NONE); + if (chainingMode != null) + { + newHandle.SetCipherMode(chainingMode); + } + + existingHandle = Interlocked.CompareExchange(ref handle, newHandle, null); + if (existingHandle != null) + { + newHandle.Dispose(); + return existingHandle; + } + else + { + return newHandle; + } + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/BCryptAeadHandleCache.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/BCryptAeadHandleCache.cs new file mode 100644 index 00000000000000..60941688533ba6 --- /dev/null +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/BCryptAeadHandleCache.cs @@ -0,0 +1,52 @@ +// 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.Security.Cryptography; +using System.Threading; +using Internal.NativeCrypto; + +namespace Internal.Cryptography +{ + internal static class BCryptAeadHandleCache + { + private static SafeAlgorithmHandle? s_aesCcm; + private static SafeAlgorithmHandle? s_aesGcm; + private static SafeAlgorithmHandle? s_chaCha20Poly1305; + + internal static SafeAlgorithmHandle AesCcm => GetCachedAlgorithmHandle(ref s_aesCcm, Cng.BCRYPT_AES_ALGORITHM, Cng.BCRYPT_CHAIN_MODE_CCM); + internal static SafeAlgorithmHandle AesGcm => GetCachedAlgorithmHandle(ref s_aesGcm, Cng.BCRYPT_AES_ALGORITHM, Cng.BCRYPT_CHAIN_MODE_GCM); + + internal static bool IsChaCha20Poly1305Supported { get; } = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 20142); + internal static SafeAlgorithmHandle ChaCha20Poly1305 => GetCachedAlgorithmHandle(ref s_chaCha20Poly1305, Cng.BCRYPT_CHACHA20_POLY1305_ALGORITHM); + + private static SafeAlgorithmHandle GetCachedAlgorithmHandle(ref SafeAlgorithmHandle? handle, string algId, string? chainingMode = null) + { + // Do we already have a handle to this algorithm? + SafeAlgorithmHandle? existingHandle = Volatile.Read(ref handle); + if (existingHandle != null) + { + return existingHandle; + } + + // No cached handle exists; create a new handle. It's ok if multiple threads call + // this concurrently. Only one handle will "win" and the rest will be destroyed. + SafeAlgorithmHandle newHandle = Cng.BCryptOpenAlgorithmProvider(algId, null, Cng.OpenAlgorithmProviderFlags.NONE); + if (chainingMode != null) + { + newHandle.SetCipherMode(chainingMode); + } + + existingHandle = Interlocked.CompareExchange(ref handle, newHandle, null); + if (existingHandle != null) + { + newHandle.Dispose(); + return existingHandle; + } + else + { + return newHandle; + } + } + } +} diff --git a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs index 7642b9faeafa5a..273a0a89863aad 100644 --- a/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs +++ b/src/libraries/Common/src/Interop/Windows/BCrypt/Cng.cs @@ -60,6 +60,7 @@ public enum OpenAlgorithmProviderFlags : int public const string BCRYPT_3DES_ALGORITHM = "3DES"; public const string BCRYPT_AES_ALGORITHM = "AES"; + public const string BCRYPT_CHACHA20_POLY1305_ALGORITHM = "CHACHA20_POLY1305"; public const string BCRYPT_DES_ALGORITHM = "DES"; public const string BCRYPT_RC2_ALGORITHM = "RC2"; diff --git a/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs b/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs index 7423845019a787..8d3302158e07b7 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/ref/System.Security.Cryptography.Algorithms.cs @@ -19,6 +19,7 @@ public sealed partial class AesCcm : System.IDisposable { public AesCcm(byte[] key) { } public AesCcm(System.ReadOnlySpan key) { } + public static bool IsSupported { get { throw null; } } public static System.Security.Cryptography.KeySizes NonceByteSizes { get { throw null; } } public static System.Security.Cryptography.KeySizes TagByteSizes { get { throw null; } } public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[]? associatedData = null) { } @@ -32,6 +33,7 @@ public sealed partial class AesGcm : System.IDisposable { public AesGcm(byte[] key) { } public AesGcm(System.ReadOnlySpan key) { } + public static bool IsSupported { get { throw null; } } public static System.Security.Cryptography.KeySizes NonceByteSizes { get { throw null; } } public static System.Security.Cryptography.KeySizes TagByteSizes { get { throw null; } } public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[]? associatedData = null) { } @@ -97,6 +99,18 @@ protected AsymmetricSignatureFormatter() { } public abstract void SetHashAlgorithm(string strName); public abstract void SetKey(System.Security.Cryptography.AsymmetricAlgorithm key); } + [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] + public sealed partial class ChaCha20Poly1305 : System.IDisposable + { + public ChaCha20Poly1305(byte[] key) { } + public ChaCha20Poly1305(System.ReadOnlySpan key) { } + public static bool IsSupported { get { throw null; } } + public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[]? associatedData = null) { } + public void Decrypt(System.ReadOnlySpan nonce, System.ReadOnlySpan ciphertext, System.ReadOnlySpan tag, System.Span plaintext, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan)) { } + public void Dispose() { } + public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[]? associatedData = null) { } + public void Encrypt(System.ReadOnlySpan nonce, System.ReadOnlySpan plaintext, System.Span ciphertext, System.Span tag, System.ReadOnlySpan associatedData = default(System.ReadOnlySpan)) { } + } public partial class CryptoConfig { public CryptoConfig() { } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/ExcludeApiList.PNSE.Browser.txt b/src/libraries/System.Security.Cryptography.Algorithms/src/ExcludeApiList.PNSE.Browser.txt index bca0f309a1f712..39c223fa55ea82 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/ExcludeApiList.PNSE.Browser.txt +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/ExcludeApiList.PNSE.Browser.txt @@ -1,3 +1,6 @@ +M:System.Security.Cryptography.AesCcm.IsSupported +M:System.Security.Cryptography.AesGcm.IsSupported +M:System.Security.Cryptography.ChaCha20Poly1305.IsSupported T:System.Security.Cryptography.CryptoConfig T:System.Security.Cryptography.RandomNumberGenerator T:System.Security.Cryptography.IncrementalHash diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj index fc83d7af8faa9b..35bf864a44808e 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System.Security.Cryptography.Algorithms.csproj @@ -1,4 +1,4 @@ - + true $(DefineConstants);INTERNAL_ASYMMETRIC_IMPLEMENTATIONS @@ -27,6 +27,7 @@ + @@ -36,6 +37,7 @@ + @@ -276,9 +278,10 @@ + - + @@ -304,6 +307,8 @@ Link="Common\Interop\Windows\Interop.Libraries.cs" /> + + @@ -673,6 +679,7 @@ + @@ -693,6 +700,9 @@ + + + @@ -711,6 +721,7 @@ + diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.Windows.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AeadCommon.Windows.cs similarity index 66% rename from src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.Windows.cs rename to src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AeadCommon.Windows.cs index ef7bbd6f0bc15f..fe10a0ed2ca10e 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.Windows.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AeadCommon.Windows.cs @@ -5,13 +5,14 @@ using Internal.NativeCrypto; using static Interop.BCrypt; using System.Diagnostics; +using System.Runtime.InteropServices; +using System.Runtime.CompilerServices; namespace System.Security.Cryptography { - internal static partial class AesAEAD + internal static partial class AeadCommon { public static unsafe void Encrypt( - SafeAlgorithmHandle algorithm, SafeKeyHandle keyHandle, ReadOnlySpan nonce, ReadOnlySpan associatedData, @@ -19,11 +20,12 @@ public static unsafe void Encrypt( Span ciphertext, Span tag) { - fixed (byte* plaintextBytes = plaintext) - fixed (byte* nonceBytes = nonce) - fixed (byte* ciphertextBytes = ciphertext) - fixed (byte* tagBytes = tag) - fixed (byte* associatedDataBytes = associatedData) + // bcrypt sometimes misbehaves when given nullptr buffers; ensure non-nullptr + fixed (byte* plaintextBytes = &GetNonNullPinnableReference(plaintext)) + fixed (byte* nonceBytes = &GetNonNullPinnableReference(nonce)) + fixed (byte* ciphertextBytes = &GetNonNullPinnableReference(ciphertext)) + fixed (byte* tagBytes = &GetNonNullPinnableReference(tag)) + fixed (byte* associatedDataBytes = &GetNonNullPinnableReference(associatedData)) { BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Create(); authInfo.pbNonce = nonceBytes; @@ -55,7 +57,6 @@ public static unsafe void Encrypt( } public static unsafe void Decrypt( - SafeAlgorithmHandle algorithm, SafeKeyHandle keyHandle, ReadOnlySpan nonce, ReadOnlySpan associatedData, @@ -64,11 +65,12 @@ public static unsafe void Decrypt( Span plaintext, bool clearPlaintextOnFailure) { - fixed (byte* plaintextBytes = plaintext) - fixed (byte* nonceBytes = nonce) - fixed (byte* ciphertextBytes = ciphertext) - fixed (byte* tagBytes = tag) - fixed (byte* associatedDataBytes = associatedData) + // bcrypt sometimes misbehaves when given nullptr buffers; ensure non-nullptr + fixed (byte* plaintextBytes = &GetNonNullPinnableReference(plaintext)) + fixed (byte* nonceBytes = &GetNonNullPinnableReference(nonce)) + fixed (byte* ciphertextBytes = &GetNonNullPinnableReference(ciphertext)) + fixed (byte* tagBytes = &GetNonNullPinnableReference(tag)) + fixed (byte* associatedDataBytes = &GetNonNullPinnableReference(associatedData)) { BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO authInfo = BCRYPT_AUTHENTICATED_CIPHER_MODE_INFO.Create(); authInfo.pbNonce = nonceBytes; @@ -108,5 +110,15 @@ public static unsafe void Decrypt( } } } + + // Implementations below based on internal MemoryMarshal.GetNonNullPinnableReference methods. + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ref readonly byte GetNonNullPinnableReference(ReadOnlySpan buffer) + => ref buffer.Length != 0 ? ref MemoryMarshal.GetReference(buffer) : ref Unsafe.AsRef((void*)1); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static unsafe ref byte GetNonNullPinnableReference(Span buffer) + => ref buffer.Length != 0 ? ref MemoryMarshal.GetReference(buffer) : ref Unsafe.AsRef((void*)1); } } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AeadCommon.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AeadCommon.cs new file mode 100644 index 00000000000000..d2e9320e3e87f4 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AeadCommon.cs @@ -0,0 +1,29 @@ +// 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 AeadCommon + { + public static void CheckArgumentsForNull( + byte[] nonce, + byte[] plaintext, + byte[] ciphertext, + byte[] tag) + { + if (nonce == null) + throw new ArgumentNullException(nameof(nonce)); + + if (plaintext == null) + throw new ArgumentNullException(nameof(plaintext)); + + if (ciphertext == null) + throw new ArgumentNullException(nameof(ciphertext)); + + if (tag == null) + throw new ArgumentNullException(nameof(tag)); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.cs index b919b04eac0d5a..26322fe82265a6 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesAEAD.cs @@ -7,31 +7,12 @@ namespace System.Security.Cryptography { internal static partial class AesAEAD { - public static void CheckKeySize(int keySizeInBits) + public static void CheckKeySize(int keySizeInBytes) { - if (keySizeInBits != 128 && keySizeInBits != 192 && keySizeInBits != 256) + if (keySizeInBytes != (128 / 8) && keySizeInBytes != (192 / 8) && keySizeInBytes != (256 / 8)) { throw new CryptographicException(SR.Cryptography_InvalidKeySize); } } - - public static void CheckArgumentsForNull( - byte[] nonce, - byte[] plaintext, - byte[] ciphertext, - byte[] tag) - { - if (nonce == null) - throw new ArgumentNullException(nameof(nonce)); - - if (plaintext == null) - throw new ArgumentNullException(nameof(plaintext)); - - if (ciphertext == null) - throw new ArgumentNullException(nameof(ciphertext)); - - if (tag == null) - throw new ArgumentNullException(nameof(tag)); - } } } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Android.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Android.cs index d6c5b9726dbaa9..07c20a176beb63 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Android.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Android.cs @@ -17,7 +17,7 @@ private void ImportKey(ReadOnlySpan key) _key = key.ToArray(); } - private void EncryptInternal( + private void EncryptCore( ReadOnlySpan nonce, ReadOnlySpan plaintext, Span ciphertext, @@ -97,7 +97,7 @@ private void EncryptInternal( } } - private void DecryptInternal( + private void DecryptCore( ReadOnlySpan nonce, ReadOnlySpan ciphertext, ReadOnlySpan tag, diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.NotSupported.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.NotSupported.cs new file mode 100644 index 00000000000000..170540eec2cee7 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.NotSupported.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 +{ + public partial class AesCcm + { + public static bool IsSupported => false; + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Unix.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Unix.cs index 03da1e4ad730be..cd455c31abc4d3 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Unix.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Unix.cs @@ -19,7 +19,7 @@ private void ImportKey(ReadOnlySpan key) _key = key.ToArray(); } - private void EncryptInternal( + private void EncryptCore( ReadOnlySpan nonce, ReadOnlySpan plaintext, Span ciphertext, @@ -73,7 +73,7 @@ private void EncryptInternal( } } - private void DecryptInternal( + private void DecryptCore( ReadOnlySpan nonce, ReadOnlySpan ciphertext, ReadOnlySpan tag, diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Windows.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Windows.cs index f858454edd2ac6..95884bea90202d 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Windows.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.Windows.cs @@ -9,26 +9,25 @@ namespace System.Security.Cryptography { public sealed partial class AesCcm { - private static readonly SafeAlgorithmHandle s_aesCcm = AesBCryptModes.OpenAesAlgorithm(Cng.BCRYPT_CHAIN_MODE_CCM).Value; private SafeKeyHandle _keyHandle; [MemberNotNull(nameof(_keyHandle))] private void ImportKey(ReadOnlySpan key) { - _keyHandle = Interop.BCrypt.BCryptImportKey(s_aesCcm, key); + _keyHandle = Interop.BCrypt.BCryptImportKey(BCryptAeadHandleCache.AesCcm, key); } - private void EncryptInternal( + private void EncryptCore( ReadOnlySpan nonce, ReadOnlySpan plaintext, Span ciphertext, Span tag, ReadOnlySpan associatedData = default) { - AesAEAD.Encrypt(s_aesCcm, _keyHandle, nonce, associatedData, plaintext, ciphertext, tag); + AeadCommon.Encrypt(_keyHandle, nonce, associatedData, plaintext, ciphertext, tag); } - private void DecryptInternal( + private void DecryptCore( ReadOnlySpan nonce, ReadOnlySpan ciphertext, ReadOnlySpan tag, @@ -36,7 +35,7 @@ private void DecryptInternal( ReadOnlySpan associatedData = default) { // BCrypt implementation of CCM clears plaintext for you on failure - AesAEAD.Decrypt(s_aesCcm, _keyHandle, nonce, associatedData, ciphertext, tag, plaintext, clearPlaintextOnFailure: false); + AeadCommon.Decrypt(_keyHandle, nonce, associatedData, ciphertext, tag, plaintext, clearPlaintextOnFailure: false); } public void Dispose() diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.cs index 1e1f32adc87462..5940ecece6c2e6 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesCcm.cs @@ -14,7 +14,7 @@ public sealed partial class AesCcm : IDisposable public AesCcm(ReadOnlySpan key) { - AesAEAD.CheckKeySize(key.Length * 8); + AesAEAD.CheckKeySize(key.Length); ImportKey(key); } @@ -23,13 +23,15 @@ public AesCcm(byte[] key) if (key == null) throw new ArgumentNullException(nameof(key)); - AesAEAD.CheckKeySize(key.Length * 8); + AesAEAD.CheckKeySize(key.Length); ImportKey(key); } + public static bool IsSupported => true; + public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[]? associatedData = null) { - AesAEAD.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); + AeadCommon.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); Encrypt((ReadOnlySpan)nonce, plaintext, ciphertext, tag, associatedData); } @@ -41,12 +43,12 @@ public void Encrypt( ReadOnlySpan associatedData = default) { CheckParameters(plaintext, ciphertext, nonce, tag); - EncryptInternal(nonce, plaintext, ciphertext, tag, associatedData); + EncryptCore(nonce, plaintext, ciphertext, tag, associatedData); } public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[]? associatedData = null) { - AesAEAD.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); + AeadCommon.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); Decrypt((ReadOnlySpan)nonce, ciphertext, tag, plaintext, associatedData); } @@ -58,7 +60,7 @@ public void Decrypt( ReadOnlySpan associatedData = default) { CheckParameters(plaintext, ciphertext, nonce, tag); - DecryptInternal(nonce, ciphertext, tag, plaintext, associatedData); + DecryptCore(nonce, ciphertext, tag, plaintext, associatedData); } private static void CheckParameters( diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Android.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Android.cs index fd6b898db67c7f..8a699770102456 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Android.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Android.cs @@ -26,7 +26,7 @@ private void ImportKey(ReadOnlySpan key) Interop.Crypto.CipherSetNonceLength(_ctxHandle, NonceSize); } - private void EncryptInternal( + private void EncryptCore( ReadOnlySpan nonce, ReadOnlySpan plaintext, Span ciphertext, @@ -101,7 +101,7 @@ private void EncryptInternal( } } - private void DecryptInternal( + private void DecryptCore( ReadOnlySpan nonce, ReadOnlySpan ciphertext, ReadOnlySpan tag, diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.NotSupported.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.NotSupported.cs new file mode 100644 index 00000000000000..efbf1a729b57c7 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.NotSupported.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 +{ + public partial class AesGcm + { + public static bool IsSupported => false; + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Unix.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Unix.cs index d050604168a0e5..b18ce7f9c58118 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Unix.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Unix.cs @@ -25,7 +25,7 @@ private void ImportKey(ReadOnlySpan key) Interop.Crypto.EvpCipherSetGcmNonceLength(_ctxHandle, NonceSize); } - private void EncryptInternal( + private void EncryptCore( ReadOnlySpan nonce, ReadOnlySpan plaintext, Span ciphertext, @@ -70,7 +70,7 @@ private void EncryptInternal( Interop.Crypto.EvpCipherGetGcmTag(_ctxHandle, tag); } - private void DecryptInternal( + private void DecryptCore( ReadOnlySpan nonce, ReadOnlySpan ciphertext, ReadOnlySpan tag, diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Windows.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Windows.cs index 7798787e5b5f50..31e6f647835535 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Windows.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.Windows.cs @@ -9,33 +9,32 @@ namespace System.Security.Cryptography { public partial class AesGcm { - private static readonly SafeAlgorithmHandle s_aesGcm = AesBCryptModes.OpenAesAlgorithm(Cng.BCRYPT_CHAIN_MODE_GCM).Value; private SafeKeyHandle _keyHandle; [MemberNotNull(nameof(_keyHandle))] private void ImportKey(ReadOnlySpan key) { - _keyHandle = Interop.BCrypt.BCryptImportKey(s_aesGcm, key); + _keyHandle = Interop.BCrypt.BCryptImportKey(BCryptAeadHandleCache.AesGcm, key); } - private void EncryptInternal( + private void EncryptCore( ReadOnlySpan nonce, ReadOnlySpan plaintext, Span ciphertext, Span tag, ReadOnlySpan associatedData = default) { - AesAEAD.Encrypt(s_aesGcm, _keyHandle, nonce, associatedData, plaintext, ciphertext, tag); + AeadCommon.Encrypt(_keyHandle, nonce, associatedData, plaintext, ciphertext, tag); } - private void DecryptInternal( + private void DecryptCore( ReadOnlySpan nonce, ReadOnlySpan ciphertext, ReadOnlySpan tag, Span plaintext, ReadOnlySpan associatedData = default) { - AesAEAD.Decrypt(s_aesGcm, _keyHandle, nonce, associatedData, ciphertext, tag, plaintext, clearPlaintextOnFailure: true); + AeadCommon.Decrypt(_keyHandle, nonce, associatedData, ciphertext, tag, plaintext, clearPlaintextOnFailure: true); } public void Dispose() diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.cs index 07bbb6daa0eb33..db1e51dd87f2d9 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/AesGcm.cs @@ -15,7 +15,7 @@ public sealed partial class AesGcm : IDisposable public AesGcm(ReadOnlySpan key) { - AesAEAD.CheckKeySize(key.Length * 8); + AesAEAD.CheckKeySize(key.Length); ImportKey(key); } @@ -24,13 +24,15 @@ public AesGcm(byte[] key) if (key == null) throw new ArgumentNullException(nameof(key)); - AesAEAD.CheckKeySize(key.Length * 8); + AesAEAD.CheckKeySize(key.Length); ImportKey(key); } + public static bool IsSupported => true; + public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[]? associatedData = null) { - AesAEAD.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); + AeadCommon.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); Encrypt((ReadOnlySpan)nonce, plaintext, ciphertext, tag, associatedData); } @@ -42,12 +44,12 @@ public void Encrypt( ReadOnlySpan associatedData = default) { CheckParameters(plaintext, ciphertext, nonce, tag); - EncryptInternal(nonce, plaintext, ciphertext, tag, associatedData); + EncryptCore(nonce, plaintext, ciphertext, tag, associatedData); } public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[]? associatedData = null) { - AesAEAD.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); + AeadCommon.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); Decrypt((ReadOnlySpan)nonce, ciphertext, tag, plaintext, associatedData); } @@ -59,7 +61,7 @@ public void Decrypt( ReadOnlySpan associatedData = default) { CheckParameters(plaintext, ciphertext, nonce, tag); - DecryptInternal(nonce, ciphertext, tag, plaintext, associatedData); + DecryptCore(nonce, ciphertext, tag, plaintext, associatedData); } private static void CheckParameters( diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.NotSupported.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.NotSupported.cs new file mode 100644 index 00000000000000..05e4d39531f6e0 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.NotSupported.cs @@ -0,0 +1,48 @@ +// 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; + +namespace System.Security.Cryptography +{ + public partial class ChaCha20Poly1305 + { + public static bool IsSupported => false; + +#if !BROWSER // allow GenFacades to handle browser target + private void ImportKey(ReadOnlySpan key) + { + Debug.Fail("Instance ctor should fail before we reach this point."); + throw new NotImplementedException(); + } + + private void EncryptCore( + ReadOnlySpan nonce, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag, + ReadOnlySpan associatedData = default) + { + Debug.Fail("Instance ctor should fail before we reach this point."); + throw new NotImplementedException(); + } + + private void DecryptCore( + ReadOnlySpan nonce, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext, + ReadOnlySpan associatedData = default) + { + Debug.Fail("Instance ctor should fail before we reach this point."); + throw new NotImplementedException(); + } + + public void Dispose() + { + Debug.Fail("Instance ctor should fail before we reach this point."); + // no-op + } +#endif + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Windows.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Windows.cs new file mode 100644 index 00000000000000..984a8da718fc1d --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.Windows.cs @@ -0,0 +1,47 @@ +// 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.CodeAnalysis; +using Internal.Cryptography; +using Internal.NativeCrypto; + +namespace System.Security.Cryptography +{ + public partial class ChaCha20Poly1305 + { + private SafeKeyHandle _keyHandle; + + public static bool IsSupported => BCryptAeadHandleCache.IsChaCha20Poly1305Supported; + + [MemberNotNull(nameof(_keyHandle))] + private void ImportKey(ReadOnlySpan key) + { + _keyHandle = Interop.BCrypt.BCryptImportKey(BCryptAeadHandleCache.ChaCha20Poly1305, key); + } + + private void EncryptCore( + ReadOnlySpan nonce, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag, + ReadOnlySpan associatedData = default) + { + AeadCommon.Encrypt(_keyHandle, nonce, associatedData, plaintext, ciphertext, tag); + } + + private void DecryptCore( + ReadOnlySpan nonce, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext, + ReadOnlySpan associatedData = default) + { + AeadCommon.Decrypt(_keyHandle, nonce, associatedData, ciphertext, tag, plaintext, clearPlaintextOnFailure: true); + } + + public void Dispose() + { + _keyHandle.Dispose(); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.cs b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.cs new file mode 100644 index 00000000000000..874133cc39f717 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/src/System/Security/Cryptography/ChaCha20Poly1305.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.Versioning; +using Internal.Cryptography; + +namespace System.Security.Cryptography +{ + [UnsupportedOSPlatform("browser")] + public sealed partial class ChaCha20Poly1305 : IDisposable + { + // Per https://tools.ietf.org/html/rfc7539, ChaCha20Poly1305 AEAD requires a 256-bit key and 96-bit nonce, + // and it produces a 128-bit tag. We don't expose NonceByteSizes / TagByteSizes properties because callers + // are expected to know this. + + private const int KeySizeInBytes = 256 / 8; + private const int NonceSizeInBytes = 96 / 8; + private const int TagSizeInBytes = 128 / 8; + + public ChaCha20Poly1305(ReadOnlySpan key) + { + ThrowIfNotSupported(); + + CheckKeySize(key.Length); + ImportKey(key); + } + + public ChaCha20Poly1305(byte[] key) + { + ThrowIfNotSupported(); + + if (key == null) + throw new ArgumentNullException(nameof(key)); + + CheckKeySize(key.Length); + ImportKey(key); + } + + private static void CheckKeySize(int keySizeInBytes) + { + if (keySizeInBytes != KeySizeInBytes) + { + throw new CryptographicException(SR.Cryptography_InvalidKeySize); + } + } + + public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[]? associatedData = null) + { + AeadCommon.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); + Encrypt((ReadOnlySpan)nonce, plaintext, ciphertext, tag, associatedData); + } + + public void Encrypt( + ReadOnlySpan nonce, + ReadOnlySpan plaintext, + Span ciphertext, + Span tag, + ReadOnlySpan associatedData = default) + { + CheckParameters(plaintext, ciphertext, nonce, tag); + EncryptCore(nonce, plaintext, ciphertext, tag, associatedData); + } + + public void Decrypt(byte[] nonce, byte[] ciphertext, byte[] tag, byte[] plaintext, byte[]? associatedData = null) + { + AeadCommon.CheckArgumentsForNull(nonce, plaintext, ciphertext, tag); + Decrypt((ReadOnlySpan)nonce, ciphertext, tag, plaintext, associatedData); + } + + public void Decrypt( + ReadOnlySpan nonce, + ReadOnlySpan ciphertext, + ReadOnlySpan tag, + Span plaintext, + ReadOnlySpan associatedData = default) + { + CheckParameters(plaintext, ciphertext, nonce, tag); + DecryptCore(nonce, ciphertext, tag, plaintext, associatedData); + } + + private static void CheckParameters( + ReadOnlySpan plaintext, + ReadOnlySpan ciphertext, + ReadOnlySpan nonce, + ReadOnlySpan tag) + { + if (plaintext.Length != ciphertext.Length) + throw new ArgumentException(SR.Cryptography_PlaintextCiphertextLengthMismatch); + + if (nonce.Length != NonceSizeInBytes) + throw new ArgumentException(SR.Cryptography_InvalidNonceLength, nameof(nonce)); + + if (tag.Length != TagSizeInBytes) + throw new ArgumentException(SR.Cryptography_InvalidTagLength, nameof(tag)); + } + + private static void ThrowIfNotSupported() + { + if (!IsSupported) + { + throw new PlatformNotSupportedException(SR.Format(SR.Cryptography_AlgorithmNotSupported, nameof(ChaCha20Poly1305))); + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/AesCcmTests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/AesCcmTests.cs index 3e60346f40ab67..6f5f77aa501849 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/AesCcmTests.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/AesCcmTests.cs @@ -8,21 +8,11 @@ namespace System.Security.Cryptography.Algorithms.Tests { - [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - public class AesCcmTests : AesAEADTests + [ConditionalClass(typeof(AesCcm), nameof(AesCcm.IsSupported))] + public class AesCcmTests : CommonAEADTests { [Theory] - [InlineData(0, 1)] - [InlineData(0, 30)] - [InlineData(1, 1)] - [InlineData(1, 100)] - [InlineData(7, 12)] - [InlineData(16, 16)] - [InlineData(17, 29)] - [InlineData(32, 7)] - [InlineData(41, 25)] - [InlineData(48, 22)] - [InlineData(50, 5)] + [MemberData(nameof(EncryptTamperAADDecryptTestInputs))] public static void EncryptTamperAADDecrypt(int dataLength, int additionalDataLength) { byte[] additionalData = new byte[additionalDataLength]; @@ -190,12 +180,7 @@ public static void TwoEncryptionsAndDecryptionsUsingOneInstance() } [Theory] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(3, 4)] - [InlineData(4, 3)] - [InlineData(20, 120)] - [InlineData(120, 20)] + [MemberData(nameof(PlaintextAndCiphertextSizeDifferTestInputs))] public static void PlaintextAndCiphertextSizeDiffer(int ptLen, int ctLen) { byte[] key = new byte[16]; @@ -690,4 +675,18 @@ public static IEnumerable GetNistCcmTestCasesWithNonEmptyPT() }, }; } + + public class AesCcmIsSupportedTests + { + public class AesGcmIsSupportedTests + { + [Fact] + public static void CheckIsSupported() + { + bool expectedIsSupported = !PlatformDetection.IsBrowser; + + Assert.Equal(expectedIsSupported, AesCcm.IsSupported); + } + } + } } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/AesGcmTests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/AesGcmTests.cs index 70188d08f4238a..d95482f2c478d8 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/AesGcmTests.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/AesGcmTests.cs @@ -8,21 +8,11 @@ namespace System.Security.Cryptography.Algorithms.Tests { - [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - public class AesGcmTests : AesAEADTests + [ConditionalClass(typeof(AesGcm), nameof(AesGcm.IsSupported))] + public class AesGcmTests : CommonAEADTests { [Theory] - [InlineData(0, 1)] - [InlineData(0, 30)] - [InlineData(1, 1)] - [InlineData(1, 100)] - [InlineData(7, 12)] - [InlineData(16, 16)] - [InlineData(17, 29)] - [InlineData(32, 7)] - [InlineData(41, 25)] - [InlineData(48, 22)] - [InlineData(50, 5)] + [MemberData(nameof(EncryptTamperAADDecryptTestInputs))] public static void EncryptTamperAADDecrypt(int dataLength, int additionalDataLength) { byte[] additionalData = new byte[additionalDataLength]; @@ -190,12 +180,7 @@ public static void TwoEncryptionsAndDecryptionsUsingOneInstance() } [Theory] - [InlineData(0, 1)] - [InlineData(1, 0)] - [InlineData(3, 4)] - [InlineData(4, 3)] - [InlineData(20, 120)] - [InlineData(120, 20)] + [MemberData(nameof(PlaintextAndCiphertextSizeDifferTestInputs))] public static void PlaintextAndCiphertextSizeDiffer(int ptLen, int ctLen) { byte[] key = new byte[16]; @@ -278,7 +263,7 @@ public static void EncryptDecryptNullTag() } [Fact] - public static void InplaceEncrypDecrypt() + public static void InplaceEncryptDecrypt() { byte[] key = "d5a194ed90cfe08abecd4691997ceb2c".HexToByteArray(); byte[] nonce = new byte[12]; @@ -298,7 +283,7 @@ public static void InplaceEncrypDecrypt() } [Fact] - public static void InplaceEncrypTamperTagDecrypt() + public static void InplaceEncryptTamperTagDecrypt() { byte[] key = "d5a194ed90cfe08abecd4691997ceb2c".HexToByteArray(); byte[] nonce = new byte[12]; @@ -851,4 +836,15 @@ private static IEnumerable GetNistTests() }, }; } + + public class AesGcmIsSupportedTests + { + [Fact] + public static void CheckIsSupported() + { + bool expectedIsSupported = !PlatformDetection.IsBrowser; + + Assert.Equal(expectedIsSupported, AesGcm.IsSupported); + } + } } diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs new file mode 100644 index 00000000000000..abd5a55508026f --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/ChaCha20Poly1305Tests.cs @@ -0,0 +1,454 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; +using System.Linq; +using Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Algorithms.Tests +{ + [ConditionalClass(typeof(ChaCha20Poly1305), nameof(ChaCha20Poly1305.IsSupported))] + public class ChaCha20Poly1305Tests : CommonAEADTests + { + private const int KeySizeInBytes = 256 / 8; + private const int NonceSizeInBytes = 96 / 8; + private const int TagSizeInBytes = 128 / 8; + + [Theory] + [MemberData(nameof(EncryptTamperAADDecryptTestInputs))] + public static void EncryptTamperAADDecrypt(int dataLength, int additionalDataLength) + { + byte[] additionalData = new byte[additionalDataLength]; + RandomNumberGenerator.Fill(additionalData); + + byte[] plaintext = Enumerable.Range(1, dataLength).Select((x) => (byte)x).ToArray(); + byte[] ciphertext = new byte[dataLength]; + byte[] key = RandomNumberGenerator.GetBytes(KeySizeInBytes); + byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeInBytes); + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + chaChaPoly.Encrypt(nonce, plaintext, ciphertext, tag, additionalData); + + additionalData[0] ^= 1; + + byte[] decrypted = new byte[dataLength]; + Assert.Throws( + () => chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted, additionalData)); + } + } + + [Theory] + [InlineData(0)] + [InlineData(1)] + [InlineData(2)] + [InlineData(16)] // 128-bit keys disallowed + [InlineData(17)] + [InlineData(24)] // 192-bit keys disallowed + [InlineData(29)] + [InlineData(33)] + public static void InvalidKeyLength(int keyLength) + { + byte[] key = new byte[keyLength]; + Assert.Throws(() => new ChaCha20Poly1305(key)); + } + + [Theory] + [MemberData(nameof(GetInvalidNonceSizes))] + public static void InvalidNonceSize(int nonceSize) + { + int dataLength = 30; + byte[] plaintext = Enumerable.Range(1, dataLength).Select((x) => (byte)x).ToArray(); + byte[] ciphertext = new byte[dataLength]; + byte[] key = RandomNumberGenerator.GetBytes(KeySizeInBytes); + byte[] nonce = RandomNumberGenerator.GetBytes(nonceSize); + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws("nonce", () => chaChaPoly.Encrypt(nonce, plaintext, ciphertext, tag)); + } + } + + [Theory] + [MemberData(nameof(GetInvalidTagSizes))] + public static void InvalidTagSize(int tagSize) + { + int dataLength = 30; + byte[] plaintext = Enumerable.Range(1, dataLength).Select((x) => (byte)x).ToArray(); + byte[] ciphertext = new byte[dataLength]; + byte[] key = RandomNumberGenerator.GetBytes(KeySizeInBytes); + byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeInBytes); + byte[] tag = new byte[tagSize]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws("tag", () => chaChaPoly.Encrypt(nonce, plaintext, ciphertext, tag)); + } + } + + [Fact] + public static void ValidNonceAndTagSize() + { + const int dataLength = 35; + byte[] plaintext = Enumerable.Range(1, dataLength).Select((x) => (byte)x).ToArray(); + byte[] ciphertext = new byte[dataLength]; + byte[] key = RandomNumberGenerator.GetBytes(KeySizeInBytes); + byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeInBytes); + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + chaChaPoly.Encrypt(nonce, plaintext, ciphertext, tag); + + byte[] decrypted = new byte[dataLength]; + chaChaPoly.Decrypt(nonce, ciphertext, tag, decrypted); + Assert.Equal(plaintext, decrypted); + } + } + + [Fact] + public static void TwoEncryptionsAndDecryptionsUsingOneInstance() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] originalData1 = Enumerable.Range(1, 15).Select((x) => (byte)x).ToArray(); + byte[] originalData2 = Enumerable.Range(14, 97).Select((x) => (byte)x).ToArray(); + byte[] associatedData2 = Enumerable.Range(100, 109).Select((x) => (byte)x).ToArray(); + byte[] nonce1 = "b41329dd64af2c3036661b46".HexToByteArray(); + byte[] nonce2 = "8ba10892e8b87d031196bf99".HexToByteArray(); + + byte[] expectedCiphertext1 = "75f5aafbbabab80a3cfa2ecfd1bc58".HexToByteArray(); + byte[] expectedTag1 = "1ed70acc454fba01f0354e93eba9b428".HexToByteArray(); + + byte[] expectedCiphertext2 = ( + "f95cc19929463ba96a2cfc21fac5345ec308e2748995ba285af6b21ca3d665bc" + + "00144604b38e9645fb2d5f5893fc78871bd8f5fc91caaa013eac5f80397fd65c" + + "358c239f013f3c75da17ddbd14de01eb67f5204dfa787986fb27a098fe21b2c5" + + "07").HexToByteArray(); + byte[] expectedTag2 = "9877f87f29f68b5f9efb071c1351ccf6".HexToByteArray(); + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + byte[] ciphertext1 = new byte[originalData1.Length]; + byte[] tag1 = new byte[expectedTag1.Length]; + chaChaPoly.Encrypt(nonce1, originalData1, ciphertext1, tag1); + Assert.Equal(expectedCiphertext1, ciphertext1); + Assert.Equal(expectedTag1, tag1); + + byte[] ciphertext2 = new byte[originalData2.Length]; + byte[] tag2 = new byte[expectedTag2.Length]; + chaChaPoly.Encrypt(nonce2, originalData2, ciphertext2, tag2, associatedData2); + Assert.Equal(expectedCiphertext2, ciphertext2); + Assert.Equal(expectedTag2, tag2); + + byte[] plaintext1 = new byte[originalData1.Length]; + chaChaPoly.Decrypt(nonce1, ciphertext1, tag1, plaintext1); + Assert.Equal(originalData1, plaintext1); + + byte[] plaintext2 = new byte[originalData2.Length]; + chaChaPoly.Decrypt(nonce2, ciphertext2, tag2, plaintext2, associatedData2); + Assert.Equal(originalData2, plaintext2); + } + } + + [Theory] + [MemberData(nameof(PlaintextAndCiphertextSizeDifferTestInputs))] + public static void PlaintextAndCiphertextSizeDiffer(int ptLen, int ctLen) + { + byte[] key = new byte[KeySizeInBytes]; + byte[] nonce = new byte[NonceSizeInBytes]; + byte[] plaintext = new byte[ptLen]; + byte[] ciphertext = new byte[ctLen]; + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws(() => chaChaPoly.Encrypt(nonce, plaintext, ciphertext, tag)); + Assert.Throws(() => chaChaPoly.Decrypt(nonce, ciphertext, tag, plaintext)); + } + } + + [Fact] + public static void NullKey() + { + Assert.Throws(() => new ChaCha20Poly1305((byte[])null)); + } + + [Fact] + public static void EncryptDecryptNullNonce() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] plaintext = new byte[0]; + byte[] ciphertext = new byte[0]; + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws(() => chaChaPoly.Encrypt((byte[])null, plaintext, ciphertext, tag)); + Assert.Throws(() => chaChaPoly.Decrypt((byte[])null, ciphertext, tag, plaintext)); + } + } + + [Fact] + public static void EncryptDecryptNullPlaintext() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] nonce = new byte[NonceSizeInBytes]; + byte[] ciphertext = new byte[0]; + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws(() => chaChaPoly.Encrypt(nonce, (byte[])null, ciphertext, tag)); + Assert.Throws(() => chaChaPoly.Decrypt(nonce, ciphertext, tag, (byte[])null)); + } + } + + [Fact] + public static void EncryptDecryptNullCiphertext() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] nonce = new byte[NonceSizeInBytes]; + byte[] plaintext = new byte[0]; + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws(() => chaChaPoly.Encrypt(nonce, plaintext, (byte[])null, tag)); + Assert.Throws(() => chaChaPoly.Decrypt(nonce, (byte[])null, tag, plaintext)); + } + } + + [Fact] + public static void EncryptDecryptNullTag() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] nonce = new byte[NonceSizeInBytes]; + byte[] plaintext = new byte[0]; + byte[] ciphertext = new byte[0]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + Assert.Throws(() => chaChaPoly.Encrypt(nonce, plaintext, ciphertext, (byte[])null)); + Assert.Throws(() => chaChaPoly.Decrypt(nonce, ciphertext, (byte[])null, plaintext)); + } + } + + [Fact] + public static void InplaceEncryptDecrypt() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeInBytes); + byte[] originalPlaintext = new byte[] { 1, 2, 8, 12, 16, 99, 0 }; + byte[] data = (byte[])originalPlaintext.Clone(); + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + chaChaPoly.Encrypt(nonce, data, data, tag); + Assert.NotEqual(originalPlaintext, data); + + chaChaPoly.Decrypt(nonce, data, tag, data); + Assert.Equal(originalPlaintext, data); + } + } + + [Fact] + public static void InplaceEncryptTamperTagDecrypt() + { + byte[] key = "fde37f01fe9ca260f432e0ed98b3e0bb23895ca1ca1ce2cfcaaca2ccc98889d7".HexToByteArray(); + byte[] nonce = RandomNumberGenerator.GetBytes(NonceSizeInBytes); + byte[] originalPlaintext = new byte[] { 1, 2, 8, 12, 16, 99, 0 }; + byte[] data = (byte[])originalPlaintext.Clone(); + byte[] tag = new byte[TagSizeInBytes]; + + using (var chaChaPoly = new ChaCha20Poly1305(key)) + { + chaChaPoly.Encrypt(nonce, data, data, tag); + Assert.NotEqual(originalPlaintext, data); + + tag[0] ^= 1; + + Assert.Throws( + () => chaChaPoly.Decrypt(nonce, data, tag, data)); + Assert.Equal(new byte[data.Length], data); + } + } + + [Theory] + [MemberData(nameof(GetRfc8439TestCases))] + public static void Rfc8439Tests(AEADTest testCase) + { + using (var chaChaPoly = new ChaCha20Poly1305(testCase.Key)) + { + byte[] ciphertext = new byte[testCase.Plaintext.Length]; + byte[] tag = new byte[testCase.Tag.Length]; + chaChaPoly.Encrypt(testCase.Nonce, testCase.Plaintext, ciphertext, tag, testCase.AssociatedData); + Assert.Equal(testCase.Ciphertext, ciphertext); + Assert.Equal(testCase.Tag, tag); + + byte[] plaintext = new byte[testCase.Plaintext.Length]; + chaChaPoly.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData); + Assert.Equal(testCase.Plaintext, plaintext); + } + } + + [Theory] + [MemberData(nameof(GetRfc8439TestCases))] + public static void Rfc8439TestsTamperTag(AEADTest testCase) + { + using (var chaChaPoly = new ChaCha20Poly1305(testCase.Key)) + { + byte[] ciphertext = new byte[testCase.Plaintext.Length]; + byte[] tag = new byte[testCase.Tag.Length]; + chaChaPoly.Encrypt(testCase.Nonce, testCase.Plaintext, ciphertext, tag, testCase.AssociatedData); + Assert.Equal(testCase.Ciphertext, ciphertext); + Assert.Equal(testCase.Tag, tag); + + tag[0] ^= 1; + + byte[] plaintext = RandomNumberGenerator.GetBytes(testCase.Plaintext.Length); + Assert.Throws( + () => chaChaPoly.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData)); + Assert.Equal(new byte[plaintext.Length], plaintext); + } + } + + public static IEnumerable GetInvalidNonceSizes() + { + yield return new object[] { 0 }; + yield return new object[] { 8 }; + yield return new object[] { 11 }; + yield return new object[] { 13 }; + yield return new object[] { 16 }; + } + + public static IEnumerable GetInvalidTagSizes() + { + yield return new object[] { 0 }; + yield return new object[] { 8 }; + yield return new object[] { 12 }; + yield return new object[] { 15 }; + yield return new object[] { 17 }; + } + + // https://tools.ietf.org/html/rfc8439 + private const string Rfc8439TestVectors = "RFC 8439 Test Vectors"; + + public static IEnumerable GetRfc8439TestCases() + { + foreach (AEADTest test in s_rfc8439TestVectors) + { + yield return new object[] { test }; + } + } + + // CaseId is unique per test case + private static readonly AEADTest[] s_rfc8439TestVectors = new AEADTest[] + { + new AEADTest + { + Source = Rfc8439TestVectors, + CaseId = 1, // RFC 8439, Sec. 2.8.2 + Key = "808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f".HexToByteArray(), + Nonce = "070000004041424344454647".HexToByteArray(), + Plaintext = ( + "4c616469657320616e642047656e746c" + + "656d656e206f662074686520636c6173" + + "73206f66202739393a20496620492063" + + "6f756c64206f6666657220796f75206f" + + "6e6c79206f6e652074697020666f7220" + + "746865206675747572652c2073756e73" + + "637265656e20776f756c642062652069" + + "742e").HexToByteArray(), + AssociatedData = "50515253c0c1c2c3c4c5c6c7".HexToByteArray(), + Ciphertext = ( + "d31a8d34648e60db7b86afbc53ef7ec2" + + "a4aded51296e08fea9e2b5a736ee62d6" + + "3dbea45e8ca9671282fafb69da92728b" + + "1a71de0a9e060b2905d6a5b67ecd3b36" + + "92ddbd7f2d778b8c9803aee328091b58" + + "fab324e4fad675945585808b4831d7bc" + + "3ff4def08e4b7a9de576d26586cec64b" + + "6116").HexToByteArray(), + Tag = "1ae10b594f09e26a7e902ecbd0600691".HexToByteArray() + }, + new AEADTest + { + Source = Rfc8439TestVectors, + CaseId = 2, // RFC 8439, Appendix A.5 + Key = "1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0".HexToByteArray(), + Nonce = "000000000102030405060708".HexToByteArray(), + Plaintext = ( + "496e7465726e65742d44726166747320" + + "61726520647261667420646f63756d65" + + "6e74732076616c696420666f72206120" + + "6d6178696d756d206f6620736978206d" + + "6f6e74687320616e64206d6179206265" + + "20757064617465642c207265706c6163" + + "65642c206f72206f62736f6c65746564" + + "206279206f7468657220646f63756d65" + + "6e747320617420616e792074696d652e" + + "20497420697320696e617070726f7072" + + "6961746520746f2075736520496e7465" + + "726e65742d4472616674732061732072" + + "65666572656e6365206d617465726961" + + "6c206f7220746f206369746520746865" + + "6d206f74686572207468616e20617320" + + "2fe2809c776f726b20696e2070726f67" + + "726573732e2fe2809d").HexToByteArray(), + AssociatedData = "f33388860000000000004e91".HexToByteArray(), + Ciphertext = ( + "64a0861575861af460f062c79be643bd" + + "5e805cfd345cf389f108670ac76c8cb2" + + "4c6cfc18755d43eea09ee94e382d26b0" + + "bdb7b73c321b0100d4f03b7f355894cf" + + "332f830e710b97ce98c8a84abd0b9481" + + "14ad176e008d33bd60f982b1ff37c855" + + "9797a06ef4f0ef61c186324e2b350638" + + "3606907b6a7c02b0f9f6157b53c867e4" + + "b9166c767b804d46a59b5216cde7a4e9" + + "9040c5a40433225ee282a1b0a06c523e" + + "af4534d7f83fa1155b0047718cbc546a" + + "0d072b04b3564eea1b422273f548271a" + + "0bb2316053fa76991955ebd63159434e" + + "cebb4e466dae5a1073a6727627097a10" + + "49e617d91d361094fa68f0ff77987130" + + "305beaba2eda04df997b714d6c6f2c29" + + "a6ad5cb4022b02709b").HexToByteArray(), + Tag = "eead9d67890cbb22392336fea1851f38".HexToByteArray() + } + }; + } + + public class ChaCha20Poly1305IsSupportedTests + { + public static bool RuntimeSaysIsNotSupported => !ChaCha20Poly1305.IsSupported; + + [ConditionalFact(nameof(RuntimeSaysIsNotSupported))] + public static void CtorThrowsPNSEIfNotSupported() + { + byte[] key = RandomNumberGenerator.GetBytes(256 / 8); + + Assert.Throws(() => new ChaCha20Poly1305(key)); + Assert.Throws(() => new ChaCha20Poly1305(key.AsSpan())); + } + + [Fact] + public static void CheckIsSupported() + { + bool expectedIsSupported = false; // assume not supported unless environment advertises support + + if (PlatformDetection.IsWindows) + { + // Runtime uses a hardcoded OS version to determine support. + // The test queries the OS directly to ensure our version check is correct. + expectedIsSupported = CngUtility.IsAlgorithmSupported("CHACHA20_POLY1305"); + } + + Assert.Equal(expectedIsSupported, ChaCha20Poly1305.IsSupported); + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/CngUtility.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/CngUtility.cs new file mode 100644 index 00000000000000..e22d6ae8e78717 --- /dev/null +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/CngUtility.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.Runtime.InteropServices; +using Xunit; + +namespace System.Security.Cryptography.Algorithms.Tests +{ + internal static class CngUtility + { + private const string BCRYPT_LIB = "bcrypt.dll"; + private const string MS_PRIMITIVE_PROVIDER = "Microsoft Primitive Provider"; + + public static bool IsAlgorithmSupported(string algId, string implementation = MS_PRIMITIVE_PROVIDER) + { + Assert.True(PlatformDetection.IsWindows, "Caller should not invoke this method for non-Windows platforms."); + + int ntStatus = BCryptOpenAlgorithmProvider(out SafeBCryptAlgorithmHandle handle, algId, implementation, 0); + bool isSupported = ntStatus == 0 && handle != null && !handle.IsInvalid; + handle?.Dispose(); + return isSupported; + } + + // https://docs.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptclosealgorithmprovider + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern int BCryptCloseAlgorithmProvider( + [In] IntPtr hAlgorithm, + [In] uint dwFlags); + + // https://docs.microsoft.com/windows/win32/api/bcrypt/nf-bcrypt-bcryptopenalgorithmprovider + [DllImport(BCRYPT_LIB, CallingConvention = CallingConvention.Winapi)] + [DefaultDllImportSearchPaths(DllImportSearchPath.System32)] + private static extern int BCryptOpenAlgorithmProvider( + [Out] out SafeBCryptAlgorithmHandle phAlgorithm, + [In, MarshalAs(UnmanagedType.LPWStr)] string pszAlgId, + [In, MarshalAs(UnmanagedType.LPWStr)] string pszImplementation, + [In] uint dwFlags); + + internal sealed class SafeBCryptAlgorithmHandle : SafeHandle + { + public SafeBCryptAlgorithmHandle() + : base(IntPtr.Zero, ownsHandle: true) + { + } + + public override bool IsInvalid => handle == IntPtr.Zero; + + protected sealed override bool ReleaseHandle() + { + int ntStatus = BCryptCloseAlgorithmProvider(handle, 0); + return ntStatus == 0; + } + } + } +} diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/AesAEADTests.cs b/src/libraries/System.Security.Cryptography.Algorithms/tests/CommonAEADTests.cs similarity index 63% rename from src/libraries/System.Security.Cryptography.Algorithms/tests/AesAEADTests.cs rename to src/libraries/System.Security.Cryptography.Algorithms/tests/CommonAEADTests.cs index ba8d60b11ab3b6..eb774ecd879b84 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/AesAEADTests.cs +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/CommonAEADTests.cs @@ -5,8 +5,35 @@ namespace System.Security.Cryptography.Algorithms.Tests { - public abstract class AesAEADTests + public abstract class CommonAEADTests { + public static IEnumerable EncryptTamperAADDecryptTestInputs() + { + // yield return { dataLength, additionalDataLength }; + yield return new object[] { 0, 1 }; + yield return new object[] { 0, 30 }; + yield return new object[] { 1, 1 }; + yield return new object[] { 1, 100 }; + yield return new object[] { 7, 12 }; + yield return new object[] { 16, 16 }; + yield return new object[] { 17, 29 }; + yield return new object[] { 32, 7 }; + yield return new object[] { 41, 25 }; + yield return new object[] { 48, 22 }; + yield return new object[] { 50, 5 }; + } + + public static IEnumerable PlaintextAndCiphertextSizeDifferTestInputs() + { + // yield return { ptLen, ctLen }; + yield return new object[] { 0, 1 }; + yield return new object[] { 1, 0 }; + yield return new object[] { 3, 4 }; + yield return new object[] { 4, 3 }; + yield return new object[] { 20, 120 }; + yield return new object[] { 120, 20 }; + } + protected static bool MatchesKeySizes(int size, KeySizes keySizes) { if (size < keySizes.MinSize || size > keySizes.MaxSize) diff --git a/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj b/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj index 77ce3aa220dfc4..c70b83e36a9dbf 100644 --- a/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.Algorithms/tests/System.Security.Cryptography.Algorithms.Tests.csproj @@ -236,9 +236,11 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\RSA\RSASignatureFormatter.cs" /> - + + +