subcog/security/
encryption.rs1#[cfg(feature = "encryption")]
39mod implementation {
40 use crate::{Error, Result};
41
42 use aes_gcm::{
43 Aes256Gcm, Key, Nonce,
44 aead::{Aead, KeyInit},
45 };
46 use base64::Engine;
47 use rand::RngCore;
48
49 pub const MAGIC_HEADER: &[u8] = b"SUBCOG_ENC_V1\0";
52
53 const NONCE_SIZE: usize = 12;
55
56 const KEY_SIZE: usize = 32;
58
59 const ENV_ENCRYPTION_KEY: &str = "SUBCOG_ENCRYPTION_KEY";
61
62 #[derive(Debug, Clone)]
64 pub struct EncryptionConfig {
65 key: [u8; KEY_SIZE],
67 }
68
69 impl EncryptionConfig {
70 pub fn from_base64(key_b64: &str) -> Result<Self> {
76 let key_bytes = base64::engine::general_purpose::STANDARD
77 .decode(key_b64.trim())
78 .map_err(|e| Error::InvalidInput(format!("Invalid base64 encryption key: {e}")))?;
79
80 if key_bytes.len() != KEY_SIZE {
81 return Err(Error::InvalidInput(format!(
82 "Encryption key must be {} bytes, got {}",
83 KEY_SIZE,
84 key_bytes.len()
85 )));
86 }
87
88 let mut key = [0u8; KEY_SIZE];
89 key.copy_from_slice(&key_bytes);
90
91 Ok(Self { key })
92 }
93
94 pub fn from_env() -> Result<Self> {
100 let key_b64 = std::env::var(ENV_ENCRYPTION_KEY).map_err(|_| {
101 Error::InvalidInput(format!(
102 "Encryption enabled but {ENV_ENCRYPTION_KEY} not set. \
103 Generate a key with: openssl rand -base64 32"
104 ))
105 })?;
106
107 Self::from_base64(&key_b64)
108 }
109
110 #[must_use]
112 pub fn try_from_env() -> Option<Self> {
113 Self::from_env().ok()
114 }
115 }
116
117 pub struct Encryptor {
119 cipher: Aes256Gcm,
120 }
121
122 impl Encryptor {
123 pub fn new(config: EncryptionConfig) -> Result<Self> {
129 let key = Key::<Aes256Gcm>::from(config.key);
130 let cipher = Aes256Gcm::new(&key);
131 Ok(Self { cipher })
132 }
133
134 pub fn from_env() -> Result<Self> {
140 let config = EncryptionConfig::from_env()?;
141 Self::new(config)
142 }
143
144 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
152 let mut nonce_bytes = [0u8; NONCE_SIZE];
154 rand::rng().fill_bytes(&mut nonce_bytes);
155 let nonce = Nonce::from(nonce_bytes);
156
157 let ciphertext =
159 self.cipher
160 .encrypt(&nonce, plaintext)
161 .map_err(|e| Error::OperationFailed {
162 operation: "encrypt".to_string(),
163 cause: format!("AES-256-GCM encryption failed: {e}"),
164 })?;
165
166 let mut output = Vec::with_capacity(MAGIC_HEADER.len() + NONCE_SIZE + ciphertext.len());
168 output.extend_from_slice(MAGIC_HEADER);
169 output.extend_from_slice(&nonce_bytes);
170 output.extend_from_slice(&ciphertext);
171
172 tracing::debug!(
173 plaintext_len = plaintext.len(),
174 encrypted_len = output.len(),
175 "Encrypted data"
176 );
177
178 Ok(output)
179 }
180
181 pub fn decrypt(&self, encrypted: &[u8]) -> Result<Vec<u8>> {
187 let min_size = MAGIC_HEADER.len() + NONCE_SIZE + 16; if encrypted.len() < min_size {
189 return Err(Error::InvalidInput(format!(
190 "Encrypted data too short: {} bytes, minimum {}",
191 encrypted.len(),
192 min_size
193 )));
194 }
195
196 if !encrypted.starts_with(MAGIC_HEADER) {
198 return Err(Error::InvalidInput(
199 "Invalid encrypted file: missing magic header".to_string(),
200 ));
201 }
202
203 let nonce_start = MAGIC_HEADER.len();
205 let nonce_end = nonce_start + NONCE_SIZE;
206 let nonce_array: [u8; NONCE_SIZE] = encrypted[nonce_start..nonce_end]
207 .try_into()
208 .map_err(|_| Error::InvalidInput("Invalid nonce length".to_string()))?;
209 let nonce = Nonce::from(nonce_array);
210 let ciphertext = &encrypted[nonce_end..];
211
212 let plaintext =
214 self.cipher
215 .decrypt(&nonce, ciphertext)
216 .map_err(|e| Error::OperationFailed {
217 operation: "decrypt".to_string(),
218 cause: format!(
219 "AES-256-GCM decryption failed (wrong key or corrupted data): {e}"
220 ),
221 })?;
222
223 tracing::debug!(
224 encrypted_len = encrypted.len(),
225 plaintext_len = plaintext.len(),
226 "Decrypted data"
227 );
228
229 Ok(plaintext)
230 }
231 }
232
233 #[must_use]
235 pub fn is_encrypted(data: &[u8]) -> bool {
236 data.starts_with(MAGIC_HEADER)
237 }
238
239 #[cfg(test)]
240 mod tests {
241 use super::*;
242
243 fn test_config() -> EncryptionConfig {
244 EncryptionConfig {
246 key: [
247 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
248 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19,
249 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f,
250 ],
251 }
252 }
253
254 #[test]
255 fn test_encrypt_decrypt_roundtrip() {
256 let encryptor = Encryptor::new(test_config()).unwrap();
257 let plaintext = b"Hello, World! This is a test of AES-256-GCM encryption.";
258
259 let encrypted = encryptor.encrypt(plaintext).unwrap();
260 assert!(is_encrypted(&encrypted));
261 assert_ne!(encrypted, plaintext);
262
263 let decrypted = encryptor.decrypt(&encrypted).unwrap();
264 assert_eq!(decrypted, plaintext);
265 }
266
267 #[test]
268 fn test_encrypt_decrypt_empty() {
269 let encryptor = Encryptor::new(test_config()).unwrap();
270 let plaintext = b"";
271
272 let encrypted = encryptor.encrypt(plaintext).unwrap();
273 let decrypted = encryptor.decrypt(&encrypted).unwrap();
274 assert_eq!(decrypted, plaintext);
275 }
276
277 #[test]
278 fn test_encrypt_decrypt_large() {
279 let encryptor = Encryptor::new(test_config()).unwrap();
280 let plaintext: Vec<u8> = (0u32..10000).map(|i| (i % 256) as u8).collect();
281
282 let encrypted = encryptor.encrypt(&plaintext).unwrap();
283 let decrypted = encryptor.decrypt(&encrypted).unwrap();
284 assert_eq!(decrypted, plaintext);
285 }
286
287 #[test]
288 fn test_different_nonces_produce_different_ciphertext() {
289 let encryptor = Encryptor::new(test_config()).unwrap();
290 let plaintext = b"Same plaintext";
291
292 let encrypted1 = encryptor.encrypt(plaintext).unwrap();
293 let encrypted2 = encryptor.encrypt(plaintext).unwrap();
294
295 assert_ne!(encrypted1, encrypted2);
297
298 let decrypted1 = encryptor.decrypt(&encrypted1).unwrap();
300 let decrypted2 = encryptor.decrypt(&encrypted2).unwrap();
301 assert_eq!(decrypted1, decrypted2);
302 }
303
304 #[test]
305 fn test_wrong_key_fails() {
306 let encryptor1 = Encryptor::new(test_config()).unwrap();
307 let mut wrong_config = test_config();
308 wrong_config.key[0] ^= 0xff; let encryptor2 = Encryptor::new(wrong_config).unwrap();
310
311 let plaintext = b"Secret data";
312 let encrypted = encryptor1.encrypt(plaintext).unwrap();
313
314 let result = encryptor2.decrypt(&encrypted);
316 assert!(result.is_err());
317 }
318
319 #[test]
320 fn test_tampered_ciphertext_fails() {
321 let encryptor = Encryptor::new(test_config()).unwrap();
322 let plaintext = b"Secret data";
323
324 let mut encrypted = encryptor.encrypt(plaintext).unwrap();
325 let last = encrypted.len() - 1;
327 encrypted[last] ^= 0xff;
328
329 let result = encryptor.decrypt(&encrypted);
330 assert!(result.is_err());
331 }
332
333 #[test]
334 fn test_is_encrypted() {
335 assert!(is_encrypted(MAGIC_HEADER));
336 assert!(is_encrypted(b"SUBCOG_ENC_V1\0some_data"));
337 assert!(!is_encrypted(b"plain text"));
338 assert!(!is_encrypted(b"{}"));
339 assert!(!is_encrypted(b""));
340 }
341
342 #[test]
343 fn test_config_from_base64() {
344 let key_b64 = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
346 let config = EncryptionConfig::from_base64(key_b64).unwrap();
347 assert_eq!(config.key.len(), 32);
348 }
349
350 #[test]
351 fn test_config_from_base64_invalid() {
352 let result = EncryptionConfig::from_base64("AAEC");
354 assert!(result.is_err());
355
356 let result = EncryptionConfig::from_base64("not-valid-base64!!!");
358 assert!(result.is_err());
359 }
360
361 #[test]
362 fn test_too_short_encrypted_data() {
363 let encryptor = Encryptor::new(test_config()).unwrap();
364 let result = encryptor.decrypt(b"too short");
365 assert!(result.is_err());
366 }
367
368 #[test]
369 fn test_missing_magic_header() {
370 let encryptor = Encryptor::new(test_config()).unwrap();
371 let fake_data = vec![0u8; 100];
373 let result = encryptor.decrypt(&fake_data);
374 assert!(result.is_err());
375 }
376 }
377}
378
379#[cfg(feature = "encryption")]
380pub use implementation::{EncryptionConfig, Encryptor, MAGIC_HEADER, is_encrypted};
381
382#[cfg(not(feature = "encryption"))]
384mod stub {
385 use crate::{Error, Result};
386
387 #[derive(Debug, Clone)]
389 pub struct EncryptionConfig;
390
391 impl EncryptionConfig {
392 pub fn from_env() -> Result<Self> {
398 Err(Error::FeatureNotEnabled(
399 "encryption feature not compiled".to_string(),
400 ))
401 }
402
403 #[must_use]
405 pub const fn try_from_env() -> Option<Self> {
406 None
407 }
408 }
409
410 pub struct Encryptor;
412
413 impl Encryptor {
414 pub fn from_env() -> Result<Self> {
420 Err(Error::FeatureNotEnabled(
421 "encryption feature not compiled".to_string(),
422 ))
423 }
424 }
425
426 #[must_use]
428 pub fn is_encrypted(data: &[u8]) -> bool {
429 data.starts_with(b"SUBCOG_ENC_V1\0")
430 }
431}
432
433#[cfg(not(feature = "encryption"))]
434pub use stub::{EncryptionConfig, Encryptor, is_encrypted};