add project
This commit is contained in:
324
Services/EncryptionService.cs
Normal file
324
Services/EncryptionService.cs
Normal file
@ -0,0 +1,324 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user