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
}