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 _hashOptions; private readonly IOptionsMonitor _complianceOptions; private readonly ILogger _logger; [ActivatorUtilitiesConstructor] public DefaultCryptoHash( IOptionsMonitor hashOptions, IOptionsMonitor? complianceOptions = null, ILogger? logger = null) { _hashOptions = hashOptions ?? throw new ArgumentNullException(nameof(hashOptions)); _complianceOptions = complianceOptions ?? new StaticComplianceOptionsMonitor(new CryptoComplianceOptions()); _logger = logger ?? NullLogger.Instance; } internal DefaultCryptoHash(CryptoHashOptions? hashOptions = null, CryptoComplianceOptions? complianceOptions = null) : this( new StaticOptionsMonitor(hashOptions ?? new CryptoHashOptions()), new StaticComplianceOptionsMonitor(complianceOptions ?? new CryptoComplianceOptions()), NullLogger.Instance) { } /// /// Creates a new instance for use in tests. /// Uses default options with no compliance profile. /// public static DefaultCryptoHash CreateForTests() => new(new CryptoHashOptions(), new CryptoComplianceOptions()); public byte[] ComputeHash(ReadOnlySpan data, string? algorithmId = null) { var algorithm = NormalizeAlgorithm(algorithmId); return ComputeHashWithAlgorithm(data, algorithm); } private static byte[] ComputeHashWithAlgorithm(ReadOnlySpan 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 data, string? algorithmId = null) => Convert.ToHexStringLower(ComputeHash(data, algorithmId)); public string ComputeHashBase64(ReadOnlySpan data, string? algorithmId = null) => Convert.ToBase64String(ComputeHash(data, algorithmId)); public async ValueTask 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 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 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 data) { Span buffer = stackalloc byte[32]; SHA256.HashData(data, buffer); return buffer.ToArray(); } private static byte[] ComputeSha512(ReadOnlySpan data) { Span buffer = stackalloc byte[64]; SHA512.HashData(data, buffer); return buffer.ToArray(); } private static async ValueTask ComputeShaStreamAsync(HashAlgorithmName name, Stream stream, CancellationToken cancellationToken) { using var incremental = IncrementalHash.CreateHash(name); var buffer = ArrayPool.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.Shared.Return(buffer); } } private static async ValueTask ComputeGostStreamAsync(bool use256, Stream stream, CancellationToken cancellationToken) { var digest = GostDigestUtilities.CreateDigest(use256); var buffer = ArrayPool.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.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 data, string purpose) { var algorithm = GetAlgorithmForPurpose(purpose); return ComputeHashWithAlgorithm(data, algorithm); } public string ComputeHashHexForPurpose(ReadOnlySpan data, string purpose) => Convert.ToHexStringLower(ComputeHashForPurpose(data, purpose)); public string ComputeHashBase64ForPurpose(ReadOnlySpan data, string purpose) => Convert.ToBase64String(ComputeHashForPurpose(data, purpose)); public async ValueTask 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 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 data, string purpose) { var prefix = GetHashPrefix(purpose); var hash = ComputeHashHexForPurpose(data, purpose); return prefix + hash; } #endregion #region Algorithm implementations private static byte[] ComputeSha384(ReadOnlySpan data) { Span buffer = stackalloc byte[48]; SHA384.HashData(data, buffer); return buffer.ToArray(); } private static byte[] ComputeBlake3(ReadOnlySpan data) { using var hasher = Hasher.New(); hasher.Update(data); var hash = hasher.Finalize(); return hash.AsSpan().ToArray(); } private static byte[] ComputeSm3(ReadOnlySpan data) { var digest = new SM3Digest(); digest.BlockUpdate(data); var output = new byte[digest.GetDigestSize()]; digest.DoFinal(output, 0); return output; } private static async ValueTask ComputeBlake3StreamAsync(Stream stream, CancellationToken cancellationToken) { using var hasher = Hasher.New(); var buffer = ArrayPool.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.Shared.Return(buffer); } } private static async ValueTask ComputeSm3StreamAsync(Stream stream, CancellationToken cancellationToken) { var digest = new SM3Digest(); var buffer = ArrayPool.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.Shared.Return(buffer); } } #endregion #region Static options monitors private sealed class StaticOptionsMonitor : IOptionsMonitor { private readonly CryptoHashOptions _options; public StaticOptionsMonitor(CryptoHashOptions options) => _options = options; public CryptoHashOptions CurrentValue => _options; public CryptoHashOptions Get(string? name) => _options; public IDisposable OnChange(Action listener) => NullDisposable.Instance; } private sealed class StaticComplianceOptionsMonitor : IOptionsMonitor { private readonly CryptoComplianceOptions _options; public StaticComplianceOptionsMonitor(CryptoComplianceOptions options) => _options = options; public CryptoComplianceOptions CurrentValue => _options; public CryptoComplianceOptions Get(string? name) => _options; public IDisposable OnChange(Action listener) => NullDisposable.Instance; } private sealed class NullDisposable : IDisposable { public static readonly NullDisposable Instance = new(); public void Dispose() { } } #endregion }