using StellaOps.Cryptography; namespace StellaOps.Audit.ReplayToken; /// /// Generates replay tokens using SHA-256 hashing with deterministic canonicalization. /// public sealed partial class Sha256ReplayTokenGenerator : IReplayTokenGenerator { private readonly ICryptoHash _cryptoHash; private readonly TimeProvider _timeProvider; public Sha256ReplayTokenGenerator(ICryptoHash cryptoHash, TimeProvider timeProvider) { _cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash)); _timeProvider = timeProvider ?? throw new ArgumentNullException(nameof(timeProvider)); } public ReplayToken Generate(ReplayTokenRequest request) { ArgumentNullException.ThrowIfNull(request); var hashHex = ComputeTokenValue(request, ReplayToken.DefaultVersion); return new ReplayToken(hashHex, _timeProvider.GetUtcNow()); } public ReplayToken GenerateWithExpiration(ReplayTokenRequest request, TimeSpan? expiration = null) { ArgumentNullException.ThrowIfNull(request); var effectiveExpiration = expiration ?? ReplayToken.DefaultExpiration; if (effectiveExpiration <= TimeSpan.Zero) { throw new ArgumentOutOfRangeException(nameof(expiration), "Expiration must be positive."); } var hashHex = ComputeTokenValue(request, ReplayToken.VersionWithExpiration); var now = _timeProvider.GetUtcNow(); var expiresAt = now + effectiveExpiration; return new ReplayToken(hashHex, now, expiresAt, ReplayToken.DefaultAlgorithm, ReplayToken.VersionWithExpiration); } public bool Verify(ReplayToken token, ReplayTokenRequest request) { ArgumentNullException.ThrowIfNull(token); ArgumentNullException.ThrowIfNull(request); var computed = ComputeTokenValue(request, token.Version); return string.Equals(token.Value, computed, StringComparison.OrdinalIgnoreCase); } public ReplayTokenVerificationResult VerifyWithExpiration(ReplayToken token, ReplayTokenRequest request) { ArgumentNullException.ThrowIfNull(token); ArgumentNullException.ThrowIfNull(request); var computed = ComputeTokenValue(request, token.Version); if (!string.Equals(token.Value, computed, StringComparison.OrdinalIgnoreCase)) { return ReplayTokenVerificationResult.Invalid; } if (token.IsExpired(_timeProvider.GetUtcNow())) { return ReplayTokenVerificationResult.Expired; } return ReplayTokenVerificationResult.Valid; } }