// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.Diagnostics;
using System.Runtime.Versioning;
using Internal.Cryptography;
private const int NonceSize = 12;
public static KeySizes NonceByteSizes { get; } = new KeySizes(NonceSize, NonceSize, 1);
+ [Obsolete(Obsoletions.AesGcmTagConstructorMessage, DiagnosticId = Obsoletions.AesGcmTagConstructorDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
public AesGcm(ReadOnlySpan<byte> key)
{
ThrowIfNotSupported();
ImportKey(key);
}
+ [Obsolete(Obsoletions.AesGcmTagConstructorMessage, DiagnosticId = Obsoletions.AesGcmTagConstructorDiagId, UrlFormat = Obsoletions.SharedUrlFormat)]
public AesGcm(byte[] key)
+ : this(new ReadOnlySpan<byte>(key ?? throw new ArgumentNullException(nameof(key))))
{
- ThrowIfNotSupported();
+ }
- ArgumentNullException.ThrowIfNull(key);
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AesGcm" /> class with a provided key and required tag size.
+ /// </summary>
+ /// <param name="key">The secret key to use for this instance.</param>
+ /// <param name="tagSizeInBytes">The size of the tag, in bytes, that encryption and decryption must use.</param>
+ /// <exception cref="CryptographicException">
+ /// The <paramref name="key" /> parameter length is other than 16, 24, or 32 bytes (128, 192, or 256 bits).
+ /// </exception>
+ /// <exception cref="ArgumentException">
+ /// The <paramref name="tagSizeInBytes" /> parameter is an unsupported tag size indicated by
+ /// <see cref="TagByteSizes" />.
+ /// </exception>
+ /// <exception cref="PlatformNotSupportedException">
+ /// The current platform does not support AES-GCM.
+ /// </exception>
+ /// <remarks>
+ /// The <paramref name="tagSizeInBytes" /> parameter is used to indicate that the tag parameter in <c>Encrypt</c>
+ /// or <c>Decrypt</c> must be exactly this size. Indicating the required tag size prevents issues where callers
+ /// of <c>Decrypt</c> may supply a tag as input and that input is truncated to an unexpected size.
+ /// </remarks>
+ public AesGcm(ReadOnlySpan<byte> key, int tagSizeInBytes)
+ {
+ ThrowIfNotSupported();
AesAEAD.CheckKeySize(key.Length);
+
+ if (!tagSizeInBytes.IsLegalSize(TagByteSizes))
+ {
+ throw new ArgumentException(SR.Cryptography_InvalidTagLength, nameof(tagSizeInBytes));
+ }
+
+ TagSizeInBytes = tagSizeInBytes;
ImportKey(key);
}
+ /// <summary>
+ /// Initializes a new instance of the <see cref="AesGcm" /> class with a provided key and required tag size.
+ /// </summary>
+ /// <param name="key">The secret key to use for this instance.</param>
+ /// <param name="tagSizeInBytes">The size of the tag, in bytes, that encryption and decryption must use.</param>
+ /// <exception cref="ArgumentNullException">The <paramref name="key" /> parameter is null.</exception>
+ /// <exception cref="CryptographicException">
+ /// The <paramref name="key" /> parameter length is other than 16, 24, or 32 bytes (128, 192, or 256 bits).
+ /// </exception>
+ /// <exception cref="ArgumentException">
+ /// The <paramref name="tagSizeInBytes" /> parameter is an unsupported tag size indicated by
+ /// <see cref="TagByteSizes" />.
+ /// </exception>
+ /// <exception cref="PlatformNotSupportedException">
+ /// The current platform does not support AES-GCM.
+ /// </exception>
+ /// <remarks>
+ /// The <paramref name="tagSizeInBytes" /> parameter is used to indicate that the tag parameter in <c>Encrypt</c>
+ /// or <c>Decrypt</c> must be exactly this size. Indicating the required tag size prevents issues where callers
+ /// of <c>Decrypt</c> may supply a tag as input and that input is truncated to an unexpected size.
+ /// </remarks>
+ public AesGcm(byte[] key, int tagSizeInBytes)
+ : this(new ReadOnlySpan<byte>(key ?? throw new ArgumentNullException(nameof(key))), tagSizeInBytes)
+ {
+ }
+
+ /// <summary>
+ /// Gets the size of the tag, in bytes.
+ /// </summary>
+ /// <value>
+ /// The size of the tag that must be used for encryption or decryption, or <see langword="null" /> if the
+ /// tag size is unspecified.
+ /// </value>
+ public int? TagSizeInBytes { get; }
+
public void Encrypt(byte[] nonce, byte[] plaintext, byte[] ciphertext, byte[] tag, byte[]? associatedData = null)
{
ArgumentNullException.ThrowIfNull(nonce);
DecryptCore(nonce, ciphertext, tag, plaintext, associatedData);
}
- private static void CheckParameters(
+ private void CheckParameters(
ReadOnlySpan<byte> plaintext,
ReadOnlySpan<byte> ciphertext,
ReadOnlySpan<byte> nonce,
if (!nonce.Length.IsLegalSize(NonceByteSizes))
throw new ArgumentException(SR.Cryptography_InvalidNonceLength, nameof(nonce));
- if (!tag.Length.IsLegalSize(TagByteSizes))
+ if (TagSizeInBytes is int tagSizeInBytes)
+ {
+ // constructor promise
+ Debug.Assert(tagSizeInBytes.IsLegalSize(TagByteSizes));
+
+ if (tag.Length != tagSizeInBytes)
+ {
+ throw new ArgumentException(SR.Format(SR.Cryptography_IncorrectTagLength, tagSizeInBytes), nameof(tag));
+ }
+ }
+ else if (!tag.Length.IsLegalSize(TagByteSizes))
+ {
throw new ArgumentException(SR.Cryptography_InvalidTagLength, nameof(tag));
+ }
}
private static void ThrowIfNotSupported()
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(nonce);
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag, additionalData);
public static void InvalidKeyLength(int keyLength)
{
byte[] key = new byte[keyLength];
+#pragma warning disable SYSLIB0053
Assert.Throws<CryptographicException>(() => new AesGcm(key));
+ Assert.Throws<CryptographicException>(() => new AesGcm(key.AsSpan()));
+#pragma warning restore SYSLIB0053
+ Assert.Throws<CryptographicException>(() => new AesGcm(key, AesGcm.TagByteSizes.MinSize));
+ Assert.Throws<CryptographicException>(() => new AesGcm(key.AsSpan(), AesGcm.TagByteSizes.MinSize));
}
[Theory]
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(nonce);
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MinSize))
{
Assert.Throws<ArgumentException>("nonce", () => aesGcm.Encrypt(nonce, plaintext, ciphertext, tag));
}
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(nonce);
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, AesGcm.TagByteSizes.MinSize))
{
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag);
[Theory]
[MemberData(nameof(GetInvalidTagSizes))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
- public static void InvalidTagSize(int tagSize)
+ public static void InvalidTagSizeForUnspecifiedRequiredTag(int tagSize)
{
int dataLength = 30;
byte[] plaintext = Enumerable.Range(1, dataLength).Select((x) => (byte)x).ToArray();
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(nonce);
+#pragma warning disable SYSLIB0053
using (var aesGcm = new AesGcm(key))
+#pragma warning restore SYSLIB0053
{
Assert.Throws<ArgumentException>("tag", () => aesGcm.Encrypt(nonce, plaintext, ciphertext, tag));
+ Assert.Throws<ArgumentException>("tag", () => aesGcm.Decrypt(nonce, ciphertext, tag, plaintext));
}
}
[Theory]
+ [MemberData(nameof(GetInvalidTagSizes))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
+ public static void InvalidTagSizeForRequiredTag(int tagSize)
+ {
+ byte[] key = new byte[32];
+ Assert.Throws<ArgumentException>("tagSizeInBytes", () => new AesGcm(key, tagSize));
+ Assert.Throws<ArgumentException>("tagSizeInBytes", () => new AesGcm(key.AsSpan(), tagSize));
+ }
+
+ [Theory]
[MemberData(nameof(GetValidTagSizes))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
public static void ValidTagSize(int tagSize)
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(nonce);
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tagSize))
{
aesGcm.Encrypt(nonce, plaintext, ciphertext, tag);
}
}
+ [Theory]
+ [InlineData(12)]
+ [InlineData(13)]
+ [InlineData(14)]
+ [InlineData(15)]
+ public static void TagSizeDoesNotMatchConstructorRequirement(int wrongTagSize)
+ {
+ byte[] key = new byte[16];
+ byte[] nonce = new byte[12];
+ byte[] plaintext = new byte[1];
+ byte[] ciphertext = new byte[1];
+ byte[] tag = new byte[wrongTagSize];
+ int tagSize = 16;
+
+ using (var aesGcm = new AesGcm(key, tagSize))
+ {
+ ArgumentException ex;
+ ex = Assert.Throws<ArgumentException>("tag", () => aesGcm.Encrypt(nonce, plaintext, ciphertext, tag));
+ Assert.Contains(tagSize.ToString(), ex.Message);
+
+ ex = Assert.Throws<ArgumentException>("tag", () => aesGcm.Encrypt(nonce.AsSpan(), plaintext, ciphertext, tag));
+ Assert.Contains(tagSize.ToString(), ex.Message);
+
+ ex = Assert.Throws<ArgumentException>("tag", () => aesGcm.Decrypt(nonce, ciphertext, tag, plaintext));
+ Assert.Contains(tagSize.ToString(), ex.Message);
+
+ ex = Assert.Throws<ArgumentException>("tag", () => aesGcm.Decrypt(nonce.AsSpan(), ciphertext, tag, plaintext));
+ Assert.Contains(tagSize.ToString(), ex.Message);
+ }
+ }
+
[Fact]
[ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
public static void TwoEncryptionsAndDecryptionsUsingOneInstance()
"da").HexToByteArray();
byte[] expectedTag2 = "9c75d006640ff4fb68c60c9548a45cf8".HexToByteArray();
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, expectedTag1.Length))
{
byte[] ciphertext1 = new byte[originalData1.Length];
byte[] tag1 = new byte[expectedTag1.Length];
Assert.Equal(expectedCiphertext1, ciphertext1);
Assert.Equal(expectedTag1, tag1);
+ byte[] plaintext1 = new byte[originalData1.Length];
+ aesGcm.Decrypt(nonce1, ciphertext1, tag1, plaintext1);
+ Assert.Equal(originalData1, plaintext1);
+ }
+
+ using (var aesGcm = new AesGcm(key, expectedTag2.Length))
+ {
byte[] ciphertext2 = new byte[originalData2.Length];
byte[] tag2 = new byte[expectedTag2.Length];
aesGcm.Encrypt(nonce2, originalData2, ciphertext2, tag2, associatedData2);
Assert.Equal(expectedCiphertext2, ciphertext2);
Assert.Equal(expectedTag2, tag2);
- byte[] plaintext1 = new byte[originalData1.Length];
- aesGcm.Decrypt(nonce1, ciphertext1, tag1, plaintext1);
- Assert.Equal(originalData1, plaintext1);
-
byte[] plaintext2 = new byte[originalData2.Length];
aesGcm.Decrypt(nonce2, ciphertext2, tag2, plaintext2, associatedData2);
Assert.Equal(originalData2, plaintext2);
byte[] ciphertext = new byte[ctLen];
byte[] tag = new byte[16];
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
Assert.Throws<ArgumentException>(() => aesGcm.Encrypt(nonce, plaintext, ciphertext, tag));
Assert.Throws<ArgumentException>(() => aesGcm.Decrypt(nonce, ciphertext, tag, plaintext));
[Fact]
public static void NullKey()
{
+#pragma warning disable SYSLIB0053
Assert.Throws<ArgumentNullException>(() => new AesGcm((byte[])null));
+#pragma warning restore SYSLIB0053
+ Assert.Throws<ArgumentNullException>(() => new AesGcm((byte[])null, AesGcm.TagByteSizes.MinSize));
}
[Fact]
byte[] ciphertext = new byte[0];
byte[] tag = new byte[16];
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
Assert.Throws<ArgumentNullException>(() => aesGcm.Encrypt((byte[])null, plaintext, ciphertext, tag));
Assert.Throws<ArgumentNullException>(() => aesGcm.Decrypt((byte[])null, ciphertext, tag, plaintext));
byte[] ciphertext = new byte[0];
byte[] tag = new byte[16];
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
Assert.Throws<ArgumentNullException>(() => aesGcm.Encrypt(nonce, (byte[])null, ciphertext, tag));
Assert.Throws<ArgumentNullException>(() => aesGcm.Decrypt(nonce, ciphertext, tag, (byte[])null));
byte[] plaintext = new byte[0];
byte[] tag = new byte[16];
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
Assert.Throws<ArgumentNullException>(() => aesGcm.Encrypt(nonce, plaintext, (byte[])null, tag));
Assert.Throws<ArgumentNullException>(() => aesGcm.Decrypt(nonce, (byte[])null, tag, plaintext));
byte[] plaintext = new byte[0];
byte[] ciphertext = new byte[0];
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tagSizeInBytes: 16))
{
Assert.Throws<ArgumentNullException>(() => aesGcm.Encrypt(nonce, plaintext, ciphertext, (byte[])null));
Assert.Throws<ArgumentNullException>(() => aesGcm.Decrypt(nonce, ciphertext, (byte[])null, plaintext));
byte[] tag = new byte[16];
RandomNumberGenerator.Fill(nonce);
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
aesGcm.Encrypt(nonce, data, data, tag);
Assert.NotEqual(originalPlaintext, data);
byte[] tag = new byte[16];
RandomNumberGenerator.Fill(nonce);
- using (var aesGcm = new AesGcm(key))
+ using (var aesGcm = new AesGcm(key, tag.Length))
{
aesGcm.Encrypt(nonce, data, data, tag);
Assert.NotEqual(originalPlaintext, data);
[Theory]
[MemberData(nameof(GetNistGcmTestCases))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
- public static void AesGcmNistTests(AEADTest testCase)
+ public static void AesGcmNistTestsUnspecifiedTagSize(AEADTest testCase)
{
+#pragma warning disable SYSLIB0053
using (var aesGcm = new AesGcm(testCase.Key))
+#pragma warning restore SYSLIB0053
{
byte[] ciphertext = new byte[testCase.Plaintext.Length];
byte[] tag = new byte[testCase.Tag.Length];
[Theory]
[MemberData(nameof(GetNistGcmTestCases))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
- public static void AesGcmNistTestsTamperTag(AEADTest testCase)
+ public static void AesGcmNistTestsSpecifiedTagSize(AEADTest testCase)
{
- using (var aesGcm = new AesGcm(testCase.Key))
+ if (PlatformDetection.IsOSX && testCase.Tag.Length != CryptoKitSupportedTagSizeInBytes)
{
- if (PlatformDetection.IsOSX && testCase.Tag.Length != CryptoKitSupportedTagSizeInBytes)
- {
- byte[] plaintext = new byte[testCase.Plaintext.Length];
- byte[] tamperedTag = testCase.Tag.AsSpan().ToArray();
- tamperedTag[0] ^= 1;
+ Assert.Throws<ArgumentException>("tagSizeInBytes", () => new AesGcm(testCase.Key, testCase.Tag.Length));
+ }
+ else
+ {
+ byte[] ciphertext = new byte[testCase.Plaintext.Length];
+ byte[] tag = new byte[testCase.Tag.Length];
- Assert.Throws<ArgumentException>("tag", () =>
- {
- aesGcm.Decrypt(testCase.Nonce, testCase.Ciphertext, tamperedTag, plaintext, testCase.AssociatedData);
- });
- }
- else
+ using (var aesGcm = new AesGcm(testCase.Key, testCase.Tag.Length))
{
- byte[] ciphertext = new byte[testCase.Plaintext.Length];
- byte[] tag = new byte[testCase.Tag.Length];
aesGcm.Encrypt(testCase.Nonce, testCase.Plaintext, ciphertext, tag, testCase.AssociatedData);
Assert.Equal(testCase.Ciphertext, ciphertext);
Assert.Equal(testCase.Tag, tag);
- tag[0] ^= 1;
-
byte[] plaintext = new byte[testCase.Plaintext.Length];
- RandomNumberGenerator.Fill(plaintext);
- Assert.Throws<AuthenticationTagMismatchException>(
- () => aesGcm.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData));
- Assert.Equal(new byte[plaintext.Length], plaintext);
+ aesGcm.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData);
+ Assert.Equal(testCase.Plaintext, plaintext);
}
}
}
[Theory]
+ [MemberData(nameof(GetNistGcmTestCases))]
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
+ public static void AesGcmNistTestsTamperTag(AEADTest testCase)
+ {
+ if (PlatformDetection.IsOSX && testCase.Tag.Length != CryptoKitSupportedTagSizeInBytes)
+ {
+ return;
+ }
+
+ using (var aesGcm = new AesGcm(testCase.Key, testCase.Tag.Length))
+ {
+ byte[] ciphertext = new byte[testCase.Plaintext.Length];
+ byte[] tag = new byte[testCase.Tag.Length];
+ aesGcm.Encrypt(testCase.Nonce, testCase.Plaintext, ciphertext, tag, testCase.AssociatedData);
+ Assert.Equal(testCase.Ciphertext, ciphertext);
+ Assert.Equal(testCase.Tag, tag);
+
+ tag[0] ^= 1;
+
+ byte[] plaintext = new byte[testCase.Plaintext.Length];
+ RandomNumberGenerator.Fill(plaintext);
+ Assert.Throws<AuthenticationTagMismatchException>(
+ () => aesGcm.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData));
+ Assert.Equal(new byte[plaintext.Length], plaintext);
+ }
+ }
+
+ [Theory]
[MemberData(nameof(GetNistGcmTestCasesWithNonEmptyPT))]
[ActiveIssue("https://github.com/dotnet/runtime/issues/51332", TestPlatforms.iOS | TestPlatforms.tvOS | TestPlatforms.MacCatalyst)]
public static void AesGcmNistTestsTamperCiphertext(AEADTest testCase)
{
- using (var aesGcm = new AesGcm(testCase.Key))
+ if (PlatformDetection.IsOSX && testCase.Tag.Length != CryptoKitSupportedTagSizeInBytes)
{
- if (PlatformDetection.IsOSX && testCase.Tag.Length != CryptoKitSupportedTagSizeInBytes)
- {
- byte[] tamperedCiphertext = testCase.Ciphertext.AsSpan().ToArray();
- tamperedCiphertext[0] ^= 1;
- Assert.Throws<ArgumentException>("tag", () =>
- {
- aesGcm.Decrypt(testCase.Nonce, tamperedCiphertext, testCase.Tag, testCase.Plaintext, testCase.AssociatedData);
- });
- }
- else
- {
- byte[] ciphertext = new byte[testCase.Plaintext.Length];
- byte[] tag = new byte[testCase.Tag.Length];
- aesGcm.Encrypt(testCase.Nonce, testCase.Plaintext, ciphertext, tag, testCase.AssociatedData);
- Assert.Equal(testCase.Ciphertext, ciphertext);
- Assert.Equal(testCase.Tag, tag);
+ return;
+ }
- ciphertext[0] ^= 1;
+ using (var aesGcm = new AesGcm(testCase.Key, testCase.Tag.Length))
+ {
+ byte[] ciphertext = new byte[testCase.Plaintext.Length];
+ byte[] tag = new byte[testCase.Tag.Length];
+ aesGcm.Encrypt(testCase.Nonce, testCase.Plaintext, ciphertext, tag, testCase.AssociatedData);
+ Assert.Equal(testCase.Ciphertext, ciphertext);
+ Assert.Equal(testCase.Tag, tag);
- byte[] plaintext = RandomNumberGenerator.GetBytes(testCase.Plaintext.Length);
- Assert.Throws<AuthenticationTagMismatchException>(
- () => aesGcm.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData));
- AssertExtensions.FilledWith<byte>(0, plaintext);
- }
+ ciphertext[0] ^= 1;
+
+ byte[] plaintext = RandomNumberGenerator.GetBytes(testCase.Plaintext.Length);
+ Assert.Throws<AuthenticationTagMismatchException>(
+ () => aesGcm.Decrypt(testCase.Nonce, ciphertext, tag, plaintext, testCase.AssociatedData));
+ AssertExtensions.FilledWith<byte>(0, plaintext);
}
}
byte[] ciphertext = Array.Empty<byte>();
byte[] tag = "58e2fccefa7e3061367f1d57a4e7455a".HexToByteArray();
- AesGcm aesGcm = new AesGcm(key);
+ AesGcm aesGcm = new AesGcm(key, tag.Length);
aesGcm.Dispose();
Assert.Throws<ObjectDisposedException>(() => aesGcm.Encrypt(nonce, plaintext, ciphertext, new byte[tag.Length]));
{
byte[] key = RandomNumberGenerator.GetBytes(256 / 8);
+#pragma warning disable SYSLIB0053
Assert.Throws<PlatformNotSupportedException>(() => new AesGcm(key));
Assert.Throws<PlatformNotSupportedException>(() => new AesGcm(key.AsSpan()));
+#pragma warning restore SYSLIB0053
+
+ Assert.Throws<PlatformNotSupportedException>(() => new AesGcm(key, AesGcm.TagByteSizes.MinSize));
+ Assert.Throws<PlatformNotSupportedException>(() => new AesGcm(key.AsSpan(), AesGcm.TagByteSizes.MinSize));
}
[Fact]