CODEX_ADK/BACKEND/Codex.Dal/Services/AesEncryptionService.cs
Svrnty 229a0698a3 Initial commit: CODEX_ADK monorepo
Multi-agent AI laboratory with ASP.NET Core 8.0 backend and Flutter frontend.
Implements CQRS architecture, OpenAPI contract-first API design.

BACKEND: Agent management, conversations, executions with PostgreSQL + Ollama
FRONTEND: Cross-platform UI with strict typing and Result-based error handling

Co-Authored-By: Jean-Philippe Brule <jp@svrnty.io>
2025-10-26 23:12:32 -04:00

134 lines
4.9 KiB
C#

using System.Security.Cryptography;
using System.Text;
using Microsoft.Extensions.Configuration;
namespace Codex.Dal.Services;
/// <summary>
/// AES-256 encryption service with random IV generation.
/// Thread-safe implementation for encrypting sensitive data like API keys.
/// </summary>
public class AesEncryptionService : IEncryptionService
{
private readonly byte[] _key;
/// <summary>
/// Initializes the encryption service with a key from configuration.
/// </summary>
/// <param name="configuration">Application configuration</param>
/// <exception cref="InvalidOperationException">Thrown when encryption key is missing or invalid</exception>
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");
}
}
/// <summary>
/// Encrypts plain text using AES-256-CBC with a random IV.
/// Format: [16-byte IV][encrypted data]
/// </summary>
/// <param name="plainText">The text to encrypt</param>
/// <returns>Base64-encoded string containing IV + ciphertext</returns>
/// <exception cref="ArgumentNullException">Thrown when plainText is null</exception>
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);
}
/// <summary>
/// Decrypts text that was encrypted using the Encrypt method.
/// Extracts IV from the first 16 bytes of the encrypted data.
/// </summary>
/// <param name="encryptedText">Base64-encoded string containing IV + ciphertext</param>
/// <returns>Decrypted plain text</returns>
/// <exception cref="ArgumentNullException">Thrown when encryptedText is null</exception>
/// <exception cref="CryptographicException">Thrown when decryption fails (wrong key or corrupted data)</exception>
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);
}
}