Skip to main content

fraiseql_server/encryption/
mod.rs

1// Phase 12.3: Field-Level Encryption
2//! Encryption for sensitive database fields using AES-256-GCM
3//!
4//! Provides transparent encryption/decryption for:
5//! - User emails
6//! - Phone numbers
7//! - SSN/tax IDs
8//! - Credit card data
9//! - API keys
10//! - OAuth tokens
11
12use 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; // 96 bits for GCM
50#[allow(dead_code)]
51const TAG_SIZE: usize = 16; // 128 bits authentication tag (used in Phase 12.3+ cycles)
52const KEY_SIZE: usize = 32; // 256 bits for AES-256
53
54/// Cipher for field-level encryption using AES-256-GCM
55///
56/// Encrypts sensitive database fields with authenticated encryption.
57/// Each encryption uses a random nonce, preventing identical plaintexts
58/// from producing identical ciphertexts.
59///
60/// # Example
61/// ```ignore
62/// let cipher = FieldEncryption::new("encryption-key".as_bytes());
63/// let encrypted = cipher.encrypt("user@example.com")?;
64/// let decrypted = cipher.decrypt(&encrypted)?;
65/// assert_eq!(decrypted, "user@example.com");
66/// ```
67#[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    /// Create new field encryption cipher
82    ///
83    /// # Arguments
84    /// * `key` - Encryption key bytes (must be exactly 32 bytes for AES-256)
85    ///
86    /// # Returns
87    /// FieldEncryption cipher ready for encrypt/decrypt operations
88    ///
89    /// # Panics
90    /// If key length is not exactly 32 bytes
91    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    /// Generate random nonce for encryption
102    ///
103    /// Uses cryptographically secure random number generation to ensure
104    /// each encryption produces a unique nonce, preventing pattern analysis.
105    #[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    /// Validate and extract nonce from encrypted data
113    ///
114    /// # Arguments
115    /// * `encrypted` - Encrypted data with nonce prefix
116    ///
117    /// # Returns
118    /// Tuple of (nonce, ciphertext) or error if too short
119    #[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    /// Convert bytes to UTF-8 string with context
139    ///
140    /// Provides clear error messages on encoding failures for debugging
141    #[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    /// Encrypt plaintext field using AES-256-GCM
149    ///
150    /// Generates random 96-bit nonce, encrypts with authenticated encryption,
151    /// and returns [nonce || ciphertext] format for decryption.
152    ///
153    /// # Arguments
154    /// * `plaintext` - Data to encrypt
155    ///
156    /// # Returns
157    /// Encrypted data in format: [12-byte nonce][ciphertext + 16-byte tag]
158    ///
159    /// # Errors
160    /// Returns EncryptionError if encryption fails
161    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        // Return [nonce || ciphertext]
171        let mut result = nonce_bytes.to_vec();
172        result.extend_from_slice(&ciphertext);
173        Ok(result)
174    }
175
176    /// Decrypt encrypted field using AES-256-GCM
177    ///
178    /// Expects data in format: [12-byte nonce][ciphertext + 16-byte tag]
179    /// Extracts nonce, decrypts, and verifies authentication tag.
180    ///
181    /// # Arguments
182    /// * `encrypted` - Encrypted data from encrypt()
183    ///
184    /// # Returns
185    /// Decrypted plaintext as String
186    ///
187    /// # Errors
188    /// Returns EncryptionError if:
189    /// - Data too short for nonce
190    /// - Decryption fails (wrong key or corrupted data)
191    /// - Plaintext is not valid UTF-8
192    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    /// Encrypt field with additional context for audit/security
205    ///
206    /// Includes context (e.g., user_id, field_name) in authenticated data
207    /// but not in ciphertext, providing audit trail without bloating storage.
208    ///
209    /// # Arguments
210    /// * `plaintext` - Data to encrypt
211    /// * `context` - Additional authenticated data (e.g., "user:123:email")
212    ///
213    /// # Returns
214    /// Encrypted data in format: [12-byte nonce][ciphertext + 16-byte tag]
215    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    /// Decrypt field with additional context verification
238    ///
239    /// Context must match the value used during encryption for verification to succeed.
240    ///
241    /// # Arguments
242    /// * `encrypted` - Encrypted data from encrypt_with_context()
243    /// * `context` - Context that was used during encryption
244    ///
245    /// # Returns
246    /// Decrypted plaintext as String
247    ///
248    /// # Errors
249    /// Returns EncryptionError if context doesn't match or decryption fails
250    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 FieldEncryption creation
276    #[test]
277    fn test_field_encryption_creation() {
278        let key = [0u8; KEY_SIZE];
279        let _cipher = FieldEncryption::new(&key);
280        assert!(true); // Just verify creation succeeds
281    }
282
283    /// Test basic encryption/decryption roundtrip
284    #[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 that same plaintext produces different ciphertexts
298    #[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        // Different random nonces produce different ciphertexts
308        assert_ne!(encrypted1, encrypted2);
309
310        // But both decrypt to same plaintext
311        assert_eq!(cipher.decrypt(&encrypted1).unwrap(), plaintext);
312        assert_eq!(cipher.decrypt(&encrypted2).unwrap(), plaintext);
313    }
314
315    /// Test encryption with context
316    #[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 context verification fails with wrong context
331    #[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        // Decryption with wrong context should fail
343        let result = cipher.decrypt_with_context(&encrypted, wrong_context);
344        assert!(result.is_err());
345    }
346
347    /// Test various data types
348    #[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            "", // Empty string
361            "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 invalid key size panics
373    #[test]
374    #[should_panic(expected = "must be exactly 32 bytes")]
375    fn test_field_encryption_invalid_key_size() {
376        let invalid_key = [0u8; 16]; // Too short
377        let _cipher = FieldEncryption::new(&invalid_key);
378    }
379
380    /// Test corrupted ciphertext fails to decrypt
381    #[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        // Corrupt a byte in the ciphertext (not the nonce)
390        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 short ciphertext fails gracefully
399    #[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]; // Too short for nonce
405        let result = cipher.decrypt(&short_data);
406        assert!(result.is_err());
407    }
408}