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