feat: Phase 4 Tasks 3-4 - CLI encryption integration
Integrated encryption into backup workflow: cmd/encryption.go: - loadEncryptionKey() - loads from file or env var - Supports base64-encoded keys (32 bytes) - Supports raw 32-byte keys - Supports passphrases (PBKDF2 derivation) - Priority: --encryption-key-file > DBBACKUP_ENCRYPTION_KEY cmd/backup_impl.go: - encryptLatestBackup() - finds and encrypts single backups - encryptLatestClusterBackup() - encrypts cluster backups - findLatestBackup() - locates most recent backup file - findLatestClusterBackup() - locates cluster backup - Encryption applied after successful backup - Integrated into all backup modes (cluster, single, sample) internal/backup/encryption.go: - EncryptBackupFile() - encrypts backup in-place - DecryptBackupFile() - decrypts to new file - IsBackupEncrypted() - checks metadata/file format - Updates .meta.json with encryption info - Replaces original with encrypted version internal/metadata/metadata.go: - Added Encrypted bool field - Added EncryptionAlgorithm string field - Tracks encryption status in backup metadata internal/metadata/save.go: - Helper to save BackupMetadata to .meta.json tests/encryption_smoke_test.sh: - Basic smoke test for encryption/decryption - Verifies data integrity - Tests with env var key source CLI Flags (already existed): --encrypt Enable encryption --encryption-key-file PATH Key file path --encryption-key-env VAR Env var name (default: DBBACKUP_ENCRYPTION_KEY) Usage Examples: # Encrypt with key file ./dbbackup backup single mydb --encrypt --encryption-key-file /path/to/key # Encrypt with env var export DBBACKUP_ENCRYPTION_KEY="base64_encoded_key" ./dbbackup backup single mydb --encrypt # Cluster backup with encryption ./dbbackup backup cluster --encrypt --encryption-key-file key.txt Features: ✅ Post-backup encryption (doesn't slow down backup itself) ✅ In-place encryption (overwrites original) ✅ Metadata tracking (encrypted flag) ✅ Multiple key sources (file/env/passphrase) ✅ Base64 and raw key support ✅ PBKDF2 for passphrases ✅ Automatic latest backup detection ✅ Works with all backup modes Status: ENCRYPTION FULLY INTEGRATED ✅ Next: Task 5 - Restore decryption integration
This commit is contained in:
114
internal/backup/encryption.go
Normal file
114
internal/backup/encryption.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package backup
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"dbbackup/internal/crypto"
|
||||
"dbbackup/internal/logger"
|
||||
"dbbackup/internal/metadata"
|
||||
)
|
||||
|
||||
// EncryptBackupFile encrypts a backup file in-place
|
||||
// The original file is replaced with the encrypted version
|
||||
func EncryptBackupFile(backupPath string, key []byte, log logger.Logger) error {
|
||||
log.Info("Encrypting backup file", "file", filepath.Base(backupPath))
|
||||
|
||||
// Validate key
|
||||
if err := crypto.ValidateKey(key); err != nil {
|
||||
return fmt.Errorf("invalid encryption key: %w", err)
|
||||
}
|
||||
|
||||
// Create encryptor
|
||||
encryptor := crypto.NewAESEncryptor()
|
||||
|
||||
// Generate encrypted file path
|
||||
encryptedPath := backupPath + ".encrypted.tmp"
|
||||
|
||||
// Encrypt file
|
||||
if err := encryptor.EncryptFile(backupPath, encryptedPath, key); err != nil {
|
||||
// Clean up temp file on failure
|
||||
os.Remove(encryptedPath)
|
||||
return fmt.Errorf("encryption failed: %w", err)
|
||||
}
|
||||
|
||||
// Update metadata to indicate encryption
|
||||
metaPath := backupPath + ".meta.json"
|
||||
if _, err := os.Stat(metaPath); err == nil {
|
||||
// Load existing metadata
|
||||
meta, err := metadata.Load(metaPath)
|
||||
if err != nil {
|
||||
log.Warn("Failed to load metadata for encryption update", "error", err)
|
||||
} else {
|
||||
// Mark as encrypted
|
||||
meta.Encrypted = true
|
||||
meta.EncryptionAlgorithm = string(crypto.AlgorithmAES256GCM)
|
||||
|
||||
// Save updated metadata
|
||||
if err := metadata.Save(metaPath, meta); err != nil {
|
||||
log.Warn("Failed to update metadata with encryption info", "error", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove original unencrypted file
|
||||
if err := os.Remove(backupPath); err != nil {
|
||||
log.Warn("Failed to remove original unencrypted file", "error", err)
|
||||
// Don't fail - encrypted file exists
|
||||
}
|
||||
|
||||
// Rename encrypted file to original name
|
||||
if err := os.Rename(encryptedPath, backupPath); err != nil {
|
||||
return fmt.Errorf("failed to rename encrypted file: %w", err)
|
||||
}
|
||||
|
||||
log.Info("Backup encrypted successfully", "file", filepath.Base(backupPath))
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsBackupEncrypted checks if a backup file is encrypted
|
||||
func IsBackupEncrypted(backupPath string) bool {
|
||||
// Check metadata first
|
||||
metaPath := backupPath + ".meta.json"
|
||||
if meta, err := metadata.Load(metaPath); err == nil {
|
||||
return meta.Encrypted
|
||||
}
|
||||
|
||||
// Fallback: check if file starts with encryption nonce
|
||||
file, err := os.Open(backupPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Try to read nonce - if it succeeds, likely encrypted
|
||||
nonce := make([]byte, crypto.NonceSize)
|
||||
if n, err := file.Read(nonce); err != nil || n != crypto.NonceSize {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// DecryptBackupFile decrypts an encrypted backup file
|
||||
// Creates a new decrypted file
|
||||
func DecryptBackupFile(encryptedPath, outputPath string, key []byte, log logger.Logger) error {
|
||||
log.Info("Decrypting backup file", "file", filepath.Base(encryptedPath))
|
||||
|
||||
// Validate key
|
||||
if err := crypto.ValidateKey(key); err != nil {
|
||||
return fmt.Errorf("invalid decryption key: %w", err)
|
||||
}
|
||||
|
||||
// Create encryptor
|
||||
encryptor := crypto.NewAESEncryptor()
|
||||
|
||||
// Decrypt file
|
||||
if err := encryptor.DecryptFile(encryptedPath, outputPath, key); err != nil {
|
||||
return fmt.Errorf("decryption failed (wrong key?): %w", err)
|
||||
}
|
||||
|
||||
log.Info("Backup decrypted successfully", "output", filepath.Base(outputPath))
|
||||
return nil
|
||||
}
|
||||
@@ -30,6 +30,10 @@ type BackupMetadata struct {
|
||||
Duration float64 `json:"duration_seconds"`
|
||||
ExtraInfo map[string]string `json:"extra_info,omitempty"`
|
||||
|
||||
// Encryption fields (v2.3+)
|
||||
Encrypted bool `json:"encrypted"` // Whether backup is encrypted
|
||||
EncryptionAlgorithm string `json:"encryption_algorithm,omitempty"` // e.g., "aes-256-gcm"
|
||||
|
||||
// Incremental backup fields (v2.2+)
|
||||
Incremental *IncrementalMetadata `json:"incremental,omitempty"` // Only present for incremental backups
|
||||
}
|
||||
|
||||
21
internal/metadata/save.go
Normal file
21
internal/metadata/save.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package metadata
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Save writes BackupMetadata to a .meta.json file
|
||||
func Save(metaPath string, metadata *BackupMetadata) error {
|
||||
data, err := json.MarshalIndent(metadata, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal metadata: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(metaPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write metadata file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user