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>
134 lines
4.9 KiB
C#
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);
|
|
}
|
|
}
|