From 16b7f32abbed073c11668a881a5999086c9f5110 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Aug 2026 12:09:03 -0400 Subject: [PATCH 1/7] Implement non-Core methods --- .../ref/System.Security.Cryptography.cs | 10 + .../src/Resources/Strings.resx | 6 + .../src/System/Security/Cryptography/Aes.cs | 306 ++++++++++++++++++ 3 files changed, 322 insertions(+) diff --git a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs index 4a177bcaa21fa3..9e9e74d7baf34f 100644 --- a/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs +++ b/src/libraries/System.Security.Cryptography/ref/System.Security.Cryptography.cs @@ -52,15 +52,25 @@ protected Aes() { } [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCodeAttribute("The default algorithm implementations might be removed, use strong type references like 'RSA.Create()' instead.")] [System.ObsoleteAttribute("Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead.", DiagnosticId="SYSLIB0045", UrlFormat="https://aka.ms/dotnet-warnings/{0}")] public static new System.Security.Cryptography.Aes? Create(string algorithmName) { throw null; } + public byte[] DecryptKeyWrap(byte[] ciphertext) { throw null; } + public byte[] DecryptKeyWrap(System.ReadOnlySpan ciphertext) { throw null; } + public int DecryptKeyWrap(System.ReadOnlySpan ciphertext, System.Span destination) { throw null; } + protected virtual int DecryptKeyWrapCore(System.ReadOnlySpan source, System.Span destination) { throw null; } public byte[] DecryptKeyWrapPadded(byte[] ciphertext) { throw null; } public byte[] DecryptKeyWrapPadded(System.ReadOnlySpan ciphertext) { throw null; } public int DecryptKeyWrapPadded(System.ReadOnlySpan ciphertext, System.Span destination) { throw null; } protected virtual int DecryptKeyWrapPaddedCore(System.ReadOnlySpan source, System.Span destination) { throw null; } + public byte[] EncryptKeyWrap(byte[] plaintext) { throw null; } + public byte[] EncryptKeyWrap(System.ReadOnlySpan plaintext) { throw null; } + public void EncryptKeyWrap(System.ReadOnlySpan plaintext, System.Span destination) { } + protected virtual void EncryptKeyWrapCore(System.ReadOnlySpan source, System.Span destination) { } public byte[] EncryptKeyWrapPadded(byte[] plaintext) { throw null; } public byte[] EncryptKeyWrapPadded(System.ReadOnlySpan plaintext) { throw null; } public void EncryptKeyWrapPadded(System.ReadOnlySpan plaintext, System.Span destination) { } protected virtual void EncryptKeyWrapPaddedCore(System.ReadOnlySpan source, System.Span destination) { } + public static int GetKeyWrapLength(int plaintextLengthInBytes) { throw null; } public static int GetKeyWrapPaddedLength(int plaintextLengthInBytes) { throw null; } + public bool TryDecryptKeyWrap(System.ReadOnlySpan ciphertext, System.Span destination, out int bytesWritten) { throw null; } public bool TryDecryptKeyWrapPadded(System.ReadOnlySpan ciphertext, System.Span destination, out int bytesWritten) { throw null; } } [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("browser")] diff --git a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx index 2d4865137731a8..1fdd5a3e8257e5 100644 --- a/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx +++ b/src/libraries/System.Security.Cryptography/src/Resources/Strings.resx @@ -573,12 +573,18 @@ The key is too small for the requested operation. + + The ciphertext is not a valid length for the Key Wrap algorithm. + The ciphertext did not successfully decrypt with the instance key. The ciphertext is not a valid length for the Key Wrap with Padding Unwrap algorithm. + + The plaintext is not a valid length for the Key Wrap algorithm. + The specified key is not the correct size for the indicated algorithm. 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 45436e08e799d2..959ba07214e9f6 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 @@ -35,6 +35,273 @@ protected Aes() return (Aes?)CryptoConfig.CreateFromName(algorithmName); } + /// + /// Computes the output length of the IETF RFC 3394 AES Key Wrap Algorithm + /// for the specified plaintext length. + /// + /// + /// The length of the plaintext to be wrapped, in bytes. + /// + /// + /// The length of the key wrap for the specified plaintext. + /// + /// + /// + /// is less than 16 or is not a multiple of 8. + /// + /// -or- + /// + /// represents a plaintext length + /// that, when wrapped, has a length that cannot be represented as a signed + /// 32-bit integer. + /// + /// + public static int GetKeyWrapLength(int plaintextLengthInBytes) + { + const int MaxSupportedValue = 0x7FFF_FFF0; + const int MinSupportedValue = 16; // RFC 3394 requires at least two 64-bit blocks. + + if (plaintextLengthInBytes < MinSupportedValue || (plaintextLengthInBytes % 8) != 0) + { + throw new ArgumentOutOfRangeException( + nameof(plaintextLengthInBytes), + SR.Cryptography_KeyWrap_Plaintext_InvalidLength); + } + + if (plaintextLengthInBytes > MaxSupportedValue) + { + throw new ArgumentOutOfRangeException( + nameof(plaintextLengthInBytes), + SR.Cryptography_PlaintextTooLarge); + } + + return checked(plaintextLengthInBytes + 8); + } + + /// + /// + /// is . + /// + public byte[] EncryptKeyWrap(byte[] plaintext) + { + ArgumentNullException.ThrowIfNull(plaintext); + return EncryptKeyWrap(new ReadOnlySpan(plaintext)); + } + + /// + /// Wraps a key using the IETF RFC 3394 AES Key Wrap algorithm. + /// + /// The data to wrap. + /// The wrapped data. + /// + /// has a length that is less than 16 bytes or is not a multiple of 8 bytes. + /// + /// An error occurred during the cryptographic operation. + public byte[] EncryptKeyWrap(ReadOnlySpan plaintext) + { + int outputLength = GetKeyWrapCiphertextLength(plaintext); + byte[] output = new byte[outputLength]; + EncryptKeyWrapCore(plaintext, output); + return output; + } + + /// + /// Wraps a key using the IETF RFC 3394 AES Key Wrap algorithm, + /// writing the result to a specified buffer. + /// + /// The data to wrap. + /// The buffer to receive the wrapped data. + /// + /// + /// has a length that is less than 16 bytes or is not a multiple of 8 bytes. + /// + /// -or- + /// + /// is not precisely sized to the value returned by + /// for the plaintext length. + /// + /// + /// + /// and overlap. + /// -or- + /// An error occurred during the cryptographic operation. + /// + /// + public void EncryptKeyWrap(ReadOnlySpan plaintext, Span destination) + { + int requiredLength = GetKeyWrapCiphertextLength(plaintext); + + if (destination.Length != requiredLength) + { + throw new ArgumentException( + SR.Format(SR.Argument_DestinationImprecise, requiredLength), + nameof(destination)); + } + + if (plaintext.Overlaps(destination)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + EncryptKeyWrapCore(plaintext, destination); + } + + /// + /// + /// is . + /// + public byte[] DecryptKeyWrap(byte[] ciphertext) + { + ArgumentNullException.ThrowIfNull(ciphertext); + return DecryptKeyWrap(new ReadOnlySpan(ciphertext)); + } + + /// + /// Unwraps a key that was wrapped using the IETF RFC 3394 AES Key Wrap algorithm. + /// + /// The data to unwrap. + /// The unwrapped key. + /// + /// has a length that is less than 24 bytes or is not a multiple of 8 bytes. + /// + /// + /// The unwrap algorithm failed to unwrap the ciphertext. + /// -or- + /// An error occurred during the cryptographic operation. + /// + public byte[] DecryptKeyWrap(ReadOnlySpan ciphertext) + { + int outputLength = GetKeyWrapPlaintextLength(ciphertext); + byte[] output = new byte[outputLength]; + int written; + + try + { + written = DecryptKeyWrapCore(ciphertext, output); + } + catch + { + CryptographicOperations.ZeroMemory(output); + throw; + } + + if (written != outputLength) + { + // The virtual implementation did not write the amount required. + CryptographicOperations.ZeroMemory(output); + throw new CryptographicException(); + } + + return output; + } + + /// + /// Unwraps a key that was wrapped using the IETF RFC 3394 AES Key Wrap algorithm, + /// writing the result to a specified buffer. + /// + /// The data to unwrap. + /// The buffer to receive the unwrapped key. + /// The number of bytes written to . + /// + /// + /// has a length that is less than 24 bytes or is not a multiple of 8 bytes. + /// + /// -or- + /// + /// is too short to receive the unwrapped key. + /// + /// + /// + /// and overlap. + /// -or- + /// The unwrap algorithm failed to unwrap the ciphertext. + /// -or- + /// An error occurred during the cryptographic operation. + /// + public int DecryptKeyWrap(ReadOnlySpan ciphertext, Span destination) + { + if (TryDecryptKeyWrap(ciphertext, destination, out int bytesWritten)) + { + return bytesWritten; + } + + throw new ArgumentException(SR.Argument_DestinationTooShort, nameof(destination)); + } + + /// + /// Attempts to unwrap a key that was wrapped using the IETF RFC 3394 AES Key Wrap algorithm. + /// + /// The data to unwrap. + /// The buffer to receive the unwrapped key. + /// + /// When this method returns, contains the number of bytes written to . + /// This parameter is treated as uninitialized. + /// + /// + /// if is long enough to receive the unwrapped key; + /// otherwise, . + /// + /// + /// has a length that is less than 24 bytes or is not a multiple of 8 bytes. + /// + /// + /// and overlap. + /// -or- + /// The unwrap algorithm failed to unwrap the ciphertext. + /// -or- + /// An error occurred during the cryptographic operation. + /// + public bool TryDecryptKeyWrap(ReadOnlySpan ciphertext, Span destination, out int bytesWritten) + { + int requiredLength = GetKeyWrapPlaintextLength(ciphertext); + + if (destination.Length < requiredLength) + { + bytesWritten = 0; + return false; + } + + destination = destination.Slice(0, requiredLength); + + if (ciphertext.Overlaps(destination)) + { + throw new CryptographicException(SR.Cryptography_OverlappingBuffers); + } + + int written; + + try + { + written = DecryptKeyWrapCore(ciphertext, destination); + } + catch + { + CryptographicOperations.ZeroMemory(destination); + throw; + } + + if (written != requiredLength) + { + // Even though this method returns an int indicating how much was written, we validate that the + // virtual implementation did the right thing instead of blindly passing it through. + CryptographicOperations.ZeroMemory(destination); + throw new CryptographicException(); + } + + bytesWritten = written; + return true; + } + + protected virtual int DecryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + throw new NotImplementedException(); + } + + protected virtual void EncryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + throw new NotImplementedException(); + } + /// /// Computes the output length of the IETF RFC 5649 AES Key Wrap with Padding /// Algorithm for the specified plaintext length. @@ -429,6 +696,7 @@ protected virtual unsafe void EncryptKeyWrapPaddedCore(ReadOnlySpan source this, static (instance, source, destination) => instance.EncryptEcb(source, destination, PaddingMode.None)); } + private protected void EncryptKeyWrapPaddedCore( ReadOnlySpan source, Span destination, @@ -556,6 +824,44 @@ private ulong Rfc3394Unwrap( return BinaryPrimitives.ReadUInt64BigEndian(A); } + private static int GetKeyWrapCiphertextLength(ReadOnlySpan plaintext) + { + const int MaxSupportedValue = 0x7FFF_FFF0; + const int MinSupportedValue = 16; // RFC 3394 requires at least two 64-bit blocks. + int plaintextLengthInBytes = plaintext.Length; + + if (plaintextLengthInBytes < MinSupportedValue || (plaintextLengthInBytes % 8) != 0) + { + throw new ArgumentException( + SR.Cryptography_KeyWrap_Plaintext_InvalidLength, + nameof(plaintext)); + } + + if (plaintextLengthInBytes > MaxSupportedValue) + { + throw new ArgumentException( + SR.Cryptography_PlaintextTooLarge, + nameof(plaintext)); + } + + return checked(plaintextLengthInBytes + 8); + } + + private static int GetKeyWrapPlaintextLength(ReadOnlySpan ciphertext) + { + const int MinSupportedValue = 24; // The minimum plaintext is 16 bytes, plus 8 bytes for the check register. + int ciphertextLengthInBytes = ciphertext.Length; + + if (ciphertextLengthInBytes < MinSupportedValue || (ciphertextLengthInBytes % 8) != 0) + { + throw new ArgumentException( + SR.Cryptography_KeyWrap_Ciphertext_InvalidLength, + nameof(ciphertext)); + } + + return checked(ciphertextLengthInBytes - 8); + } + private static readonly KeySizes[] s_legalBlockSizes = { new KeySizes(128, 128, 0) }; private static readonly KeySizes[] s_legalKeySizes = { new KeySizes(128, 256, 64) }; } From 75d0841473c2fc8678f35354d976265e0d724b64 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Aug 2026 12:31:25 -0400 Subject: [PATCH 2/7] Move existing key wrap tests to key wrap padded tests --- .../AES/{KeyWrapTests.cs => KeyWrapPaddedTests.cs} | 12 ++++++------ .../tests/System.Security.Cryptography.Tests.csproj | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/{KeyWrapTests.cs => KeyWrapPaddedTests.cs} (98%) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapPaddedTests.cs similarity index 98% rename from src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs rename to src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapPaddedTests.cs index 41877050462cb2..9fb48407a4fbfd 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapPaddedTests.cs @@ -10,7 +10,7 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests using Aes = System.Security.Cryptography.Aes; [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - public sealed class KeyWrapTests_AesCreate_KeyProp : KeyWrapTests + public sealed class KeyWrapPaddedTests_AesCreate_KeyProp : KeyWrapPaddedTests { protected override Aes CreateKey(byte[] key) { @@ -21,7 +21,7 @@ protected override Aes CreateKey(byte[] key) } [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - public sealed class KeyWrapTests_AesCreate_SetKey : KeyWrapTests + public sealed class KeyWrapPaddedTests_AesCreate_SetKey : KeyWrapPaddedTests { protected override Aes CreateKey(byte[] key) { @@ -32,7 +32,7 @@ protected override Aes CreateKey(byte[] key) } [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - public static class KeyWrapTests_AesCryptoServiceProvider + public static class KeyWrapPaddedTests_AesCryptoServiceProvider { [Fact] public static void NotValidForAesCsp() @@ -48,7 +48,7 @@ public static void NotValidForAesCsp() } [PlatformSpecific(TestPlatforms.Windows)] - public sealed class KeyWrapTests_AesCng : KeyWrapTests + public sealed class KeyWrapPaddedTests_AesCng : KeyWrapPaddedTests { protected override Aes CreateKey(byte[] key) { @@ -58,7 +58,7 @@ protected override Aes CreateKey(byte[] key) } } - public static class KeyWrapContractTests + public static class KeyWrapPaddedContractTests { [Theory] [InlineData(1, 16)] @@ -598,7 +598,7 @@ protected override int DecryptKeyWrapPaddedCore(ReadOnlySpan source, Span< } } - public abstract class KeyWrapTests + public abstract class KeyWrapPaddedTests { protected abstract Aes CreateKey(byte[] key); diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index dcc43dd74d4bcc..0252e6f172781c 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -290,8 +290,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\DecryptorReusability.cs" /> - + Date: Fri, 14 Aug 2026 14:25:44 -0400 Subject: [PATCH 3/7] Add contract tests --- .../AES/KeyWrapTests.cs | 918 ++++++++++++++++++ .../System.Security.Cryptography.Tests.csproj | 2 + 2 files changed, 920 insertions(+) create mode 100644 src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs new file mode 100644 index 00000000000000..bf02ff8de2e223 --- /dev/null +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs @@ -0,0 +1,918 @@ +// 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 Test.Cryptography; +using Xunit; + +namespace System.Security.Cryptography.Encryption.Aes.Tests +{ + using Aes = System.Security.Cryptography.Aes; + + // [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] + // public sealed class KeyWrapTests_AesCreate_KeyProp : KeyWrapTests + // { + // protected override Aes CreateKey(byte[] key) + // { + // Aes aes = Aes.Create(); + // aes.Key = key; + // return aes; + // } + // } + + // [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] + // public sealed class KeyWrapTests_AesCreate_SetKey : KeyWrapTests + // { + // protected override Aes CreateKey(byte[] key) + // { + // Aes aes = Aes.Create(); + // aes.SetKey(key); + // return aes; + // } + // } + + // [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] + // public static class KeyWrapTests_AesCryptoServiceProvider + // { + // [Fact] + // public static void NotValidForAesCsp() + // { + // byte[] input = new byte[24]; + + // using (Aes aes = new AesCryptoServiceProvider()) + // { + // Assert.Throws(() => aes.EncryptKeyWrap(input)); + // Assert.Throws(() => aes.DecryptKeyWrap(input)); + // } + // } + // } + + // [PlatformSpecific(TestPlatforms.Windows)] + // public sealed class KeyWrapTests_AesCng : KeyWrapTests + // { + // protected override Aes CreateKey(byte[] key) + // { + // Aes aes = new AesCng(); + // aes.Key = key; + // return aes; + // } + // } + + public static class KeyWrapContractTests + { + [Theory] + [InlineData(16, 24)] + [InlineData(0x7FFF_FFF0, 0x7FFF_FFF8)] + public static void VerifyGetLength(int inputLength, int expectedLength) + { + Assert.Equal(expectedLength, Aes.GetKeyWrapLength(inputLength)); + } + + [Fact] + public static void VerifyGetLength_Random() + { + int value = Random.Shared.Next(16, 0x7FFF_FFF1) & ~0b111; + int actual = Aes.GetKeyWrapLength(value); + Assert.Equal(value + 8, actual); + } + + [Fact] + public static void GetLength_TooLarge() + { + int i = int.MaxValue; + + for (; i >= 0x7FFF_FFF1; i--) + { + AssertExtensions.Throws( + "plaintextLengthInBytes", + () => Aes.GetKeyWrapLength(i)); + } + + Assert.Equal(0x7FFF_FFF8, Aes.GetKeyWrapLength(i)); + } + + [Theory] + [InlineData(15)] + [InlineData(8)] + [InlineData(0)] + [InlineData(-1)] + [InlineData(int.MinValue)] + public static void GetLengthTooSmall(int len) + { + AssertExtensions.Throws( + "plaintextLengthInBytes", + () => Aes.GetKeyWrapLength(len)); + } + + [Theory] + [InlineData(17)] + [InlineData(19)] + [InlineData(25)] + public static void GetLengthNotAlignedToMultiple(int len) + { + AssertExtensions.Throws( + "plaintextLengthInBytes", + () => Aes.GetKeyWrapLength(len)); + } + + [Fact] + public static void NeverCalledWithEmpty() + { + using (TestAes key = new TestAes()) + { + byte[] output = new byte[24]; + + AssertExtensions.Throws( + "plaintext", + () => key.EncryptKeyWrap(null)); + + AssertExtensions.Throws( + "plaintext", + () => key.EncryptKeyWrap(Array.Empty())); + + AssertExtensions.Throws( + "plaintext", + () => key.EncryptKeyWrap(ReadOnlySpan.Empty)); + + AssertExtensions.Throws( + "plaintext", + () => key.EncryptKeyWrap(ReadOnlySpan.Empty, output)); + + AssertExtensions.Throws( + "ciphertext", + () => key.DecryptKeyWrap(null)); + + AssertExtensions.Throws( + "ciphertext", + () => key.DecryptKeyWrap(ReadOnlySpan.Empty, output)); + + AssertExtensions.Throws( + "ciphertext", + () => key.DecryptKeyWrap(ReadOnlySpan.Empty)); + + AssertExtensions.Throws( + "ciphertext", + () => key.DecryptKeyWrap(ReadOnlySpan.Empty, output)); + + AssertExtensions.Throws( + "ciphertext", + () => key.TryDecryptKeyWrap(ReadOnlySpan.Empty, output, out _)); + + Assert.Equal(0, key.DecryptKeyWrapCallCount); + } + } + + [Fact] + public static void DecryptNeverCalledWithPartialBlocks() + { + byte[] input = new byte[129]; + byte[] buffer = new byte[input.Length]; + + using (TestAes key = new TestAes()) + { + key.DecryptOverride = (source, destination) => source.Length - 8; + + Assert.ThrowsAny(() => key.DecryptKeyWrap(Array.Empty())); + Assert.Equal(0, key.DecryptKeyWrapCallCount); + + int expectedCallCount = 0; + const int MinCiphertextLength = 24; + + for (int i = input.Length; i >= 0; i--) + { + if (i % 8 == 0 && i >= MinCiphertextLength) + { + // Assert.NoThrow + key.DecryptKeyWrap(new ReadOnlySpan(input, 0, i)); + expectedCallCount++; + } + else + { + AssertExtensions.Throws( + "ciphertext", + () => key.DecryptKeyWrap(new ReadOnlySpan(input, 0, i))); + + AssertExtensions.Throws( + "ciphertext", + () => key.TryDecryptKeyWrap(new ReadOnlySpan(input, 0, i), buffer, out _)); + } + + Assert.Equal(expectedCallCount, key.DecryptKeyWrapCallCount); + } + } + } + + [Fact] + public static void EncryptNeverCalledWithPartialBlocks() + { + byte[] input = new byte[32]; + byte[] buffer = new byte[input.Length + 8]; + + using (TestAes key = new TestAes()) + { + key.EncryptOverride = (source, destination) => {}; + + + Assert.Throws(() => key.EncryptKeyWrap(new byte[15])); + Assert.Throws(() => key.EncryptKeyWrap(new ReadOnlySpan(new byte[15]))); + Assert.Throws(() => key.EncryptKeyWrap(new ReadOnlySpan(new byte[15]), buffer)); + Assert.Equal(0, key.EncryptKeyWrapCallCount); + + int expectedCallCount = 0; + const int MinPlaintextLength = 16; + + for (int i = input.Length; i >= 0; i--) + { + if (i % 8 == 0 && i >= MinPlaintextLength) + { + // Assert.NoThrow + key.EncryptKeyWrap(new ReadOnlySpan(input, 0, i)); + expectedCallCount++; + } + else + { + AssertExtensions.Throws( + "plaintext", + () => key.EncryptKeyWrap(new ReadOnlySpan(input, 0, i))); + + AssertExtensions.Throws( + "plaintext", + () => key.EncryptKeyWrap(new ReadOnlySpan(input, 0, i), buffer)); + } + + Assert.Equal(expectedCallCount, key.EncryptKeyWrapCallCount); + } + } + } + + [Fact] + public static void DecryptMustReportCorrectLength() + { + using (TestAes key = new TestAes()) + { + byte[] input = new byte[32]; + byte[] output = new byte[24]; + int expectedCallCount = 0; + + foreach (int reportedLength in new[] { 23, 25 }) + { + key.DecryptOverride = + (source, destination) => + { + destination.Fill(0xDD); + return reportedLength; + }; + + Assert.Throws(() => key.DecryptKeyWrap(input)); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + + Assert.Throws( + () => key.DecryptKeyWrap(new ReadOnlySpan(input))); + + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + + output.AsSpan().Fill(0xFF); + Assert.Throws(() => key.DecryptKeyWrap(input, output)); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(output.IndexOfAnyExcept((byte)0) == -1); + + output.AsSpan().Fill(0xFF); + Assert.Throws(() => key.TryDecryptKeyWrap(input, output, out _)); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(output.IndexOfAnyExcept((byte)0) == -1); + } + + key.DecryptOverride = (source, destination) => source.Length - 8; + byte[] ret = key.DecryptKeyWrap(input); + Assert.Equal(24, ret.Length); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + } + } + + [Fact] + public static void DecryptClearsDestinationWhenVirtualThrows() + { + byte[] input = new byte[24]; + byte[] output = new byte[24]; + const int OutputLength = 16; + const byte PreFill = 0xB5; + + using (TestAes key = new TestAes()) + { + key.DecryptOverride = + (source, destination) => + { + destination.Fill(0xDD); + throw new CryptographicException(); + }; + + Array.Fill(output, PreFill); + Assert.Throws(() => key.DecryptKeyWrap(input, output)); + Assert.Equal(1, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(output.AsSpan(0, OutputLength).IndexOfAnyExcept((byte)0) == -1); + AssertExtensions.TrueExpression(output.AsSpan(OutputLength).IndexOfAnyExcept(PreFill) == -1); + + Array.Fill(output, PreFill); + Assert.Throws(() => key.TryDecryptKeyWrap(input, output, out _)); + Assert.Equal(2, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(output.AsSpan(0, OutputLength).IndexOfAnyExcept((byte)0) == -1); + AssertExtensions.TrueExpression(output.AsSpan(OutputLength).IndexOfAnyExcept(PreFill) == -1); + } + } + + [Fact] + public static void EncryptAlwaysSeesSource() + { + using (TestAes key = new TestAes()) + { + byte[] input = new byte[64]; + int callLen = input.Length; + + key.EncryptOverride = + (source, destination) => + { + AssertExtensions.TrueExpression(source.Overlaps(input)); + }; + + for (; callLen >= 16; callLen -= 8) + { + key.EncryptKeyWrap(input.AsSpan(0, callLen)); + } + } + } + + [Fact] + public static void EncryptNeverSeesInexactDestination() + { + using (TestAes key = new TestAes()) + { + byte[] input = new byte[16]; + byte[] output = new byte[32]; + int expectedCallCount = 0; + + key.EncryptOverride = + (source, destination) => + { + Assert.Equal(Aes.GetKeyWrapLength(source.Length), destination.Length); + AssertExtensions.TrueExpression(destination.Overlaps(output, out int offset)); + Assert.Equal(0, offset); + }; + + int correctLength = Aes.GetKeyWrapLength(input.Length); + + for (int i = 0; i <= output.Length; i++) + { + if (i == correctLength) + { + // Assert.NoThrow + key.EncryptKeyWrap(input, output.AsSpan(0, i)); + Assert.Equal(++expectedCallCount, key.EncryptKeyWrapCallCount); + } + else + { + AssertExtensions.Throws( + "destination", + () => key.EncryptKeyWrap(input, output.AsSpan(0, i))); + Assert.Equal(expectedCallCount, key.EncryptKeyWrapCallCount); + } + } + + Assert.Equal(1, key.EncryptKeyWrapCallCount); + } + } + + [Fact] + public static void DecryptNeverSeesSmallDestination() + { + using (TestAes key = new TestAes()) + { + byte[] input = new byte[64]; + byte[] output = new byte[64]; + int callLen = output.Length; + int expectedCallCount = 0; + + key.DecryptOverride = + (source, destination) => + { + // Since decrypt is unpadded we never need to rent a destination buffer, so we should always + // be decrypting into the caller supplied buffer. + int outputLength = source.Length - 8; + Assert.Equal(outputLength, destination.Length); + AssertExtensions.TrueExpression(destination.Overlaps(output, out int offset)); + Assert.Equal(0, offset); + return source.Length - 8; + }; + + for (; callLen >= input.Length - 8; callLen--) + { + key.DecryptKeyWrap(input, output.AsSpan(0, callLen)); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + + AssertExtensions.TrueExpression(key.TryDecryptKeyWrap(input, output.AsSpan(0, callLen), out _)); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + } + + // Now that callLen is too short, we should get an ArgumentException with no increase in call count. + AssertExtensions.Throws( + "destination", + () => key.DecryptKeyWrap(input, output.AsSpan(0, callLen))); + + Assert.Equal(expectedCallCount, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(expectedCallCount > 0); + + // TryDecrypt doesn't throw, but also doesn't call the virtual + AssertExtensions.FalseExpression(key.TryDecryptKeyWrap(input, output.AsSpan(0, callLen), out _)); + Assert.Equal(expectedCallCount, key.DecryptKeyWrapCallCount); + } + } + + [Fact] + public static void DecryptCallsVirtualWhenDestinationIsBigEnough() + { + using (TestAes key = new TestAes()) + { + byte[] input = new byte[24]; + byte[] output = new byte[32]; + int retLen = input.Length - 8; + int expectedCallCount = 0; + + const byte CallFill = 0xDD; + const byte PreFill = 0xB5; + + key.DecryptOverride = + (source, destination) => + { + destination.Fill(CallFill); + return destination.Length; + }; + + for (int outputLen = output.Length; outputLen >= 0; outputLen--) + { + int outputOffset = (output.Length - outputLen + 1) / 2; + int trimmedLen = int.Min(retLen, outputLen); + Span destination = output.AsSpan(outputOffset, outputLen); + + ReadOnlySpan preDest = output.AsSpan(0, outputOffset); + ReadOnlySpan postDest = output.AsSpan(outputOffset + trimmedLen); + + if (outputLen >= retLen) + { + Array.Fill(output, PreFill); + int ret = key.DecryptKeyWrap(input, destination); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + + ReadOnlySpan answer = destination.Slice(0, retLen); + + AssertExtensions.TrueExpression(answer.IndexOfAnyExcept(CallFill) == -1); + AssertExtensions.TrueExpression(preDest.IndexOfAnyExcept(PreFill) == -1); + AssertExtensions.TrueExpression(postDest.IndexOfAnyExcept(PreFill) == -1); + + Array.Fill(output, PreFill); + AssertExtensions.TrueExpression(key.TryDecryptKeyWrap(input, destination, out ret)); + Assert.Equal(++expectedCallCount, key.DecryptKeyWrapCallCount); + + AssertExtensions.TrueExpression(answer.IndexOfAnyExcept(CallFill) == -1); + AssertExtensions.TrueExpression(preDest.IndexOfAnyExcept(PreFill) == -1); + AssertExtensions.TrueExpression(postDest.IndexOfAnyExcept(PreFill) == -1); + } + else + { + Array.Fill(output, PreFill); + + AssertExtensions.Throws( + "destination", + () => key.DecryptKeyWrap(input, output.AsSpan(outputOffset, outputLen))); + + Assert.Equal(expectedCallCount, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(output.IndexOfAnyExcept(PreFill) == -1); + + Array.Fill(output, PreFill); + AssertExtensions.FalseExpression(key.TryDecryptKeyWrap(input, destination, out int ret)); + Assert.Equal(expectedCallCount, key.DecryptKeyWrapCallCount); + AssertExtensions.TrueExpression(output.IndexOfAnyExcept(PreFill) == -1); + Assert.Equal(0, ret); + } + } + } + } + + [Fact] + public static void NoOverlapForEncrypt() + { + byte[] buffer = new byte[40]; + + using (TestAes key = new TestAes()) + { + AssertExtensions.Throws( + () => key.EncryptKeyWrap(buffer.AsSpan(24, 16), buffer.AsSpan(1, 24))); + + Assert.Equal(0, key.EncryptKeyWrapCallCount); + + key.EncryptOverride = (source, destination) => { }; + + // Adjacent is OK + key.EncryptKeyWrap(buffer.AsSpan(24, 16), buffer.AsSpan(0, 24)); + Assert.Equal(1, key.EncryptKeyWrapCallCount); + } + } + + [Fact] + public static void NoOverlapForDecrypt() + { + byte[] buffer = new byte[40]; + + using (TestAes key = new TestAes()) + { + AssertExtensions.Throws( + () => key.DecryptKeyWrap(buffer.AsSpan(0, 24), buffer.AsSpan(23, 16))); + + AssertExtensions.Throws( + () => key.TryDecryptKeyWrap(buffer.AsSpan(0, 24), buffer.AsSpan(23, 16), out _)); + + Assert.Equal(0, key.DecryptKeyWrapCallCount); + + key.DecryptOverride = (source, destination) => source.Length - 8; + + // Adjacent is OK + key.DecryptKeyWrap(buffer.AsSpan(0, 24), buffer.AsSpan(24, 16)); + Assert.Equal(1, key.DecryptKeyWrapCallCount); + + AssertExtensions.TrueExpression(key.TryDecryptKeyWrap(buffer.AsSpan(0, 24), buffer.AsSpan(24, 16), out _)); + Assert.Equal(2, key.DecryptKeyWrapCallCount); + } + } + + private class TestAes : Aes + { + public delegate void EncryptCallback(ReadOnlySpan source, Span destination); + public delegate int DecryptCallback(ReadOnlySpan source, Span destination); + + public int EncryptKeyWrapCallCount { get; private set; } + public int DecryptKeyWrapCallCount { get; private set; } + public EncryptCallback EncryptOverride { get; set; } + public DecryptCallback DecryptOverride { get; set; } + + public override void GenerateIV() + { + } + + public override void GenerateKey() + { + } + + public override ICryptoTransform CreateDecryptor(byte[] rgbKey, byte[]? rgbIV) + { + Assert.Fail("CreateDecryptor should never be called"); + return null; + } + + public override ICryptoTransform CreateEncryptor(byte[] rgbKey, byte[]? rgbIV) + { + Assert.Fail("CreateEncryptor should never be called"); + return null; + } + + protected override void EncryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + EncryptKeyWrapCallCount++; + + if (EncryptOverride is not null) + { + EncryptOverride(source, destination); + } + else + { + Assert.Fail("Unexpected call to EncryptKeyWrapCore"); + } + } + + protected override int DecryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + DecryptKeyWrapCallCount++; + + if (DecryptOverride is not null) + { + return DecryptOverride(source, destination); + } + + Assert.Fail("Unexpected call to EncryptKeyWrapCore"); + return -1; + } + } + } + + // public abstract class KeyWrapPaddedTests + // { + // protected abstract Aes CreateKey(byte[] key); + + // [Theory] + // [MemberData(nameof(KnownAnswerTests))] + // public void VerifyKnownAnswer(KnownAnswerTest kat) + // { + // using (Aes key = CreateKey(kat.Key)) + // { + // VerifyWrap(key, kat.Plaintext, kat.Ciphertext); + // VerifyUnwrap(key, kat.Ciphertext, kat.Plaintext); + // } + // } + + // [Theory] + // [InlineData(128, 1, 16)] + // [InlineData(128, 96, 103)] + // [InlineData(192, 1, 16)] + // [InlineData(192, 96, 103)] + // [InlineData(256, 1, 16)] + // [InlineData(256, 96, 103)] + // public void VerifyRoundtrip(int kekSize, int ptMin, int ptMax) + // { + // byte[] kek = new byte[kekSize / 8]; + // RandomNumberGenerator.Fill(kek); + + // using (Aes key = CreateKey(kek)) + // { + // for (int i = ptMin; i <= ptMax; i++) + // { + // // Round plaintext up to the nearest multiple of 8, + // // and add the 8 bytes for the IV semi-block. + // int expectedSize = (i + 7) / 8 * 8 + 8; + + // byte[] plaintext = new byte[i]; + // RandomNumberGenerator.Fill(plaintext); + // byte[] ciphertext = key.EncryptKeyWrapPadded(plaintext); + // Assert.Equal(expectedSize, ciphertext.Length); + + // VerifyUnwrap(key, ciphertext, plaintext); + // VerifyWrap(key, plaintext, ciphertext); + // } + // } + // } + + // [Fact] + // public void UnwrapBadIV_SingleBlock() + // { + // // At the end of unwrap, the header block will have the incorrect value A65959A7. + // byte[] kek = "9FC9E4BA68CA3EC8BAC82B02223EADDAAA1A67350E12510D0016083095B32BBC".HexToByteArray(); + // byte[] ciphertext = "1B2DE25B6990AA8B74087499294ECB39".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapBadIV_MultiBlock() + // { + // // At the end of unwrap, the header block will have the incorrect value A65959A7. + // byte[] kek = "B1D18A0296DF025443EE1677ED783FB6C137A98814E09FE1".HexToByteArray(); + // byte[] ciphertext = ( + // "67A3F00F801" + + // "A1CDAFF2D324C7AC393EB97938556FA8D54C5DB303F9EBB6321B84BCED6DD3A80EC98B3047110" + + // "89A8EF9ADADA14A3ADD324E55BEFB6A5598ABB90A40CA8F36CB175498FAA3BDC11FDAC1113042" + + // "E3229B790FA4BD0240830933FA9D0C8255CD271D5B7C301DDF85F098C62").HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapLengthTooBig_Single() + // { + // // At the end of unwrap, the length segment will report 8 bytes more than the original input was, + // // which requires reading beyond the end of the processed buffer. + // byte[] kek = "3D7C64D35E1CEC5BBEA04867073F5E9F6DB671B28EA325215FA6DA3B1B561F48".HexToByteArray(); + // byte[] ciphertext = "9EDBCAFD999E7A1CEEC4529DC192797E".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapLengthTooBig_MultiBlock() + // { + // // At the end of unwrap, the length segment will report 8 bytes more than the original input was, + // // which requires reading beyond the end of the processed buffer. + // byte[] kek = "3D7C64D35E1CEC5BBEA04867073F5E9F6DB671B28EA325215FA6DA3B1B561F48".HexToByteArray(); + // byte[] ciphertext = "1A91401A927296BEF253F857C6124B20A2FEFB580FF472F5".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapLengthZero_Single() + // { + // // At the end of unwrap, the length segment will report zero, which is always invalid. + // byte[] kek = "B870467A475D675AEE893430A09FD77F".HexToByteArray(); + // byte[] ciphertext = "024D1848259597D20FFDCE39BC3E461D".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapLengthZero_MultiBlock() + // { + // // At the end of unwrap, the length segment will report zero, which is always invalid. + // byte[] kek = "DA536B4D274173D0DAD5DBB8FF21F6E27AC8BB6F9E12F51A".HexToByteArray(); + // byte[] ciphertext = "29288FFE637F4CCBF3D44FEAB22300C67796C14AAFB682E3".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapBadPadding_Single() + // { + // // At the end of unwrap, some of the "padding" bytes will be non-zero. + // byte[] kek = "838E662F79DC11058ED1EC27928DE835119BAC751B689A1DFC09011BD634842E".HexToByteArray(); + // byte[] ciphertext = "E9D57F431E9A9A2878E6629A890E4C3E".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapBadPadding_MultiBlock() + // { + // // At the end of unwrap, some of the "padding" bytes will be non-zero. + // byte[] kek = "6BA88BFEA55ECE448898BFEE524244B965C5EB3CADA463E0".HexToByteArray(); + // byte[] ciphertext = "852CA39B8A1DE2FD2EF10DA6F01AF860F1DF6E16F0593E85".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // [Fact] + // public void UnwrapLengthTooShort() + // { + // // At the end of unwrap, this length will report 8 less than the original input was, + // // which means the ciphertext should have had one block fewer than it does. + // // + // // Subtracting 8 from the length of a single-block ciphertext is either zero (already covered), + // // or "extremely large" (already covered), so there is not a single-block variant of this. + // byte[] kek = "7F0B9A269A182935200F4D92FFE291F94D132D9FBCB8982F".HexToByteArray(); + // byte[] ciphertext = "9C21E8325D1D7406DE94B2009D3E67152EE6C7DBC0E5B911".HexToByteArray(); + + // VerifyUnwrapFails(kek, ciphertext); + // } + + // private static void VerifyWrap(Aes key, byte[] plaintext, byte[] ciphertext) + // { + // // EncryptKeyWrapPadded(byte[]) + // byte[] wrapped = key.EncryptKeyWrapPadded(plaintext); + // AssertExtensions.SequenceEqual(ciphertext, wrapped); + + // // EncryptKeyWrapPadded(ReadOnlySpan) + // wrapped = key.EncryptKeyWrapPadded(new ReadOnlySpan(plaintext)); + // AssertExtensions.SequenceEqual(ciphertext, wrapped); + + // // void EncryptKeyWrapPadded(ReadOnlySpan, Span) + // Array.Clear(wrapped); + // key.EncryptKeyWrapPadded(plaintext, wrapped.AsSpan()); + // AssertExtensions.SequenceEqual(ciphertext, wrapped); + // } + + // private static void VerifyUnwrap(Aes key, byte[] ciphertext, byte[] plaintext) + // { + // // DecryptKeyWrapPadded(byte[]) + // byte[] unwrapped = key.DecryptKeyWrapPadded(ciphertext); + // AssertExtensions.SequenceEqual(plaintext, unwrapped); + + // // DecryptKeyWrapPadded(ReadOnlySpan) + // unwrapped = key.DecryptKeyWrapPadded(new ReadOnlySpan(ciphertext)); + // AssertExtensions.SequenceEqual(plaintext, unwrapped); + + // byte[] tooBig = new byte[ciphertext.Length]; + // tooBig.AsSpan().Fill(0xFF); + // int maxOutput = ciphertext.Length - 8; + // int minOutput = maxOutput - 7; + // int expectedPadding = maxOutput - plaintext.Length; + // ReadOnlySpan paddingSlice = tooBig.AsSpan(plaintext.Length, expectedPadding); + // ReadOnlySpan untouchedSlice = tooBig.AsSpan(maxOutput); + + // // The tooBig buffer will be composed of [the right answer][padding][bytes that are not touched] + // // Our slice point in this loop is always within [bytes that are not touched], so we expect all + // // padding bytes to be written (as 0) and all untouched bytes to remain 0xFF. + // for (int i = tooBig.Length; i >= maxOutput; i--) + // { + // Span targetSlice = tooBig.AsSpan(0, i); + // int written = key.DecryptKeyWrapPadded(ciphertext, targetSlice); + // ReadOnlySpan answerSlice = targetSlice.Slice(0, written); + // // SequenceEqual will also check that `written` is correct + // AssertExtensions.SequenceEqual(plaintext, answerSlice); + + // // Since `written` is correct, paddingSlice and untouchedSlice are sliced correctly. + // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); + // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); + + // // Repeat with TryDecryptKeyWrapPadded + // tooBig.AsSpan().Fill(0xFF); + // AssertExtensions.TrueExpression(key.TryDecryptKeyWrapPadded(ciphertext, targetSlice, out written)); + // answerSlice = targetSlice.Slice(0, written); + // // SequenceEqual will also check that `written` is correct + // AssertExtensions.SequenceEqual(plaintext, answerSlice); + + // // Since `written` is correct, paddingSlice and untouchedSlice are sliced correctly. + // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); + // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); + // } + + // // In this loop, the input buffer is plausibly big enough, but not guaranteed big enough, + // // so the implementation is going to use rented space to compute the unwrap. + // // + // // Any surplus bytes should be set to 0 (as the padding), and we can still assert that + // // the untouched range is 0xFF, but this loop never even sees it. + // for (int i = maxOutput - 1; i >= plaintext.Length; i--) + // { + // Span targetSlice = tooBig.AsSpan(0, i); + // untouchedSlice = targetSlice.Slice(i); + + // int written = key.DecryptKeyWrapPadded(ciphertext, targetSlice); + // ReadOnlySpan answerSlice = targetSlice.Slice(0, written); + // // SequenceEqual will also check that `written` is correct + // AssertExtensions.SequenceEqual(plaintext, answerSlice); + + // paddingSlice = targetSlice.Slice(written); + // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); + // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); + + // // Repeat with TryDecryptKeyWrapPadded + // tooBig.AsSpan().Fill(0xFF); + // AssertExtensions.TrueExpression(key.TryDecryptKeyWrapPadded(ciphertext, targetSlice, out written)); + // answerSlice = targetSlice.Slice(0, written); + // // SequenceEqual will also check that `written` is correct + // AssertExtensions.SequenceEqual(plaintext, answerSlice); + + // // Since `written` is correct, paddingSlice and untouchedSlice are still sliced correctly. + // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); + // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); + // } + + // tooBig.AsSpan().Fill(0xFF); + + // // targetSlice is now too small to hold the plaintext, but that can only be determined after + // // running the algorithm. + // // In this case, we should never touch the destination buffer. + // for (int i = plaintext.Length - 1; i >= minOutput; i--) + // { + // AssertExtensions.Throws( + // "destination", + // () => key.DecryptKeyWrapPadded(ciphertext, tooBig.AsSpan(0, i))); + + // AssertExtensions.TrueExpression(tooBig.IndexOfAnyExcept((byte)0xFF) == -1); + + // AssertExtensions.FalseExpression( + // key.TryDecryptKeyWrapPadded(ciphertext, tooBig.AsSpan(0, i), out int written)); + // Assert.Equal(0, written); + + // AssertExtensions.TrueExpression(tooBig.IndexOfAnyExcept((byte)0xFF) == -1); + // } + // } + + // private void VerifyUnwrapFails(byte[] kek, byte[] ciphertext) + // { + // using (Aes key = CreateKey(kek)) + // { + // byte[] dest = new byte[ciphertext.Length]; + + // Assert.ThrowsAny(() => key.DecryptKeyWrapPadded(ciphertext)); + // Assert.ThrowsAny(() => key.DecryptKeyWrapPadded(new ReadOnlySpan(ciphertext))); + // Assert.ThrowsAny(() => key.DecryptKeyWrapPadded(ciphertext, dest)); + // Assert.ThrowsAny(() => key.TryDecryptKeyWrapPadded(ciphertext, dest, out _)); + // } + // } + + // public static IEnumerable KnownAnswerTests { get; } = + // [ + // new object[] + // { + // new KnownAnswerTest( + // "RFC 5649 Example 1", + // "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8".HexToByteArray(), + // "c37b7e6492584340bed12207808941155068f738".HexToByteArray(), + // "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a".HexToByteArray()) + // }, + + // new object[] + // { + // new KnownAnswerTest( + // "RFC 5649 Example 2", + // "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8".HexToByteArray(), + // "466f7250617369".HexToByteArray(), + // "afbeb0f07dfbf5419200f2ccb50bb24f".HexToByteArray()) + // }, + // ]; + + // public struct KnownAnswerTest + // { + // public string Name { get; } + // public byte[] Key { get; } + // public byte[] Plaintext { get; } + // public byte[] Ciphertext { get; } + + // public KnownAnswerTest(string name, byte[] key, byte[] plaintext, byte[] ciphertext) + // { + // Name = name; + // Key = key; + // Plaintext = plaintext; + // Ciphertext = ciphertext; + // } + + // public override string ToString() + // { + // return Name; + // } + // } + // } +} diff --git a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj index 0252e6f172781c..0111b5943403bc 100644 --- a/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj +++ b/src/libraries/System.Security.Cryptography/tests/System.Security.Cryptography.Tests.csproj @@ -292,6 +292,8 @@ Link="CommonTest\System\Security\Cryptography\AlgorithmImplementations\AES\AesFactory.cs" /> + Date: Fri, 14 Aug 2026 17:14:31 -0400 Subject: [PATCH 4/7] Implement managed AES-KW --- .../AES/KeyWrapTests.cs | 620 ++++++++---------- .../src/System/Security/Cryptography/Aes.cs | 67 +- 2 files changed, 322 insertions(+), 365 deletions(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs index bf02ff8de2e223..dbde0417de55a6 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs @@ -9,54 +9,61 @@ namespace System.Security.Cryptography.Encryption.Aes.Tests { using Aes = System.Security.Cryptography.Aes; - // [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - // public sealed class KeyWrapTests_AesCreate_KeyProp : KeyWrapTests - // { - // protected override Aes CreateKey(byte[] key) - // { - // Aes aes = Aes.Create(); - // aes.Key = key; - // return aes; - // } - // } - - // [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - // public sealed class KeyWrapTests_AesCreate_SetKey : KeyWrapTests - // { - // protected override Aes CreateKey(byte[] key) - // { - // Aes aes = Aes.Create(); - // aes.SetKey(key); - // return aes; - // } - // } - - // [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] - // public static class KeyWrapTests_AesCryptoServiceProvider - // { - // [Fact] - // public static void NotValidForAesCsp() - // { - // byte[] input = new byte[24]; - - // using (Aes aes = new AesCryptoServiceProvider()) - // { - // Assert.Throws(() => aes.EncryptKeyWrap(input)); - // Assert.Throws(() => aes.DecryptKeyWrap(input)); - // } - // } - // } - - // [PlatformSpecific(TestPlatforms.Windows)] - // public sealed class KeyWrapTests_AesCng : KeyWrapTests - // { - // protected override Aes CreateKey(byte[] key) - // { - // Aes aes = new AesCng(); - // aes.Key = key; - // return aes; - // } - // } + [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] + public sealed class KeyWrapTests_AesCreate_KeyProp : KeyWrapTests + { + protected override Aes CreateKey(byte[] key) + { + Aes aes = Aes.Create(); + aes.Key = key; + return aes; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] + public sealed class KeyWrapTests_AesCreate_SetKey : KeyWrapTests + { + protected override Aes CreateKey(byte[] key) + { + Aes aes = Aes.Create(); + aes.SetKey(key); + return aes; + } + } + + [SkipOnPlatform(TestPlatforms.Browser, "Not supported on Browser")] + public static class KeyWrapTests_AesCryptoServiceProvider + { + [Fact] + public static void NotValidForAesCsp() + { + byte[] plaintext = new byte[16]; + byte[] ciphertext = new byte[24]; + + using (Aes aes = new AesCryptoServiceProvider()) + { + Assert.Throws(() => aes.EncryptKeyWrap(plaintext)); + Assert.Throws(() => aes.EncryptKeyWrap(new ReadOnlySpan(plaintext))); + Assert.Throws(() => aes.EncryptKeyWrap(plaintext, ciphertext)); + + Assert.Throws(() => aes.DecryptKeyWrap(ciphertext)); + Assert.Throws(() => aes.DecryptKeyWrap(new ReadOnlySpan(ciphertext))); + Assert.Throws(() => aes.DecryptKeyWrap(ciphertext, plaintext)); + Assert.Throws(() => aes.TryDecryptKeyWrap(ciphertext, plaintext, out _)); + } + } + } + + [PlatformSpecific(TestPlatforms.Windows)] + public sealed class KeyWrapTests_AesCng : KeyWrapTests + { + protected override Aes CreateKey(byte[] key) + { + Aes aes = new AesCng(); + aes.Key = key; + return aes; + } + } public static class KeyWrapContractTests { @@ -601,318 +608,207 @@ protected override int DecryptKeyWrapCore(ReadOnlySpan source, Span } } - // public abstract class KeyWrapPaddedTests - // { - // protected abstract Aes CreateKey(byte[] key); - - // [Theory] - // [MemberData(nameof(KnownAnswerTests))] - // public void VerifyKnownAnswer(KnownAnswerTest kat) - // { - // using (Aes key = CreateKey(kat.Key)) - // { - // VerifyWrap(key, kat.Plaintext, kat.Ciphertext); - // VerifyUnwrap(key, kat.Ciphertext, kat.Plaintext); - // } - // } - - // [Theory] - // [InlineData(128, 1, 16)] - // [InlineData(128, 96, 103)] - // [InlineData(192, 1, 16)] - // [InlineData(192, 96, 103)] - // [InlineData(256, 1, 16)] - // [InlineData(256, 96, 103)] - // public void VerifyRoundtrip(int kekSize, int ptMin, int ptMax) - // { - // byte[] kek = new byte[kekSize / 8]; - // RandomNumberGenerator.Fill(kek); - - // using (Aes key = CreateKey(kek)) - // { - // for (int i = ptMin; i <= ptMax; i++) - // { - // // Round plaintext up to the nearest multiple of 8, - // // and add the 8 bytes for the IV semi-block. - // int expectedSize = (i + 7) / 8 * 8 + 8; - - // byte[] plaintext = new byte[i]; - // RandomNumberGenerator.Fill(plaintext); - // byte[] ciphertext = key.EncryptKeyWrapPadded(plaintext); - // Assert.Equal(expectedSize, ciphertext.Length); - - // VerifyUnwrap(key, ciphertext, plaintext); - // VerifyWrap(key, plaintext, ciphertext); - // } - // } - // } - - // [Fact] - // public void UnwrapBadIV_SingleBlock() - // { - // // At the end of unwrap, the header block will have the incorrect value A65959A7. - // byte[] kek = "9FC9E4BA68CA3EC8BAC82B02223EADDAAA1A67350E12510D0016083095B32BBC".HexToByteArray(); - // byte[] ciphertext = "1B2DE25B6990AA8B74087499294ECB39".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapBadIV_MultiBlock() - // { - // // At the end of unwrap, the header block will have the incorrect value A65959A7. - // byte[] kek = "B1D18A0296DF025443EE1677ED783FB6C137A98814E09FE1".HexToByteArray(); - // byte[] ciphertext = ( - // "67A3F00F801" + - // "A1CDAFF2D324C7AC393EB97938556FA8D54C5DB303F9EBB6321B84BCED6DD3A80EC98B3047110" + - // "89A8EF9ADADA14A3ADD324E55BEFB6A5598ABB90A40CA8F36CB175498FAA3BDC11FDAC1113042" + - // "E3229B790FA4BD0240830933FA9D0C8255CD271D5B7C301DDF85F098C62").HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapLengthTooBig_Single() - // { - // // At the end of unwrap, the length segment will report 8 bytes more than the original input was, - // // which requires reading beyond the end of the processed buffer. - // byte[] kek = "3D7C64D35E1CEC5BBEA04867073F5E9F6DB671B28EA325215FA6DA3B1B561F48".HexToByteArray(); - // byte[] ciphertext = "9EDBCAFD999E7A1CEEC4529DC192797E".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapLengthTooBig_MultiBlock() - // { - // // At the end of unwrap, the length segment will report 8 bytes more than the original input was, - // // which requires reading beyond the end of the processed buffer. - // byte[] kek = "3D7C64D35E1CEC5BBEA04867073F5E9F6DB671B28EA325215FA6DA3B1B561F48".HexToByteArray(); - // byte[] ciphertext = "1A91401A927296BEF253F857C6124B20A2FEFB580FF472F5".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapLengthZero_Single() - // { - // // At the end of unwrap, the length segment will report zero, which is always invalid. - // byte[] kek = "B870467A475D675AEE893430A09FD77F".HexToByteArray(); - // byte[] ciphertext = "024D1848259597D20FFDCE39BC3E461D".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapLengthZero_MultiBlock() - // { - // // At the end of unwrap, the length segment will report zero, which is always invalid. - // byte[] kek = "DA536B4D274173D0DAD5DBB8FF21F6E27AC8BB6F9E12F51A".HexToByteArray(); - // byte[] ciphertext = "29288FFE637F4CCBF3D44FEAB22300C67796C14AAFB682E3".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapBadPadding_Single() - // { - // // At the end of unwrap, some of the "padding" bytes will be non-zero. - // byte[] kek = "838E662F79DC11058ED1EC27928DE835119BAC751B689A1DFC09011BD634842E".HexToByteArray(); - // byte[] ciphertext = "E9D57F431E9A9A2878E6629A890E4C3E".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapBadPadding_MultiBlock() - // { - // // At the end of unwrap, some of the "padding" bytes will be non-zero. - // byte[] kek = "6BA88BFEA55ECE448898BFEE524244B965C5EB3CADA463E0".HexToByteArray(); - // byte[] ciphertext = "852CA39B8A1DE2FD2EF10DA6F01AF860F1DF6E16F0593E85".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // [Fact] - // public void UnwrapLengthTooShort() - // { - // // At the end of unwrap, this length will report 8 less than the original input was, - // // which means the ciphertext should have had one block fewer than it does. - // // - // // Subtracting 8 from the length of a single-block ciphertext is either zero (already covered), - // // or "extremely large" (already covered), so there is not a single-block variant of this. - // byte[] kek = "7F0B9A269A182935200F4D92FFE291F94D132D9FBCB8982F".HexToByteArray(); - // byte[] ciphertext = "9C21E8325D1D7406DE94B2009D3E67152EE6C7DBC0E5B911".HexToByteArray(); - - // VerifyUnwrapFails(kek, ciphertext); - // } - - // private static void VerifyWrap(Aes key, byte[] plaintext, byte[] ciphertext) - // { - // // EncryptKeyWrapPadded(byte[]) - // byte[] wrapped = key.EncryptKeyWrapPadded(plaintext); - // AssertExtensions.SequenceEqual(ciphertext, wrapped); - - // // EncryptKeyWrapPadded(ReadOnlySpan) - // wrapped = key.EncryptKeyWrapPadded(new ReadOnlySpan(plaintext)); - // AssertExtensions.SequenceEqual(ciphertext, wrapped); - - // // void EncryptKeyWrapPadded(ReadOnlySpan, Span) - // Array.Clear(wrapped); - // key.EncryptKeyWrapPadded(plaintext, wrapped.AsSpan()); - // AssertExtensions.SequenceEqual(ciphertext, wrapped); - // } - - // private static void VerifyUnwrap(Aes key, byte[] ciphertext, byte[] plaintext) - // { - // // DecryptKeyWrapPadded(byte[]) - // byte[] unwrapped = key.DecryptKeyWrapPadded(ciphertext); - // AssertExtensions.SequenceEqual(plaintext, unwrapped); - - // // DecryptKeyWrapPadded(ReadOnlySpan) - // unwrapped = key.DecryptKeyWrapPadded(new ReadOnlySpan(ciphertext)); - // AssertExtensions.SequenceEqual(plaintext, unwrapped); - - // byte[] tooBig = new byte[ciphertext.Length]; - // tooBig.AsSpan().Fill(0xFF); - // int maxOutput = ciphertext.Length - 8; - // int minOutput = maxOutput - 7; - // int expectedPadding = maxOutput - plaintext.Length; - // ReadOnlySpan paddingSlice = tooBig.AsSpan(plaintext.Length, expectedPadding); - // ReadOnlySpan untouchedSlice = tooBig.AsSpan(maxOutput); - - // // The tooBig buffer will be composed of [the right answer][padding][bytes that are not touched] - // // Our slice point in this loop is always within [bytes that are not touched], so we expect all - // // padding bytes to be written (as 0) and all untouched bytes to remain 0xFF. - // for (int i = tooBig.Length; i >= maxOutput; i--) - // { - // Span targetSlice = tooBig.AsSpan(0, i); - // int written = key.DecryptKeyWrapPadded(ciphertext, targetSlice); - // ReadOnlySpan answerSlice = targetSlice.Slice(0, written); - // // SequenceEqual will also check that `written` is correct - // AssertExtensions.SequenceEqual(plaintext, answerSlice); - - // // Since `written` is correct, paddingSlice and untouchedSlice are sliced correctly. - // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); - // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); - - // // Repeat with TryDecryptKeyWrapPadded - // tooBig.AsSpan().Fill(0xFF); - // AssertExtensions.TrueExpression(key.TryDecryptKeyWrapPadded(ciphertext, targetSlice, out written)); - // answerSlice = targetSlice.Slice(0, written); - // // SequenceEqual will also check that `written` is correct - // AssertExtensions.SequenceEqual(plaintext, answerSlice); - - // // Since `written` is correct, paddingSlice and untouchedSlice are sliced correctly. - // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); - // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); - // } - - // // In this loop, the input buffer is plausibly big enough, but not guaranteed big enough, - // // so the implementation is going to use rented space to compute the unwrap. - // // - // // Any surplus bytes should be set to 0 (as the padding), and we can still assert that - // // the untouched range is 0xFF, but this loop never even sees it. - // for (int i = maxOutput - 1; i >= plaintext.Length; i--) - // { - // Span targetSlice = tooBig.AsSpan(0, i); - // untouchedSlice = targetSlice.Slice(i); - - // int written = key.DecryptKeyWrapPadded(ciphertext, targetSlice); - // ReadOnlySpan answerSlice = targetSlice.Slice(0, written); - // // SequenceEqual will also check that `written` is correct - // AssertExtensions.SequenceEqual(plaintext, answerSlice); - - // paddingSlice = targetSlice.Slice(written); - // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); - // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); - - // // Repeat with TryDecryptKeyWrapPadded - // tooBig.AsSpan().Fill(0xFF); - // AssertExtensions.TrueExpression(key.TryDecryptKeyWrapPadded(ciphertext, targetSlice, out written)); - // answerSlice = targetSlice.Slice(0, written); - // // SequenceEqual will also check that `written` is correct - // AssertExtensions.SequenceEqual(plaintext, answerSlice); - - // // Since `written` is correct, paddingSlice and untouchedSlice are still sliced correctly. - // AssertExtensions.TrueExpression(paddingSlice.IndexOfAnyExcept((byte)0) == -1); - // AssertExtensions.TrueExpression(untouchedSlice.IndexOfAnyExcept((byte)0xFF) == -1); - // } - - // tooBig.AsSpan().Fill(0xFF); - - // // targetSlice is now too small to hold the plaintext, but that can only be determined after - // // running the algorithm. - // // In this case, we should never touch the destination buffer. - // for (int i = plaintext.Length - 1; i >= minOutput; i--) - // { - // AssertExtensions.Throws( - // "destination", - // () => key.DecryptKeyWrapPadded(ciphertext, tooBig.AsSpan(0, i))); - - // AssertExtensions.TrueExpression(tooBig.IndexOfAnyExcept((byte)0xFF) == -1); - - // AssertExtensions.FalseExpression( - // key.TryDecryptKeyWrapPadded(ciphertext, tooBig.AsSpan(0, i), out int written)); - // Assert.Equal(0, written); - - // AssertExtensions.TrueExpression(tooBig.IndexOfAnyExcept((byte)0xFF) == -1); - // } - // } - - // private void VerifyUnwrapFails(byte[] kek, byte[] ciphertext) - // { - // using (Aes key = CreateKey(kek)) - // { - // byte[] dest = new byte[ciphertext.Length]; - - // Assert.ThrowsAny(() => key.DecryptKeyWrapPadded(ciphertext)); - // Assert.ThrowsAny(() => key.DecryptKeyWrapPadded(new ReadOnlySpan(ciphertext))); - // Assert.ThrowsAny(() => key.DecryptKeyWrapPadded(ciphertext, dest)); - // Assert.ThrowsAny(() => key.TryDecryptKeyWrapPadded(ciphertext, dest, out _)); - // } - // } - - // public static IEnumerable KnownAnswerTests { get; } = - // [ - // new object[] - // { - // new KnownAnswerTest( - // "RFC 5649 Example 1", - // "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8".HexToByteArray(), - // "c37b7e6492584340bed12207808941155068f738".HexToByteArray(), - // "138bdeaa9b8fa7fc61f97742e72248ee5ae6ae5360d1ae6a5f54f373fa543b6a".HexToByteArray()) - // }, - - // new object[] - // { - // new KnownAnswerTest( - // "RFC 5649 Example 2", - // "5840df6e29b02af1ab493b705bf16ea1ae8338f4dcc176a8".HexToByteArray(), - // "466f7250617369".HexToByteArray(), - // "afbeb0f07dfbf5419200f2ccb50bb24f".HexToByteArray()) - // }, - // ]; - - // public struct KnownAnswerTest - // { - // public string Name { get; } - // public byte[] Key { get; } - // public byte[] Plaintext { get; } - // public byte[] Ciphertext { get; } - - // public KnownAnswerTest(string name, byte[] key, byte[] plaintext, byte[] ciphertext) - // { - // Name = name; - // Key = key; - // Plaintext = plaintext; - // Ciphertext = ciphertext; - // } - - // public override string ToString() - // { - // return Name; - // } - // } - // } + public abstract class KeyWrapTests + { + protected abstract Aes CreateKey(byte[] key); + + [Theory] + [MemberData(nameof(KnownAnswerTests))] + public void VerifyKnownAnswer(KnownAnswerTest kat) + { + using (Aes key = CreateKey(kat.Key)) + { + VerifyWrap(key, kat.Plaintext, kat.Ciphertext); + VerifyUnwrap(key, kat.Ciphertext, kat.Plaintext); + } + } + + [Theory] + [InlineData(128, 16)] + [InlineData(128, 96)] + [InlineData(128, 128)] + [InlineData(192, 16)] + [InlineData(192, 96)] + [InlineData(192, 128)] + [InlineData(256, 16)] + [InlineData(256, 96)] + [InlineData(256, 128)] + public void VerifyRoundtrip(int kekSize, int plaintextSize) + { + byte[] kek = new byte[kekSize / 8]; + RandomNumberGenerator.Fill(kek); + + using (Aes key = CreateKey(kek)) + { + int expectedSize = plaintextSize + 8; + + byte[] plaintext = new byte[plaintextSize]; + RandomNumberGenerator.Fill(plaintext); + byte[] ciphertext = key.EncryptKeyWrap(plaintext); + Assert.Equal(expectedSize, ciphertext.Length); + + VerifyUnwrap(key, ciphertext, plaintext); + VerifyWrap(key, plaintext, ciphertext); + } + } + + [Theory] + [MemberData(nameof(KnownAnswerTests))] + public void RejectsTamperedCiphertext(KnownAnswerTest kat) + { + byte[] tampered = (byte[])kat.Ciphertext.Clone(); + const byte TamperedBit = 1 << 2; + + for (int i = 0; i < tampered.Length; i++) + { + tampered[i] ^= TamperedBit; + VerifyUnwrapFails(kat.Key, tampered); + tampered[i] ^= TamperedBit; // Put the tampered bit back so only one bit is tampered at a time. + } + } + + [Theory] + [InlineData("079E449C7E8504B8D559EDA0387724C78820C1E93F4F9716")] + [InlineData("BAB95D7021F1196EE8BC5146D20167F58362B46EED49CB9E")] + public void RejectsIncorrectInitialValue(string ciphertextHex) + { + // Each of these chosen ciphertexts produces a recovered IV that is off by a single bit, one in the top 32-bit + // half and the other in the lower 32-bit half (A7A6A6A6A6A6A6A6 and A6A6A6A6A7A6A6A6, respectively). + byte[] kek = "000102030405060708090A0B0C0D0E0F".HexToByteArray(); + byte[] ciphertext = ciphertextHex.HexToByteArray(); + + VerifyUnwrapFails(kek, ciphertext); + } + + private static void VerifyWrap(Aes key, byte[] plaintext, byte[] ciphertext) + { + // EncryptKeyWrap(byte[]) + byte[] wrapped = key.EncryptKeyWrap(plaintext); + AssertExtensions.SequenceEqual(ciphertext, wrapped); + + // EncryptKeyWrap(ReadOnlySpan) + wrapped = key.EncryptKeyWrap(new ReadOnlySpan(plaintext)); + AssertExtensions.SequenceEqual(ciphertext, wrapped); + + // void EncryptKeyWrap(ReadOnlySpan, Span) + Array.Clear(wrapped); + key.EncryptKeyWrap(plaintext, wrapped.AsSpan()); + AssertExtensions.SequenceEqual(ciphertext, wrapped); + } + + private static void VerifyUnwrap(Aes key, byte[] ciphertext, byte[] plaintext) + { + // DecryptKeyWrap(byte[]) + byte[] unwrapped = key.DecryptKeyWrap(ciphertext); + AssertExtensions.SequenceEqual(plaintext, unwrapped); + + // DecryptKeyWrap(ReadOnlySpan) + unwrapped = key.DecryptKeyWrap(new ReadOnlySpan(ciphertext)); + AssertExtensions.SequenceEqual(plaintext, unwrapped); + + // DecryptKeyWrap(ReadOnlySpan, Span) + Array.Clear(unwrapped); + int written = key.DecryptKeyWrap(new ReadOnlySpan(ciphertext), unwrapped); + Assert.Equal(unwrapped.Length, written); + AssertExtensions.SequenceEqual(plaintext, unwrapped); + + // TryDecryptKeyWrap(ReadOnlySpan, Span, out int) + Array.Clear(unwrapped); + bool result = key.TryDecryptKeyWrap(new ReadOnlySpan(ciphertext), unwrapped, out written); + AssertExtensions.TrueExpression(result); + Assert.Equal(unwrapped.Length, written); + AssertExtensions.SequenceEqual(plaintext, unwrapped); + } + + private void VerifyUnwrapFails(byte[] kek, byte[] ciphertext) + { + using (Aes key = CreateKey(kek)) + { + byte[] dest = new byte[ciphertext.Length]; + + Assert.ThrowsAny(() => key.DecryptKeyWrap(ciphertext)); + Assert.ThrowsAny(() => key.DecryptKeyWrap(new ReadOnlySpan(ciphertext))); + Assert.ThrowsAny(() => key.DecryptKeyWrap(ciphertext, dest)); + Assert.ThrowsAny(() => key.TryDecryptKeyWrap(ciphertext, dest, out _)); + } + } + + public static IEnumerable KnownAnswerTests { get; } = + [ + new object[] + { + new KnownAnswerTest( + "RFC 3394 4.1", + "000102030405060708090A0B0C0D0E0F".HexToByteArray(), + "00112233445566778899AABBCCDDEEFF".HexToByteArray(), + "1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5".HexToByteArray()) + }, + + new object[] + { + new KnownAnswerTest( + "RFC 3394 4.2", + "000102030405060708090A0B0C0D0E0F1011121314151617".HexToByteArray(), + "00112233445566778899AABBCCDDEEFF".HexToByteArray(), + "96778B25AE6CA435F92B5B97C050AED2468AB8A17AD84E5D".HexToByteArray()) + }, + + new object[] + { + new KnownAnswerTest( + "RFC 3394 4.3", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F".HexToByteArray(), + "00112233445566778899AABBCCDDEEFF".HexToByteArray(), + "64E8C3F9CE0F5BA263E9777905818A2A93C8191E7D6E8AE7".HexToByteArray()) + }, + + new object[] + { + new KnownAnswerTest( + "RFC 3394 4.4", + "000102030405060708090A0B0C0D0E0F1011121314151617".HexToByteArray(), + "00112233445566778899AABBCCDDEEFF0001020304050607".HexToByteArray(), + "031D33264E15D33268F24EC260743EDCE1C6C7DDEE725A936BA814915C6762D2".HexToByteArray()) + }, + + new object[] + { + new KnownAnswerTest( + "RFC 3394 4.5", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F".HexToByteArray(), + "00112233445566778899AABBCCDDEEFF0001020304050607".HexToByteArray(), + "A8F9BC1612C68B3FF6E6F4FBE30E71E4769C8B80A32CB8958CD5D17D6B254DA1".HexToByteArray()) + }, + + new object[] + { + new KnownAnswerTest( + "RFC 3394 4.6", + "000102030405060708090A0B0C0D0E0F101112131415161718191A1B1C1D1E1F".HexToByteArray(), + "00112233445566778899AABBCCDDEEFF000102030405060708090A0B0C0D0E0F".HexToByteArray(), + "28C9F404C4B810F4CBCCB35CFB87F8263F5786E2D80ED326CBC7F0E71A99F43BFB988B9B7A02DD21".HexToByteArray()) + }, + ]; + + public struct KnownAnswerTest + { + public string Name { get; } + public byte[] Key { get; } + public byte[] Plaintext { get; } + public byte[] Ciphertext { get; } + + public KnownAnswerTest(string name, byte[] key, byte[] plaintext, byte[] ciphertext) + { + Name = name; + Key = key; + Plaintext = plaintext; + Ciphertext = ciphertext; + } + + public override string ToString() + { + return Name; + } + } + } } 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 959ba07214e9f6..a80f7552bdf660 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 @@ -292,14 +292,75 @@ public bool TryDecryptKeyWrap(ReadOnlySpan ciphertext, Span destinat return true; } + /// + /// Unwraps a key that was wrapped using the IETF RFC 3394 AES Key Wrap algorithm. + /// + /// The data to unwrap. + /// The buffer to receive the unwrapped key. + /// The number of bytes in the unwrapped key. + /// + /// The unwrap algorithm failed to unwrap the ciphertext. + /// -or- + /// An error occurred during the cryptographic operation. + /// + /// + /// + /// When called by the base class, + /// is pre-validated to be at least 24 bytes long and a multiple of 8 bytes. + /// + /// + /// When called by the base class, + /// will always be exactly 8 bytes shorter than , + /// so any valid value will always fit. + /// + /// protected virtual int DecryptKeyWrapCore(ReadOnlySpan source, Span destination) { - throw new NotImplementedException(); + ulong a0 = Rfc3394Unwrap( + source, + destination, + this, + static (self, source, destination) => self.DecryptEcb(source, destination, PaddingMode.None)); + + // Check that a0 is equal to 0xA6A6A6A6A6A6A6A6UL using 32-bit branchless checks only. + uint hiCheck = (uint)(a0 >> 32) ^ 0xA6A6A6A6U; + uint loCheck = (uint)a0 ^ 0xA6A6A6A6U; + + if ((hiCheck | loCheck) != 0) + { + throw new CryptographicException(SR.Cryptography_KeyWrap_DecryptFailed); + } + + return source.Length - 8; } + /// + /// Wraps a key using the IETF RFC 3394 AES Key Wrap algorithm, + /// writing the result to a specified buffer. + /// + /// The data to wrap. + /// The buffer to receive the wrapped data. + /// An error occurred during the cryptographic operation. + /// + /// + /// When called by the base class, + /// is pre-validated to be at least 16 bytes long and a multiple of 8 bytes. + /// + /// + /// When called by the base class, + /// is pre-validated to be exactly the length returned by + /// for the given input. + /// + /// protected virtual void EncryptKeyWrapCore(ReadOnlySpan source, Span destination) { - throw new NotImplementedException(); + const ulong DefaultInitialValue = 0xA6A6A6A6A6A6A6A6UL; + Rfc3394Wrap( + DefaultInitialValue, + source, + destination, + this, + static (self, source, destination) => self.EncryptEcb(source, destination, PaddingMode.None)); } /// @@ -757,7 +818,7 @@ private void Rfc3394Wrap( { Debug.Assert(source.Length % 8 == 0); Debug.Assert(source.Length >= 16); - Debug.Assert(destination.Length == GetKeyWrapPaddedLength(source.Length)); + Debug.Assert(destination.Length == source.Length + 8); Span B = stackalloc byte[16]; Span A = B.Slice(0, 8); From ebd1560ee8f06725be5221a17227143be3fe106c Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Aug 2026 17:17:52 -0400 Subject: [PATCH 5/7] Model feedback --- .../AlgorithmImplementations/AES/KeyWrapTests.cs | 12 +++++++++++- .../tests/ShimHelpers.cs | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs index dbde0417de55a6..48003d756bfc3c 100644 --- a/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/AlgorithmImplementations/AES/KeyWrapTests.cs @@ -151,7 +151,7 @@ public static void NeverCalledWithEmpty() AssertExtensions.Throws( "ciphertext", - () => key.DecryptKeyWrap(ReadOnlySpan.Empty, output)); + () => key.DecryptKeyWrap(Array.Empty())); AssertExtensions.Throws( "ciphertext", @@ -725,11 +725,21 @@ private void VerifyUnwrapFails(byte[] kek, byte[] ciphertext) using (Aes key = CreateKey(kek)) { byte[] dest = new byte[ciphertext.Length]; + int plaintextLength = ciphertext.Length - 8; + const byte PreFill = 0xB5; Assert.ThrowsAny(() => key.DecryptKeyWrap(ciphertext)); Assert.ThrowsAny(() => key.DecryptKeyWrap(new ReadOnlySpan(ciphertext))); + + Array.Fill(dest, PreFill); Assert.ThrowsAny(() => key.DecryptKeyWrap(ciphertext, dest)); + AssertExtensions.TrueExpression(dest.AsSpan(0, plaintextLength).IndexOfAnyExcept((byte)0) == -1); + AssertExtensions.TrueExpression(dest.AsSpan(plaintextLength).IndexOfAnyExcept(PreFill) == -1); + + Array.Fill(dest, PreFill); Assert.ThrowsAny(() => key.TryDecryptKeyWrap(ciphertext, dest, out _)); + AssertExtensions.TrueExpression(dest.AsSpan(0, plaintextLength).IndexOfAnyExcept((byte)0) == -1); + AssertExtensions.TrueExpression(dest.AsSpan(plaintextLength).IndexOfAnyExcept(PreFill) == -1); } } diff --git a/src/libraries/System.Security.Cryptography/tests/ShimHelpers.cs b/src/libraries/System.Security.Cryptography/tests/ShimHelpers.cs index d60d3c21363a20..03ae0ace0e5353 100644 --- a/src/libraries/System.Security.Cryptography/tests/ShimHelpers.cs +++ b/src/libraries/System.Security.Cryptography/tests/ShimHelpers.cs @@ -55,6 +55,8 @@ public static void VerifyAllBaseMembersOverridden(Type shimType) "TryDecryptCbcCore", "TryEncryptCfbCore", "TryDecryptCfbCore", + "EncryptKeyWrapCore", + "DecryptKeyWrapCore", "EncryptKeyWrapPaddedCore", "DecryptKeyWrapPaddedCore", }; From ad08b5932bf42877f509b7e2c8e6ce16e0948712 Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Aug 2026 17:40:52 -0400 Subject: [PATCH 6/7] Implement AES-KW with CryptoKit on Apple platforms Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6b1cb57e-d970-404f-b8c6-c23af8d8910d --- .../Interop.AesKeyWrap.cs | 84 +++++++++++++++++++ .../src/System.Security.Cryptography.csproj | 2 + .../Cryptography/AesImplementation.Apple.cs | 67 +++++++++++++++ .../entrypoints.c | 2 + .../pal_swiftbindings.h | 2 + .../pal_swiftbindings.swift | 30 +++++++ 6 files changed, 187 insertions(+) create mode 100644 src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.AesKeyWrap.cs diff --git a/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.AesKeyWrap.cs b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.AesKeyWrap.cs new file mode 100644 index 00000000000000..0c802506deb07f --- /dev/null +++ b/src/libraries/Common/src/Interop/OSX/System.Security.Cryptography.Native.Apple/Interop.AesKeyWrap.cs @@ -0,0 +1,84 @@ +// 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.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Runtime.InteropServices.Swift; +using System.Security.Cryptography; +using System.Security.Cryptography.Apple; +using Swift.Runtime; + +#pragma warning disable CS3016 // Arrays as attribute arguments are not CLS Compliant + +internal static partial class Interop +{ + internal static partial class AppleCrypto + { + internal static unsafe int AesKeyWrapEncrypt( + ReadOnlySpan key, + ReadOnlySpan plaintext, + Span ciphertext) + { + fixed (byte* keyPtr = key) + fixed (byte* plaintextPtr = plaintext) + fixed (byte* ciphertextPtr = ciphertext) + { + int written = AppleCryptoNative_AesKeyWrapEncrypt( + new UnsafeBufferPointer(keyPtr, key.Length), + new UnsafeBufferPointer(plaintextPtr, plaintext.Length), + new UnsafeMutableBufferPointer(ciphertextPtr, ciphertext.Length), + out SwiftError error); + + if (error.Value != null) + { + CryptographicOperations.ZeroMemory(ciphertext); + throw new CryptographicException(); + } + + return written; + } + } + + internal static unsafe int AesKeyWrapDecrypt( + ReadOnlySpan key, + ReadOnlySpan ciphertext, + Span plaintext) + { + fixed (byte* keyPtr = key) + fixed (byte* ciphertextPtr = ciphertext) + fixed (byte* plaintextPtr = plaintext) + { + int written = AppleCryptoNative_AesKeyWrapDecrypt( + new UnsafeBufferPointer(keyPtr, key.Length), + new UnsafeBufferPointer(ciphertextPtr, ciphertext.Length), + new UnsafeMutableBufferPointer(plaintextPtr, plaintext.Length), + out SwiftError error); + + if (error.Value != null) + { + CryptographicOperations.ZeroMemory(plaintext); + throw new CryptographicException(); + } + + return written; + } + } + + [LibraryImport(Libraries.AppleCryptoNative)] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static unsafe partial int AppleCryptoNative_AesKeyWrapEncrypt( + UnsafeBufferPointer key, + UnsafeBufferPointer plaintext, + UnsafeMutableBufferPointer ciphertext, + out SwiftError error); + + [LibraryImport(Libraries.AppleCryptoNative)] + [UnmanagedCallConv(CallConvs = [ typeof(CallConvSwift) ])] + private static unsafe partial int AppleCryptoNative_AesKeyWrapDecrypt( + UnsafeBufferPointer key, + UnsafeBufferPointer ciphertext, + UnsafeMutableBufferPointer plaintext, + out SwiftError error); + } +} 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 0d1a2027654947..8bd54b9fd5a488 100644 --- a/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj +++ b/src/libraries/System.Security.Cryptography/src/System.Security.Cryptography.csproj @@ -1342,6 +1342,8 @@ Link="Common\Interop\OSX\Swift.Runtime\UnsafeBufferPointer.cs" /> + source, Span destination) + { + if (!s_hasCryptoKitKeyWrap) + { + base.EncryptKeyWrapCore(source, destination); + return; + } + + FixedMemoryKeyBox keyBox = GetKey(); + bool addedRef = false; + + try + { + keyBox.DangerousAddRef(ref addedRef); + int written = Interop.AppleCrypto.AesKeyWrapEncrypt(keyBox.DangerousKeySpan, source, destination); + + if (written != destination.Length) + { + throw new CryptographicException(); + } + } + finally + { + if (addedRef) + { + keyBox.DangerousRelease(); + } + } + } + + protected override int DecryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + if (!s_hasCryptoKitKeyWrap) + { + return base.DecryptKeyWrapCore(source, destination); + } + + FixedMemoryKeyBox keyBox = GetKey(); + bool addedRef = false; + + try + { + keyBox.DangerousAddRef(ref addedRef); + int written = Interop.AppleCrypto.AesKeyWrapDecrypt(keyBox.DangerousKeySpan, source, destination); + + if (written != destination.Length) + { + throw new CryptographicException(); + } + + return written; + } + finally + { + if (addedRef) + { + keyBox.DangerousRelease(); + } + } + } + protected override void EncryptKeyWrapPaddedCore(ReadOnlySpan source, Span destination) { Debug.Assert(destination.Length == GetKeyWrapPaddedLength(source.Length)); diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c index 7ddefdcca13db3..65996b5471eaab 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/entrypoints.c @@ -29,6 +29,8 @@ static const Entry s_cryptoAppleNative[] = { DllImportEntry(AppleCryptoNative_AesGcmEncrypt) DllImportEntry(AppleCryptoNative_AesGcmDecrypt) + DllImportEntry(AppleCryptoNative_AesKeyWrapEncrypt) + DllImportEntry(AppleCryptoNative_AesKeyWrapDecrypt) DllImportEntry(AppleCryptoNative_ChaCha20Poly1305Encrypt) DllImportEntry(AppleCryptoNative_ChaCha20Poly1305Decrypt) DllImportEntry(AppleCryptoNative_DigestClone) diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h index 392d09ab0004a6..1200ceeb13b10f 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.h @@ -10,6 +10,8 @@ EXTERN_C void* AppleCryptoNative_ChaCha20Poly1305Encrypt; EXTERN_C void* AppleCryptoNative_ChaCha20Poly1305Decrypt; EXTERN_C void* AppleCryptoNative_AesGcmEncrypt; EXTERN_C void* AppleCryptoNative_AesGcmDecrypt; +EXTERN_C void* AppleCryptoNative_AesKeyWrapEncrypt; +EXTERN_C void* AppleCryptoNative_AesKeyWrapDecrypt; EXTERN_C void* AppleCryptoNative_IsAuthenticationFailure; EXTERN_C void* AppleCryptoNative_HKDFDeriveKey; diff --git a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift index 464f63dcc543b5..28a3da7e14aeee 100644 --- a/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift +++ b/src/native/libs/System.Security.Cryptography.Native.Apple/pal_swiftbindings.swift @@ -208,6 +208,36 @@ public func AppleCryptoNative_AesGcmDecrypt( aad: aad); } +@_silgen_name("AppleCryptoNative_AesKeyWrapEncrypt") +@available(macOS 12.0, iOS 15.0, tvOS 15.0, macCatalyst 15.0, *) +public func AppleCryptoNative_AesKeyWrapEncrypt( + key: UnsafeBufferPointer, + plaintext: UnsafeBufferPointer, + ciphertext: UnsafeMutableBufferPointer +) throws -> Int32 { + let result = try AES.KeyWrap.wrap( + SymmetricKey(data: plaintext), + using: SymmetricKey(data: key)) + + return Int32(result.copyBytes(to: ciphertext)) +} + +@_silgen_name("AppleCryptoNative_AesKeyWrapDecrypt") +@available(macOS 12.0, iOS 15.0, tvOS 15.0, macCatalyst 15.0, *) +public func AppleCryptoNative_AesKeyWrapDecrypt( + key: UnsafeBufferPointer, + ciphertext: UnsafeBufferPointer, + plaintext: UnsafeMutableBufferPointer +) throws -> Int32 { + let result = try AES.KeyWrap.unwrap( + ciphertext, + using: SymmetricKey(data: key)) + + return result.withUnsafeBytes { + Int32($0.copyBytes(to: plaintext)) + } +} + @_silgen_name("AppleCryptoNative_IsAuthenticationFailure") public func AppleCryptoNative_IsAuthenticationFailure(error: Error) -> Bool { if let error = error as? CryptoKitError { From 1f4088ca402fd7afe964ceb9f71263df8a42cd2a Mon Sep 17 00:00:00 2001 From: Kevin Jones Date: Fri, 14 Aug 2026 19:37:35 -0400 Subject: [PATCH 7/7] Add OpenSSL-backed implementation --- .../Interop.EVP.Cipher.cs | 9 ++++ .../Cryptography/AesImplementation.Apple.cs | 2 + .../Cryptography/AesImplementation.OpenSsl.cs | 53 ++++++++++++++----- .../entrypoints.c | 3 ++ .../opensslshim.h | 6 +++ .../pal_evp_cipher.c | 18 +++++++ .../pal_evp_cipher.h | 24 +++++++++ 7 files changed, 102 insertions(+), 13 deletions(-) diff --git a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Cipher.cs b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Cipher.cs index 1a90ae1c95a371..fe75f3ffa0e5c8 100644 --- a/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Cipher.cs +++ b/src/libraries/Common/src/Interop/Unix/System.Security.Cryptography.Native/Interop.EVP.Cipher.cs @@ -258,6 +258,9 @@ internal static void EvpCipherSetCcmTagLength(SafeEvpCipherCtxHandle ctx, int ta [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes128Ccm")] internal static partial IntPtr EvpAes128Ccm(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes128Wrap")] + internal static partial IntPtr EvpAes128Wrap(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes128WrapPad")] internal static partial IntPtr EvpAes128WrapPad(); @@ -279,6 +282,9 @@ internal static void EvpCipherSetCcmTagLength(SafeEvpCipherCtxHandle ctx, int ta [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes192Ccm")] internal static partial IntPtr EvpAes192Ccm(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes192Wrap")] + internal static partial IntPtr EvpAes192Wrap(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes192WrapPad")] internal static partial IntPtr EvpAes192WrapPad(); @@ -300,6 +306,9 @@ internal static void EvpCipherSetCcmTagLength(SafeEvpCipherCtxHandle ctx, int ta [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes256Ccm")] internal static partial IntPtr EvpAes256Ccm(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes256Wrap")] + internal static partial IntPtr EvpAes256Wrap(); + [LibraryImport(Libraries.CryptoNative, EntryPoint = "CryptoNative_EvpAes256WrapPad")] internal static partial IntPtr EvpAes256WrapPad(); diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Apple.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Apple.cs index b6ac9d6166decc..3a29fd4e74d40d 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Apple.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.Apple.cs @@ -74,6 +74,7 @@ protected override void EncryptKeyWrapCore(ReadOnlySpan source, Span if (written != destination.Length) { + Debug.Fail($"CryptoKit wrote {written} bytes; expected {destination.Length}."); throw new CryptographicException(); } } @@ -103,6 +104,7 @@ protected override int DecryptKeyWrapCore(ReadOnlySpan source, Span if (written != destination.Length) { + Debug.Fail($"CryptoKit wrote {written} bytes; expected {destination.Length}."); throw new CryptographicException(); } diff --git a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.OpenSsl.cs b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.OpenSsl.cs index 38c622efb2b1ce..c696d514c479be 100644 --- a/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.OpenSsl.cs +++ b/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/AesImplementation.OpenSsl.cs @@ -41,33 +41,57 @@ private static OpenSslCipherLite CreateLiteCipher( protected override void EncryptKeyWrapPaddedCore(ReadOnlySpan source, Span destination) { - int written = KeyWrap(source, destination, enc: 1); + int written = KeyWrap(source, destination, enc: 1, padded: true); Debug.Assert(written == destination.Length); } protected override int DecryptKeyWrapPaddedCore(ReadOnlySpan source, Span destination) { - return KeyWrap(source, destination, enc: 0); + return KeyWrap(source, destination, enc: 0, padded: true); } - private int KeyWrap(ReadOnlySpan source, Span destination, int enc) + protected override void EncryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + int written = KeyWrap(source, destination, enc: 1, padded: false); + + if (written != destination.Length) + { + Debug.Fail($"OpenSSL wrote {written} bytes; expected {destination.Length}."); + throw new CryptographicException(); + } + } + + protected override int DecryptKeyWrapCore(ReadOnlySpan source, Span destination) + { + int written = KeyWrap(source, destination, enc: 0, padded: false); + + if (written != destination.Length) + { + Debug.Fail($"OpenSSL wrote {written} bytes; expected {destination.Length}."); + throw new CryptographicException(); + } + + return written; + } + + private int KeyWrap(ReadOnlySpan source, Span destination, int enc, bool padded) { Debug.Assert(enc is 0 or 1); SafeEvpCipherCtxHandle ctx = GetKey().UseKey( - state: enc, - static (enc, key) => + state: (Enc: enc, Padded: padded), + static (state, key) => { int keySizeInBits = key.Length * 8; - IntPtr algorithm = GetKeyWrapAlgorithm(keySizeInBits); + IntPtr algorithm = GetKeyWrapAlgorithm(keySizeInBits, state.Padded); SafeEvpCipherCtxHandle ctx = Interop.Crypto.EvpCipherCreate( algorithm, ref MemoryMarshal.GetReference(key), key.Length * 8, ref MemoryMarshal.GetReference(ReadOnlySpan.Empty), - enc); + state.Enc); if (ctx.IsInvalid) { @@ -82,7 +106,7 @@ ref MemoryMarshal.GetReference(ReadOnlySpan.Empty), using (ctx) { - // OpenSSL AES-KWP requires that the destination be at least as large as the source length plus the block size. + // OpenSSL AES key wrap requires that the destination be at least as large as the source length plus the block size. const int AesBlockSizeBytes = 16; using (CryptoPoolLease lease = CryptoPoolLease.RentConditionally( checked(source.Length + AesBlockSizeBytes), @@ -152,12 +176,15 @@ private static IntPtr GetAlgorithm(int keySize, int feedback, CipherMode cipherM new CryptographicException(SR.Cryptography_InvalidKeySize)), }; - private static IntPtr GetKeyWrapAlgorithm(int keySize) => - keySize switch + private static IntPtr GetKeyWrapAlgorithm(int keySize, bool padded) => + (keySize, padded) switch { - 128 => Interop.Crypto.EvpAes128WrapPad(), - 192 => Interop.Crypto.EvpAes192WrapPad(), - 256 => Interop.Crypto.EvpAes256WrapPad(), + (128, false) => Interop.Crypto.EvpAes128Wrap(), + (128, true) => Interop.Crypto.EvpAes128WrapPad(), + (192, false) => Interop.Crypto.EvpAes192Wrap(), + (192, true) => Interop.Crypto.EvpAes192WrapPad(), + (256, false) => Interop.Crypto.EvpAes256Wrap(), + (256, true) => Interop.Crypto.EvpAes256WrapPad(), _ => throw new CryptographicException(SR.Cryptography_InvalidKeySize), }; } diff --git a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c index 1ac15294492ded..f086723428540b 100644 --- a/src/native/libs/System.Security.Cryptography.Native/entrypoints.c +++ b/src/native/libs/System.Security.Cryptography.Native/entrypoints.c @@ -105,6 +105,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_EvpAes128Cfb8) DllImportEntry(CryptoNative_EvpAes128Ecb) DllImportEntry(CryptoNative_EvpAes128Gcm) + DllImportEntry(CryptoNative_EvpAes128Wrap) DllImportEntry(CryptoNative_EvpAes128WrapPad) DllImportEntry(CryptoNative_EvpAes192Cbc) DllImportEntry(CryptoNative_EvpAes192Ccm) @@ -112,6 +113,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_EvpAes192Cfb8) DllImportEntry(CryptoNative_EvpAes192Ecb) DllImportEntry(CryptoNative_EvpAes192Gcm) + DllImportEntry(CryptoNative_EvpAes192Wrap) DllImportEntry(CryptoNative_EvpAes192WrapPad) DllImportEntry(CryptoNative_EvpAes256Cbc) DllImportEntry(CryptoNative_EvpAes256Ccm) @@ -119,6 +121,7 @@ static const Entry s_cryptoNative[] = DllImportEntry(CryptoNative_EvpAes256Cfb8) DllImportEntry(CryptoNative_EvpAes256Ecb) DllImportEntry(CryptoNative_EvpAes256Gcm) + DllImportEntry(CryptoNative_EvpAes256Wrap) DllImportEntry(CryptoNative_EvpAes256WrapPad) DllImportEntry(CryptoNative_EvpChaCha20Poly1305) DllImportEntry(CryptoNative_EvpCipherCreate2) diff --git a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h index f2f05870ed857d..26f263052fbc8e 100644 --- a/src/native/libs/System.Security.Cryptography.Native/opensslshim.h +++ b/src/native/libs/System.Security.Cryptography.Native/opensslshim.h @@ -463,6 +463,7 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(EVP_aes_128_cfb8) \ REQUIRED_FUNCTION(EVP_aes_128_ecb) \ REQUIRED_FUNCTION(EVP_aes_128_gcm) \ + REQUIRED_FUNCTION(EVP_aes_128_wrap) \ REQUIRED_FUNCTION(EVP_aes_128_wrap_pad) \ REQUIRED_FUNCTION(EVP_aes_192_cbc) \ REQUIRED_FUNCTION(EVP_aes_192_ccm) \ @@ -470,6 +471,7 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(EVP_aes_192_cfb8) \ REQUIRED_FUNCTION(EVP_aes_192_ecb) \ REQUIRED_FUNCTION(EVP_aes_192_gcm) \ + REQUIRED_FUNCTION(EVP_aes_192_wrap) \ REQUIRED_FUNCTION(EVP_aes_192_wrap_pad) \ REQUIRED_FUNCTION(EVP_aes_256_cbc) \ REQUIRED_FUNCTION(EVP_aes_256_ccm) \ @@ -477,6 +479,7 @@ extern bool g_libSslUses32BitTime; REQUIRED_FUNCTION(EVP_aes_256_cfb8) \ REQUIRED_FUNCTION(EVP_aes_256_ecb) \ REQUIRED_FUNCTION(EVP_aes_256_gcm) \ + REQUIRED_FUNCTION(EVP_aes_256_wrap) \ REQUIRED_FUNCTION(EVP_aes_256_wrap_pad) \ LIGHTUP_FUNCTION(EVP_chacha20_poly1305) \ REQUIRED_FUNCTION(EVP_CIPHER_CTX_ctrl) \ @@ -1062,6 +1065,7 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define EVP_aes_128_ecb EVP_aes_128_ecb_ptr #define EVP_aes_128_gcm EVP_aes_128_gcm_ptr #define EVP_aes_128_ccm EVP_aes_128_ccm_ptr +#define EVP_aes_128_wrap EVP_aes_128_wrap_ptr #define EVP_aes_128_wrap_pad EVP_aes_128_wrap_pad_ptr #define EVP_aes_192_cbc EVP_aes_192_cbc_ptr #define EVP_aes_192_cfb8 EVP_aes_192_cfb8_ptr @@ -1069,6 +1073,7 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define EVP_aes_192_ecb EVP_aes_192_ecb_ptr #define EVP_aes_192_gcm EVP_aes_192_gcm_ptr #define EVP_aes_192_ccm EVP_aes_192_ccm_ptr +#define EVP_aes_192_wrap EVP_aes_192_wrap_ptr #define EVP_aes_192_wrap_pad EVP_aes_192_wrap_pad_ptr #define EVP_aes_256_cbc EVP_aes_256_cbc_ptr #define EVP_aes_256_cfb8 EVP_aes_256_cfb8_ptr @@ -1076,6 +1081,7 @@ extern TYPEOF(OPENSSL_gmtime)* OPENSSL_gmtime_ptr; #define EVP_aes_256_ecb EVP_aes_256_ecb_ptr #define EVP_aes_256_gcm EVP_aes_256_gcm_ptr #define EVP_aes_256_ccm EVP_aes_256_ccm_ptr +#define EVP_aes_256_wrap EVP_aes_256_wrap_ptr #define EVP_aes_256_wrap_pad EVP_aes_256_wrap_pad_ptr #define EVP_chacha20_poly1305 EVP_chacha20_poly1305_ptr #define EVP_CIPHER_CTX_ctrl EVP_CIPHER_CTX_ctrl_ptr diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.c b/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.c index 17562f965e6fda..ea34be3da131c4 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.c +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.c @@ -286,6 +286,12 @@ const EVP_CIPHER* CryptoNative_EvpAes128WrapPad(void) return EVP_aes_128_wrap_pad(); } +const EVP_CIPHER* CryptoNative_EvpAes128Wrap(void) +{ + // No error queue impact. + return EVP_aes_128_wrap(); +} + const EVP_CIPHER* CryptoNative_EvpAes192Ecb(void) { // No error queue impact. @@ -328,6 +334,12 @@ const EVP_CIPHER* CryptoNative_EvpAes192WrapPad(void) return EVP_aes_192_wrap_pad(); } +const EVP_CIPHER* CryptoNative_EvpAes192Wrap(void) +{ + // No error queue impact. + return EVP_aes_192_wrap(); +} + const EVP_CIPHER* CryptoNative_EvpAes256Ecb(void) { // No error queue impact. @@ -370,6 +382,12 @@ const EVP_CIPHER* CryptoNative_EvpAes256WrapPad(void) return EVP_aes_256_wrap_pad(); } +const EVP_CIPHER* CryptoNative_EvpAes256Wrap(void) +{ + // No error queue impact. + return EVP_aes_256_wrap(); +} + const EVP_CIPHER* CryptoNative_EvpDesEcb(void) { // No error queue impact. diff --git a/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.h b/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.h index 90410d86e7c059..61c5b547cb5717 100644 --- a/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.h +++ b/src/native/libs/System.Security.Cryptography.Native/pal_evp_cipher.h @@ -156,6 +156,14 @@ Direct shim to EVP_aes_128_ccm. */ PALEXPORT const EVP_CIPHER* CryptoNative_EvpAes128Ccm(void); +/* +Function: +EvpAes128Wrap + +Direct shim to EVP_aes_128_wrap. +*/ +PALEXPORT const EVP_CIPHER* CryptoNative_EvpAes128Wrap(void); + /* Function: EvpAes128WrapPad @@ -212,6 +220,14 @@ Direct shim to EVP_aes_192_ccm. */ PALEXPORT const EVP_CIPHER* CryptoNative_EvpAes192Ccm(void); +/* +Function: +EvpAes192Wrap + +Direct shim to EVP_aes_192_wrap. +*/ +PALEXPORT const EVP_CIPHER* CryptoNative_EvpAes192Wrap(void); + /* Function: EvpAes192WrapPad @@ -268,6 +284,14 @@ Direct shim to EVP_aes_256_ccm. */ PALEXPORT const EVP_CIPHER* CryptoNative_EvpAes256Ccm(void); +/* +Function: +EvpAes256Wrap + +Direct shim to EVP_aes_256_wrap. +*/ +PALEXPORT const EVP_CIPHER* CryptoNative_EvpAes256Wrap(void); + /* Function: EvpAes256WrapPad