Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
2db4c46
docs: spec the keyed hash delivery, split from authenticated encrypti…
matt-edmondson Aug 25, 2026
3c882c2
docs: add the keyed hash provider implementation plan [patch]
matt-edmondson Aug 25, 2026
acefdbf
feat: add FixedTimeComparison for authentication tag comparison [patch]
matt-edmondson Aug 25, 2026
0a9bb4d
docs: add comment explaining CS1574 pragma suppression for forward re…
matt-edmondson Aug 25, 2026
fffe822
feat: add IKeyedHashProvider with buffering incremental default [patch]
matt-edmondson Aug 25, 2026
702c339
feat: add HMAC-SHA-256 keyed hash provider over a shared core [patch]
matt-edmondson Aug 25, 2026
f4485aa
fix: zero bytesWritten on length mismatch and drop unreachable CA1859…
matt-edmondson Aug 25, 2026
0e1e4d0
feat: add HMAC-SHA-384 and HMAC-SHA-512 keyed hash providers [patch]
matt-edmondson Aug 25, 2026
36a6a32
feat: register keyed hash providers in Essentials.All [patch]
matt-edmondson Aug 25, 2026
d7d9fd7
docs: use consistent bundled keyed hash provider wording [patch]
matt-edmondson Aug 25, 2026
6294c82
docs: state that encryption providers give confidentiality only [patch]
matt-edmondson Aug 25, 2026
2c6f723
docs: authenticate the IV together with the ciphertext, not ciphertex…
matt-edmondson Aug 25, 2026
f43577a
feat: add keyed hash providers for message authentication [minor]
matt-edmondson Aug 25, 2026
2b5d195
docs: correct plan defects found during execution [patch]
matt-edmondson Aug 25, 2026
f87fb8a
docs: authenticate the IV together with the ciphertext in the keyed h…
matt-edmondson Aug 25, 2026
851c40e
fix: address final review findings on keyed hash provider branch [patch]
matt-edmondson Aug 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ This is a .NET library (`ktsu.Essentials`) providing high-performance interfaces
- `Essentials/IncrementalHashAdapter.cs` - Public adapter over `System.Security.Cryptography.IncrementalHash`, shared by the cryptographic hash providers
- `Essentials/BufferingIncrementalHash.cs` - Internal buffering fallback behind the `CreateIncremental()` default body
- `Shared/NonCryptoIncrementalHash.cs` - Adapter over `NonCryptographicHashAlgorithm`, linked into the six `System.IO.Hashing` providers rather than placed in the interfaces-only package
- `Essentials/IKeyedHashProvider.cs` - Keyed hashing (HMAC) interface for authenticating data with a secret key
- `Essentials/FixedTimeComparison.cs` - Static fixed-time byte comparison for tags obtained outside `IKeyedHashProvider.Verify`
- `Shared/HmacKeyedHashCore.cs` - HMAC implementation shared across algorithms, linked into the three keyed hash provider projects rather than placed in the interfaces package
- `Essentials/ISerializationProvider.cs` - Object serialization/deserialization interface
- `Essentials/ISerializationOptions.cs` - Configurable serialization options (naming, inclusion, boxing policies)
- `Essentials/ICacheProvider.cs` - Generic cache interface with expiration and get-or-add
Expand All @@ -58,6 +61,7 @@ Each provider implementation ships as its own project/package named `Essentials.
- **ObfuscationProviders**: Xor, Caesar, Reverse, BitRotate, Base64, Hex, Composite
- **EncryptionProviders**: Aes
- **HashProviders**: MD5, SHA1, SHA256, SHA384, SHA512, FNV1_32, FNV1a_32, FNV1_64, FNV1a_64, CRC32, CRC64, XxHash32, XxHash64, XxHash3, XxHash128
- **KeyedHashProviders**: HmacSha256, HmacSha384, HmacSha512
- **SerializationProviders**: Json (System.Text.Json), NewtonsoftJson, Yaml, Toml
- **FileSystemProviders**: Native
- **CommandExecutors**: Native
Expand All @@ -84,7 +88,7 @@ All provider interfaces follow a consistent three-tier pattern:

1. **Core Try\* methods**: Buffer-based methods over `Span<byte>` or `Stream`. Span overloads are `bool TryX(source, destination, out int bytesWritten)`, paired with a `GetMax…Length` bound per category so callers can size buffers. These are the only methods implementers must provide.
2. **Convenience methods**: Self-allocating methods that call Try\* methods and manage buffers automatically. Provided via default interface implementations.
3. **Async variants**: Task-based async versions with `CancellationToken` support. The stream paths of the compression providers and of `AesEncryptionProvider`, along with `IHashProvider.TryHashAsync(Stream, ...)`, are genuinely asynchronous — real `ReadAsync`/`WriteAsync`, no thread held. The rest are still `Task.Run` wrappers over synchronous work via `ProviderHelpers.RunAsync()`; see issue #8. A provider makes its stream paths genuine by declaring the two `Try…Async(Stream, Stream, ...)` primitives itself, which replaces the default implementation; the four derived stream defaults compose over those primitives, so overriding two members converts all six. Span-destination async overloads do not exist — an `out` parameter cannot cross an async boundary.
3. **Async variants**: Task-based async versions with `CancellationToken` support. The stream paths of the compression providers and of `AesEncryptionProvider`, along with `IHashProvider.TryHashAsync(Stream, ...)` and `IKeyedHashProvider.TryHashAsync(ReadOnlyMemory<byte>, Stream, ...)`, are genuinely asynchronous — real `ReadAsync`/`WriteAsync`, no thread held. The rest are still `Task.Run` wrappers over synchronous work via `ProviderHelpers.RunAsync()`; see issue #8. A provider makes its stream paths genuine by declaring the two `Try…Async(Stream, Stream, ...)` primitives itself, which replaces the default implementation; the four derived stream defaults compose over those primitives, so overriding two members converts all six. Span-destination async overloads do not exist — an `out` parameter cannot cross an async boundary.

Common patterns are centralized in `ProviderHelpers.cs`:

Expand All @@ -103,6 +107,7 @@ Tests use **MSTest.Sdk** targeting net10.0 only. The test project (`Essentials.T

- `HashProviderTests.cs` - Tests all 15 hash provider implementations
- `IncrementalHashTests.cs` - Tests `CreateIncremental()` and async stream hashing across all 15 hash providers, asserting incremental output equals one-shot output
- `KeyedHashProviderTests.cs` - Tests all 3 HMAC keyed hash providers, `Verify`, and `FixedTimeComparison`
- `CacheProviderTests.cs` - Tests cache operations including expiration
- `CommandExecutorTests.cs` - Tests command execution
- `EncodingProviderTests.cs` - Tests Base64 and Hex encoding
Expand Down
2 changes: 1 addition & 1 deletion DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
A comprehensive .NET library providing high-performance interfaces and ready-to-use implementations for common cross-cutting concerns including compression (Gzip, Brotli, Deflate, ZLib), encoding (Base64, Hex), obfuscation (XOR, Caesar, bit-rotation, byte-reversal, Base64, Hex, and composable chains), encryption (AES), hashing (15 algorithms including SHA, MD5, CRC, FNV, XxHash), serialization (System.Text.Json, Newtonsoft.Json, YAML, TOML), caching, persistence, validation, logging, navigation, command execution, and filesystem access. Features zero-allocation Span-based operations, default interface implementations to minimize boilerplate, and async variants with CancellationToken support. Install everything with the ktsu.Essentials.All meta-package, or cherry-pick individual provider packages.
A comprehensive .NET library providing high-performance interfaces and ready-to-use implementations for common cross-cutting concerns including compression (Gzip, Brotli, Deflate, ZLib), encoding (Base64, Hex), obfuscation (XOR, Caesar, bit-rotation, byte-reversal, Base64, Hex, and composable chains), encryption (AES), hashing (15 algorithms including SHA, MD5, CRC, FNV, XxHash), keyed hashing and message authentication (HMAC-SHA256/384/512, with fixed-time tag verification), serialization (System.Text.Json, Newtonsoft.Json, YAML, TOML), caching, persistence, validation, logging, navigation, command execution, and filesystem access. Features zero-allocation Span-based operations, default interface implementations to minimize boilerplate, and async variants with CancellationToken support. Install everything with the ktsu.Essentials.All meta-package, or cherry-pick individual provider packages.
3 changes: 3 additions & 0 deletions Essentials.All/Essentials.All.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@
<ProjectReference Include="..\Essentials.HashProviders.XxHash3\Essentials.HashProviders.XxHash3.csproj" />
<ProjectReference Include="..\Essentials.HashProviders.XxHash32\Essentials.HashProviders.XxHash32.csproj" />
<ProjectReference Include="..\Essentials.HashProviders.XxHash64\Essentials.HashProviders.XxHash64.csproj" />
<ProjectReference Include="..\Essentials.KeyedHashProviders.HmacSha256\Essentials.KeyedHashProviders.HmacSha256.csproj" />
<ProjectReference Include="..\Essentials.KeyedHashProviders.HmacSha384\Essentials.KeyedHashProviders.HmacSha384.csproj" />
<ProjectReference Include="..\Essentials.KeyedHashProviders.HmacSha512\Essentials.KeyedHashProviders.HmacSha512.csproj" />
<ProjectReference Include="..\Essentials.LoggingProviders.Console\Essentials.LoggingProviders.Console.csproj" />
<ProjectReference Include="..\Essentials.NavigationProviders.InMemory\Essentials.NavigationProviders.InMemory.csproj" />
<ProjectReference Include="..\Essentials.ObfuscationProviders.Base64\Essentials.ObfuscationProviders.Base64.csproj" />
Expand Down
19 changes: 19 additions & 0 deletions Essentials.All/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ namespace ktsu.Essentials.All;
using ktsu.Essentials.HashProviders.XxHash3;
using ktsu.Essentials.HashProviders.XxHash32;
using ktsu.Essentials.HashProviders.XxHash64;
using ktsu.Essentials.KeyedHashProviders.HmacSha256;
using ktsu.Essentials.KeyedHashProviders.HmacSha384;
using ktsu.Essentials.KeyedHashProviders.HmacSha512;
using ktsu.Essentials.LoggingProviders.Console;
using ktsu.Essentials.NavigationProviders.InMemory;
using ktsu.Essentials.ObfuscationProviders.Base64;
Expand Down Expand Up @@ -73,6 +76,7 @@ public static IServiceCollection AddEssentials(this IServiceCollection services)
.AddEncryptionProviders()
.AddFileSystemProviders()
.AddHashProviders()
.AddKeyedHashProviders()
.AddLoggingProviders()
.AddNavigationProviders()
.AddObfuscationProviders()
Expand Down Expand Up @@ -155,6 +159,21 @@ public static IServiceCollection AddHashProviders(this IServiceCollection servic
.AddXxHash128HashProvider();
}

/// <summary>
/// Registers every bundled keyed hash provider.
/// </summary>
/// <param name="services">The service collection to add the providers to.</param>
/// <returns>The same service collection, to allow chaining.</returns>
public static IServiceCollection AddKeyedHashProviders(this IServiceCollection services)
{
Ensure.NotNull(services);

return services
.AddHmacSha256KeyedHashProvider()
.AddHmacSha384KeyedHashProvider()
.AddHmacSha512KeyedHashProvider();
}

/// <summary>
/// Registers every bundled obfuscation provider that has a usable default configuration.
/// </summary>
Expand Down
12 changes: 12 additions & 0 deletions Essentials.EncryptionProviders.Aes/AesEncryptionProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ namespace ktsu.Essentials.EncryptionProviders.Aes;
/// This type is stateless and safe to share across threads — every operation creates its own
/// <see cref="System.Security.Cryptography.Aes"/> instance from the caller-supplied key and IV.
/// It is therefore safe to register as a singleton.
/// <para>
/// This provider is AES in CBC mode with PKCS7 padding, which is what <c>Aes.Create()</c> defaults to.
/// CBC ciphertext is malleable: an attacker who can modify it can make predictable changes to the
/// decrypted plaintext without knowing the key. Decryption reports padding failures, so a caller who
/// decrypts attacker-supplied input and reveals whether it parsed becomes a padding oracle.
/// </para>
/// <para>
/// Authenticate the initialization vector and the ciphertext together before decrypting them. CBC
/// recovers the first plaintext block as the initialization vector XORed with the decryption of the
/// first ciphertext block, so a tag covering only the ciphertext still leaves that block rewritable.
/// See the remarks on <see cref="IEncryptionProvider"/>.
/// </para>
/// </remarks>
public class AesEncryptionProvider : IEncryptionProvider
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="ktsu.Sdk" />

<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0;net7.0;net6.0;netstandard2.1</TargetFrameworks>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Essentials\Essentials.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Polyfill" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\HmacKeyedHashCore.cs" Link="HmacKeyedHashCore.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ktsu.Essentials.Tests" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Essentials.KeyedHashProviders.HmacSha256;

using System;
using System.IO;
using System.Security.Cryptography;
using ktsu.Essentials;

/// <summary>
/// A keyed hash provider that uses HMAC-SHA-256 to authenticate data.
/// </summary>
/// <remarks>
/// This type is stateless and safe to share across threads, because the key is supplied per call
/// rather than held in a field. Every operation delegates to the shared HMAC core, which owns key
/// copying and zeroing.
/// </remarks>
public class HmacSha256KeyedHashProvider : IKeyedHashProvider
{
/// <summary>
/// The length of the HMAC-SHA-256 tag in bytes (32 bytes / 256 bits).
/// </summary>
public int HashLengthBytes => 32;

/// <inheritdoc/>
public bool TryHash(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination, out int bytesWritten)
=> HmacKeyedHashCore.TryHash(HashAlgorithmName.SHA256, HashLengthBytes, key, data, destination, out bytesWritten);

/// <inheritdoc/>
public bool TryHash(ReadOnlySpan<byte> key, Stream data, Span<byte> destination, out int bytesWritten)
=> HmacKeyedHashCore.TryHash(HashAlgorithmName.SHA256, HashLengthBytes, key, data, destination, out bytesWritten);

/// <inheritdoc/>
public IIncrementalHash CreateIncremental(ReadOnlySpan<byte> key)
=> HmacKeyedHashCore.CreateIncremental(HashAlgorithmName.SHA256, HashLengthBytes, key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Essentials.KeyedHashProviders.HmacSha256;

using ktsu.Essentials;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

/// <summary>
/// Dependency injection registration for the HMAC-SHA-256 keyed hashing provider.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers the HMAC-SHA-256 keyed hashing provider.
/// </summary>
/// <remarks>
/// The provider is registered as a singleton, both as its concrete type and as an additional
/// <see cref="IKeyedHashProvider"/> in the resolvable set, so it can be resolved either way. The
/// container constructs and owns each registration. Calling this more than once is a no-op.
/// </remarks>
/// <param name="services">The service collection to add the provider to.</param>
/// <returns>The same service collection, to allow chaining.</returns>
public static IServiceCollection AddHmacSha256KeyedHashProvider(this IServiceCollection services)
{
Ensure.NotNull(services);

services.TryAddSingleton<HmacSha256KeyedHashProvider>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IKeyedHashProvider, HmacSha256KeyedHashProvider>());
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="ktsu.Sdk" />

<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0;net7.0;net6.0;netstandard2.1</TargetFrameworks>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Essentials\Essentials.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Polyfill" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\HmacKeyedHashCore.cs" Link="HmacKeyedHashCore.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ktsu.Essentials.Tests" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Essentials.KeyedHashProviders.HmacSha384;

using System;
using System.IO;
using System.Security.Cryptography;
using ktsu.Essentials;

/// <summary>
/// A keyed hash provider that uses HMAC-SHA-384 to authenticate data.
/// </summary>
/// <remarks>
/// This type is stateless and safe to share across threads, because the key is supplied per call
/// rather than held in a field. Every operation delegates to the shared HMAC core, which owns key
/// copying and zeroing.
/// </remarks>
public class HmacSha384KeyedHashProvider : IKeyedHashProvider
{
/// <summary>
/// The length of the HMAC-SHA-384 tag in bytes (48 bytes / 384 bits).
/// </summary>
public int HashLengthBytes => 48;

/// <inheritdoc/>
public bool TryHash(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination, out int bytesWritten)
=> HmacKeyedHashCore.TryHash(HashAlgorithmName.SHA384, HashLengthBytes, key, data, destination, out bytesWritten);

/// <inheritdoc/>
public bool TryHash(ReadOnlySpan<byte> key, Stream data, Span<byte> destination, out int bytesWritten)
=> HmacKeyedHashCore.TryHash(HashAlgorithmName.SHA384, HashLengthBytes, key, data, destination, out bytesWritten);

/// <inheritdoc/>
public IIncrementalHash CreateIncremental(ReadOnlySpan<byte> key)
=> HmacKeyedHashCore.CreateIncremental(HashAlgorithmName.SHA384, HashLengthBytes, key);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Essentials.KeyedHashProviders.HmacSha384;

using ktsu.Essentials;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;

/// <summary>
/// Dependency injection registration for the HMAC-SHA-384 keyed hashing provider.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Registers the HMAC-SHA-384 keyed hashing provider.
/// </summary>
/// <remarks>
/// The provider is registered as a singleton, both as its concrete type and as an additional
/// <see cref="IKeyedHashProvider"/> in the resolvable set, so it can be resolved either way. The
/// container constructs and owns each registration. Calling this more than once is a no-op.
/// </remarks>
/// <param name="services">The service collection to add the provider to.</param>
/// <returns>The same service collection, to allow chaining.</returns>
public static IServiceCollection AddHmacSha384KeyedHashProvider(this IServiceCollection services)
{
Ensure.NotNull(services);

services.TryAddSingleton<HmacSha384KeyedHashProvider>();
services.TryAddEnumerable(ServiceDescriptor.Singleton<IKeyedHashProvider, HmacSha384KeyedHashProvider>());
return services;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Sdk Name="ktsu.Sdk" />

<PropertyGroup>
<TargetFrameworks>net10.0;net9.0;net8.0;net7.0;net6.0;netstandard2.1</TargetFrameworks>
<SuppressTfmSupportBuildWarnings>true</SuppressTfmSupportBuildWarnings>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\Essentials\Essentials.csproj" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
<PackageReference Include="Polyfill" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\HmacKeyedHashCore.cs" Link="HmacKeyedHashCore.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ktsu.Essentials.Tests" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Essentials.KeyedHashProviders.HmacSha512;

using System;
using System.IO;
using System.Security.Cryptography;
using ktsu.Essentials;

/// <summary>
/// A keyed hash provider that uses HMAC-SHA-512 to authenticate data.
/// </summary>
/// <remarks>
/// This type is stateless and safe to share across threads, because the key is supplied per call
/// rather than held in a field. Every operation delegates to the shared HMAC core, which owns key
/// copying and zeroing.
/// </remarks>
public class HmacSha512KeyedHashProvider : IKeyedHashProvider
{
/// <summary>
/// The length of the HMAC-SHA-512 tag in bytes (64 bytes / 512 bits).
/// </summary>
public int HashLengthBytes => 64;

/// <inheritdoc/>
public bool TryHash(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, Span<byte> destination, out int bytesWritten)
=> HmacKeyedHashCore.TryHash(HashAlgorithmName.SHA512, HashLengthBytes, key, data, destination, out bytesWritten);

/// <inheritdoc/>
public bool TryHash(ReadOnlySpan<byte> key, Stream data, Span<byte> destination, out int bytesWritten)
=> HmacKeyedHashCore.TryHash(HashAlgorithmName.SHA512, HashLengthBytes, key, data, destination, out bytesWritten);

/// <inheritdoc/>
public IIncrementalHash CreateIncremental(ReadOnlySpan<byte> key)
=> HmacKeyedHashCore.CreateIncremental(HashAlgorithmName.SHA512, HashLengthBytes, key);
}
Loading