326 lines
12 KiB
C#
326 lines
12 KiB
C#
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Default implementation of <see cref="ICryptoHmac"/> with compliance profile support.
|
|
/// </summary>
|
|
public sealed class DefaultCryptoHmac : ICryptoHmac
|
|
{
|
|
private readonly IOptionsMonitor<CryptoComplianceOptions> _complianceOptions;
|
|
private readonly ILogger<DefaultCryptoHmac> _logger;
|
|
|
|
[ActivatorUtilitiesConstructor]
|
|
public DefaultCryptoHmac(
|
|
IOptionsMonitor<CryptoComplianceOptions>? complianceOptions = null,
|
|
ILogger<DefaultCryptoHmac>? logger = null)
|
|
{
|
|
_complianceOptions = complianceOptions ?? new StaticComplianceOptionsMonitor(new CryptoComplianceOptions());
|
|
_logger = logger ?? NullLogger<DefaultCryptoHmac>.Instance;
|
|
}
|
|
|
|
internal DefaultCryptoHmac(CryptoComplianceOptions? complianceOptions)
|
|
: this(
|
|
new StaticComplianceOptionsMonitor(complianceOptions ?? new CryptoComplianceOptions()),
|
|
NullLogger<DefaultCryptoHmac>.Instance)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a new <see cref="DefaultCryptoHmac"/> instance for use in tests.
|
|
/// Uses default options with no compliance profile.
|
|
/// </summary>
|
|
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<byte> key, ReadOnlySpan<byte> data, string purpose)
|
|
{
|
|
var algorithm = GetAlgorithmForPurpose(purpose);
|
|
return ComputeHmacWithAlgorithm(key, data, algorithm);
|
|
}
|
|
|
|
public string ComputeHmacHexForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string purpose)
|
|
=> Convert.ToHexStringLower(ComputeHmacForPurpose(key, data, purpose));
|
|
|
|
public string ComputeHmacBase64ForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data, string purpose)
|
|
=> Convert.ToBase64String(ComputeHmacForPurpose(key, data, purpose));
|
|
|
|
public async ValueTask<byte[]> ComputeHmacForPurposeAsync(ReadOnlyMemory<byte> 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<string> ComputeHmacHexForPurposeAsync(ReadOnlyMemory<byte> 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<byte> key, ReadOnlySpan<byte> data, ReadOnlySpan<byte> expectedHmac, string purpose)
|
|
{
|
|
var computed = ComputeHmacForPurpose(key, data, purpose);
|
|
return CryptographicOperations.FixedTimeEquals(computed, expectedHmac);
|
|
}
|
|
|
|
public bool VerifyHmacHexForPurpose(ReadOnlySpan<byte> key, ReadOnlySpan<byte> 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<byte> key, ReadOnlySpan<byte> 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<byte> key, ReadOnlySpan<byte> 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<byte[]> ComputeHmacWithAlgorithmAsync(ReadOnlyMemory<byte> 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<byte> key, ReadOnlySpan<byte> data)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[32];
|
|
HMACSHA256.HashData(key, data, buffer);
|
|
return buffer.ToArray();
|
|
}
|
|
|
|
private static byte[] ComputeHmacSha384(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[48];
|
|
HMACSHA384.HashData(key, data, buffer);
|
|
return buffer.ToArray();
|
|
}
|
|
|
|
private static byte[] ComputeHmacSha512(ReadOnlySpan<byte> key, ReadOnlySpan<byte> data)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[64];
|
|
HMACSHA512.HashData(key, data, buffer);
|
|
return buffer.ToArray();
|
|
}
|
|
|
|
private static byte[] ComputeHmacGost3411(ReadOnlySpan<byte> key, ReadOnlySpan<byte> 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<byte> key, ReadOnlySpan<byte> 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<byte[]> ComputeHmacShaStreamAsync(HashAlgorithmName name, ReadOnlyMemory<byte> 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<byte[]> ComputeHmacGost3411StreamAsync(ReadOnlyMemory<byte> 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<byte>.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<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
private static async ValueTask<byte[]> ComputeHmacSm3StreamAsync(ReadOnlyMemory<byte> key, Stream stream, CancellationToken cancellationToken)
|
|
{
|
|
var digest = new SM3Digest();
|
|
var hmac = new HMac(digest);
|
|
hmac.Init(new KeyParameter(key.ToArray()));
|
|
|
|
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)
|
|
{
|
|
hmac.BlockUpdate(buffer, 0, bytesRead);
|
|
}
|
|
|
|
var output = new byte[hmac.GetMacSize()];
|
|
hmac.DoFinal(output, 0);
|
|
return output;
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Static options monitor
|
|
|
|
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
|
|
}
|