1use crate::error::{Error, Result};
7
8use aes_gcm::{
9 Aes256Gcm,
10 aead::{Aead, KeyInit, Nonce},
11};
12
13pub struct ContentEncryption {
15 key: [u8; 32],
16}
17
18impl ContentEncryption {
19 pub fn new(key: [u8; 32]) -> Self {
21 Self { key }
22 }
23
24 pub fn from_hex(hex_key: &str) -> Result<Self> {
26 let bytes =
27 hex::decode(hex_key).map_err(|e| Error::Validation(format!("invalid hex key: {e}")))?;
28 if bytes.len() != 32 {
29 return Err(Error::Validation(format!(
30 "key must be 32 bytes, got {}",
31 bytes.len()
32 )));
33 }
34 let mut key = [0u8; 32];
35 key.copy_from_slice(&bytes);
36 Ok(Self { key })
37 }
38
39 pub fn from_env() -> Result<Self> {
41 let hex_key = std::env::var("MNEMO_ENCRYPTION_KEY")
42 .map_err(|_| Error::Validation("MNEMO_ENCRYPTION_KEY not set".to_string()))?;
43 Self::from_hex(&hex_key)
44 }
45
46 pub fn encrypt(&self, plaintext: &[u8]) -> Result<Vec<u8>> {
50 let cipher = Aes256Gcm::new_from_slice(&self.key)
51 .map_err(|e| Error::Internal(format!("invalid AES-256 key: {e}")))?;
52
53 let mut nonce_bytes = [0u8; 12];
57 getrandom::getrandom(&mut nonce_bytes)
58 .map_err(|e| Error::Internal(format!("nonce RNG failed: {e}")))?;
59 let nonce: Nonce<Aes256Gcm> = nonce_bytes.into();
60
61 let ciphertext = cipher
62 .encrypt(&nonce, plaintext)
63 .map_err(|e| Error::Internal(format!("encryption failed: {e}")))?;
64
65 let mut output = Vec::with_capacity(12 + ciphertext.len());
66 output.extend_from_slice(&nonce_bytes);
67 output.extend_from_slice(&ciphertext);
68 Ok(output)
69 }
70
71 pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
73 if data.len() < 28 {
74 return Err(Error::Validation("encrypted data too short".to_string()));
76 }
77
78 let cipher = Aes256Gcm::new_from_slice(&self.key)
79 .map_err(|e| Error::Internal(format!("invalid AES-256 key: {e}")))?;
80
81 let nonce = <Nonce<Aes256Gcm>>::try_from(&data[..12])
82 .map_err(|_| Error::Validation("invalid nonce".to_string()))?;
83 let ciphertext = &data[12..];
84
85 cipher
86 .decrypt(&nonce, ciphertext)
87 .map_err(|_| Error::Validation("decryption tag mismatch".to_string()))
88 }
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_encryption_round_trip() {
97 let key = [0x42u8; 32];
98 let enc = ContentEncryption::new(key);
99
100 let plaintext = b"Hello, encrypted world!";
101 let encrypted = enc.encrypt(plaintext).unwrap();
102
103 assert_ne!(&encrypted[12..encrypted.len() - 16], plaintext);
104
105 let decrypted = enc.decrypt(&encrypted).unwrap();
106 assert_eq!(decrypted, plaintext);
107 }
108
109 #[test]
110 fn test_encryption_from_hex() {
111 let hex_key = "42".repeat(32);
112 let enc = ContentEncryption::from_hex(&hex_key).unwrap();
113
114 let plaintext = b"test";
115 let encrypted = enc.encrypt(plaintext).unwrap();
116 let decrypted = enc.decrypt(&encrypted).unwrap();
117 assert_eq!(decrypted, plaintext);
118 }
119
120 #[test]
121 fn test_invalid_hex_key_length() {
122 let result = ContentEncryption::from_hex("abcd");
123 assert!(result.is_err());
124 }
125
126 #[test]
127 fn test_tampered_ciphertext_fails() {
128 let key = [0x42u8; 32];
129 let enc = ContentEncryption::new(key);
130
131 let encrypted = enc.encrypt(b"secret data").unwrap();
132 let mut tampered = encrypted.clone();
133 tampered[15] ^= 0xff; let result = enc.decrypt(&tampered);
136 assert!(result.is_err());
137 }
138
139 #[test]
140 fn test_aes_gcm_round_trip() {
141 let key = [0xABu8; 32];
142 let enc = ContentEncryption::new(key);
143
144 for size in [0, 1, 16, 100, 1024, 65536] {
146 let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
147 let encrypted = enc.encrypt(&plaintext).unwrap();
148 let decrypted = enc.decrypt(&encrypted).unwrap();
149 assert_eq!(decrypted, plaintext, "round-trip failed for size {size}");
150 }
151 }
152
153 #[test]
154 fn test_aes_gcm_tamper_detection() {
155 let key = [0xCDu8; 32];
156 let enc = ContentEncryption::new(key);
157 let encrypted = enc.encrypt(b"sensitive data").unwrap();
158
159 let mut tampered = encrypted.clone();
161 tampered[0] ^= 0x01;
162 assert!(enc.decrypt(&tampered).is_err());
163
164 let mut tampered = encrypted.clone();
166 tampered[14] ^= 0x01;
167 assert!(enc.decrypt(&tampered).is_err());
168
169 let mut tampered = encrypted.clone();
171 let last = tampered.len() - 1;
172 tampered[last] ^= 0x01;
173 assert!(enc.decrypt(&tampered).is_err());
174 }
175}