using System.Collections.Concurrent; using System.Security.Cryptography; using System.Text; using Amazon.SecretsManager; using Amazon.SecretsManager.Model; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SeaHaven.Services.Configuration; using SeaHaven.Services.Interfaces; namespace Api.SeaHavenIndustries.Infrastructure { public sealed class AwsWorkOrderWebhookSecretProvider : IWorkOrderWebhookSecretProvider { private readonly IAmazonSecretsManager _client; private readonly IOptionsMonitor _options; private readonly TimeProvider _timeProvider; private readonly ILogger _logger; private readonly ConcurrentDictionary _cache = new(StringComparer.Ordinal); private readonly ConcurrentDictionary _locks = new(StringComparer.Ordinal); public AwsWorkOrderWebhookSecretProvider( IAmazonSecretsManager client, IOptionsMonitor options, TimeProvider timeProvider, ILogger logger) { _client = client; _options = options; _timeProvider = timeProvider; _logger = logger; } public async Task GetSecretAsync( string keyId, CancellationToken cancellationToken) { var options = _options.CurrentValue; if (!options.KeySecrets.TryGetValue(keyId, out var secretArn) || string.IsNullOrWhiteSpace(secretArn)) { if (_cache.TryRemove(keyId, out var removed)) CryptographicOperations.ZeroMemory(removed.Secret); return new WorkOrderWebhookSecretResult(WorkOrderWebhookSecretStatus.UnknownKey); } var now = _timeProvider.GetUtcNow(); if (_cache.TryGetValue(keyId, out var cached) && cached.SecretArn == secretArn && cached.ExpiresAt > now) return FoundCopy(cached.Secret); var keyLock = _locks.GetOrAdd(keyId, _ => new SemaphoreSlim(1, 1)); await keyLock.WaitAsync(cancellationToken); try { now = _timeProvider.GetUtcNow(); if (_cache.TryGetValue(keyId, out cached) && cached.SecretArn == secretArn && cached.ExpiresAt > now) return FoundCopy(cached.Secret); var response = await _client.GetSecretValueAsync( new GetSecretValueRequest { SecretId = secretArn }, cancellationToken); var secret = ReadSecret(response); if (secret.Length == 0) return new WorkOrderWebhookSecretResult(WorkOrderWebhookSecretStatus.Unavailable); var ttl = TimeSpan.FromSeconds(Math.Clamp(options.SecretCacheSeconds, 1, 3600)); var replacement = new CacheEntry(secretArn, secret, now.Add(ttl)); _cache.AddOrUpdate( keyId, replacement, (_, prior) => { CryptographicOperations.ZeroMemory(prior.Secret); return replacement; }); return FoundCopy(secret); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { throw; } catch (Exception) { // AWS exceptions can contain the configured secret ARN. Keep // diagnostics intentionally generic for this secret-bearing path. _logger.LogError("Work-order webhook signing secret retrieval failed."); return new WorkOrderWebhookSecretResult(WorkOrderWebhookSecretStatus.Unavailable); } finally { keyLock.Release(); } } private static byte[] ReadSecret(GetSecretValueResponse response) { if (!string.IsNullOrEmpty(response.SecretString)) return Encoding.UTF8.GetBytes(response.SecretString); return response.SecretBinary?.ToArray() ?? Array.Empty(); } private static WorkOrderWebhookSecretResult FoundCopy(byte[] secret) => new(WorkOrderWebhookSecretStatus.Found, (byte[])secret.Clone()); private sealed record CacheEntry( string SecretArn, byte[] Secret, DateTimeOffset ExpiresAt); } }