using System.Security.Cryptography; namespace FinalFactory.Rendezvous.Server.Provisioning; internal interface ISecretProvider { bool TryGetSecret(string reference, out SecretMaterial? secret); } internal sealed class SecretMaterial : IDisposable { private byte[]? _bytes; public SecretMaterial(ReadOnlySpan bytes) { if (bytes.Length == 0) { throw new ArgumentException("Secret material cannot be empty.", nameof(bytes)); } _bytes = bytes.ToArray(); } public int Length => _bytes?.Length ?? 0; public byte[] CopyBytes() => _bytes?.ToArray() ?? throw new ObjectDisposedException(nameof(SecretMaterial)); public void Dispose() { if (_bytes is not null) { CryptographicOperations.ZeroMemory(_bytes); _bytes = null; } } public override string ToString() => "[REDACTED SECRET]"; } internal sealed class EnvironmentSecretProvider : ISecretProvider { private const string EnvironmentPrefix = "env:"; private const string FilePrefix = "file:"; private const int MaximumSecretBytes = 4096; public bool TryGetSecret(string reference, out SecretMaterial? secret) { secret = null; if (reference.StartsWith(EnvironmentPrefix, StringComparison.Ordinal) && reference.Length > EnvironmentPrefix.Length) { return TryGetEnvironmentSecret(reference[EnvironmentPrefix.Length..], out secret); } if (reference.StartsWith(FilePrefix, StringComparison.Ordinal) && reference.Length > FilePrefix.Length) { return TryGetFileSecret(reference[FilePrefix.Length..], out secret); } return false; } private static bool TryGetEnvironmentSecret(string variableName, out SecretMaterial? secret) { secret = null; string? encoded = Environment.GetEnvironmentVariable(variableName); if (string.IsNullOrEmpty(encoded)) { return false; } try { byte[] bytes = Convert.FromBase64String(encoded); if (bytes.Length is 0 or > MaximumSecretBytes) { CryptographicOperations.ZeroMemory(bytes); return false; } secret = new SecretMaterial(bytes); CryptographicOperations.ZeroMemory(bytes); return true; } catch (FormatException) { return false; } } private static bool TryGetFileSecret(string path, out SecretMaterial? secret) { secret = null; byte[]? bytes = null; try { FileInfo file = new(path); if (!file.Exists || !Path.IsPathFullyQualified(path) || file.LinkTarget is not null || file.Length is <= 0 or > MaximumSecretBytes) { return false; } bytes = File.ReadAllBytes(path); if (bytes.Length is 0 or > MaximumSecretBytes) { return false; } secret = new SecretMaterial(bytes); return true; } catch (Exception exception) when (exception is IOException or UnauthorizedAccessException or ArgumentException or NotSupportedException or System.Security.SecurityException) { return false; } finally { if (bytes is not null) { CryptographicOperations.ZeroMemory(bytes); } } } } internal sealed class EphemeralDevelopmentSecretProvider : ISecretProvider, IDisposable { private const string Prefix = "development:ephemeral/"; private readonly Dictionary _secrets = new(StringComparer.Ordinal); public bool TryGetSecret(string reference, out SecretMaterial? secret) { secret = null; if (!reference.StartsWith(Prefix, StringComparison.Ordinal) || reference.Length == Prefix.Length) { return false; } if (!_secrets.TryGetValue(reference, out byte[]? bytes)) { bytes = RandomNumberGenerator.GetBytes(32); _secrets.Add(reference, bytes); } secret = new SecretMaterial(bytes); return true; } public void Dispose() { foreach (byte[] bytes in _secrets.Values) { CryptographicOperations.ZeroMemory(bytes); } _secrets.Clear(); } public override string ToString() => "[EphemeralDevelopmentSecretProvider]"; } internal sealed class DictionarySecretProvider : ISecretProvider, IDisposable { private readonly Dictionary _secrets; public DictionarySecretProvider(IReadOnlyDictionary secrets) => _secrets = secrets.ToDictionary( static item => item.Key, static item => item.Value.ToArray(), StringComparer.Ordinal); public bool TryGetSecret(string reference, out SecretMaterial? secret) { if (_secrets.TryGetValue(reference, out byte[]? bytes)) { secret = new SecretMaterial(bytes); return true; } secret = null; return false; } public void Dispose() { foreach (byte[] bytes in _secrets.Values) { CryptographicOperations.ZeroMemory(bytes); } _secrets.Clear(); } public override string ToString() => "[DictionarySecretProvider: REDACTED]"; }