-
Notifications
You must be signed in to change notification settings - Fork 110
Using Encryption and Compression
The foundation of our backup system uses MemoryStream to handle data in memory before applying compression and encryption:
public MemoryStream ExportDatabaseToMemoryStream(string connectionString, bool exportRows = true)
{
MemoryStream ms = new MemoryStream();
try
{
using (var conn = new MySqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ExportInfo.ExportRows = exportRows;
mb.ExportToStream(ms);
}
ms.Position = 0; // Reset position for subsequent operations
return ms;
}
catch
{
ms?.Dispose();
throw;
}
}Compression significantly reduces backup file size (typically 50-70% smaller). We use the DeflateStream for optimal compression:
public static class CompressorHelper
{
public static byte[] Compress(byte[] data)
{
using (var output = new MemoryStream())
{
using (var deflateStream = new DeflateStream(output, CompressionLevel.Optimal))
{
deflateStream.Write(data, 0, data.Length);
}
return output.ToArray();
}
}
public static byte[] Decompress(byte[] compressedData)
{
using (var input = new MemoryStream(compressedData))
using (var output = new MemoryStream())
{
using (var deflateStream = new DeflateStream(input, CompressionMode.Decompress))
{
deflateStream.CopyTo(output);
}
return output.ToArray();
}
}
}For secure backups, we implement AES encryption with proper key derivation:
public static class AesHelper
{
private const int SaltSize = 16;
private const int KeySize = 32; // 256 bits
private const int IvSize = 16; // 128 bits
private const int Iterations = 100000; // OWASP recommended minimum
public static byte[] Encrypt(byte[] data, byte[] password)
{
// Generate random salt for each encryption
byte[] salt = new byte[SaltSize];
RandomNumberGenerator.Fill(salt);
using (var pdb = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256))
{
byte[] key = pdb.GetBytes(KeySize);
byte[] iv = pdb.GetBytes(IvSize);
byte[] encryptedData = PerformAesOperation(data, key, iv, true);
// Prepend salt to encrypted data for storage
byte[] result = new byte[salt.Length + encryptedData.Length];
Buffer.BlockCopy(salt, 0, result, 0, salt.Length);
Buffer.BlockCopy(encryptedData, 0, result, salt.Length, encryptedData.Length);
return result;
}
}
public static byte[] Decrypt(byte[] encryptedDataWithSalt, byte[] password)
{
if (encryptedDataWithSalt.Length < SaltSize)
throw new ArgumentException("Invalid encrypted data format");
// Extract salt from the beginning
byte[] salt = new byte[SaltSize];
byte[] encryptedData = new byte[encryptedDataWithSalt.Length - SaltSize];
Buffer.BlockCopy(encryptedDataWithSalt, 0, salt, 0, SaltSize);
Buffer.BlockCopy(encryptedDataWithSalt, SaltSize, encryptedData, 0, encryptedData.Length);
using (var pdb = new Rfc2898DeriveBytes(password, salt, Iterations, HashAlgorithmName.SHA256))
{
byte[] key = pdb.GetBytes(KeySize);
byte[] iv = pdb.GetBytes(IvSize);
return PerformAesOperation(encryptedData, key, iv, false);
}
}
private static byte[] PerformAesOperation(byte[] data, byte[] key, byte[] iv, bool encrypt)
{
using (var aes = Aes.Create())
{
aes.Key = key;
aes.IV = iv;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
using (var memoryStream = new MemoryStream())
using (var cryptoStream = new CryptoStream(memoryStream,
encrypt ? aes.CreateEncryptor() : aes.CreateDecryptor(),
CryptoStreamMode.Write))
{
cryptoStream.Write(data, 0, data.Length);
cryptoStream.FlushFinalBlock();
return memoryStream.ToArray();
}
}
}
}Here's a comprehensive backup method that combines all components:
public void CreateBackup(string connectionString, bool compress = true, bool encrypt = true, string password = null)
{
MemoryStream ms = null;
try
{
// Step 1: Export database to MemoryStream
ms = ExportDatabaseToMemoryStream(connectionString);
// Step 2: Apply compression if requested
if (compress)
{
byte[] compressedData = CompressorHelper.Compress(ms.ToArray());
ms.Dispose(); // Important: dispose before reassigning
ms = new MemoryStream(compressedData);
}
// Step 3: Apply encryption if requested
if (encrypt)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password is required for encryption");
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] encryptedData = AesHelper.Encrypt(ms.ToArray(), passwordBytes);
ms.Dispose(); // Important: dispose before reassigning
ms = new MemoryStream(encryptedData);
}
// Step 4: Generate appropriate filename
string filename = GenerateBackupFilename(compress, encrypt);
// Step 5: Save to file or send as response
SaveBackupToFile(ms, filename);
// OR for web applications: SendAsDownload(ms, filename);
}
finally
{
ms?.Dispose(); // Always dispose of MemoryStream
}
}
private string GenerateBackupFilename(bool compressed, bool encrypted)
{
string suffix = "";
if (compressed) suffix += "-compressed";
if (encrypted) suffix += "-encrypted";
string extension = encrypted ? ".bin" : (compressed ? ".gz" : ".sql");
return $"backup{suffix}-{DateTime.Now:yyyy-MM-dd_HHmmss}{extension}";
}The restore process reverses the backup operations:
public void RestoreDatabase(string connectionString, byte[] backupData, bool wasCompressed, bool wasEncrypted, string password = null)
{
MemoryStream ms = new MemoryStream(backupData);
try
{
// Step 1: Decrypt if necessary
if (wasEncrypted)
{
if (string.IsNullOrWhiteSpace(password))
throw new ArgumentException("Password is required for decryption");
byte[] passwordBytes = Encoding.UTF8.GetBytes(password);
byte[] decryptedData = AesHelper.Decrypt(ms.ToArray(), passwordBytes);
ms.Dispose();
ms = new MemoryStream(decryptedData);
}
// Step 2: Decompress if necessary
if (wasCompressed)
{
byte[] decompressedData = CompressorHelper.Decompress(ms.ToArray());
ms.Dispose();
ms = new MemoryStream(decompressedData);
}
// Step 3: Import to database
ms.Position = 0;
using (var conn = new MySqlConnection(connectionString))
using (var cmd = conn.CreateCommand())
using (var mb = new MySqlBackup(cmd))
{
conn.Open();
mb.ImportFromStream(ms);
}
}
finally
{
ms?.Dispose();
}
}Always dispose of MemoryStream objects properly to prevent memory leaks. The pattern shown above ensures proper cleanup even when exceptions occur.
- Use strong passwords for encryption
- Consider using more advanced key derivation functions like PBKDF2 for production environments
- Store passwords securely and never hardcode them in your application
- For large databases, consider processing data in chunks rather than loading everything into memory
- Monitor memory usage during backup operations
- Consider implementing progress reporting for long-running operations
Implement comprehensive error handling to gracefully manage various failure scenarios:
try
{
// Backup operations
}
catch (MySqlException ex)
{
// Handle database-specific errors
LogError($"Database error during backup: {ex.Message}");
throw new BackupException("Database backup failed", ex);
}
catch (CryptographicException ex)
{
// Handle encryption/decryption errors
LogError($"Encryption error: {ex.Message}");
throw new BackupException("Encryption operation failed", ex);
}
catch (Exception ex)
{
// Handle general errors
LogError($"Unexpected error: {ex.Message}");
throw;
}For web applications, you can provide backup downloads directly to users:
public void SendBackupAsDownload(MemoryStream backupStream, string filename)
{
backupStream.Position = 0;
Response.Clear();
Response.ContentType = "application/octet-stream";
Response.Headers.Add("Content-Length", backupStream.Length.ToString());
Response.Headers.Add("Content-Disposition", $"attachment; filename=\"{filename}\"");
backupStream.CopyTo(Response.OutputStream);
Response.Flush();
Response.End();
}This provides a robust, secure, and efficient method for database backup and restore operations. The combination of compression and encryption ensures that your backups are both space-efficient and secure, while proper memory management prevents resource leaks in production environments.
Remember to test your backup and restore procedures thoroughly in a development environment before deploying to production, and always verify that restored data maintains its integrity.