CODEX_ADK/BACKEND/Codex.Dal/Services/AesEncryptionService.cs
jean-philippe 3fae2fcbe1 Initial commit: CODEX_ADK (Svrnty Console) MVP v1.0.0
This is the initial commit for the CODEX_ADK project, a full-stack AI agent
management platform featuring:

BACKEND (ASP.NET Core 8.0):
- CQRS architecture with 6 commands and 7 queries
- 16 API endpoints (all working and tested)
- PostgreSQL database with 5 entities
- AES-256 encryption for API keys
- FluentValidation on all commands
- Rate limiting and CORS configured
- OpenAPI/Swagger documentation
- Docker Compose setup (PostgreSQL + Ollama)

FRONTEND (Flutter 3.x):
- Dark theme with Svrnty branding
- Collapsible sidebar navigation
- CQRS API client with Result<T> error handling
- Type-safe endpoints from OpenAPI schema
- Multi-platform support (Web, iOS, Android, macOS, Linux, Windows)

DOCUMENTATION:
- Comprehensive API reference
- Architecture documentation
- Development guidelines for Claude Code
- API integration guides
- context-claude.md project overview

Status: Backend ready (Grade A-), Frontend integration pending

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-26 18:32:38 -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);
}
}