Skip to content

Commit

Permalink
Add Span support for AesCtr
Browse files Browse the repository at this point in the history
  • Loading branch information
dorssel committed Jan 9, 2025
1 parent d3fffb5 commit 53c8875
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 15 deletions.
1 change: 1 addition & 0 deletions .github/linters/vs-spell-exclusion.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ dotnet
Encryptor
inliner
IntelliSense
netstandard
NIST
xorend
124 changes: 122 additions & 2 deletions AesExtra/AesCtr.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ public sealed class AesCtr
const CipherMode FixedCipherMode = CipherMode.ECB; // DevSkim: ignore DS187371
const PaddingMode FixedPaddingMode = PaddingMode.None;
const int FixedFeedbackSize = FixedBlockSize * 8; // bits
static readonly byte[] BlockOfZeros = new byte[FixedBlockSize];

/// <inheritdoc cref="Aes.Create()" />
public static new Aes Create()
public static new AesCtr Create()
{
return new AesCtr();
}
Expand Down Expand Up @@ -129,7 +130,7 @@ AesCtrTransform CreateTransform(byte[] rgbKey, byte[]? rgbIV)
{
// ECB.Encrypt === ECB.Decrypt; the transform is entirely symmetric.
// ECB does not use an IV; the IV we received is actually the initial counter for AES-CTR.
return new(rgbIV ?? new byte[FixedBlockSize], AesEcb.CreateEncryptor(rgbKey, new byte[FixedBlockSize]));
return new(rgbIV ?? BlockOfZeros, AesEcb.CreateEncryptor(rgbKey, BlockOfZeros));
}

/// <inheritdoc cref="AesManaged.CreateDecryptor(byte[], byte[])" />
Expand All @@ -155,4 +156,123 @@ public override void GenerateKey()
{
AesEcb.GenerateKey();
}

#region Modern_SymmetricAlgorithm
bool TryTransformCtr(ReadOnlySpan<byte> input, Span<byte> destination, out int bytesWritten)
{
if (destination.Length < input.Length)
{
bytesWritten = 0;
return false;
}
using var transform = new AesCtrTransform(IV, AesEcb.CreateEncryptor(Key, BlockOfZeros));
var inputSlice = input;
var destinationSlice = destination;
while (inputSlice.Length >= FixedBlockSize)
{
// full blocks
transform.TransformBlock(inputSlice, destinationSlice);
inputSlice = inputSlice[FixedBlockSize..];
destinationSlice = destinationSlice[FixedBlockSize..];
}
if (!inputSlice.IsEmpty)
{
// final partial block (if any)
Span<byte> block = stackalloc byte[FixedBlockSize];
inputSlice.CopyTo(block);
transform.TransformBlock(block, block);
block[0..inputSlice.Length].CopyTo(destinationSlice);
CryptographicOperations.ZeroMemory(block);
}
bytesWritten = input.Length;
return true;
}

byte[] TransformCtr(ReadOnlySpan<byte> input)
{
var output = new byte[input.Length];
_ = TryTransformCtr(input, output, out _);
return output;
}

int TransformCtr(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{
return TryTransformCtr(plaintext, destination, out var bytesWritten) ? bytesWritten
: throw new ArgumentException("Destination is too short.");
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="plaintext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public byte[] EncryptCtr(byte[] plaintext)
{
return TransformCtr(plaintext);
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="plaintext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public byte[] EncryptCtr(ReadOnlySpan<byte> plaintext)
{
return TransformCtr(plaintext);
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="plaintext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <param name="destination">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public int EncryptCtr(ReadOnlySpan<byte> plaintext, Span<byte> destination)
{
return TransformCtr(plaintext, destination);
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="plaintext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <param name="destination">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <param name="bytesWritten">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public bool TryEncryptCtr(ReadOnlySpan<byte> plaintext, Span<byte> destination, out int bytesWritten)
{
return TryTransformCtr(plaintext, destination, out bytesWritten);
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="ciphertext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public byte[] DecryptCtr(byte[] ciphertext)
{
return TransformCtr(ciphertext);
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="ciphertext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public byte[] DecryptCtr(ReadOnlySpan<byte> ciphertext)
{
return TransformCtr(ciphertext);
}

/// <summary>
/// TODO

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// </summary>
/// <param name="ciphertext">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <param name="destination">TODO</param>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
/// <returns>TODO</returns>

Check notice

Code scanning / devskim

A "TODO" or similar was left in source code, possibly indicating incomplete functionality Note

Suspicious comment
public int DecryptCtr(ReadOnlySpan<byte> ciphertext, Span<byte> destination)
{
return TransformCtr(ciphertext, destination);
}
#endregion
}
11 changes: 6 additions & 5 deletions AesExtra/AesCtrTransform.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ internal AesCtrTransform(byte[] initialCounter, ICryptoTransform aesEcbTransform
}
Counter = initialCounter;
}

void Purge()
{
CryptographicOperations.ZeroMemory(XorBlock);
Expand Down Expand Up @@ -93,12 +94,12 @@ void CIPH_K(byte[] X, byte[] output)

readonly byte[] XorBlock = new byte[BLOCKSIZE];

void TransformBlock(byte[] inputBlockBase, int inputBlockOffset, byte[] outputBlockBase, int outputBlockOffset)
internal void TransformBlock(ReadOnlySpan<byte> inputBlock, Span<byte> destination)
{
CIPH_K(Counter, XorBlock);
for (var i = 0; i < BLOCKSIZE; ++i)
{
outputBlockBase[outputBlockOffset + i] = (byte)(inputBlockBase[inputBlockOffset + i] ^ XorBlock[i]);
destination[i] = (byte)(inputBlock[i] ^ XorBlock[i]);
}
IncrementCounter();
}
Expand All @@ -122,12 +123,12 @@ int ICryptoTransform.TransformBlock(byte[] inputBuffer, int inputOffset, int inp
// NOTE: All other validation is implicitly done by the array access itself.
if (inputCount % BLOCKSIZE != 0)
{
throw new ArgumentOutOfRangeException(nameof(inputCount));
throw new ArgumentException("Input must be a multiple of the block size.", nameof(inputCount));
}

for (var i = 0; i < inputCount / BLOCKSIZE; ++i)
{
TransformBlock(inputBuffer, inputOffset + (i * BLOCKSIZE), outputBuffer, outputOffset + (i * BLOCKSIZE));
TransformBlock(inputBuffer.AsSpan(inputOffset + (i * BLOCKSIZE), BLOCKSIZE), outputBuffer.AsSpan(outputOffset + (i * BLOCKSIZE)));
}
return inputCount;
}
Expand All @@ -153,7 +154,7 @@ byte[] ICryptoTransform.TransformFinalBlock(byte[] inputBuffer, int inputOffset,

var block = new byte[BLOCKSIZE];
Array.Copy(inputBuffer, inputOffset, block, 0, inputCount);
TransformBlock(block, 0, block, 0);
TransformBlock(block, block);
Array.Resize(ref block, inputCount);
HasProcessedFinal = true;

Expand Down
8 changes: 6 additions & 2 deletions AesExtra/AesExtra.csproj
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<!--
SPDX-FileCopyrightText: 2022 Frans van Dorsselaer
Expand All @@ -7,7 +7,7 @@ SPDX-License-Identifier: MIT
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFrameworks>netstandard2.0;net8.0</TargetFrameworks>
<RootNamespace>Dorssel.Security.Cryptography</RootNamespace>
<AssemblyName>Dorssel.Security.Cryptography.AesExtra</AssemblyName>

Expand All @@ -21,4 +21,8 @@ SPDX-License-Identifier: MIT
<None Include="..\README.md" Pack="true" PackagePath="" Visible="false" />
</ItemGroup>

<ItemGroup Condition=" '$(TargetFramework)' == 'netstandard2.0' ">
<PackageReference Include="Microsoft.Bcl.Memory" />
</ItemGroup>

</Project>
2 changes: 1 addition & 1 deletion AesExtra/AesSiv.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ byte[] S2V(byte[][] associatedData, byte[] plaintext)
_ = Cmac.TransformBlock(plaintext, 0, plaintext.Length - BLOCKSIZE, null, 0);
_ = Cmac.TransformBlock(D, 0, BLOCKSIZE, null, 0);
_ = Cmac.TransformFinalBlock([], 0, 0);
return Cmac.Hash;
return Cmac.Hash!;
}
else
{
Expand Down
10 changes: 7 additions & 3 deletions AesExtra/CryptographicOperations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@
//
// SPDX-License-Identifier: MIT

#if NETSTANDARD2_0

using System.Runtime.CompilerServices;

namespace Dorssel.Security.Cryptography;

/// <summary>
/// This is a backport of .NET 9. Since this library is for .NET Standard 2.0 it uses <see cref="byte"/>[] instead of Span.
/// This is a backport of .NET 9.
/// <para/>
/// See:
/// <see href="https://github.com/dotnet/runtime/blob/main/src/libraries/System.Security.Cryptography/src/System/Security/Cryptography/CryptographicOperations.cs"/>
/// </summary>
static class CryptographicOperations
{
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static bool FixedTimeEquals(byte[] left, byte[] right)
public static bool FixedTimeEquals(ReadOnlySpan<byte> left, ReadOnlySpan<byte> right)
{
// NoOptimization because we want this method to be exactly as non-short-circuiting
// as written.
Expand All @@ -40,7 +42,7 @@ public static bool FixedTimeEquals(byte[] left, byte[] right)
}

[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static void ZeroMemory(byte[] buffer)
public static void ZeroMemory(Span<byte> buffer)
{
// NoOptimize to prevent the optimizer from deciding this call is unnecessary
// NoInlining to prevent the inliner from forgetting that the method was no-optimize
Expand All @@ -50,3 +52,5 @@ public static void ZeroMemory(byte[] buffer)
}
}
}

#endif
2 changes: 2 additions & 0 deletions AesExtra/GlobalSuppressions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@

[assembly: SuppressMessage("Security", "CA5358:Review cipher mode usage with cryptography experts", Justification = "Done :)")]
[assembly: SuppressMessage("Security", "CA5401:Do not use CreateEncryptor with non-default IV", Justification = "We only use ECB, which does not use an IV")] // DevSkim: ignore DS187371
[assembly: SuppressMessage("Maintainability", "CA1512:Use ArgumentOutOfRangeException throw helper", Justification = "Not available in netstandard2.0.")]
[assembly: SuppressMessage("Maintainability", "CA1513:Use ObjectDisposedException throw helper", Justification = "Not available in netstandard2.0.")]
3 changes: 3 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,11 @@ SPDX-License-Identifier: MIT
<SelfContained>false</SelfContained>
<IsPackable>false</IsPackable>
<IsPublishable>false</IsPublishable>
<IsTrimmable Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net6.0'))">true</IsTrimmable>
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\strongname.snk</AssemblyOriginatorKeyFile>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<OptimizeImplicitlyTriggeredBuild>True</OptimizeImplicitlyTriggeredBuild>
<DisableRuntimeMarshalling>True</DisableRuntimeMarshalling>

<!-- Assembly metadata -->
<Product>dotnet-aes-extra</Product>
Expand Down
2 changes: 2 additions & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ SPDX-License-Identifier: MIT
<ItemGroup>
<!-- all -->
<PackageVersion Include="Dorssel.GitVersion.MsBuild" Version="1.1.0" />
<!-- netstandard2.0 -->
<PackageVersion Include="Microsoft.Bcl.Memory" Version="9.0.0" />
<!-- UnitTests -->
<PackageVersion Include="Moq" Version="4.20.72" />
</ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion UnitTests/AesCtrTransform_Tests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ public void TransformBlock_ValidSize(int size)
[DataRow(BLOCKSIZE + 1)]
public void TransformBlock_InvalidSizeFails(int size)
{
_ = Assert.ThrowsException<ArgumentOutOfRangeException>(() =>
_ = Assert.ThrowsException<ArgumentException>(() =>
{
using ICryptoTransform transform = new AesCtrTransform(InitialCounter, AesEcbTransform);
_ = transform.TransformBlock(new byte[size], 0, size, new byte[size], 0);
Expand Down
12 changes: 12 additions & 0 deletions UnitTests/AesCtr_KAT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ public void Encrypt_Write(NistAesCtrSampleTestVector testVector)
CollectionAssert.AreEqual(testVector.Ciphertext.ToArray(), ciphertextStream.ToArray());
}

[TestMethod]
[TestCategory("NIST")]
[NistAesCtrSampleDataSource]
public void EncryptCtr(NistAesCtrSampleTestVector testVector)
{
using var aes = AesCtr.Create();
aes.Key = testVector.Key.ToArray();
aes.IV = testVector.InitialCounter.ToArray();
var ciphertext = aes.EncryptCtr(testVector.Plaintext.Span);
CollectionAssert.AreEqual(testVector.Ciphertext.ToArray(), ciphertext);
}

[TestMethod]
[TestCategory("NIST")]
[NistAesCtrSampleDataSource]
Expand Down
2 changes: 1 addition & 1 deletion UnitTests/UnitTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ SPDX-License-Identifier: MIT
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\AesExtra\AesExtra.csproj" />
<ProjectReference Include="..\AesExtra\AesExtra.csproj" AdditionalProperties="TargetFramework=netstandard2.0" />
</ItemGroup>

</Project>

0 comments on commit 53c8875

Please sign in to comment.