using System; using System.Buffers; using System.IO; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Org.BouncyCastle.Crypto; namespace StellaOps.Cryptography; public sealed class DefaultCryptoHash : ICryptoHash { private readonly IOptionsMonitor options; private readonly ILogger logger; [ActivatorUtilitiesConstructor] public DefaultCryptoHash( IOptionsMonitor options, ILogger? logger = null) { this.options = options ?? throw new ArgumentNullException(nameof(options)); this.logger = logger ?? NullLogger.Instance; } internal DefaultCryptoHash(CryptoHashOptions? options = null) : this(new StaticOptionsMonitor(options ?? new CryptoHashOptions()), NullLogger.Instance) { } public byte[] ComputeHash(ReadOnlySpan data, string? algorithmId = null) { var algorithm = NormalizeAlgorithm(algorithmId); return algorithm switch { HashAlgorithms.Sha256 => ComputeSha256(data), HashAlgorithms.Sha512 => ComputeSha512(data), HashAlgorithms.Gost3411_2012_256 => GostDigestUtilities.ComputeDigest(data, use256: true), HashAlgorithms.Gost3411_2012_512 => GostDigestUtilities.ComputeDigest(data, use256: false), _ => throw new InvalidOperationException($"Unsupported hash algorithm {algorithm}.") }; } public string ComputeHashHex(ReadOnlySpan data, string? algorithmId = null) => Convert.ToHexString(ComputeHash(data, algorithmId)).ToLowerInvariant(); 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 algorithm switch { HashAlgorithms.Sha256 => await ComputeShaStreamAsync(HashAlgorithmName.SHA256, stream, cancellationToken).ConfigureAwait(false), HashAlgorithms.Sha512 => await ComputeShaStreamAsync(HashAlgorithmName.SHA512, stream, cancellationToken).ConfigureAwait(false), HashAlgorithms.Gost3411_2012_256 => await ComputeGostStreamAsync(use256: true, stream, cancellationToken).ConfigureAwait(false), HashAlgorithms.Gost3411_2012_512 => await ComputeGostStreamAsync(use256: false, 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.ToHexString(bytes).ToLowerInvariant(); } 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 = options.CurrentValue?.DefaultAlgorithm; if (!string.IsNullOrWhiteSpace(algorithmId)) { return algorithmId.Trim().ToUpperInvariant(); } if (!string.IsNullOrWhiteSpace(defaultAlgorithm)) { return defaultAlgorithm.Trim().ToUpperInvariant(); } return HashAlgorithms.Sha256; } private sealed class StaticOptionsMonitor : IOptionsMonitor { private readonly CryptoHashOptions options; public StaticOptionsMonitor(CryptoHashOptions options) => this.options = options; public CryptoHashOptions CurrentValue => options; public CryptoHashOptions 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() { } } } }