1use super::{BackupConfig, BackupError, Result};
2use serde::{Deserialize, Serialize};
3use std::path::{Path, PathBuf};
4use std::process::Command;
5use tokio::fs;
6use tracing::{debug, error, info, warn};
7
8pub struct BackupEncryption {
10 config: BackupConfig,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct EncryptionKey {
15 pub key_id: String,
16 pub key_path: PathBuf,
17 pub algorithm: EncryptionAlgorithm,
18 pub key_size_bits: u32,
19 pub created_at: chrono::DateTime<chrono::Utc>,
20 pub expires_at: Option<chrono::DateTime<chrono::Utc>>,
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum EncryptionAlgorithm {
25 AES256GCM,
26 ChaCha20Poly1305,
27 AES256CBC,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct EncryptionMetadata {
32 pub encrypted_file_path: PathBuf,
33 pub original_file_path: PathBuf,
34 pub encryption_key_id: String,
35 pub algorithm: EncryptionAlgorithm,
36 pub iv: Vec<u8>,
37 pub checksum: String,
38 pub encrypted_at: chrono::DateTime<chrono::Utc>,
39}
40
41impl BackupEncryption {
42 pub fn new(config: BackupConfig) -> Self {
43 Self { config }
44 }
45
46 pub async fn initialize(&self) -> Result<()> {
48 info!("Initializing backup encryption system");
49
50 if !self.config.enable_encryption {
51 info!("Backup encryption is disabled");
52 return Ok(());
53 }
54
55 self.verify_encryption_tools().await?;
57
58 self.ensure_encryption_key().await?;
60
61 self.setup_key_rotation().await?;
63
64 info!("Backup encryption system initialized");
65 Ok(())
66 }
67
68 pub async fn encrypt_file(&self, file_path: &Path) -> Result<EncryptionMetadata> {
70 if !self.config.enable_encryption {
71 return Err(BackupError::EncryptionError {
72 message: "Encryption is not enabled".to_string(),
73 });
74 }
75
76 info!("Encrypting file: {}", file_path.display());
77
78 let encrypted_path = self.get_encrypted_file_path(file_path)?;
79 let key = self.load_encryption_key().await?;
80
81 let iv = self.generate_iv().await?;
83
84 self.encrypt_with_gpg(file_path, &encrypted_path, &key, &iv)
86 .await?;
87
88 let checksum = self.calculate_file_checksum(&encrypted_path).await?;
90
91 let metadata = EncryptionMetadata {
92 encrypted_file_path: encrypted_path,
93 original_file_path: file_path.to_path_buf(),
94 encryption_key_id: key.key_id.clone(),
95 algorithm: key.algorithm.clone(),
96 iv,
97 checksum,
98 encrypted_at: chrono::Utc::now(),
99 };
100
101 info!("File encrypted successfully: {}", file_path.display());
102 Ok(metadata)
103 }
104
105 pub async fn decrypt_file(&self, encrypted_path: &Path, output_path: &Path) -> Result<()> {
107 if !self.config.enable_encryption {
108 return Err(BackupError::EncryptionError {
109 message: "Encryption is not enabled".to_string(),
110 });
111 }
112
113 info!("Decrypting file: {}", encrypted_path.display());
114
115 let key = self.load_encryption_key().await?;
117
118 self.decrypt_with_gpg(encrypted_path, output_path, &key)
120 .await?;
121
122 info!("File decrypted successfully: {}", output_path.display());
123 Ok(())
124 }
125
126 pub async fn encrypt_backup_in_place(&self, backup_path: &Path) -> Result<EncryptionMetadata> {
128 if !self.config.enable_encryption {
129 return Err(BackupError::EncryptionError {
130 message: "Encryption is not enabled".to_string(),
131 });
132 }
133
134 debug!("Encrypting backup in place: {}", backup_path.display());
135
136 let _temp_path = backup_path.with_extension("tmp.enc");
138
139 let mut metadata = self.encrypt_file(backup_path).await?;
141
142 fs::rename(&metadata.encrypted_file_path, backup_path).await?;
144
145 metadata.encrypted_file_path = backup_path.to_path_buf();
147 metadata.original_file_path = backup_path.to_path_buf();
148
149 debug!("Backup encrypted in place successfully");
150 Ok(metadata)
151 }
152
153 pub async fn generate_encryption_key(&self) -> Result<EncryptionKey> {
155 info!("Generating new encryption key");
156
157 let key_id = uuid::Uuid::new_v4().to_string();
158 let key_path = self.get_key_path(&key_id);
159
160 let mut cmd = Command::new("gpg");
162 cmd.arg("--batch")
163 .arg("--gen-key")
164 .arg("--armor")
165 .arg("--output")
166 .arg(&key_path);
167
168 let key_params = self.create_key_generation_params(&key_id);
170 let mut child = cmd
171 .stdin(std::process::Stdio::piped())
172 .spawn()
173 .map_err(|e| BackupError::EncryptionError {
174 message: format!("Failed to start GPG key generation: {e}"),
175 })?;
176
177 if let Some(stdin) = child.stdin.as_mut() {
178 use std::io::Write;
179 stdin
180 .write_all(key_params.as_bytes())
181 .map_err(|e| BackupError::EncryptionError {
182 message: format!("Failed to write key parameters: {e}"),
183 })?;
184 }
185
186 let output = child.wait().map_err(|e| BackupError::EncryptionError {
187 message: format!("GPG key generation failed: {e}"),
188 })?;
189
190 if !output.success() {
191 return Err(BackupError::EncryptionError {
192 message: "GPG key generation failed".to_string(),
193 });
194 }
195
196 let key = EncryptionKey {
197 key_id,
198 key_path,
199 algorithm: EncryptionAlgorithm::AES256GCM,
200 key_size_bits: 256,
201 created_at: chrono::Utc::now(),
202 expires_at: Some(chrono::Utc::now() + chrono::Duration::days(365)), };
204
205 info!("Encryption key generated successfully: {}", key.key_id);
206 Ok(key)
207 }
208
209 pub async fn rotate_encryption_key(&self) -> Result<EncryptionKey> {
211 info!("Rotating encryption key");
212
213 let new_key = self.generate_encryption_key().await?;
215
216 warn!("Key rotation completed. Note: Existing backups still use old key");
221
222 info!("Encryption key rotated successfully");
223 Ok(new_key)
224 }
225
226 pub async fn verify_encryption_key(&self) -> Result<bool> {
228 debug!("Verifying encryption key integrity");
229
230 if let Some(key_path) = &self.config.encryption_key_path {
231 if !key_path.exists() {
232 warn!("Encryption key file not found: {}", key_path.display());
233 return Ok(false);
234 }
235
236 let test_data = b"encryption_test_data";
238 let temp_file = std::env::temp_dir().join("encryption_test.txt");
239 let encrypted_file = std::env::temp_dir().join("encryption_test.txt.enc");
240 let decrypted_file = std::env::temp_dir().join("encryption_test_decrypted.txt");
241
242 fs::write(&temp_file, test_data).await?;
244
245 let key = self.load_encryption_key().await?;
247 let iv = self.generate_iv().await?;
248
249 match self
250 .encrypt_with_gpg(&temp_file, &encrypted_file, &key, &iv)
251 .await
252 {
253 Ok(_) => {
254 match self
256 .decrypt_with_gpg(&encrypted_file, &decrypted_file, &key)
257 .await
258 {
259 Ok(_) => {
260 let decrypted_data = fs::read(&decrypted_file).await?;
262 let key_valid = decrypted_data == test_data;
263
264 let _ = fs::remove_file(&temp_file).await;
266 let _ = fs::remove_file(&encrypted_file).await;
267 let _ = fs::remove_file(&decrypted_file).await;
268
269 debug!(
270 "Encryption key verification: {}",
271 if key_valid { "PASSED" } else { "FAILED" }
272 );
273 Ok(key_valid)
274 }
275 Err(e) => {
276 error!("Key verification failed during decryption: {}", e);
277 Ok(false)
278 }
279 }
280 }
281 Err(e) => {
282 error!("Key verification failed during encryption: {}", e);
283 Ok(false)
284 }
285 }
286 } else {
287 warn!("No encryption key path configured");
288 Ok(false)
289 }
290 }
291
292 async fn verify_encryption_tools(&self) -> Result<()> {
295 debug!("Verifying encryption tools availability");
296
297 let output = Command::new("gpg").arg("--version").output().map_err(|e| {
299 BackupError::ConfigurationError {
300 message: format!("GPG not found: {e}"),
301 }
302 })?;
303
304 if !output.status.success() {
305 return Err(BackupError::ConfigurationError {
306 message: "GPG is not working properly".to_string(),
307 });
308 }
309
310 debug!("Encryption tools verified");
311 Ok(())
312 }
313
314 async fn ensure_encryption_key(&self) -> Result<()> {
315 debug!("Ensuring encryption key exists");
316
317 if let Some(key_path) = &self.config.encryption_key_path {
318 if !key_path.exists() {
319 info!("Encryption key not found, generating new key");
320
321 if let Some(parent) = key_path.parent() {
323 fs::create_dir_all(parent).await?;
324 }
325
326 let _key = self.generate_encryption_key().await?;
327 } else {
328 debug!("Encryption key already exists");
329
330 if !self.verify_encryption_key().await? {
332 warn!("Existing encryption key is invalid, generating new key");
333 let _key = self.generate_encryption_key().await?;
334 }
335 }
336 } else {
337 return Err(BackupError::ConfigurationError {
338 message: "Encryption key path not configured".to_string(),
339 });
340 }
341
342 debug!("Encryption key ensured");
343 Ok(())
344 }
345
346 async fn setup_key_rotation(&self) -> Result<()> {
347 debug!("Setting up key rotation");
348
349 info!("Key rotation is available via rotate_encryption_key() method");
352
353 Ok(())
354 }
355
356 async fn load_encryption_key(&self) -> Result<EncryptionKey> {
357 if let Some(key_path) = &self.config.encryption_key_path {
358 Ok(EncryptionKey {
361 key_id: "default".to_string(),
362 key_path: key_path.clone(),
363 algorithm: EncryptionAlgorithm::AES256GCM,
364 key_size_bits: 256,
365 created_at: chrono::Utc::now(),
366 expires_at: None,
367 })
368 } else {
369 Err(BackupError::EncryptionError {
370 message: "No encryption key path configured".to_string(),
371 })
372 }
373 }
374
375 fn get_encrypted_file_path(&self, original_path: &Path) -> Result<PathBuf> {
376 let mut new_path = original_path.to_path_buf();
377 let current_name = new_path.file_name().unwrap().to_string_lossy();
378 new_path.set_file_name(format!("{current_name}.enc"));
379 Ok(new_path)
380 }
381
382 fn get_key_path(&self, key_id: &str) -> PathBuf {
383 self.config
384 .backup_directory
385 .join(format!("encryption_key_{key_id}.gpg"))
386 }
387
388 async fn generate_iv(&self) -> Result<Vec<u8>> {
389 use rand::RngCore;
391 let mut iv = vec![0u8; 16];
392 rand::thread_rng().fill_bytes(&mut iv);
393 Ok(iv)
394 }
395
396 async fn encrypt_with_gpg(
397 &self,
398 input_path: &Path,
399 output_path: &Path,
400 key: &EncryptionKey,
401 _iv: &[u8],
402 ) -> Result<()> {
403 debug!(
404 "Encrypting with GPG: {} -> {}",
405 input_path.display(),
406 output_path.display()
407 );
408
409 let mut cmd = Command::new("gpg");
410 cmd.arg("--cipher-algo")
411 .arg("AES256")
412 .arg("--compress-algo")
413 .arg("2")
414 .arg("--symmetric")
415 .arg("--armor")
416 .arg("--output")
417 .arg(output_path)
418 .arg("--batch")
419 .arg("--yes")
420 .arg("--passphrase-file")
421 .arg(&key.key_path)
422 .arg(input_path);
423
424 let output = cmd.output().map_err(|e| BackupError::EncryptionError {
425 message: format!("Failed to execute GPG encryption: {e}"),
426 })?;
427
428 if !output.status.success() {
429 let error_msg = String::from_utf8_lossy(&output.stderr);
430 return Err(BackupError::EncryptionError {
431 message: format!("GPG encryption failed: {error_msg}"),
432 });
433 }
434
435 debug!("GPG encryption completed successfully");
436 Ok(())
437 }
438
439 async fn decrypt_with_gpg(
440 &self,
441 input_path: &Path,
442 output_path: &Path,
443 key: &EncryptionKey,
444 ) -> Result<()> {
445 debug!(
446 "Decrypting with GPG: {} -> {}",
447 input_path.display(),
448 output_path.display()
449 );
450
451 let mut cmd = Command::new("gpg");
452 cmd.arg("--decrypt")
453 .arg("--output")
454 .arg(output_path)
455 .arg("--batch")
456 .arg("--yes")
457 .arg("--passphrase-file")
458 .arg(&key.key_path)
459 .arg(input_path);
460
461 let output = cmd.output().map_err(|e| BackupError::EncryptionError {
462 message: format!("Failed to execute GPG decryption: {e}"),
463 })?;
464
465 if !output.status.success() {
466 let error_msg = String::from_utf8_lossy(&output.stderr);
467 return Err(BackupError::EncryptionError {
468 message: format!("GPG decryption failed: {error_msg}"),
469 });
470 }
471
472 debug!("GPG decryption completed successfully");
473 Ok(())
474 }
475
476 async fn calculate_file_checksum(&self, file_path: &Path) -> Result<String> {
477 use sha2::{Digest, Sha256};
478
479 let contents = fs::read(file_path).await?;
480 let mut hasher = Sha256::new();
481 hasher.update(&contents);
482 let result = hasher.finalize();
483 Ok(format!("{result:x}"))
484 }
485
486 fn create_key_generation_params(&self, key_id: &str) -> String {
487 format!(
488 "Key-Type: RSA
489Key-Length: 2048
490Subkey-Type: RSA
491Subkey-Length: 2048
492Name-Real: Codex Backup Key
493Name-Comment: Backup encryption key for {key_id}
494Name-Email: backup@codex.local
495Expire-Date: 1y
496%commit
497"
498 )
499 }
500}
501
502#[cfg(test)]
503mod tests {
504 use super::*;
505
506 #[test]
507 fn test_encryption_key_creation() {
508 let key = EncryptionKey {
509 key_id: "test-key".to_string(),
510 key_path: PathBuf::from("/tmp/test.key"),
511 algorithm: EncryptionAlgorithm::AES256GCM,
512 key_size_bits: 256,
513 created_at: chrono::Utc::now(),
514 expires_at: None,
515 };
516
517 assert_eq!(key.key_id, "test-key");
518 assert_eq!(key.key_size_bits, 256);
519 assert!(matches!(key.algorithm, EncryptionAlgorithm::AES256GCM));
520 }
521
522 #[test]
523 fn test_encryption_algorithms() {
524 let algorithms = [
525 EncryptionAlgorithm::AES256GCM,
526 EncryptionAlgorithm::ChaCha20Poly1305,
527 EncryptionAlgorithm::AES256CBC,
528 ];
529
530 for algorithm in &algorithms {
531 match algorithm {
532 EncryptionAlgorithm::AES256GCM => assert!(true),
533 EncryptionAlgorithm::ChaCha20Poly1305 => assert!(true),
534 EncryptionAlgorithm::AES256CBC => assert!(true),
535 }
536 }
537 }
538
539 #[test]
540 fn test_get_encrypted_file_path() {
541 let config = BackupConfig::default();
542 let encryption = BackupEncryption::new(config);
543
544 let original_path = Path::new("/tmp/backup.sql");
545 let encrypted_path = encryption.get_encrypted_file_path(original_path).unwrap();
546
547 assert_eq!(encrypted_path, PathBuf::from("/tmp/backup.sql.enc"));
548 }
549}