using System.Security.Cryptography; using System.Text; using Baya.Application.Contracts.Common; using Microsoft.Extensions.Options; namespace Baya.Infrastructure.CrossCutting.Seams; /// /// Local symmetric-key implementation of — AES-256-CBC with a random /// per-value IV (prepended to the ciphertext) for at-rest reversibility, and a keyed HMAC-SHA256 for /// deterministic lookup hashes. This is the mock seam: the real implementation swaps to a KMS / Key /// Vault provider behind the same interface. Plaintext is never logged. /// public sealed class SymmetricFieldEncryptor : IFieldEncryptor { private readonly byte[] _key; private readonly byte[] _hashKey; public SymmetricFieldEncryptor(IOptions options) { var settings = options.Value.FieldEncryption; // Derive a stable 32-byte AES key from whatever the operator configured (any length/format), // so a human-friendly secret still yields a valid key. SHA-256 of the configured material. _key = SHA256.HashData(Encoding.UTF8.GetBytes(Require(settings.Key, nameof(settings.Key)))); var hashMaterial = string.IsNullOrEmpty(settings.HashKey) ? settings.Key : settings.HashKey; _hashKey = SHA256.HashData(Encoding.UTF8.GetBytes(Require(hashMaterial, nameof(settings.HashKey)))); } public string Encrypt(string plaintext) { if (string.IsNullOrEmpty(plaintext)) return plaintext; using var aes = Aes.Create(); aes.Key = _key; aes.GenerateIV(); using var encryptor = aes.CreateEncryptor(); var plainBytes = Encoding.UTF8.GetBytes(plaintext); var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); var result = new byte[aes.IV.Length + cipherBytes.Length]; Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length); Buffer.BlockCopy(cipherBytes, 0, result, aes.IV.Length, cipherBytes.Length); return Convert.ToBase64String(result); } public string Decrypt(string ciphertext) { if (string.IsNullOrEmpty(ciphertext)) return ciphertext; var cipherWithIv = Convert.FromBase64String(ciphertext); using var aes = Aes.Create(); aes.Key = _key; var ivLength = aes.BlockSize / 8; var iv = new byte[ivLength]; Buffer.BlockCopy(cipherWithIv, 0, iv, 0, ivLength); aes.IV = iv; using var decryptor = aes.CreateDecryptor(); var cipherBytes = decryptor.TransformFinalBlock(cipherWithIv, ivLength, cipherWithIv.Length - ivLength); return Encoding.UTF8.GetString(cipherBytes); } public string Hash(string value) { if (string.IsNullOrEmpty(value)) return value; using var hmac = new HMACSHA256(_hashKey); var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(value)); return Convert.ToHexString(hashBytes); } private static string Require(string value, string name) => string.IsNullOrWhiteSpace(value) ? throw new InvalidOperationException($"Seams:FieldEncryption:{name} must be configured.") : value; }