using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using Org.BouncyCastle.Crypto.Digests; using Org.BouncyCastle.Crypto.Macs; using Org.BouncyCastle.Crypto.Parameters; using System; using System.Buffers; using System.IO; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; using static StellaOps.Localization.T; namespace StellaOps.Cryptography; /// /// Default implementation of with compliance profile support. /// public sealed class DefaultCryptoHmac : ICryptoHmac { private readonly IOptionsMonitor _complianceOptions; private readonly ILogger _logger; [ActivatorUtilitiesConstructor] public DefaultCryptoHmac( IOptionsMonitor? complianceOptions = null, ILogger? logger = null) { _complianceOptions = complianceOptions ?? new StaticComplianceOptionsMonitor(new CryptoComplianceOptions()); _logger = logger ?? NullLogger.Instance; } internal DefaultCryptoHmac(CryptoComplianceOptions? complianceOptions) : this( new StaticComplianceOptionsMonitor(complianceOptions ?? new CryptoComplianceOptions()), NullLogger.Instance) { } /// /// Creates a new instance for use in tests. /// Uses default options with no compliance profile. /// public static DefaultCryptoHmac CreateForTests() => new(new CryptoComplianceOptions()); #region Purpose-based methods private ComplianceProfile GetActiveProfile() { var opts = _complianceOptions.CurrentValue; opts.ApplyEnvironmentOverrides(); return ComplianceProfiles.GetProfile(opts.ProfileId); } public byte[] ComputeHmacForPurpose(ReadOnlySpan key, ReadOnlySpan data, string purpose) { var algorithm = GetAlgorithmForPurpose(purpose); return ComputeHmacWithAlgorithm(key, data, algorithm); } public string ComputeHmacHexForPurpose(ReadOnlySpan key, ReadOnlySpan data, string purpose) => Convert.ToHexStringLower(ComputeHmacForPurpose(key, data, purpose)); public string ComputeHmacBase64ForPurpose(ReadOnlySpan key, ReadOnlySpan data, string purpose) => Convert.ToBase64String(ComputeHmacForPurpose(key, data, purpose)); public async ValueTask ComputeHmacForPurposeAsync(ReadOnlyMemory key, Stream stream, string purpose, CancellationToken cancellationToken = default) { ArgumentNullException.ThrowIfNull(stream); cancellationToken.ThrowIfCancellationRequested(); var algorithm = GetAlgorithmForPurpose(purpose); return await ComputeHmacWithAlgorithmAsync(key, stream, algorithm, cancellationToken).ConfigureAwait(false); } public async ValueTask ComputeHmacHexForPurposeAsync(ReadOnlyMemory key, Stream stream, string purpose, CancellationToken cancellationToken = default) { var bytes = await ComputeHmacForPurposeAsync(key, stream, purpose, cancellationToken).ConfigureAwait(false); return Convert.ToHexStringLower(bytes); } #endregion #region Verification methods public bool VerifyHmacForPurpose(ReadOnlySpan key, ReadOnlySpan data, ReadOnlySpan expectedHmac, string purpose) { var computed = ComputeHmacForPurpose(key, data, purpose); return CryptographicOperations.FixedTimeEquals(computed, expectedHmac); } public bool VerifyHmacHexForPurpose(ReadOnlySpan key, ReadOnlySpan data, string expectedHmacHex, string purpose) { if (string.IsNullOrWhiteSpace(expectedHmacHex)) { return false; } try { var expectedBytes = Convert.FromHexString(expectedHmacHex); return VerifyHmacForPurpose(key, data, expectedBytes, purpose); } catch (FormatException) { return false; } } public bool VerifyHmacBase64ForPurpose(ReadOnlySpan key, ReadOnlySpan data, string expectedHmacBase64, string purpose) { if (string.IsNullOrWhiteSpace(expectedHmacBase64)) { return false; } try { var expectedBytes = Convert.FromBase64String(expectedHmacBase64); return VerifyHmacForPurpose(key, data, expectedBytes, purpose); } catch (FormatException) { return false; } } #endregion #region Metadata methods public string GetAlgorithmForPurpose(string purpose) { if (string.IsNullOrWhiteSpace(purpose)) { throw new ArgumentException(_t("crypto.hash.purpose_required"), nameof(purpose)); } var profile = GetActiveProfile(); return profile.GetHmacAlgorithmForPurpose(purpose); } public int GetOutputLengthForPurpose(string purpose) { var algorithm = GetAlgorithmForPurpose(purpose); return algorithm.ToUpperInvariant() switch { "HMAC-SHA256" => 32, "HMAC-SHA384" => 48, "HMAC-SHA512" => 64, "HMAC-GOST3411" => 32, // GOST R 34.11-2012 Stribog-256 "HMAC-SM3" => 32, _ => throw new InvalidOperationException(_t("crypto.hmac.algorithm_unknown", algorithm)) }; } #endregion #region Algorithm implementations private static byte[] ComputeHmacWithAlgorithm(ReadOnlySpan key, ReadOnlySpan data, string algorithm) { return algorithm.ToUpperInvariant() switch { "HMAC-SHA256" => ComputeHmacSha256(key, data), "HMAC-SHA384" => ComputeHmacSha384(key, data), "HMAC-SHA512" => ComputeHmacSha512(key, data), "HMAC-GOST3411" => ComputeHmacGost3411(key, data), "HMAC-SM3" => ComputeHmacSm3(key, data), _ => throw new InvalidOperationException(_t("crypto.hmac.algorithm_unsupported", algorithm)) }; } private static async ValueTask ComputeHmacWithAlgorithmAsync(ReadOnlyMemory key, Stream stream, string algorithm, CancellationToken cancellationToken) { return algorithm.ToUpperInvariant() switch { "HMAC-SHA256" => await ComputeHmacShaStreamAsync(HashAlgorithmName.SHA256, key, stream, cancellationToken).ConfigureAwait(false), "HMAC-SHA384" => await ComputeHmacShaStreamAsync(HashAlgorithmName.SHA384, key, stream, cancellationToken).ConfigureAwait(false), "HMAC-SHA512" => await ComputeHmacShaStreamAsync(HashAlgorithmName.SHA512, key, stream, cancellationToken).ConfigureAwait(false), "HMAC-GOST3411" => await ComputeHmacGost3411StreamAsync(key, stream, cancellationToken).ConfigureAwait(false), "HMAC-SM3" => await ComputeHmacSm3StreamAsync(key, stream, cancellationToken).ConfigureAwait(false), _ => throw new InvalidOperationException(_t("crypto.hmac.algorithm_unsupported", algorithm)) }; } private static byte[] ComputeHmacSha256(ReadOnlySpan key, ReadOnlySpan data) { Span buffer = stackalloc byte[32]; HMACSHA256.HashData(key, data, buffer); return buffer.ToArray(); } private static byte[] ComputeHmacSha384(ReadOnlySpan key, ReadOnlySpan data) { Span buffer = stackalloc byte[48]; HMACSHA384.HashData(key, data, buffer); return buffer.ToArray(); } private static byte[] ComputeHmacSha512(ReadOnlySpan key, ReadOnlySpan data) { Span buffer = stackalloc byte[64]; HMACSHA512.HashData(key, data, buffer); return buffer.ToArray(); } private static byte[] ComputeHmacGost3411(ReadOnlySpan key, ReadOnlySpan data) { var digest = new Gost3411_2012_256Digest(); var hmac = new HMac(digest); hmac.Init(new KeyParameter(key.ToArray())); hmac.BlockUpdate(data.ToArray(), 0, data.Length); var output = new byte[hmac.GetMacSize()]; hmac.DoFinal(output, 0); return output; } private static byte[] ComputeHmacSm3(ReadOnlySpan key, ReadOnlySpan data) { var digest = new SM3Digest(); var hmac = new HMac(digest); hmac.Init(new KeyParameter(key.ToArray())); hmac.BlockUpdate(data.ToArray(), 0, data.Length); var output = new byte[hmac.GetMacSize()]; hmac.DoFinal(output, 0); return output; } private static async ValueTask ComputeHmacShaStreamAsync(HashAlgorithmName name, ReadOnlyMemory key, Stream stream, CancellationToken cancellationToken) { using var hmac = name.Name switch { "SHA256" => (HMAC)new HMACSHA256(key.ToArray()), "SHA384" => new HMACSHA384(key.ToArray()), "SHA512" => new HMACSHA512(key.ToArray()), _ => throw new InvalidOperationException(_t("crypto.hash.algorithm_unsupported", name)) }; return await hmac.ComputeHashAsync(stream, cancellationToken).ConfigureAwait(false); } private static async ValueTask ComputeHmacGost3411StreamAsync(ReadOnlyMemory key, Stream stream, CancellationToken cancellationToken) { var digest = new Gost3411_2012_256Digest(); var hmac = new HMac(digest); hmac.Init(new KeyParameter(key.ToArray())); var buffer = ArrayPool.Shared.Rent(128 * 1024); try { int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { hmac.BlockUpdate(buffer, 0, bytesRead); } var output = new byte[hmac.GetMacSize()]; hmac.DoFinal(output, 0); return output; } finally { ArrayPool.Shared.Return(buffer); } } private static async ValueTask ComputeHmacSm3StreamAsync(ReadOnlyMemory key, Stream stream, CancellationToken cancellationToken) { var digest = new SM3Digest(); var hmac = new HMac(digest); hmac.Init(new KeyParameter(key.ToArray())); var buffer = ArrayPool.Shared.Rent(128 * 1024); try { int bytesRead; while ((bytesRead = await stream.ReadAsync(buffer.AsMemory(0, buffer.Length), cancellationToken).ConfigureAwait(false)) > 0) { hmac.BlockUpdate(buffer, 0, bytesRead); } var output = new byte[hmac.GetMacSize()]; hmac.DoFinal(output, 0); return output; } finally { ArrayPool.Shared.Return(buffer); } } #endregion #region Static options monitor 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 }