crawlkit_engine/
encryption.rs1use std::path::PathBuf;
2use std::sync::Arc;
3
4use parking_lot::RwLock;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct EncryptionConfig {
13 pub enabled: bool,
15 pub key_source: KeySource,
17 pub algorithm: EncryptionAlgorithm,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub enum KeySource {
24 File(PathBuf),
26 EnvVar(String),
28 Keyring(String),
30}
31
32#[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
49pub struct EncryptionManager {
67 config: EncryptionConfig,
68 initialized: Arc<RwLock<bool>>,
69 key: Arc<RwLock<Option<Vec<u8>>>>,
70}
71
72impl EncryptionManager {
73 #[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 #[must_use]
85 pub fn is_enabled(&self) -> bool {
86 self.config.enabled
87 }
88
89 pub fn initialize(&self) -> Result<(), EncryptionError> {
94 if !self.config.enabled {
95 return Ok(());
96 }
97
98 let key = self.load_key()?;
99
100 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 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 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 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 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 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 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 let mut result = nonce.to_vec();
206 result.extend_from_slice(&ciphertext);
207 Ok(result)
208 }
209
210 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 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 #[must_use]
248 pub fn is_initialized(&self) -> bool {
249 *self.initialized.read()
250 }
251
252 #[must_use]
254 pub fn config(&self) -> &EncryptionConfig {
255 &self.config
256 }
257}
258
259#[derive(Debug, thiserror::Error)]
261pub enum EncryptionError {
262 #[error("encryption key not found: {0}")]
264 KeyNotFound(String),
265
266 #[error("invalid key format: {0}")]
268 InvalidKeyFormat(String),
269
270 #[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#[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 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 *manager.key.write() = Some(key);
316 *manager.initialized.write() = true;
317
318 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 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 let encrypted = manager.encrypt(data).unwrap();
352 let decrypted = manager.decrypt(&encrypted).unwrap();
353
354 assert_eq!(data.to_vec(), decrypted);
355 }
356}