using System.Security.Cryptography; using System.Text; using Microsoft.Extensions.Configuration; namespace Codex.Dal.Services; /// /// AES-256 encryption service with random IV generation. /// Thread-safe implementation for encrypting sensitive data like API keys. /// public class AesEncryptionService : IEncryptionService { private readonly byte[] _key; /// /// Initializes the encryption service with a key from configuration. /// /// Application configuration /// Thrown when encryption key is missing or invalid public AesEncryptionService(IConfiguration configuration) { var keyBase64 = configuration["Encryption:Key"]; if (string.IsNullOrWhiteSpace(keyBase64)) { throw new InvalidOperationException( "Encryption key is not configured. Add 'Encryption:Key' to appsettings.json. " + "Generate a key with: openssl rand -base64 32"); } try { _key = Convert.FromBase64String(keyBase64); } catch (FormatException) { throw new InvalidOperationException( "Encryption key is not a valid Base64 string. " + "Generate a key with: openssl rand -base64 32"); } if (_key.Length != 32) { throw new InvalidOperationException( $"Encryption key must be 32 bytes (256 bits) for AES-256. Current length: {_key.Length} bytes. " + "Generate a key with: openssl rand -base64 32"); } } /// /// Encrypts plain text using AES-256-CBC with a random IV. /// Format: [16-byte IV][encrypted data] /// /// The text to encrypt /// Base64-encoded string containing IV + ciphertext /// Thrown when plainText is null public string Encrypt(string plainText) { if (string.IsNullOrEmpty(plainText)) { throw new ArgumentNullException(nameof(plainText), "Cannot encrypt null or empty text"); } using var aes = Aes.Create(); aes.Key = _key; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.GenerateIV(); // Generate random IV for each encryption using var encryptor = aes.CreateEncryptor(); var plainBytes = Encoding.UTF8.GetBytes(plainText); var cipherBytes = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); // Prepend IV to ciphertext: [IV][ciphertext] 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); } /// /// Decrypts text that was encrypted using the Encrypt method. /// Extracts IV from the first 16 bytes of the encrypted data. /// /// Base64-encoded string containing IV + ciphertext /// Decrypted plain text /// Thrown when encryptedText is null /// Thrown when decryption fails (wrong key or corrupted data) public string Decrypt(string encryptedText) { if (string.IsNullOrEmpty(encryptedText)) { throw new ArgumentNullException(nameof(encryptedText), "Cannot decrypt null or empty text"); } byte[] fullData; try { fullData = Convert.FromBase64String(encryptedText); } catch (FormatException ex) { throw new CryptographicException("Encrypted text is not a valid Base64 string", ex); } const int ivLength = 16; // AES IV is always 16 bytes if (fullData.Length < ivLength) { throw new CryptographicException( $"Encrypted data is too short. Expected at least {ivLength} bytes for IV, got {fullData.Length} bytes"); } using var aes = Aes.Create(); aes.Key = _key; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; // Extract IV from first 16 bytes var iv = new byte[ivLength]; Buffer.BlockCopy(fullData, 0, iv, 0, ivLength); aes.IV = iv; // Extract ciphertext (everything after IV) var cipherBytes = new byte[fullData.Length - ivLength]; Buffer.BlockCopy(fullData, ivLength, cipherBytes, 0, cipherBytes.Length); using var decryptor = aes.CreateDecryptor(); var plainBytes = decryptor.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); return Encoding.UTF8.GetString(plainBytes); } }