using System.Security.Cryptography; using System.Text; namespace DatabaseSnapshotsService.Services { public class EncryptionService { private readonly string _encryptionKey; private readonly bool _encryptionEnabled; public EncryptionService(string? encryptionKey, bool encryptionEnabled = false) { _encryptionEnabled = encryptionEnabled; if (encryptionEnabled && string.IsNullOrWhiteSpace(encryptionKey)) { throw new ArgumentException("Encryption key is required when encryption is enabled"); } _encryptionKey = encryptionKey ?? string.Empty; } public bool IsEncryptionEnabled => _encryptionEnabled; /// /// Encrypts data using AES-256-CBC /// /// Data to encrypt /// Encrypted data as base64 string public async Task EncryptAsync(string plaintext) { if (!_encryptionEnabled) { return plaintext; } if (string.IsNullOrEmpty(plaintext)) { return string.Empty; } try { using var aes = Aes.Create(); aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; // Derive key from the provided encryption key var key = DeriveKey(_encryptionKey, aes.KeySize / 8); aes.Key = key; // Generate random IV aes.GenerateIV(); using var encryptor = aes.CreateEncryptor(); var plaintextBytes = Encoding.UTF8.GetBytes(plaintext); var ciphertext = encryptor.TransformFinalBlock(plaintextBytes, 0, plaintextBytes.Length); // Combine IV and ciphertext var result = new byte[aes.IV.Length + ciphertext.Length]; Buffer.BlockCopy(aes.IV, 0, result, 0, aes.IV.Length); Buffer.BlockCopy(ciphertext, 0, result, aes.IV.Length, ciphertext.Length); return Convert.ToBase64String(result); } catch (Exception ex) { throw new InvalidOperationException($"Encryption failed: {ex.Message}", ex); } } /// /// Decrypts data using AES-256-CBC /// /// Encrypted data as base64 string /// Decrypted data public async Task DecryptAsync(string ciphertext) { if (!_encryptionEnabled) { return ciphertext; } if (string.IsNullOrEmpty(ciphertext)) { return string.Empty; } try { var encryptedData = Convert.FromBase64String(ciphertext); using var aes = Aes.Create(); aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; // Derive key from the provided encryption key var key = DeriveKey(_encryptionKey, aes.KeySize / 8); aes.Key = key; // Extract IV and ciphertext var ivSize = aes.IV.Length; var ciphertextSize = encryptedData.Length - ivSize; if (ciphertextSize < 0) { throw new ArgumentException("Invalid encrypted data format"); } var iv = new byte[ivSize]; var ciphertextBytes = new byte[ciphertextSize]; Buffer.BlockCopy(encryptedData, 0, iv, 0, ivSize); Buffer.BlockCopy(encryptedData, ivSize, ciphertextBytes, 0, ciphertextSize); aes.IV = iv; using var decryptor = aes.CreateDecryptor(); var plaintext = decryptor.TransformFinalBlock(ciphertextBytes, 0, ciphertextBytes.Length); return Encoding.UTF8.GetString(plaintext); } catch (Exception ex) { throw new InvalidOperationException($"Decryption failed: {ex.Message}", ex); } } /// /// Encrypts a file /// /// Path to the source file /// Path for the encrypted file public async Task EncryptFileAsync(string sourceFilePath, string destinationFilePath) { if (!_encryptionEnabled) { // If encryption is disabled, just copy the file File.Copy(sourceFilePath, destinationFilePath, true); return; } try { using var sourceStream = File.OpenRead(sourceFilePath); using var destinationStream = File.Create(destinationFilePath); using var aes = Aes.Create(); aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; var key = DeriveKey(_encryptionKey, aes.KeySize / 8); aes.Key = key; aes.GenerateIV(); // Write IV to the beginning of the file await destinationStream.WriteAsync(aes.IV); using var encryptor = aes.CreateEncryptor(); using var cryptoStream = new CryptoStream(destinationStream, encryptor, CryptoStreamMode.Write); await sourceStream.CopyToAsync(cryptoStream); await cryptoStream.FlushFinalBlockAsync(); } catch (Exception ex) { throw new InvalidOperationException($"File encryption failed: {ex.Message}", ex); } } /// /// Decrypts a file /// /// Path to the encrypted file /// Path for the decrypted file public async Task DecryptFileAsync(string sourceFilePath, string destinationFilePath) { if (!_encryptionEnabled) { // If encryption is disabled, just copy the file File.Copy(sourceFilePath, destinationFilePath, true); return; } try { using var sourceStream = File.OpenRead(sourceFilePath); using var destinationStream = File.Create(destinationFilePath); using var aes = Aes.Create(); aes.KeySize = 256; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; var key = DeriveKey(_encryptionKey, aes.KeySize / 8); aes.Key = key; // Read IV from the beginning of the file var iv = new byte[aes.IV.Length]; await sourceStream.ReadAsync(iv); aes.IV = iv; using var decryptor = aes.CreateDecryptor(); using var cryptoStream = new CryptoStream(sourceStream, decryptor, CryptoStreamMode.Read); await cryptoStream.CopyToAsync(destinationStream); } catch (Exception ex) { throw new InvalidOperationException($"File decryption failed: {ex.Message}", ex); } } /// /// Generates a secure encryption key /// /// Size of the key in bits (default: 256) /// Base64 encoded encryption key public static string GenerateEncryptionKey(int keySize = 256) { if (keySize != 128 && keySize != 192 && keySize != 256) { throw new ArgumentException("Key size must be 128, 192, or 256 bits"); } using var aes = Aes.Create(); aes.KeySize = keySize; aes.GenerateKey(); return Convert.ToBase64String(aes.Key); } /// /// Validates an encryption key /// /// The encryption key to validate /// True if the key is valid, false otherwise public static bool ValidateEncryptionKey(string key) { if (string.IsNullOrWhiteSpace(key)) { return false; } try { var keyBytes = Convert.FromBase64String(key); return keyBytes.Length == 16 || keyBytes.Length == 24 || keyBytes.Length == 32; // 128, 192, or 256 bits } catch { return false; } } /// /// Derives a key from a password using PBKDF2 /// /// The password to derive the key from /// Size of the derived key in bytes /// The derived key private static byte[] DeriveKey(string password, int keySize) { // Create a deterministic salt from the password hash using var sha256 = SHA256.Create(); var passwordHash = sha256.ComputeHash(Encoding.UTF8.GetBytes(password)); var salt = new byte[32]; Array.Copy(passwordHash, salt, Math.Min(passwordHash.Length, salt.Length)); using var pbkdf2 = new Rfc2898DeriveBytes(password, salt, 10000, HashAlgorithmName.SHA256); return pbkdf2.GetBytes(keySize); } /// /// Creates a checksum of encrypted data for integrity verification /// /// The data to create a checksum for /// SHA-256 hash of the data public static string CreateChecksum(byte[] data) { using var sha256 = SHA256.Create(); var hash = sha256.ComputeHash(data); return Convert.ToBase64String(hash); } /// /// Creates a checksum of a string /// /// The string to create a checksum for /// SHA-256 hash of the string public static string CreateChecksum(string data) { var bytes = Encoding.UTF8.GetBytes(data); return CreateChecksum(bytes); } /// /// Verifies the integrity of encrypted data /// /// The data to verify /// The expected checksum /// True if the checksum matches, false otherwise public static bool VerifyChecksum(byte[] data, string expectedChecksum) { var actualChecksum = CreateChecksum(data); return actualChecksum.Equals(expectedChecksum, StringComparison.Ordinal); } /// /// Verifies the integrity of a string /// /// The string to verify /// The expected checksum /// True if the checksum matches, false otherwise public static bool VerifyChecksum(string data, string expectedChecksum) { var actualChecksum = CreateChecksum(data); return actualChecksum.Equals(expectedChecksum, StringComparison.Ordinal); } } }