rust_keyvault/
backup.rs

1//! Backup and restore functionality
2
3use crate::{Algorithm, Error, Result};
4use serde::{Deserialize, Serialize};
5use std::time::SystemTime;
6
7/// Current version of the backup format
8pub const BACKUP_FORMAT_VERSION: u32 = 1;
9
10/// Configuration for creating a backup
11#[derive(Debug, Clone)]
12pub struct BackupConfig {
13    /// Include audit logs in the backup
14    pub include_audit_logs: bool,
15
16    /// Compress the backup data (reduces size by ~60-70%)
17    pub compress: bool,
18
19    /// Password for encrypting the backup
20    pub encryption_password: Vec<u8>,
21
22    /// Optional comment/description
23    pub comment: Option<String>,
24}
25
26impl Default for BackupConfig {
27    fn default() -> Self {
28        Self {
29            include_audit_logs: true,
30            compress: true,
31            encryption_password: Vec::new(),
32            comment: None,
33        }
34    }
35}
36
37/// Metadata about a backup
38#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BackupMetadata {
40    /// When the backup was created
41    pub created_at: SystemTime,
42
43    /// Number of keys in the backup
44    pub key_count: usize,
45
46    /// Backup format version
47    pub format_version: u32,
48
49    /// HMAC checksum for integrity verification
50    pub checksum: Vec<u8>,
51
52    /// Whether the backup is compressed
53    pub compressed: bool,
54
55    /// Whether audit logs are included
56    pub has_audit_logs: bool,
57
58    /// Optional comment/description
59    pub comment: Option<String>,
60
61    /// Size of encrypted data in bytes
62    pub data_size: usize,
63}
64
65/// Argon2 parameters for backup encryption
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct BackupArgon2Params {
68    /// Memory size in KiB (default: 64 MiB = 65536 KiB)
69    pub memory_kib: u32,
70    /// Number of iterations (default: 4)
71    pub time_cost: u32,
72    /// Degree of parallelism (default: 4)
73    pub parallelism: u32,
74}
75
76impl Default for BackupArgon2Params {
77    fn default() -> Self {
78        Self {
79            memory_kib: 65536, // 64 MiB
80            time_cost: 4,
81            parallelism: 4,
82        }
83    }
84}
85
86/// A complete encrypted backup of a vault
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct VaultBackup {
89    /// Backup format version
90    pub format_version: u32,
91
92    /// Backup metadata
93    pub metadata: BackupMetadata,
94
95    /// Salt for password derivation (32 bytes)
96    pub salt: Vec<u8>,
97
98    /// Argon2 parameters used
99    pub argon2_params: BackupArgon2Params,
100
101    /// Algorithm used for encryption
102    pub encryption_algorithm: Algorithm,
103
104    /// Encrypted backup data (nonce + ciphertext + tag)
105    pub encrypted_data: Vec<u8>,
106}
107
108/// Internal structure for backup data before encryption
109#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct BackupData {
111    /// All exported keys from the vault
112    pub keys: Vec<crate::export::ExportedKey>,
113
114    /// Audit log entries (if included)
115    pub audit_logs: Option<Vec<crate::audit::AuditEvent>>,
116
117    /// Vault metadata (creation time, etc.)
118    pub vault_info: VaultInfo,
119}
120
121/// Information about the vault being backed up
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct VaultInfo {
124    /// When the vault was created
125    pub created_at: SystemTime,
126
127    /// Total number of operations performed
128    pub operation_count: u64,
129}
130
131impl VaultBackup {
132    /// Create a new encrypted backup
133    pub fn new(backup_data: &BackupData, password: &[u8], config: &BackupConfig) -> Result<Self> {
134        use crate::crypto::{NonceGenerator, RandomNonceGenerator, RuntimeAead, AEAD};
135        use argon2::{Algorithm as Argon2Algo, Argon2, Params, Version};
136        use rand_chacha::ChaCha20Rng;
137        use rand_core::{RngCore, SeedableRng};
138
139        // Generate random salt
140        let mut salt = vec![0u8; 32];
141        let mut rng = ChaCha20Rng::from_entropy();
142        rng.fill_bytes(&mut salt);
143
144        // Serialize backup data
145        let serialized = serde_json::to_vec(backup_data)
146            .map_err(|e| Error::storage(format!("serialize_backup: {}", e), String::new()))?;
147
148        // Compress if requested
149        let data_to_encrypt = if config.compress {
150            use flate2::write::GzEncoder;
151            use flate2::Compression;
152            use std::io::Write;
153
154            let mut encoder = GzEncoder::new(Vec::new(), Compression::default());
155            encoder.write_all(&serialized).map_err(|e| {
156                Error::storage("compress_backup", &format!("compression failed: {}", e))
157            })?;
158            encoder.finish().map_err(|e| {
159                Error::storage("compress_backup", &format!("compression failed: {}", e))
160            })?
161        } else {
162            serialized
163        };
164
165        // Derive encryption key using Argon2id
166        let argon2_params = BackupArgon2Params::default();
167        let params = Params::new(
168            argon2_params.memory_kib,
169            argon2_params.time_cost,
170            argon2_params.parallelism,
171            Some(32), // 256-bit key
172        )
173        .map_err(|e| Error::crypto("derive_backup_key", &format!("Argon2 error: {}", e)))?;
174
175        let argon2 = Argon2::new(Argon2Algo::Argon2id, Version::V0x13, params);
176        let mut derived_key = vec![0u8; 32];
177        argon2
178            .hash_password_into(password, &salt, &mut derived_key)
179            .map_err(|e| Error::crypto("derive_backup_key", &format!("Argon2 error: {}", e)))?;
180
181        // Create a SecretKey for encryption
182        let encryption_algorithm = Algorithm::XChaCha20Poly1305;
183        let wrapping_key =
184            crate::key::SecretKey::from_bytes(derived_key.clone(), encryption_algorithm)?;
185
186        // Encrypt using RuntimeAead
187        let aead = RuntimeAead;
188        let nonce_size = 24; // XChaCha20Poly1305 uses 24-byte nonces
189
190        let mut nonce_gen = RandomNonceGenerator::new(ChaCha20Rng::from_entropy(), nonce_size);
191
192        let nonce = nonce_gen.generate_nonce(b"vault-backup")?;
193        let ciphertext = aead.encrypt(&wrapping_key, &nonce, &data_to_encrypt, &[])?;
194
195        // Combine nonce + ciphertext
196        let mut encrypted_data = nonce.to_vec();
197        encrypted_data.extend_from_slice(&ciphertext);
198
199        // Calculate HMAC for integrity
200        let checksum = Self::calculate_hmac(&encrypted_data, &derived_key)?;
201
202        let metadata = BackupMetadata {
203            created_at: SystemTime::now(),
204            key_count: backup_data.keys.len(),
205            format_version: BACKUP_FORMAT_VERSION,
206            checksum: checksum.clone(),
207            compressed: config.compress,
208            has_audit_logs: backup_data.audit_logs.is_some(),
209            comment: config.comment.clone(),
210            data_size: encrypted_data.len(),
211        };
212
213        Ok(Self {
214            format_version: BACKUP_FORMAT_VERSION,
215            metadata,
216            salt,
217            argon2_params,
218            encryption_algorithm,
219            encrypted_data,
220        })
221    }
222
223    /// Decrypt and restore a backup
224    pub fn decrypt(&self, password: &[u8]) -> Result<BackupData> {
225        use crate::crypto::{RuntimeAead, AEAD};
226        use argon2::{Algorithm as Argon2Algo, Argon2, Params, Version};
227
228        // Derive decryption key
229        let params = Params::new(
230            self.argon2_params.memory_kib,
231            self.argon2_params.time_cost,
232            self.argon2_params.parallelism,
233            Some(32),
234        )
235        .map_err(|e| Error::crypto("derive_backup_key", &format!("Argon2 error: {}", e)))?;
236
237        let argon2 = Argon2::new(Argon2Algo::Argon2id, Version::V0x13, params);
238        let mut derived_key = vec![0u8; 32];
239        argon2
240            .hash_password_into(password, &self.salt, &mut derived_key)
241            .map_err(|e| Error::crypto("derive_backup_key", &format!("Argon2 error: {}", e)))?;
242
243        // Verify HMAC
244        let calculated_hmac = Self::calculate_hmac(&self.encrypted_data, &derived_key)?;
245        if calculated_hmac != self.metadata.checksum {
246            return Err(Error::crypto(
247                "verify_backup_hmac",
248                "HMAC verification failed - backup may be corrupted",
249            ));
250        }
251
252        // Extract nonce and ciphertext
253        let nonce_size = match self.encryption_algorithm {
254            Algorithm::XChaCha20Poly1305 => 24,
255            Algorithm::ChaCha20Poly1305 | Algorithm::Aes256Gcm => 12,
256            _ => {
257                return Err(Error::crypto(
258                    "unsupported_algorithm",
259                    "unsupported encryption algorithm for backup",
260                ))
261            }
262        };
263
264        if self.encrypted_data.len() < nonce_size {
265            return Err(Error::crypto("decrypt_backup", "encrypted data too short"));
266        }
267
268        let (nonce, ciphertext) = self.encrypted_data.split_at(nonce_size);
269
270        // Create a SecretKey for decryption
271        let wrapping_key =
272            crate::key::SecretKey::from_bytes(derived_key.clone(), self.encryption_algorithm)?;
273
274        // Decrypt
275        let aead = RuntimeAead;
276        let decrypted = aead.decrypt(&wrapping_key, nonce, ciphertext, &[])?;
277
278        // Decompress if needed
279        let decompressed = if self.metadata.compressed {
280            use flate2::read::GzDecoder;
281            use std::io::Read;
282
283            let mut decoder = GzDecoder::new(&decrypted[..]);
284            let mut result = Vec::new();
285            decoder.read_to_end(&mut result).map_err(|e| {
286                Error::storage("decompress_backup", &format!("decompression failed: {}", e))
287            })?;
288            result
289        } else {
290            decrypted
291        };
292
293        // Deserialize
294        serde_json::from_slice(&decompressed).map_err(|e| {
295            Error::storage(
296                "deserialize_backup",
297                &format!("deserialization failed: {}", e),
298            )
299        })
300    }
301
302    /// Calculate HMAC-SHA256 for integrity verification
303    fn calculate_hmac(data: &[u8], key: &[u8]) -> Result<Vec<u8>> {
304        use hmac::{Hmac, Mac};
305        use sha2::Sha256;
306
307        type HmacSha256 = Hmac<Sha256>;
308
309        let mut mac = HmacSha256::new_from_slice(key)
310            .map_err(|e| Error::crypto("create_hmac", &format!("HMAC error: {}", e)))?;
311
312        mac.update(data);
313        Ok(mac.finalize().into_bytes().to_vec())
314    }
315
316    /// Serialize backup to JSON
317    pub fn to_json(&self) -> Result<String> {
318        serde_json::to_string_pretty(self).map_err(|e| {
319            Error::storage("serialize_backup", &format!("serialization failed: {}", e))
320        })
321    }
322
323    /// Deserialize backup from JSON
324    pub fn from_json(json: &str) -> Result<Self> {
325        serde_json::from_str(json).map_err(|e| {
326            Error::storage(
327                "deserialize_backup",
328                &format!("deserialization failed: {}", e),
329            )
330        })
331    }
332
333    /// Serialize backup to binary format
334    pub fn to_bytes(&self) -> Result<Vec<u8>> {
335        serde_json::to_vec(self).map_err(|e| {
336            Error::storage("serialize_backup", &format!("serialization failed: {}", e))
337        })
338    }
339
340    /// Deserialize backup from binary format
341    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
342        serde_json::from_slice(bytes).map_err(|e| {
343            Error::storage(
344                "deserialize_backup",
345                &format!("deserialization failed: {}", e),
346            )
347        })
348    }
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354    use crate::{export::ExportedKey, key::SecretKey, KeyId, KeyMetadata, KeyState};
355
356    fn create_test_backup_data() -> BackupData {
357        let key_id = KeyId::generate_base().unwrap();
358        let secret_key = SecretKey::generate(Algorithm::ChaCha20Poly1305).unwrap();
359        let metadata = KeyMetadata {
360            id: key_id.clone(),
361            base_id: key_id.clone(),
362            algorithm: Algorithm::ChaCha20Poly1305,
363            created_at: SystemTime::now(),
364            expires_at: None,
365            state: KeyState::Active,
366            version: 1,
367        };
368
369        let exported_key = ExportedKey::new(
370            &secret_key,
371            metadata,
372            b"test-password",
373            Algorithm::XChaCha20Poly1305,
374        )
375        .unwrap();
376
377        BackupData {
378            keys: vec![exported_key],
379            audit_logs: None,
380            vault_info: VaultInfo {
381                created_at: SystemTime::now(),
382                operation_count: 42,
383            },
384        }
385    }
386
387    #[test]
388    fn test_backup_encrypt_decrypt() {
389        let backup_data = create_test_backup_data();
390        let password = b"backup-password-123";
391
392        let config = BackupConfig {
393            include_audit_logs: false,
394            compress: true,
395            encryption_password: password.to_vec(),
396            comment: Some("Test backup".to_string()),
397        };
398
399        // Encrypt
400        let backup = VaultBackup::new(&backup_data, password, &config).unwrap();
401
402        assert_eq!(backup.format_version, BACKUP_FORMAT_VERSION);
403        assert_eq!(backup.metadata.key_count, 1);
404        assert!(backup.metadata.compressed);
405        assert!(!backup.metadata.has_audit_logs);
406
407        // Decrypt
408        let decrypted = backup.decrypt(password).unwrap();
409
410        assert_eq!(decrypted.keys.len(), 1);
411        assert!(decrypted.audit_logs.is_none());
412        assert_eq!(decrypted.vault_info.operation_count, 42);
413    }
414
415    #[test]
416    fn test_backup_wrong_password() {
417        let backup_data = create_test_backup_data();
418        let password = b"correct-password";
419        let wrong_password = b"wrong-password";
420
421        let config = BackupConfig::default();
422        let backup = VaultBackup::new(&backup_data, password, &config).unwrap();
423
424        // Should fail with wrong password
425        assert!(backup.decrypt(wrong_password).is_err());
426    }
427
428    #[test]
429    fn test_backup_json_serialization() {
430        let backup_data = create_test_backup_data();
431        let password = b"test-password";
432
433        let config = BackupConfig::default();
434        let backup = VaultBackup::new(&backup_data, password, &config).unwrap();
435
436        // Serialize to JSON
437        let json = backup.to_json().unwrap();
438        assert!(json.contains("format_version"));
439        assert!(json.contains("encrypted_data"));
440
441        // Deserialize
442        let deserialized = VaultBackup::from_json(&json).unwrap();
443
444        // Verify can still decrypt
445        let decrypted = deserialized.decrypt(password).unwrap();
446        assert_eq!(decrypted.keys.len(), 1);
447    }
448
449    #[test]
450    fn test_backup_hmac_verification() {
451        let backup_data = create_test_backup_data();
452        let password = b"test-password";
453
454        let config = BackupConfig::default();
455        let mut backup = VaultBackup::new(&backup_data, password, &config).unwrap();
456
457        // Corrupt the encrypted data
458        if let Some(byte) = backup.encrypted_data.get_mut(10) {
459            *byte = byte.wrapping_add(1);
460        }
461
462        // Should fail HMAC verification
463        let result = backup.decrypt(password);
464        assert!(result.is_err());
465        assert!(result.unwrap_err().to_string().contains("HMAC"));
466    }
467
468    #[test]
469    fn test_backup_compression() {
470        let backup_data = create_test_backup_data();
471        let password = b"test-password";
472
473        // Create compressed backup
474        let config_compressed = BackupConfig {
475            compress: true,
476            ..Default::default()
477        };
478        let backup_compressed =
479            VaultBackup::new(&backup_data, password, &config_compressed).unwrap();
480
481        // Create uncompressed backup
482        let config_uncompressed = BackupConfig {
483            compress: false,
484            ..Default::default()
485        };
486        let backup_uncompressed =
487            VaultBackup::new(&backup_data, password, &config_uncompressed).unwrap();
488
489        // Compressed should be smaller (usually 60-70% reduction)
490        assert!(backup_compressed.encrypted_data.len() < backup_uncompressed.encrypted_data.len());
491
492        // Both should decrypt successfully
493        assert!(backup_compressed.decrypt(password).is_ok());
494        assert!(backup_uncompressed.decrypt(password).is_ok());
495    }
496}