1use crate::{Algorithm, Error, Result};
4use serde::{Deserialize, Serialize};
5use std::time::SystemTime;
6
7pub const BACKUP_FORMAT_VERSION: u32 = 1;
9
10#[derive(Debug, Clone)]
12pub struct BackupConfig {
13 pub include_audit_logs: bool,
15
16 pub compress: bool,
18
19 pub encryption_password: Vec<u8>,
21
22 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#[derive(Debug, Clone, Serialize, Deserialize)]
39pub struct BackupMetadata {
40 pub created_at: SystemTime,
42
43 pub key_count: usize,
45
46 pub format_version: u32,
48
49 pub checksum: Vec<u8>,
51
52 pub compressed: bool,
54
55 pub has_audit_logs: bool,
57
58 pub comment: Option<String>,
60
61 pub data_size: usize,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct BackupArgon2Params {
68 pub memory_kib: u32,
70 pub time_cost: u32,
72 pub parallelism: u32,
74}
75
76impl Default for BackupArgon2Params {
77 fn default() -> Self {
78 Self {
79 memory_kib: 65536, time_cost: 4,
81 parallelism: 4,
82 }
83 }
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct VaultBackup {
89 pub format_version: u32,
91
92 pub metadata: BackupMetadata,
94
95 pub salt: Vec<u8>,
97
98 pub argon2_params: BackupArgon2Params,
100
101 pub encryption_algorithm: Algorithm,
103
104 pub encrypted_data: Vec<u8>,
106}
107
108#[derive(Debug, Clone, Serialize, Deserialize)]
110pub struct BackupData {
111 pub keys: Vec<crate::export::ExportedKey>,
113
114 pub audit_logs: Option<Vec<crate::audit::AuditEvent>>,
116
117 pub vault_info: VaultInfo,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct VaultInfo {
124 pub created_at: SystemTime,
126
127 pub operation_count: u64,
129}
130
131impl VaultBackup {
132 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 let mut salt = vec![0u8; 32];
141 let mut rng = ChaCha20Rng::from_entropy();
142 rng.fill_bytes(&mut salt);
143
144 let serialized = serde_json::to_vec(backup_data)
146 .map_err(|e| Error::storage(format!("serialize_backup: {}", e), String::new()))?;
147
148 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 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), )
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 let encryption_algorithm = Algorithm::XChaCha20Poly1305;
183 let wrapping_key =
184 crate::key::SecretKey::from_bytes(derived_key.clone(), encryption_algorithm)?;
185
186 let aead = RuntimeAead;
188 let nonce_size = 24; 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 let mut encrypted_data = nonce.to_vec();
197 encrypted_data.extend_from_slice(&ciphertext);
198
199 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 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 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 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 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 let wrapping_key =
272 crate::key::SecretKey::from_bytes(derived_key.clone(), self.encryption_algorithm)?;
273
274 let aead = RuntimeAead;
276 let decrypted = aead.decrypt(&wrapping_key, nonce, ciphertext, &[])?;
277
278 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 serde_json::from_slice(&decompressed).map_err(|e| {
295 Error::storage(
296 "deserialize_backup",
297 &format!("deserialization failed: {}", e),
298 )
299 })
300 }
301
302 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 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 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 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 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 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 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 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 let json = backup.to_json().unwrap();
438 assert!(json.contains("format_version"));
439 assert!(json.contains("encrypted_data"));
440
441 let deserialized = VaultBackup::from_json(&json).unwrap();
443
444 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 if let Some(byte) = backup.encrypted_data.get_mut(10) {
459 *byte = byte.wrapping_add(1);
460 }
461
462 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 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 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 assert!(backup_compressed.encrypted_data.len() < backup_uncompressed.encrypted_data.len());
491
492 assert!(backup_compressed.decrypt(password).is_ok());
494 assert!(backup_uncompressed.decrypt(password).is_ok());
495 }
496}