Skip to main content

crawlkit_engine/
encryption.rs

1use std::path::PathBuf;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6
7/// Encryption configuration for data at rest.
8///
9/// Controls whether encryption is enabled, where to load the key from,
10/// and which algorithm to use. Default is disabled with AES-256-GCM.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct EncryptionConfig {
13    /// Enable encryption at rest.
14    pub enabled: bool,
15    /// Encryption key source.
16    pub key_source: KeySource,
17    /// Encryption algorithm.
18    pub algorithm: EncryptionAlgorithm,
19}
20
21/// Source of encryption key.
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub enum KeySource {
24    /// Key from file path.
25    File(PathBuf),
26    /// Key from environment variable.
27    EnvVar(String),
28    /// Key from system keyring.
29    Keyring(String),
30}
31
32/// Encryption algorithm.
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub enum EncryptionAlgorithm {
35    Aes256Gcm,
36    Aes256Cbc,
37}
38
39impl Default for EncryptionConfig {
40    fn default() -> Self {
41        Self {
42            enabled: false,
43            key_source: KeySource::EnvVar("CRAWLKIT_ENCRYPTION_KEY".to_string()),
44            algorithm: EncryptionAlgorithm::Aes256Gcm,
45        }
46    }
47}
48
49/// Encryption manager for data at rest.
50///
51/// Provides AES-256-GCM encryption for sensitive data. Key is loaded from
52/// file, environment variable, or system keyring. When encryption is disabled,
53/// `encrypt` and `decrypt` are passthrough operations.
54///
55/// # Examples
56///
57/// ```rust
58/// use crawlkit_engine::{EncryptionManager, EncryptionConfig};
59///
60/// let manager = EncryptionManager::default();
61/// assert!(!manager.is_enabled());
62/// // When disabled, encrypt/decrypt are identity operations
63/// let data = b"hello";
64/// assert_eq!(manager.encrypt(data).unwrap(), data);
65/// ```
66pub struct EncryptionManager {
67    config: EncryptionConfig,
68    initialized: Arc<RwLock<bool>>,
69    key: Arc<RwLock<Option<Vec<u8>>>>,
70}
71
72impl EncryptionManager {
73    /// Create new encryption manager.
74    #[must_use]
75    pub fn new(config: EncryptionConfig) -> Self {
76        Self {
77            config,
78            initialized: Arc::new(RwLock::new(false)),
79            key: Arc::new(RwLock::new(None)),
80        }
81    }
82
83    /// Check if encryption is enabled.
84    #[must_use]
85    pub fn is_enabled(&self) -> bool {
86        self.config.enabled
87    }
88
89    /// Initialize encryption by loading the key.
90    ///
91    /// # Errors
92    /// Returns error if key cannot be loaded or is invalid.
93    pub fn initialize(&self) -> Result<(), EncryptionError> {
94        if !self.config.enabled {
95            return Ok(());
96        }
97
98        let key = self.load_key()?;
99
100        // Validate key length (256 bits = 32 bytes for AES-256)
101        if key.len() != 32 {
102            return Err(EncryptionError::InvalidKeyFormat(format!(
103                "Expected 32 bytes, got {} bytes",
104                key.len()
105            )));
106        }
107
108        *self.key.write() = Some(key);
109        *self.initialized.write() = true;
110        Ok(())
111    }
112
113    /// Load encryption key from configured source.
114    fn load_key(&self) -> Result<Vec<u8>, EncryptionError> {
115        match &self.config.key_source {
116            KeySource::File(path) => std::fs::read(path).map_err(|e| {
117                EncryptionError::KeyNotFound(format!(
118                    "Failed to read key file {}: {}",
119                    path.display(),
120                    e
121                ))
122            }),
123            KeySource::EnvVar(var_name) => {
124                std::env::var(var_name)
125                    .map(|v| v.into_bytes())
126                    .map_err(|_| {
127                        EncryptionError::KeyNotFound(format!(
128                            "Environment variable {} not set",
129                            var_name
130                        ))
131                    })
132            }
133            KeySource::Keyring(service) => {
134                // Try environment variable first as fallback
135                let env_key = format!(
136                    "CRAWLKIT_KEYRING_{}",
137                    service.to_uppercase().replace('-', "_")
138                );
139                if let Ok(key) = std::env::var(&env_key) {
140                    return Ok(key.into_bytes());
141                }
142
143                // Try file-based keyring
144                let keyring_path = dirs::home_dir()
145                    .map(|h| {
146                        h.join(".crawlkit")
147                            .join("keyrings")
148                            .join(format!("{}.key", service))
149                    })
150                    .ok_or_else(|| {
151                        EncryptionError::KeyNotFound("Cannot determine home directory".to_string())
152                    })?;
153
154                if keyring_path.exists() {
155                    std::fs::read(&keyring_path).map_err(|e| {
156                        EncryptionError::KeyNotFound(format!(
157                            "Failed to read keyring file {}: {}",
158                            keyring_path.display(),
159                            e
160                        ))
161                    })
162                } else {
163                    Err(EncryptionError::KeyNotFound(format!(
164                        "Keyring '{}' not found. Set {} environment variable or create keyring file at {}",
165                        service, env_key, keyring_path.display()
166                    )))
167                }
168            }
169        }
170    }
171
172    /// Encrypt data using AES-256-GCM.
173    ///
174    /// # Errors
175    /// Returns error if encryption fails.
176    pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
177        if !self.config.enabled {
178            return Ok(plaintext.to_vec());
179        }
180
181        let key = self.key.read();
182        let key = key.as_ref().ok_or_else(|| {
183            EncryptionError::InitializationFailed("Encryption not initialized".to_string())
184        })?;
185
186        // Generate random nonce (96 bits for AES-GCM)
187        let mut nonce = [0u8; 12];
188        getrandom::getrandom(&mut nonce).map_err(|e| {
189            EncryptionError::InitializationFailed(format!("Failed to generate nonce: {}", e))
190        })?;
191
192        // Create AES-256-GCM cipher
193
194        use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
195
196        let cipher = Aes256Gcm::new_from_slice(key)
197            .map_err(|e| EncryptionError::InvalidKeyFormat(format!("Invalid key: {}", e)))?;
198
199        let nonce = Nonce::from_slice(&nonce);
200        let ciphertext = cipher.encrypt(nonce, plaintext).map_err(|e| {
201            EncryptionError::InitializationFailed(format!("Encryption failed: {}", e))
202        })?;
203
204        // Prepend nonce to ciphertext
205        let mut result = nonce.to_vec();
206        result.extend_from_slice(&ciphertext);
207        Ok(result)
208    }
209
210    /// Decrypt data using AES-256-GCM.
211    ///
212    /// # Errors
213    /// Returns error if decryption fails.
214    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, EncryptionError> {
215        if !self.config.enabled {
216            return Ok(ciphertext.to_vec());
217        }
218
219        let key = self.key.read();
220        let key = key.as_ref().ok_or_else(|| {
221            EncryptionError::InitializationFailed("Encryption not initialized".to_string())
222        })?;
223
224        if ciphertext.len() < 12 {
225            return Err(EncryptionError::InvalidKeyFormat(
226                "Ciphertext too short".to_string(),
227            ));
228        }
229
230        // Extract nonce from ciphertext
231        let (nonce_bytes, actual_ciphertext) = ciphertext.split_at(12);
232
233        use aes_gcm::{aead::Aead, Aes256Gcm, KeyInit, Nonce};
234
235        let cipher = Aes256Gcm::new_from_slice(key)
236            .map_err(|e| EncryptionError::InvalidKeyFormat(format!("Invalid key: {}", e)))?;
237
238        let nonce = Nonce::from_slice(nonce_bytes);
239        let plaintext = cipher.decrypt(nonce, actual_ciphertext).map_err(|e| {
240            EncryptionError::InitializationFailed(format!("Decryption failed: {}", e))
241        })?;
242
243        Ok(plaintext)
244    }
245
246    /// Check if initialized.
247    #[must_use]
248    pub fn is_initialized(&self) -> bool {
249        *self.initialized.read()
250    }
251
252    /// Get configuration.
253    #[must_use]
254    pub fn config(&self) -> &EncryptionConfig {
255        &self.config
256    }
257}
258
259/// Encryption errors.
260#[derive(Debug, thiserror::Error)]
261pub enum EncryptionError {
262    /// The encryption key could not be found.
263    #[error("encryption key not found: {0}")]
264    KeyNotFound(String),
265
266    /// The encryption key has invalid format or length.
267    #[error("invalid key format: {0}")]
268    InvalidKeyFormat(String),
269
270    /// Encryption/decryption initialization failed.
271    #[error("encryption initialization failed: {0}")]
272    InitializationFailed(String),
273}
274
275impl Default for EncryptionManager {
276    fn default() -> Self {
277        Self::new(EncryptionConfig::default())
278    }
279}
280
281// ---------------------------------------------------------------------------
282// Tests
283// ---------------------------------------------------------------------------
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn test_encryption_config_default() {
291        let config = EncryptionConfig::default();
292        assert!(!config.enabled);
293    }
294
295    #[test]
296    fn test_encryption_manager_disabled() {
297        let manager = EncryptionManager::default();
298        assert!(!manager.is_enabled());
299        assert!(manager.initialize().is_ok());
300    }
301
302    #[test]
303    fn test_encryption_decrypt_roundtrip() {
304        // Create a valid 32-byte key
305        let key = vec![0u8; 32];
306        let config = EncryptionConfig {
307            enabled: true,
308            key_source: KeySource::EnvVar("TEST_KEY".to_string()),
309            algorithm: EncryptionAlgorithm::Aes256Gcm,
310        };
311
312        let manager = EncryptionManager::new(config);
313
314        // Manually set the key for testing
315        *manager.key.write() = Some(key);
316        *manager.initialized.write() = true;
317
318        // Test encrypt/decrypt roundtrip
319        let plaintext = b"Hello, World!";
320        let ciphertext = manager.encrypt(plaintext).unwrap();
321        let decrypted = manager.decrypt(&ciphertext).unwrap();
322
323        assert_eq!(plaintext.to_vec(), decrypted);
324    }
325
326    #[test]
327    fn test_encryption_different_plaintexts() {
328        let key = vec![42u8; 32];
329        let config = EncryptionConfig {
330            enabled: true,
331            key_source: KeySource::EnvVar("TEST_KEY".to_string()),
332            algorithm: EncryptionAlgorithm::Aes256Gcm,
333        };
334
335        let manager = EncryptionManager::new(config);
336        *manager.key.write() = Some(key);
337        *manager.initialized.write() = true;
338
339        // Different plaintexts should produce different ciphertexts
340        let ct1 = manager.encrypt(b"message1").unwrap();
341        let ct2 = manager.encrypt(b"message2").unwrap();
342        assert_ne!(ct1, ct2);
343    }
344
345    #[test]
346    fn test_encryption_disabled_passthrough() {
347        let manager = EncryptionManager::default();
348        let data = b"test data";
349
350        // When disabled, encrypt/decrypt should be passthrough
351        let encrypted = manager.encrypt(data).unwrap();
352        let decrypted = manager.decrypt(&encrypted).unwrap();
353
354        assert_eq!(data.to_vec(), decrypted);
355    }
356}