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);
}
}