codex_memory/backup/
encryption.rs

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
8/// Backup encryption manager for securing backup data at rest
9pub 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    /// Initialize the backup encryption system
47    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        // Verify encryption tools are available
56        self.verify_encryption_tools().await?;
57
58        // Ensure encryption key exists or create one
59        self.ensure_encryption_key().await?;
60
61        // Set up key rotation if configured
62        self.setup_key_rotation().await?;
63
64        info!("Backup encryption system initialized");
65        Ok(())
66    }
67
68    /// Encrypt a backup file
69    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        // Generate random IV
82        let iv = self.generate_iv().await?;
83
84        // Encrypt the file using GPG or OpenSSL
85        self.encrypt_with_gpg(file_path, &encrypted_path, &key, &iv)
86            .await?;
87
88        // Calculate checksum of encrypted file
89        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    /// Decrypt a backup file
106    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        // Load encryption key
116        let key = self.load_encryption_key().await?;
117
118        // Decrypt the file using GPG or OpenSSL
119        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    /// Encrypt backup in place (replaces original with encrypted version)
127    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        // Create temporary file for encryption
137        let _temp_path = backup_path.with_extension("tmp.enc");
138
139        // Encrypt to temporary file
140        let mut metadata = self.encrypt_file(backup_path).await?;
141
142        // Move encrypted file to replace original
143        fs::rename(&metadata.encrypted_file_path, backup_path).await?;
144
145        // Update metadata paths
146        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    /// Generate a new encryption key
154    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        // Generate key using GPG
161        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        // Create key generation parameters
169        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)), // 1 year
203        };
204
205        info!("Encryption key generated successfully: {}", key.key_id);
206        Ok(key)
207    }
208
209    /// Rotate encryption keys
210    pub async fn rotate_encryption_key(&self) -> Result<EncryptionKey> {
211        info!("Rotating encryption key");
212
213        // Generate new key
214        let new_key = self.generate_encryption_key().await?;
215
216        // Re-encrypt existing backups with new key if needed
217        // This is a simplified implementation - in production, you might
218        // keep old keys for decrypting old backups and only use new key
219        // for new backups
220        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    /// Verify encryption key integrity
227    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            // Test the key by performing a simple encryption/decryption test
237            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            // Write test data
243            fs::write(&temp_file, test_data).await?;
244
245            // Test encryption
246            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                    // Test decryption
255                    match self
256                        .decrypt_with_gpg(&encrypted_file, &decrypted_file, &key)
257                        .await
258                    {
259                        Ok(_) => {
260                            // Verify decrypted data matches original
261                            let decrypted_data = fs::read(&decrypted_file).await?;
262                            let key_valid = decrypted_data == test_data;
263
264                            // Clean up test files
265                            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    // Private helper methods
293
294    async fn verify_encryption_tools(&self) -> Result<()> {
295        debug!("Verifying encryption tools availability");
296
297        // Check if GPG is available
298        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                // Create key directory if it doesn't exist
322                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                // Verify the key is valid
331                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        // In a production system, this would set up automatic key rotation
350        // For now, just log that it's available
351        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            // In a real implementation, this would parse the actual key file
359            // For this mock implementation, create a dummy key
360            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        // Generate a random 16-byte IV for AES
390        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}