namespace HrynCo.Common.Security; using System; using System.Security.Cryptography; using System.Text; public sealed class SecretProtector : ISecretProtector { private readonly byte[] _key; public SecretProtector(string encryptionKey) { if (string.IsNullOrWhiteSpace(encryptionKey)) { throw new InvalidOperationException("Secret encryption key is not configured."); } _key = Convert.FromBase64String(encryptionKey); if (_key.Length != 32) { throw new InvalidOperationException("Secret encryption key must be 32 bytes encoded as Base64."); } } public string Protect(string plaintext) { byte[] nonce = RandomNumberGenerator.GetBytes(12); byte[] plaintextBytes = Encoding.UTF8.GetBytes(plaintext); byte[] ciphertext = new byte[plaintextBytes.Length]; byte[] tag = new byte[16]; using var aes = new AesGcm(_key, tagSizeInBytes: 16); aes.Encrypt(nonce, plaintextBytes, ciphertext, tag); byte[] payload = new byte[nonce.Length + tag.Length + ciphertext.Length]; Buffer.BlockCopy(nonce, 0, payload, 0, nonce.Length); Buffer.BlockCopy(tag, 0, payload, nonce.Length, tag.Length); Buffer.BlockCopy(ciphertext, 0, payload, nonce.Length + tag.Length, ciphertext.Length); return $"v1:{Convert.ToBase64String(payload)}"; } public string Unprotect(string protectedValue) { if (!protectedValue.StartsWith("v1:", StringComparison.Ordinal)) { throw new InvalidOperationException("Unsupported protected value format."); } byte[] payload = Convert.FromBase64String(protectedValue[3..]); byte[] nonce = payload[..12]; byte[] tag = payload[12..28]; byte[] ciphertext = payload[28..]; byte[] plaintext = new byte[ciphertext.Length]; using var aes = new AesGcm(_key, tagSizeInBytes: 16); aes.Decrypt(nonce, ciphertext, tag, plaintext); return Encoding.UTF8.GetString(plaintext); } }