162 lines
6.3 KiB
C#
162 lines
6.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using static StellaOps.Localization.T;
|
|
|
|
namespace StellaOps.Cryptography;
|
|
|
|
/// <summary>
|
|
/// Default implementation of <see cref="ICryptoProviderRegistry"/> with deterministic provider ordering.
|
|
/// </summary>
|
|
public sealed class CryptoProviderRegistry : ICryptoProviderRegistry
|
|
{
|
|
private readonly ReadOnlyCollection<ICryptoProvider> providers;
|
|
private readonly IReadOnlyDictionary<string, ICryptoProvider> providersByName;
|
|
private readonly IReadOnlyList<string> preferredOrder;
|
|
private readonly HashSet<string> preferredOrderSet;
|
|
private readonly CryptoRegistryProfiles profiles;
|
|
|
|
public CryptoProviderRegistry(
|
|
IEnumerable<ICryptoProvider> providers,
|
|
IEnumerable<string>? preferredProviderOrder = null,
|
|
CryptoRegistryProfiles? registryProfiles = null)
|
|
{
|
|
if (providers is null)
|
|
{
|
|
throw new ArgumentNullException(nameof(providers));
|
|
}
|
|
|
|
var providerList = providers.ToList();
|
|
if (providerList.Count == 0)
|
|
{
|
|
throw new ArgumentException(_t("crypto.registry.empty"), nameof(providers));
|
|
}
|
|
|
|
providersByName = providerList.ToDictionary(p => p.Name, StringComparer.OrdinalIgnoreCase);
|
|
this.providers = new ReadOnlyCollection<ICryptoProvider>(providerList);
|
|
|
|
var baseOrder = preferredProviderOrder?
|
|
.Where(name => providersByName.ContainsKey(name))
|
|
.Select(name => providersByName[name].Name)
|
|
.ToArray() ?? Array.Empty<string>();
|
|
profiles = registryProfiles ?? new CryptoRegistryProfiles(baseOrder, "default",
|
|
new Dictionary<string, IReadOnlyList<string>>(StringComparer.OrdinalIgnoreCase)
|
|
{
|
|
["default"] = baseOrder
|
|
});
|
|
|
|
preferredOrder = profiles.ResolvePreferredOrder();
|
|
preferredOrderSet = new HashSet<string>(preferredOrder, StringComparer.OrdinalIgnoreCase);
|
|
}
|
|
|
|
public IReadOnlyCollection<ICryptoProvider> Providers => providers;
|
|
|
|
public bool TryResolve(string preferredProvider, out ICryptoProvider provider)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(preferredProvider))
|
|
{
|
|
provider = default!;
|
|
return false;
|
|
}
|
|
|
|
return providersByName.TryGetValue(preferredProvider, out provider!);
|
|
}
|
|
|
|
public ICryptoProvider ResolveOrThrow(CryptoCapability capability, string algorithmId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(algorithmId))
|
|
{
|
|
throw new ArgumentException(_t("crypto.registry.algorithm_required"), nameof(algorithmId));
|
|
}
|
|
|
|
foreach (var provider in EnumerateCandidates())
|
|
{
|
|
if (provider.Supports(capability, algorithmId))
|
|
{
|
|
CryptoProviderMetrics.RecordProviderResolution(provider.Name, capability, algorithmId);
|
|
return provider;
|
|
}
|
|
}
|
|
|
|
CryptoProviderMetrics.RecordProviderResolutionFailure(capability, algorithmId);
|
|
var notSupportedMessage = capability switch
|
|
{
|
|
CryptoCapability.Signing => _t("crypto.registry.signing_not_supported", algorithmId),
|
|
CryptoCapability.ContentHashing => _t("crypto.registry.hash_not_supported", algorithmId),
|
|
CryptoCapability.Verification => _t("crypto.registry.verify_not_supported", algorithmId),
|
|
_ => _t("crypto.registry.signing_not_supported", algorithmId)
|
|
};
|
|
throw new InvalidOperationException(notSupportedMessage);
|
|
}
|
|
|
|
public CryptoSignerResolution ResolveSigner(
|
|
CryptoCapability capability,
|
|
string algorithmId,
|
|
CryptoKeyReference keyReference,
|
|
string? preferredProvider = null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(preferredProvider) &&
|
|
providersByName.TryGetValue(preferredProvider!, out var hinted))
|
|
{
|
|
if (!hinted.Supports(capability, algorithmId))
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Provider '{preferredProvider}' does not support capability '{capability}' and algorithm '{algorithmId}'.");
|
|
}
|
|
|
|
var signer = hinted.GetSigner(algorithmId, keyReference);
|
|
CryptoProviderMetrics.RecordProviderResolution(hinted.Name, capability, algorithmId);
|
|
return new CryptoSignerResolution(signer, hinted.Name);
|
|
}
|
|
|
|
var provider = ResolveOrThrow(capability, algorithmId);
|
|
var resolved = provider.GetSigner(algorithmId, keyReference);
|
|
CryptoProviderMetrics.RecordProviderResolution(provider.Name, capability, algorithmId);
|
|
return new CryptoSignerResolution(resolved, provider.Name);
|
|
}
|
|
|
|
public CryptoHasherResolution ResolveHasher(string algorithmId, string? preferredProvider = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(algorithmId))
|
|
{
|
|
throw new ArgumentException(_t("crypto.registry.algorithm_required"), nameof(algorithmId));
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(preferredProvider) &&
|
|
providersByName.TryGetValue(preferredProvider!, out var hinted))
|
|
{
|
|
if (!hinted.Supports(CryptoCapability.ContentHashing, algorithmId))
|
|
{
|
|
throw new InvalidOperationException(
|
|
_t("crypto.provider.no_content_hashing", preferredProvider));
|
|
}
|
|
|
|
var hasher = hinted.GetHasher(algorithmId);
|
|
CryptoProviderMetrics.RecordProviderResolution(hinted.Name, CryptoCapability.ContentHashing, algorithmId);
|
|
return new CryptoHasherResolution(hasher, hinted.Name);
|
|
}
|
|
|
|
var provider = ResolveOrThrow(CryptoCapability.ContentHashing, algorithmId);
|
|
var resolved = provider.GetHasher(algorithmId);
|
|
CryptoProviderMetrics.RecordProviderResolution(provider.Name, CryptoCapability.ContentHashing, algorithmId);
|
|
return new CryptoHasherResolution(resolved, provider.Name);
|
|
}
|
|
|
|
private IEnumerable<ICryptoProvider> EnumerateCandidates()
|
|
{
|
|
foreach (var name in preferredOrder)
|
|
{
|
|
yield return providersByName[name];
|
|
}
|
|
|
|
foreach (var provider in providers)
|
|
{
|
|
if (!preferredOrderSet.Contains(provider.Name))
|
|
{
|
|
yield return provider;
|
|
}
|
|
}
|
|
}
|
|
}
|