fraiseql_server/encryption/
mod.rs1use aes_gcm::{
13 Aes256Gcm, Nonce,
14 aead::{Aead, KeyInit, Payload},
15};
16use rand::Rng;
17
18use crate::secrets_manager::SecretsError;
19
20mod audit_logging_tests;
21mod compliance_tests;
22mod dashboard_tests;
23mod database_adapter_tests;
24mod error_recovery_tests;
25mod field_encryption_tests;
26mod mapper_integration_tests;
27mod performance_tests;
28mod query_builder_integration_tests;
29mod refresh_tests;
30mod rotation_api_tests;
31mod rotation_tests;
32mod schema_detection_tests;
33mod transaction_integration_tests;
34
35pub mod audit_logging;
36pub mod compliance;
37pub mod credential_rotation;
38pub mod dashboard;
39pub mod database_adapter;
40pub mod error_recovery;
41pub mod mapper;
42pub mod performance;
43pub mod query_builder;
44pub mod refresh_trigger;
45pub mod rotation_api;
46pub mod schema;
47pub mod transaction;
48
49const NONCE_SIZE: usize = 12; #[allow(dead_code)]
51const TAG_SIZE: usize = 16; const KEY_SIZE: usize = 32; #[derive(Clone)]
68pub struct FieldEncryption {
69 cipher: Aes256Gcm,
70}
71
72impl std::fmt::Debug for FieldEncryption {
73 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74 f.debug_struct("FieldEncryption")
75 .field("cipher", &"Aes256Gcm(redacted)")
76 .finish()
77 }
78}
79
80impl FieldEncryption {
81 pub fn new(key: &[u8]) -> Self {
92 if key.len() != KEY_SIZE {
93 panic!("Encryption key must be exactly {} bytes, got {}", KEY_SIZE, key.len());
94 }
95
96 let cipher = Aes256Gcm::new_from_slice(key).expect("Key size already validated");
97
98 FieldEncryption { cipher }
99 }
100
101 #[allow(dead_code)]
106 fn generate_nonce() -> [u8; NONCE_SIZE] {
107 let mut nonce_bytes = [0u8; NONCE_SIZE];
108 rand::thread_rng().fill(&mut nonce_bytes);
109 nonce_bytes
110 }
111
112 #[allow(dead_code)]
120 fn extract_nonce_and_ciphertext(
121 encrypted: &[u8],
122 ) -> Result<([u8; NONCE_SIZE], &[u8]), SecretsError> {
123 if encrypted.len() < NONCE_SIZE {
124 return Err(SecretsError::EncryptionError(format!(
125 "Encrypted data too short (need ≥{} bytes, got {})",
126 NONCE_SIZE,
127 encrypted.len()
128 )));
129 }
130
131 let mut nonce = [0u8; NONCE_SIZE];
132 nonce.copy_from_slice(&encrypted[0..NONCE_SIZE]);
133 let ciphertext = &encrypted[NONCE_SIZE..];
134
135 Ok((nonce, ciphertext))
136 }
137
138 #[allow(dead_code)]
142 fn bytes_to_utf8(bytes: Vec<u8>, context: &str) -> Result<String, SecretsError> {
143 String::from_utf8(bytes).map_err(|e| {
144 SecretsError::EncryptionError(format!("Invalid UTF-8 in {}: {}", context, e))
145 })
146 }
147
148 pub fn encrypt(&self, plaintext: &str) -> Result<Vec<u8>, SecretsError> {
162 let nonce_bytes = Self::generate_nonce();
163 let nonce = Nonce::from_slice(&nonce_bytes);
164
165 let ciphertext = self
166 .cipher
167 .encrypt(nonce, plaintext.as_bytes())
168 .map_err(|e| SecretsError::EncryptionError(format!("Encryption failed: {}", e)))?;
169
170 let mut result = nonce_bytes.to_vec();
172 result.extend_from_slice(&ciphertext);
173 Ok(result)
174 }
175
176 pub fn decrypt(&self, encrypted: &[u8]) -> Result<String, SecretsError> {
193 let (nonce_bytes, ciphertext) = Self::extract_nonce_and_ciphertext(encrypted)?;
194
195 let nonce = Nonce::from_slice(&nonce_bytes);
196 let plaintext_bytes = self
197 .cipher
198 .decrypt(nonce, ciphertext)
199 .map_err(|e| SecretsError::EncryptionError(format!("Decryption failed: {}", e)))?;
200
201 Self::bytes_to_utf8(plaintext_bytes, "decrypted data")
202 }
203
204 pub fn encrypt_with_context(
216 &self,
217 plaintext: &str,
218 context: &str,
219 ) -> Result<Vec<u8>, SecretsError> {
220 let nonce_bytes = Self::generate_nonce();
221 let nonce = Nonce::from_slice(&nonce_bytes);
222
223 let payload = Payload {
224 msg: plaintext.as_bytes(),
225 aad: context.as_bytes(),
226 };
227
228 let ciphertext = self.cipher.encrypt(nonce, payload).map_err(|e| {
229 SecretsError::EncryptionError(format!("Encryption with context failed: {}", e))
230 })?;
231
232 let mut result = nonce_bytes.to_vec();
233 result.extend_from_slice(&ciphertext);
234 Ok(result)
235 }
236
237 pub fn decrypt_with_context(
251 &self,
252 encrypted: &[u8],
253 context: &str,
254 ) -> Result<String, SecretsError> {
255 let (nonce_bytes, ciphertext) = Self::extract_nonce_and_ciphertext(encrypted)?;
256
257 let nonce = Nonce::from_slice(&nonce_bytes);
258 let payload = Payload {
259 msg: ciphertext,
260 aad: context.as_bytes(),
261 };
262
263 let plaintext_bytes = self.cipher.decrypt(nonce, payload).map_err(|e| {
264 SecretsError::EncryptionError(format!("Decryption with context failed: {}", e))
265 })?;
266
267 Self::bytes_to_utf8(plaintext_bytes, "decrypted data with context")
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
277 fn test_field_encryption_creation() {
278 let key = [0u8; KEY_SIZE];
279 let _cipher = FieldEncryption::new(&key);
280 assert!(true); }
282
283 #[test]
285 fn test_field_encrypt_decrypt_roundtrip() {
286 let key = [0u8; KEY_SIZE];
287 let cipher = FieldEncryption::new(&key);
288
289 let plaintext = "user@example.com";
290 let encrypted = cipher.encrypt(plaintext).unwrap();
291 let decrypted = cipher.decrypt(&encrypted).unwrap();
292
293 assert_eq!(plaintext, decrypted);
294 assert_ne!(plaintext.as_bytes(), &encrypted[NONCE_SIZE..]);
295 }
296
297 #[test]
299 fn test_field_encrypt_random_nonce() {
300 let key = [0u8; KEY_SIZE];
301 let cipher = FieldEncryption::new(&key);
302
303 let plaintext = "sensitive@data.com";
304 let encrypted1 = cipher.encrypt(plaintext).unwrap();
305 let encrypted2 = cipher.encrypt(plaintext).unwrap();
306
307 assert_ne!(encrypted1, encrypted2);
309
310 assert_eq!(cipher.decrypt(&encrypted1).unwrap(), plaintext);
312 assert_eq!(cipher.decrypt(&encrypted2).unwrap(), plaintext);
313 }
314
315 #[test]
317 fn test_field_encrypt_decrypt_with_context() {
318 let key = [0u8; KEY_SIZE];
319 let cipher = FieldEncryption::new(&key);
320
321 let plaintext = "secret123";
322 let context = "user:456:password";
323
324 let encrypted = cipher.encrypt_with_context(plaintext, context).unwrap();
325 let decrypted = cipher.decrypt_with_context(&encrypted, context).unwrap();
326
327 assert_eq!(plaintext, decrypted);
328 }
329
330 #[test]
332 fn test_field_decrypt_with_wrong_context_fails() {
333 let key = [0u8; KEY_SIZE];
334 let cipher = FieldEncryption::new(&key);
335
336 let plaintext = "secret123";
337 let correct_context = "user:456:password";
338 let wrong_context = "user:789:password";
339
340 let encrypted = cipher.encrypt_with_context(plaintext, correct_context).unwrap();
341
342 let result = cipher.decrypt_with_context(&encrypted, wrong_context);
344 assert!(result.is_err());
345 }
346
347 #[test]
349 fn test_field_encrypt_various_types() {
350 let key = [0u8; KEY_SIZE];
351 let cipher = FieldEncryption::new(&key);
352
353 let test_cases = vec![
354 "email@example.com",
355 "+1-555-123-4567",
356 "123-45-6789",
357 "4532015112830366",
358 "sk_live_abc123def456",
359 "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9",
360 "", "with\nspecial\nchars\t!@#$%",
362 "unicode: 你好世界 🔐",
363 ];
364
365 for plaintext in test_cases {
366 let encrypted = cipher.encrypt(plaintext).unwrap();
367 let decrypted = cipher.decrypt(&encrypted).unwrap();
368 assert_eq!(plaintext, decrypted);
369 }
370 }
371
372 #[test]
374 #[should_panic(expected = "must be exactly 32 bytes")]
375 fn test_field_encryption_invalid_key_size() {
376 let invalid_key = [0u8; 16]; let _cipher = FieldEncryption::new(&invalid_key);
378 }
379
380 #[test]
382 fn test_field_decrypt_corrupted_data_fails() {
383 let key = [0u8; KEY_SIZE];
384 let cipher = FieldEncryption::new(&key);
385
386 let plaintext = "data";
387 let mut encrypted = cipher.encrypt(plaintext).unwrap();
388
389 if encrypted.len() > NONCE_SIZE {
391 encrypted[NONCE_SIZE] ^= 0xFF;
392 }
393
394 let result = cipher.decrypt(&encrypted);
395 assert!(result.is_err());
396 }
397
398 #[test]
400 fn test_field_decrypt_short_data_fails() {
401 let key = [0u8; KEY_SIZE];
402 let cipher = FieldEncryption::new(&key);
403
404 let short_data = vec![0u8; 5]; let result = cipher.decrypt(&short_data);
406 assert!(result.is_err());
407 }
408}