Files
git.stella-ops.org/src/__Libraries/StellaOps.Cryptography/DefaultCryptoHash.cs
2026-02-01 21:37:40 +02:00

369 lines
13 KiB
C#

using Blake3;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Org.BouncyCastle.Crypto.Digests;
using System;
using System.Buffers;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
namespace StellaOps.Cryptography;
public sealed class DefaultCryptoHash : ICryptoHash
{
private readonly IOptionsMonitor<CryptoHashOptions> _hashOptions;
private readonly IOptionsMonitor<CryptoComplianceOptions> _complianceOptions;
private readonly ILogger<DefaultCryptoHash> _logger;
[ActivatorUtilitiesConstructor]
public DefaultCryptoHash(
IOptionsMonitor<CryptoHashOptions> hashOptions,
IOptionsMonitor<CryptoComplianceOptions>? complianceOptions = null,
ILogger<DefaultCryptoHash>? logger = null)
{
_hashOptions = hashOptions ?? throw new ArgumentNullException(nameof(hashOptions));
_complianceOptions = complianceOptions ?? new StaticComplianceOptionsMonitor(new CryptoComplianceOptions());
_logger = logger ?? NullLogger<DefaultCryptoHash>.Instance;
}
internal DefaultCryptoHash(CryptoHashOptions? hashOptions = null, CryptoComplianceOptions? complianceOptions = null)
: this(
new StaticOptionsMonitor(hashOptions ?? new CryptoHashOptions()),
new StaticComplianceOptionsMonitor(complianceOptions ?? new CryptoComplianceOptions()),
NullLogger<DefaultCryptoHash>.Instance)
{
}
/// <summary>
/// Creates a new <see cref="DefaultCryptoHash"/> instance for use in tests.
/// Uses default options with no compliance profile.
/// </summary>
public static DefaultCryptoHash CreateForTests()
=> new(new CryptoHashOptions(), new CryptoComplianceOptions());
public byte[] ComputeHash(ReadOnlySpan<byte> data, string? algorithmId = null)
{
var algorithm = NormalizeAlgorithm(algorithmId);
return ComputeHashWithAlgorithm(data, algorithm);
}
private static byte[] ComputeHashWithAlgorithm(ReadOnlySpan<byte> data, string algorithm)
{
return algorithm.ToUpperInvariant() switch
{
"SHA256" => ComputeSha256(data),
"SHA384" => ComputeSha384(data),
"SHA512" => ComputeSha512(data),
"GOST3411-2012-256" => GostDigestUtilities.ComputeDigest(data, use256: true),
"GOST3411-2012-512" => GostDigestUtilities.ComputeDigest(data, use256: false),
"BLAKE3-256" => ComputeBlake3(data),
"SM3" => ComputeSm3(data),
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
};
}
public string ComputeHashHex(ReadOnlySpan<byte> data, string? algorithmId = null)
=> Convert.ToHexStringLower(ComputeHash(data, algorithmId));
public string ComputeHashBase64(ReadOnlySpan<byte> data, string? algorithmId = null)
=> Convert.ToBase64String(ComputeHash(data, algorithmId));
public async ValueTask<byte[]> ComputeHashAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
cancellationToken.ThrowIfCancellationRequested();
var algorithm = NormalizeAlgorithm(algorithmId);
return await ComputeHashWithAlgorithmAsync(stream, algorithm, cancellationToken).ConfigureAwait(false);
}
private static async ValueTask<byte[]> ComputeHashWithAlgorithmAsync(Stream stream, string algorithm, CancellationToken cancellationToken)
{
return algorithm.ToUpperInvariant() switch
{
"SHA256" => await ComputeShaStreamAsync(HashAlgorithmName.SHA256, stream, cancellationToken).ConfigureAwait(false),
"SHA384" => await ComputeShaStreamAsync(HashAlgorithmName.SHA384, stream, cancellationToken).ConfigureAwait(false),
"SHA512" => await ComputeShaStreamAsync(HashAlgorithmName.SHA512, stream, cancellationToken).ConfigureAwait(false),
"GOST3411-2012-256" => await ComputeGostStreamAsync(use256: true, stream, cancellationToken).ConfigureAwait(false),
"GOST3411-2012-512" => await ComputeGostStreamAsync(use256: false, stream, cancellationToken).ConfigureAwait(false),
"BLAKE3-256" => await ComputeBlake3StreamAsync(stream, cancellationToken).ConfigureAwait(false),
"SM3" => await ComputeSm3StreamAsync(stream, cancellationToken).ConfigureAwait(false),
_ => throw new InvalidOperationException($"Unsupported hash algorithm '{algorithm}'.")
};
}
public async ValueTask<string> ComputeHashHexAsync(Stream stream, string? algorithmId = null, CancellationToken cancellationToken = default)
{
var bytes = await ComputeHashAsync(stream, algorithmId, cancellationToken).ConfigureAwait(false);
return Convert.ToHexStringLower(bytes);
}
private static byte[] ComputeSha256(ReadOnlySpan<byte> data)
{
Span<byte> buffer = stackalloc byte[32];
SHA256.HashData(data, buffer);
return buffer.ToArray();
}
private static byte[] ComputeSha512(ReadOnlySpan<byte> data)
{
Span<byte> buffer = stackalloc byte[64];
SHA512.HashData(data, buffer);
return buffer.ToArray();
}
private static async ValueTask<byte[]> ComputeShaStreamAsync(HashAlgorithmName name, Stream stream, CancellationToken cancellationToken)
{
using var incremental = IncrementalHash.CreateHash(name);
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
incremental.AppendData(buffer, 0, bytesRead);
}
return incremental.GetHashAndReset();
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static async ValueTask<byte[]> ComputeGostStreamAsync(bool use256, Stream stream, CancellationToken cancellationToken)
{
var digest = GostDigestUtilities.CreateDigest(use256);
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
digest.BlockUpdate(buffer, 0, bytesRead);
}
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private string NormalizeAlgorithm(string? algorithmId)
{
var defaultAlgorithm = _hashOptions.CurrentValue?.DefaultAlgorithm;
if (!string.IsNullOrWhiteSpace(algorithmId))
{
return algorithmId.Trim().ToUpperInvariant();
}
if (!string.IsNullOrWhiteSpace(defaultAlgorithm))
{
return defaultAlgorithm.Trim().ToUpperInvariant();
}
return HashAlgorithms.Sha256;
}
#region Purpose-based methods
private ComplianceProfile GetActiveProfile()
{
var opts = _complianceOptions.CurrentValue;
opts.ApplyEnvironmentOverrides();
return ComplianceProfiles.GetProfile(opts.ProfileId);
}
public byte[] ComputeHashForPurpose(ReadOnlySpan<byte> data, string purpose)
{
var algorithm = GetAlgorithmForPurpose(purpose);
return ComputeHashWithAlgorithm(data, algorithm);
}
public string ComputeHashHexForPurpose(ReadOnlySpan<byte> data, string purpose)
=> Convert.ToHexStringLower(ComputeHashForPurpose(data, purpose));
public string ComputeHashBase64ForPurpose(ReadOnlySpan<byte> data, string purpose)
=> Convert.ToBase64String(ComputeHashForPurpose(data, purpose));
public async ValueTask<byte[]> ComputeHashForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
cancellationToken.ThrowIfCancellationRequested();
var algorithm = GetAlgorithmForPurpose(purpose);
return await ComputeHashWithAlgorithmAsync(stream, algorithm, cancellationToken).ConfigureAwait(false);
}
public async ValueTask<string> ComputeHashHexForPurposeAsync(Stream stream, string purpose, CancellationToken cancellationToken = default)
{
var bytes = await ComputeHashForPurposeAsync(stream, purpose, cancellationToken).ConfigureAwait(false);
return Convert.ToHexStringLower(bytes);
}
public string GetAlgorithmForPurpose(string purpose)
{
if (string.IsNullOrWhiteSpace(purpose))
{
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
}
var opts = _complianceOptions.CurrentValue;
opts.ApplyEnvironmentOverrides();
// Check for purpose overrides first
if (opts.PurposeOverrides?.TryGetValue(purpose, out var overrideAlgorithm) == true &&
!string.IsNullOrWhiteSpace(overrideAlgorithm))
{
return overrideAlgorithm;
}
// Get from active profile
var profile = ComplianceProfiles.GetProfile(opts.ProfileId);
return profile.GetAlgorithmForPurpose(purpose);
}
public string GetHashPrefix(string purpose)
{
if (string.IsNullOrWhiteSpace(purpose))
{
throw new ArgumentException("Purpose cannot be null or empty.", nameof(purpose));
}
var profile = GetActiveProfile();
return profile.GetHashPrefix(purpose);
}
public string ComputePrefixedHashForPurpose(ReadOnlySpan<byte> data, string purpose)
{
var prefix = GetHashPrefix(purpose);
var hash = ComputeHashHexForPurpose(data, purpose);
return prefix + hash;
}
#endregion
#region Algorithm implementations
private static byte[] ComputeSha384(ReadOnlySpan<byte> data)
{
Span<byte> buffer = stackalloc byte[48];
SHA384.HashData(data, buffer);
return buffer.ToArray();
}
private static byte[] ComputeBlake3(ReadOnlySpan<byte> data)
{
using var hasher = Hasher.New();
hasher.Update(data);
var hash = hasher.Finalize();
return hash.AsSpan().ToArray();
}
private static byte[] ComputeSm3(ReadOnlySpan<byte> data)
{
var digest = new SM3Digest();
digest.BlockUpdate(data);
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;
}
private static async ValueTask<byte[]> ComputeBlake3StreamAsync(Stream stream, CancellationToken cancellationToken)
{
using var hasher = Hasher.New();
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
hasher.Update(buffer.AsSpan(0, bytesRead));
}
var hash = hasher.Finalize();
return hash.AsSpan().ToArray();
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
private static async ValueTask<byte[]> ComputeSm3StreamAsync(Stream stream, CancellationToken cancellationToken)
{
var digest = new SM3Digest();
var buffer = ArrayPool<byte>.Shared.Rent(128 * 1024);
try
{
int bytesRead;
while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0)
{
digest.BlockUpdate(buffer, 0, bytesRead);
}
var output = new byte[digest.GetDigestSize()];
digest.DoFinal(output, 0);
return output;
}
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
}
#endregion
#region Static options monitors
private sealed class StaticOptionsMonitor : IOptionsMonitor<CryptoHashOptions>
{
private readonly CryptoHashOptions _options;
public StaticOptionsMonitor(CryptoHashOptions options)
=> _options = options;
public CryptoHashOptions CurrentValue => _options;
public CryptoHashOptions Get(string? name) => _options;
public IDisposable OnChange(Action<CryptoHashOptions, string> listener)
=> NullDisposable.Instance;
}
private sealed class StaticComplianceOptionsMonitor : IOptionsMonitor<CryptoComplianceOptions>
{
private readonly CryptoComplianceOptions _options;
public StaticComplianceOptionsMonitor(CryptoComplianceOptions options)
=> _options = options;
public CryptoComplianceOptions CurrentValue => _options;
public CryptoComplianceOptions Get(string? name) => _options;
public IDisposable OnChange(Action<CryptoComplianceOptions, string> listener)
=> NullDisposable.Instance;
}
private sealed class NullDisposable : IDisposable
{
public static readonly NullDisposable Instance = new();
public void Dispose()
{
}
}
#endregion
}