diff --git a/.github/linters/vs-spell-exclusion.txt b/.github/linters/vs-spell-exclusion.txt
index b6e3af2..47bf968 100644
--- a/.github/linters/vs-spell-exclusion.txt
+++ b/.github/linters/vs-spell-exclusion.txt
@@ -7,7 +7,7 @@ Dorssel
dotnet
Encryptor
inliner
-IntelliSense
+Intelli
netstandard
NIST
xorend
diff --git a/AesExtra/AesCmac.cs b/AesExtra/AesCmac.cs
index a3a221c..dacbd79 100644
--- a/AesExtra/AesCmac.cs
+++ b/AesExtra/AesCmac.cs
@@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT
using System.Diagnostics.CodeAnalysis;
+using System.Runtime.CompilerServices;
using System.Security.Cryptography;
namespace Dorssel.Security.Cryptography;
@@ -14,6 +15,7 @@ public sealed class AesCmac
: KeyedHashAlgorithm
{
const int BLOCKSIZE = 16; // bytes
+ const int BitsPerByte = 8;
///
/// A new instance.
@@ -35,12 +37,13 @@ public sealed class AesCmac
///
/// Initializes a new instance of the class with a randomly generated key.
///
- public AesCmac()
+ /// The size, in bits, of the randomly generated key.
+ public AesCmac(int keySize = 256)
{
AesEcb = Aes.Create();
AesEcb.Mode = CipherMode.ECB; // DevSkim: ignore DS187371
- AesEcb.Padding = PaddingMode.None;
- CryptoTransform = AesEcb.CreateEncryptor();
+ AesEcb.BlockSize = BLOCKSIZE * BitsPerByte;
+ AesEcb.KeySize = keySize;
HashSizeValue = BLOCKSIZE * 8;
}
@@ -54,102 +57,144 @@ public AesCmac(byte[] key)
Key = key;
}
- void Purge()
- {
- CryptographicOperations.ZeroMemory(C);
- CryptographicOperations.ZeroMemory(Partial);
- }
-
#region IDisposable
- bool IsDisposed;
-
///
protected override void Dispose(bool disposing)
{
- if (!IsDisposed)
+ if (disposing)
{
- if (disposing)
- {
- CryptoTransform.Dispose();
- AesEcb.Dispose();
- Purge();
- }
- IsDisposed = true;
+ CryptographicOperations.ZeroMemory(KeyValue);
+ CryptographicOperations.ZeroMemory(K1Value);
+ CryptographicOperations.ZeroMemory(K2Value);
+ CryptographicOperations.ZeroMemory(C);
+ CryptographicOperations.ZeroMemory(Partial);
+ AesEcb.Dispose();
+ CryptoTransformValue?.Dispose();
+ CryptoTransformValue = null;
+ K1Value = null;
+ K2Value = null;
+ PartialLength = 0;
+ State = 0;
}
base.Dispose(disposing);
}
#endregion
- ///
+ ///
public override byte[] Key
{
get => AesEcb.Key;
set
{
- CryptoTransform.Dispose();
+ if (State != 0)
+ {
+ throw new InvalidOperationException("Key cannot be changed during a computation.");
+ }
AesEcb.Key = value;
- CryptoTransform = AesEcb.CreateEncryptor();
+ CryptographicOperations.ZeroMemory(K1Value);
+ CryptographicOperations.ZeroMemory(K2Value);
+ CryptoTransformValue?.Dispose();
+ CryptoTransformValue = null;
+ K1Value = null;
+ K2Value = null;
}
}
readonly Aes AesEcb;
- ICryptoTransform CryptoTransform;
+ ICryptoTransform? CryptoTransformValue;
+
+ ICryptoTransform CryptoTransform
+ {
+ get
+ {
+ CryptoTransformValue ??= AesEcb.CreateEncryptor();
+ return CryptoTransformValue;
+ }
+ }
// See: NIST SP 800-38B, Section 6.2, Step 5
readonly byte[] C = new byte[BLOCKSIZE];
- // See: NIST SP 800-38B, Section 4.2.2
- //
- // In-place: X = CIPH_K(X)
- void CIPH_K_InPlace(byte[] X_Base, int X_Offset = 0)
- {
- _ = CryptoTransform.TransformBlock(X_Base, X_Offset, BLOCKSIZE, X_Base, X_Offset);
+ byte[]? K1Value;
+ byte[]? K2Value;
+
+ // See: NIST SP 800-38B, Section 6.1
+ byte[] K1 {
+ get
+ {
+ if (K1Value is null)
+ {
+ // Step 1: K1Value has the role of L
+ K1Value = new byte[BLOCKSIZE];
+ CIPH_K_InPlace(K1Value);
+ // Step 2: K1Value has the role of K1
+ K1Value.dbl_InPlace();
+ }
+ // Step 4: return K1
+ return K1Value;
+ }
}
// See: NIST SP 800-38B, Section 6.1
- //
- // Returns: first ? K1 : K2
- byte[] SUBK(bool first)
+ byte[] K2
{
- var X = new byte[BLOCKSIZE];
- // Step 1: X has the role of L
- CIPH_K_InPlace(X);
- // Step 2: X has the role of K1
- X.dbl_InPlace();
- if (first)
+ get
{
- // Step 4: return K1
- return X;
+ if (K2Value is null)
+ {
+ // Step 3: K2Value has the role of K1
+ K2Value = (byte[])K1.Clone();
+ K2Value.dbl_InPlace();
+ }
+ // Step 4: return K2
+ return K2Value;
}
- // Step 3: X has the role of K1
- X.dbl_InPlace();
- // Step 4: return K2
- return X;
+ }
+
+ // See: NIST SP 800-38B, Section 4.2.2
+ //
+ // In-place: X = CIPH_K(X)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void CIPH_K_InPlace(byte[] X)
+ {
+ _ = CryptoTransform.TransformBlock(X, 0, BLOCKSIZE, X, 0);
}
///
public override void Initialize()
{
// See: NIST SP 800-38B, Section 6.2, Step 5
- Purge();
-
+ C.AsSpan().Clear();
PartialLength = 0;
+ State = 0;
}
readonly byte[] Partial = new byte[BLOCKSIZE];
int PartialLength;
// See: NIST SP 800-38B, Section 6.2, Step 6
- void AddBlock(byte[] blockBase, int blockOffset = 0)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ void AddBlock(ReadOnlySpan block)
{
- C.xor_InPlace(0, blockBase, blockOffset, BLOCKSIZE);
+ C.xor_InPlace(block);
CIPH_K_InPlace(C);
}
- ///
+ ///
protected override void HashCore(byte[] array, int ibStart, int cbSize)
{
- if (cbSize == 0)
+ HashCore(array.AsSpan(ibStart, cbSize));
+ }
+
+ ///
+#if !NETSTANDARD2_0
+ protected override
+#endif
+ void HashCore(ReadOnlySpan source)
+ {
+ State = 1;
+
+ if (source.Length == 0)
{
return;
}
@@ -157,17 +202,16 @@ protected override void HashCore(byte[] array, int ibStart, int cbSize)
// If we have a non-empty && non-full Partial block already -> append to that first.
if (PartialLength is > 0 and < BLOCKSIZE)
{
- var count = Math.Min(cbSize, BLOCKSIZE - PartialLength);
- Array.Copy(array, ibStart, Partial, PartialLength, count);
+ var count = Math.Min(source.Length, BLOCKSIZE - PartialLength);
+ source[..count].CopyTo(Partial.AsSpan(PartialLength));
PartialLength += count;
- if (count == cbSize)
+ if (count == source.Length)
{
// No more data supplied, we're done. Even if we filled up Partial completely,
// because we don't know if it will be the final block.
return;
}
- ibStart += count;
- cbSize -= count;
+ source = source[count..];
}
// We get here only if Partial is either empty or full (i.e. we are block-aligned) && there is more to "hash".
@@ -181,54 +225,57 @@ protected override void HashCore(byte[] array, int ibStart, int cbSize)
// We get here only if Partial is empty && there is more to "hash".
// Add complete, non-final blocks. Never add the last block given in this call since we don't know if that will be the final block.
- for (int i = 0, nonFinalBlockCount = (cbSize - 1) / BLOCKSIZE; i < nonFinalBlockCount; i++)
+ for (int i = 0, nonFinalBlockCount = (source.Length - 1) / BLOCKSIZE; i < nonFinalBlockCount; i++)
{
// See: NIST SP 800-38B, Section 6.2, Steps 3 and 6
- AddBlock(array, ibStart);
- ibStart += BLOCKSIZE;
- cbSize -= BLOCKSIZE;
+ AddBlock(source[..BLOCKSIZE]);
+ source = source[BLOCKSIZE..];
}
// Save what we have left (we always have some, by construction).
- Array.Copy(array, ibStart, Partial, 0, cbSize);
- PartialLength = cbSize;
+ source.CopyTo(Partial);
+ PartialLength = source.Length;
}
///
protected override byte[] HashFinal()
{
+ var result = new byte[BLOCKSIZE];
+ _ = TryHashFinal(result, out _);
+ return result;
+ }
+
+ ///
+#if !NETSTANDARD2_0
+ protected override
+#endif
+ bool TryHashFinal(Span destination, out int bytesWritten)
+ {
+ // See: NIST SP 800-38B, Section 6.2, Step 4
// Partial now has the role of Mn*
if (PartialLength == BLOCKSIZE)
{
// See: NIST SP 800-38B, Section 6.2, Step 1: K1
- var K1 = SUBK(true);
- Partial.xor_InPlace(0, K1, 0, BLOCKSIZE);
+ Partial.xor_InPlace(K1);
// Partial now has the role of Mn
}
else
{
// Add padding
Partial[PartialLength] = 0x80;
- for (var i = PartialLength + 1; i < BLOCKSIZE; ++i)
- {
- Partial[i] = 0x00;
- }
+ Partial.AsSpan(PartialLength + 1).Clear();
// See: NIST SP 800-38B, Section 6.2, Step 1: K2
- var K2 = SUBK(false);
- Partial.xor_InPlace(0, K2, 0, BLOCKSIZE);
+ Partial.xor_InPlace(K2);
// Partial now has the role of Mn
}
- // See: NIST SP 800-38B, Section 6.2, Steps 4 and 6
+ // See: NIST SP 800-38B, Section 6.2, Step 6
AddBlock(Partial);
- PartialLength = 0;
- // NOTE: KeyedHashAlgorithm exposes the returned array reference as the
- // Hash property, so we must *not* return C itself as it may be reused.
- var cmac = new byte[BLOCKSIZE];
- C.CopyTo(cmac, 0);
+ C.CopyTo(destination);
- Purge();
+ Initialize();
- return cmac;
+ bytesWritten = BLOCKSIZE;
+ return true;
}
}
diff --git a/AesExtra/AesExtra.csproj b/AesExtra/AesExtra.csproj
index 17ee546..b1600e7 100644
--- a/AesExtra/AesExtra.csproj
+++ b/AesExtra/AesExtra.csproj
@@ -13,7 +13,8 @@ SPDX-License-Identifier: MIT
true
Dorssel.Security.Cryptography.AesExtra
- True
+
+ True
diff --git a/AesExtra/AesSiv.cs b/AesExtra/AesSiv.cs
index 16a7c07..9af6c93 100644
--- a/AesExtra/AesSiv.cs
+++ b/AesExtra/AesSiv.cs
@@ -55,12 +55,12 @@ byte[] S2V(byte[][] associatedData, byte[] plaintext)
foreach (var S in associatedData)
{
D.dbl_InPlace();
- D.xor_InPlace(0, Cmac.ComputeHash(S), 0, BLOCKSIZE);
+ D.xor_InPlace(Cmac.ComputeHash(S));
}
if (plaintext.Length >= BLOCKSIZE)
{
// D takes the role of the "end" in "xorend"
- D.xor_InPlace(0, plaintext, plaintext.Length - BLOCKSIZE, BLOCKSIZE);
+ D.xor_InPlace(plaintext.AsSpan(plaintext.Length - BLOCKSIZE));
// Using Transform instead of Compute prevents cloning plaintext.
_ = Cmac.TransformBlock(plaintext, 0, plaintext.Length - BLOCKSIZE, null, 0);
_ = Cmac.TransformBlock(D, 0, BLOCKSIZE, null, 0);
@@ -71,7 +71,7 @@ byte[] S2V(byte[][] associatedData, byte[] plaintext)
{
D.dbl_InPlace();
// This implements pad() as well.
- D.xor_InPlace(0, plaintext, 0, plaintext.Length);
+ D.AsSpan(0, plaintext.Length).xor_InPlace(plaintext);
D[plaintext.Length] ^= 0x80;
return Cmac.ComputeHash(D);
}
diff --git a/AesExtra/ExtensionMethods.cs b/AesExtra/ExtensionMethods.cs
index 543d065..ce2faef 100644
--- a/AesExtra/ExtensionMethods.cs
+++ b/AesExtra/ExtensionMethods.cs
@@ -2,6 +2,9 @@
//
// SPDX-License-Identifier: MIT
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
namespace Dorssel.Security.Cryptography;
static class ExtensionMethods
@@ -51,12 +54,24 @@ public static void dbl_InPlace(this byte[] S)
//
// In place: X = (X xor Y)
#pragma warning disable IDE1006 // Naming Styles
- public static void xor_InPlace(this byte[] X_Base, int X_Offset, byte[] Y_Base, int Y_Offset, int count)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void xor_InPlace(this byte[] X, ReadOnlySpan Y)
+#pragma warning restore IDE1006 // Naming Styles
+ {
+ X.AsSpan().xor_InPlace(Y);
+ }
+
+ // See: NIST SP 800-38B, Section 4.2.2
+ //
+ // In place: X = (X xor Y)
+#pragma warning disable IDE1006 // Naming Styles
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static void xor_InPlace(this Span X, ReadOnlySpan Y)
#pragma warning restore IDE1006 // Naming Styles
{
- for (var i = 0; i < count; ++i)
+ for (var i = 0; i < X.Length; ++i)
{
- X_Base[X_Offset + i] ^= Y_Base[Y_Offset + i];
+ X[i] ^= Y[i];
}
}
}
diff --git a/Directory.Build.props b/Directory.Build.props
index 805b3dc..9c3a224 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -48,7 +48,7 @@ SPDX-License-Identifier: MIT
$(Product)
$(Company)
- .NET Standard 2.0 implementation of AES-CTR, AES-CMAC, and SIV-AES.
+ Provides types for additional cryptographic algorithms: AES-CTR, AES-CMAC, and SIV-AES.
MIT
README.md
false
diff --git a/UnitTests/AesCmac_Tests.cs b/UnitTests/AesCmac_Tests.cs
index b2a29bf..8243e41 100644
--- a/UnitTests/AesCmac_Tests.cs
+++ b/UnitTests/AesCmac_Tests.cs
@@ -106,6 +106,86 @@ public void Key_Change()
}
}
+ sealed class TestStream() : Stream
+ {
+ public ManualResetEventSlim IsWaitingToEnd = new();
+ public ManualResetEventSlim EndStream = new();
+
+ long _Position;
+
+ protected override void Dispose(bool disposing)
+ {
+ IsWaitingToEnd.Dispose();
+ EndStream.Dispose();
+ base.Dispose(disposing);
+ }
+
+ public override bool CanRead => true;
+
+ public override bool CanSeek => false;
+
+ public override bool CanWrite => false;
+
+ public override long Length => throw new NotImplementedException();
+
+ public override long Position { get => _Position; set => throw new NotImplementedException(); }
+
+ public override void Flush()
+ {
+ throw new NotImplementedException();
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ if (_Position >= 64)
+ {
+ IsWaitingToEnd.Set();
+ EndStream.Wait();
+ return 0;
+ }
+ buffer.AsSpan(offset, count).Clear();
+ _Position += count;
+ return count;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void SetLength(long value)
+ {
+ throw new NotImplementedException();
+ }
+
+ public override void Write(byte[] buffer, int offset, int count)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ [TestMethod]
+ public void Key_ChangeWhileBusy()
+ {
+ using var aesCmac = new AesCmac();
+ using var allowRead = new ManualResetEventSlim();
+ using var testStream = new TestStream();
+ var task = aesCmac.ComputeHashAsync(testStream);
+ testStream.IsWaitingToEnd.Wait();
+ // hashing has begun, and the stream is blocking
+
+ Assert.ThrowsException(() =>
+ {
+ aesCmac.Key = new byte[aesCmac.Key.Length];
+ });
+
+ // continue with the existing operation
+ testStream.EndStream.Set();
+ task.Wait();
+
+ Assert.IsTrue(task.IsCompletedSuccessfully);
+ }
+
[TestMethod]
public void ComputeHash_Segmented()
{