324 lines
12 KiB
C#
324 lines
12 KiB
C#
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;
|
|
|
|
/// <summary>
|
|
/// Encrypts data using AES-256-CBC
|
|
/// </summary>
|
|
/// <param name="plaintext">Data to encrypt</param>
|
|
/// <returns>Encrypted data as base64 string</returns>
|
|
public async Task<string> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrypts data using AES-256-CBC
|
|
/// </summary>
|
|
/// <param name="ciphertext">Encrypted data as base64 string</param>
|
|
/// <returns>Decrypted data</returns>
|
|
public async Task<string> 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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Encrypts a file
|
|
/// </summary>
|
|
/// <param name="sourceFilePath">Path to the source file</param>
|
|
/// <param name="destinationFilePath">Path for the encrypted file</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Decrypts a file
|
|
/// </summary>
|
|
/// <param name="sourceFilePath">Path to the encrypted file</param>
|
|
/// <param name="destinationFilePath">Path for the decrypted file</param>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates a secure encryption key
|
|
/// </summary>
|
|
/// <param name="keySize">Size of the key in bits (default: 256)</param>
|
|
/// <returns>Base64 encoded encryption key</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Validates an encryption key
|
|
/// </summary>
|
|
/// <param name="key">The encryption key to validate</param>
|
|
/// <returns>True if the key is valid, false otherwise</returns>
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Derives a key from a password using PBKDF2
|
|
/// </summary>
|
|
/// <param name="password">The password to derive the key from</param>
|
|
/// <param name="keySize">Size of the derived key in bytes</param>
|
|
/// <returns>The derived key</returns>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a checksum of encrypted data for integrity verification
|
|
/// </summary>
|
|
/// <param name="data">The data to create a checksum for</param>
|
|
/// <returns>SHA-256 hash of the data</returns>
|
|
public static string CreateChecksum(byte[] data)
|
|
{
|
|
using var sha256 = SHA256.Create();
|
|
var hash = sha256.ComputeHash(data);
|
|
return Convert.ToBase64String(hash);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a checksum of a string
|
|
/// </summary>
|
|
/// <param name="data">The string to create a checksum for</param>
|
|
/// <returns>SHA-256 hash of the string</returns>
|
|
public static string CreateChecksum(string data)
|
|
{
|
|
var bytes = Encoding.UTF8.GetBytes(data);
|
|
return CreateChecksum(bytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the integrity of encrypted data
|
|
/// </summary>
|
|
/// <param name="data">The data to verify</param>
|
|
/// <param name="expectedChecksum">The expected checksum</param>
|
|
/// <returns>True if the checksum matches, false otherwise</returns>
|
|
public static bool VerifyChecksum(byte[] data, string expectedChecksum)
|
|
{
|
|
var actualChecksum = CreateChecksum(data);
|
|
return actualChecksum.Equals(expectedChecksum, StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Verifies the integrity of a string
|
|
/// </summary>
|
|
/// <param name="data">The string to verify</param>
|
|
/// <param name="expectedChecksum">The expected checksum</param>
|
|
/// <returns>True if the checksum matches, false otherwise</returns>
|
|
public static bool VerifyChecksum(string data, string expectedChecksum)
|
|
{
|
|
var actualChecksum = CreateChecksum(data);
|
|
return actualChecksum.Equals(expectedChecksum, StringComparison.Ordinal);
|
|
}
|
|
}
|
|
} |