rust_secure_logger/
encryption.rs

1//! Log encryption module for secure logging v2.0
2//!
3//! Provides AES-256-GCM encryption for sensitive log entries.
4
5use aes_gcm::{
6    aead::{Aead, KeyInit, OsRng},
7    Aes256Gcm, Nonce,
8};
9use base64::{engine::general_purpose::STANDARD as BASE64, Engine};
10use rand::RngCore;
11use serde::{Deserialize, Serialize};
12use thiserror::Error;
13
14/// Encryption errors
15#[derive(Error, Debug)]
16pub enum EncryptionError {
17    #[error("Encryption failed: {0}")]
18    EncryptionFailed(String),
19
20    #[error("Decryption failed: {0}")]
21    DecryptionFailed(String),
22
23    #[error("Invalid key length")]
24    InvalidKeyLength,
25
26    #[error("Base64 decode error: {0}")]
27    Base64Error(String),
28}
29
30/// Encrypted log entry structure
31#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct EncryptedLogEntry {
33    pub ciphertext: String, // Base64 encoded
34    pub nonce: String,      // Base64 encoded
35    pub timestamp: chrono::DateTime<chrono::Utc>,
36    pub entry_id: String,
37}
38
39/// Log encryption key with secure handling
40pub struct EncryptionKey {
41    key: Vec<u8>,
42}
43
44impl EncryptionKey {
45    /// Generate a new random encryption key
46    pub fn generate() -> Self {
47        let mut key = vec![0u8; 32];
48        OsRng.fill_bytes(&mut key);
49        Self { key }
50    }
51
52    /// Create from existing bytes
53    pub fn from_bytes(bytes: &[u8]) -> Result<Self, EncryptionError> {
54        if bytes.len() != 32 {
55            return Err(EncryptionError::InvalidKeyLength);
56        }
57        Ok(Self {
58            key: bytes.to_vec(),
59        })
60    }
61
62    /// Get key bytes
63    pub fn as_bytes(&self) -> &[u8] {
64        &self.key
65    }
66}
67
68impl Drop for EncryptionKey {
69    fn drop(&mut self) {
70        // Zero out key on drop for security
71        for byte in &mut self.key {
72            *byte = 0;
73        }
74    }
75}
76
77/// Log encryptor for secure log entries
78pub struct LogEncryptor {
79    cipher: Aes256Gcm,
80}
81
82impl LogEncryptor {
83    /// Create a new log encryptor with the given key
84    pub fn new(key: &EncryptionKey) -> Result<Self, EncryptionError> {
85        let cipher = Aes256Gcm::new_from_slice(key.as_bytes())
86            .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
87        Ok(Self { cipher })
88    }
89
90    /// Encrypt a log entry
91    pub fn encrypt(&self, plaintext: &str, entry_id: &str) -> Result<EncryptedLogEntry, EncryptionError> {
92        let mut nonce_bytes = [0u8; 12];
93        OsRng.fill_bytes(&mut nonce_bytes);
94        let nonce = Nonce::from_slice(&nonce_bytes);
95
96        let ciphertext = self
97            .cipher
98            .encrypt(nonce, plaintext.as_bytes())
99            .map_err(|e| EncryptionError::EncryptionFailed(e.to_string()))?;
100
101        Ok(EncryptedLogEntry {
102            ciphertext: BASE64.encode(&ciphertext),
103            nonce: BASE64.encode(nonce_bytes),
104            timestamp: chrono::Utc::now(),
105            entry_id: entry_id.to_string(),
106        })
107    }
108
109    /// Decrypt a log entry
110    pub fn decrypt(&self, encrypted: &EncryptedLogEntry) -> Result<String, EncryptionError> {
111        let ciphertext = BASE64
112            .decode(&encrypted.ciphertext)
113            .map_err(|e| EncryptionError::Base64Error(e.to_string()))?;
114
115        let nonce_bytes = BASE64
116            .decode(&encrypted.nonce)
117            .map_err(|e| EncryptionError::Base64Error(e.to_string()))?;
118
119        if nonce_bytes.len() != 12 {
120            return Err(EncryptionError::DecryptionFailed("Invalid nonce length".to_string()));
121        }
122
123        let nonce = Nonce::from_slice(&nonce_bytes);
124
125        let plaintext = self
126            .cipher
127            .decrypt(nonce, ciphertext.as_ref())
128            .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))?;
129
130        String::from_utf8(plaintext)
131            .map_err(|e| EncryptionError::DecryptionFailed(e.to_string()))
132    }
133
134    /// Encrypt multiple log entries in batch
135    pub fn encrypt_batch(&self, entries: &[(&str, &str)]) -> Vec<Result<EncryptedLogEntry, EncryptionError>> {
136        entries
137            .iter()
138            .map(|(plaintext, entry_id)| self.encrypt(plaintext, entry_id))
139            .collect()
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_encryption_decryption() {
149        let key = EncryptionKey::generate();
150        let encryptor = LogEncryptor::new(&key).unwrap();
151
152        let plaintext = "Sensitive log entry: User 12345 accessed financial records";
153        let entry_id = "LOG-001";
154
155        let encrypted = encryptor.encrypt(plaintext, entry_id).unwrap();
156        let decrypted = encryptor.decrypt(&encrypted).unwrap();
157
158        assert_eq!(plaintext, decrypted);
159        assert_eq!(encrypted.entry_id, entry_id);
160    }
161
162    #[test]
163    fn test_different_nonces() {
164        let key = EncryptionKey::generate();
165        let encryptor = LogEncryptor::new(&key).unwrap();
166
167        let plaintext = "Same message";
168        let enc1 = encryptor.encrypt(plaintext, "LOG-001").unwrap();
169        let enc2 = encryptor.encrypt(plaintext, "LOG-002").unwrap();
170
171        // Same plaintext should produce different ciphertext due to random nonce
172        assert_ne!(enc1.ciphertext, enc2.ciphertext);
173        assert_ne!(enc1.nonce, enc2.nonce);
174    }
175
176    #[test]
177    fn test_invalid_key_length() {
178        let short_key = vec![0u8; 16];
179        let result = EncryptionKey::from_bytes(&short_key);
180        assert!(result.is_err());
181    }
182
183    #[test]
184    fn test_batch_encryption() {
185        let key = EncryptionKey::generate();
186        let encryptor = LogEncryptor::new(&key).unwrap();
187
188        let entries = vec![
189            ("Log entry 1", "LOG-001"),
190            ("Log entry 2", "LOG-002"),
191            ("Log entry 3", "LOG-003"),
192        ];
193
194        let results = encryptor.encrypt_batch(&entries);
195        assert_eq!(results.len(), 3);
196        assert!(results.iter().all(|r| r.is_ok()));
197    }
198
199    #[test]
200    fn test_encrypted_entry_serialization() {
201        let key = EncryptionKey::generate();
202        let encryptor = LogEncryptor::new(&key).unwrap();
203
204        let encrypted = encryptor.encrypt("Test message", "LOG-001").unwrap();
205        let json = serde_json::to_string(&encrypted).unwrap();
206        let deserialized: EncryptedLogEntry = serde_json::from_str(&json).unwrap();
207
208        let decrypted = encryptor.decrypt(&deserialized).unwrap();
209        assert_eq!(decrypted, "Test message");
210    }
211}