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
185 lines
5.9 KiB
Go
185 lines
5.9 KiB
Go
package metadata
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
)
|
|
|
|
// BackupMetadata contains comprehensive information about a backup
|
|
type BackupMetadata struct {
|
|
Version string `json:"version"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Database string `json:"database"`
|
|
DatabaseType string `json:"database_type"` // postgresql, mysql, mariadb
|
|
DatabaseVersion string `json:"database_version"` // e.g., "PostgreSQL 15.3"
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
User string `json:"user"`
|
|
BackupFile string `json:"backup_file"`
|
|
SizeBytes int64 `json:"size_bytes"`
|
|
SHA256 string `json:"sha256"`
|
|
Compression string `json:"compression"` // none, gzip, pigz
|
|
BackupType string `json:"backup_type"` // full, incremental (for v2.2)
|
|
BaseBackup string `json:"base_backup,omitempty"`
|
|
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
|
|
}
|
|
|
|
// IncrementalMetadata contains metadata specific to incremental backups
|
|
type IncrementalMetadata struct {
|
|
BaseBackupID string `json:"base_backup_id"` // SHA-256 of base backup
|
|
BaseBackupPath string `json:"base_backup_path"` // Filename of base backup
|
|
BaseBackupTimestamp time.Time `json:"base_backup_timestamp"` // When base backup was created
|
|
IncrementalFiles int `json:"incremental_files"` // Number of changed files
|
|
TotalSize int64 `json:"total_size"` // Total size of changed files (bytes)
|
|
BackupChain []string `json:"backup_chain"` // Chain: [base, incr1, incr2, ...]
|
|
}
|
|
|
|
// ClusterMetadata contains metadata for cluster backups
|
|
type ClusterMetadata struct {
|
|
Version string `json:"version"`
|
|
Timestamp time.Time `json:"timestamp"`
|
|
ClusterName string `json:"cluster_name"`
|
|
DatabaseType string `json:"database_type"`
|
|
Host string `json:"host"`
|
|
Port int `json:"port"`
|
|
Databases []BackupMetadata `json:"databases"`
|
|
TotalSize int64 `json:"total_size_bytes"`
|
|
Duration float64 `json:"duration_seconds"`
|
|
ExtraInfo map[string]string `json:"extra_info,omitempty"`
|
|
}
|
|
|
|
// CalculateSHA256 computes the SHA-256 checksum of a file
|
|
func CalculateSHA256(filePath string) (string, error) {
|
|
f, err := os.Open(filePath)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to open file: %w", err)
|
|
}
|
|
defer f.Close()
|
|
|
|
hasher := sha256.New()
|
|
if _, err := io.Copy(hasher, f); err != nil {
|
|
return "", fmt.Errorf("failed to calculate checksum: %w", err)
|
|
}
|
|
|
|
return hex.EncodeToString(hasher.Sum(nil)), nil
|
|
}
|
|
|
|
// Save writes metadata to a .meta.json file
|
|
func (m *BackupMetadata) Save() error {
|
|
metaPath := m.BackupFile + ".meta.json"
|
|
|
|
data, err := json.MarshalIndent(m, "", " ")
|
|
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
|
|
}
|
|
|
|
// Load reads metadata from a .meta.json file
|
|
func Load(backupFile string) (*BackupMetadata, error) {
|
|
metaPath := backupFile + ".meta.json"
|
|
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read metadata file: %w", err)
|
|
}
|
|
|
|
var meta BackupMetadata
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return nil, fmt.Errorf("failed to parse metadata: %w", err)
|
|
}
|
|
|
|
return &meta, nil
|
|
}
|
|
|
|
// SaveCluster writes cluster metadata to a .meta.json file
|
|
func (m *ClusterMetadata) Save(targetFile string) error {
|
|
metaPath := targetFile + ".meta.json"
|
|
|
|
data, err := json.MarshalIndent(m, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal cluster metadata: %w", err)
|
|
}
|
|
|
|
if err := os.WriteFile(metaPath, data, 0644); err != nil {
|
|
return fmt.Errorf("failed to write cluster metadata file: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LoadCluster reads cluster metadata from a .meta.json file
|
|
func LoadCluster(targetFile string) (*ClusterMetadata, error) {
|
|
metaPath := targetFile + ".meta.json"
|
|
|
|
data, err := os.ReadFile(metaPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read cluster metadata file: %w", err)
|
|
}
|
|
|
|
var meta ClusterMetadata
|
|
if err := json.Unmarshal(data, &meta); err != nil {
|
|
return nil, fmt.Errorf("failed to parse cluster metadata: %w", err)
|
|
}
|
|
|
|
return &meta, nil
|
|
}
|
|
|
|
// ListBackups scans a directory for backup files and returns their metadata
|
|
func ListBackups(dir string) ([]*BackupMetadata, error) {
|
|
pattern := filepath.Join(dir, "*.meta.json")
|
|
matches, err := filepath.Glob(pattern)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan directory: %w", err)
|
|
}
|
|
|
|
var backups []*BackupMetadata
|
|
for _, metaFile := range matches {
|
|
// Extract backup file path (remove .meta.json suffix)
|
|
backupFile := metaFile[:len(metaFile)-len(".meta.json")]
|
|
|
|
meta, err := Load(backupFile)
|
|
if err != nil {
|
|
// Skip invalid metadata files
|
|
continue
|
|
}
|
|
|
|
backups = append(backups, meta)
|
|
}
|
|
|
|
return backups, nil
|
|
}
|
|
|
|
// FormatSize returns human-readable size
|
|
func FormatSize(bytes int64) string {
|
|
const unit = 1024
|
|
if bytes < unit {
|
|
return fmt.Sprintf("%d B", bytes)
|
|
}
|
|
div, exp := int64(unit), 0
|
|
for n := bytes / unit; n >= unit; n /= unit {
|
|
div *= unit
|
|
exp++
|
|
}
|
|
return fmt.Sprintf("%.1f %ciB", float64(bytes)/float64(div), "KMGTPE"[exp])
|
|
}
|