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() {