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];
58 getrandom::fill(&mut nonce_bytes)
59 .map_err(|e| Error::Internal(format!("nonce RNG failed: {e}")))?;
60 let nonce: Nonce<Aes256Gcm> = nonce_bytes.into();
61
62 let ciphertext = cipher
63 .encrypt(&nonce, plaintext)
64 .map_err(|e| Error::Internal(format!("encryption failed: {e}")))?;
65
66 let mut output = Vec::with_capacity(12 + ciphertext.len());
67 output.extend_from_slice(&nonce_bytes);
68 output.extend_from_slice(&ciphertext);
69 Ok(output)
70 }
71
72 pub fn decrypt(&self, data: &[u8]) -> Result<Vec<u8>> {
74 if data.len() < 28 {
75 return Err(Error::Validation("encrypted data too short".to_string()));
77 }
78
79 let cipher = Aes256Gcm::new_from_slice(&self.key)
80 .map_err(|e| Error::Internal(format!("invalid AES-256 key: {e}")))?;
81
82 let nonce = <Nonce<Aes256Gcm>>::try_from(&data[..12])
83 .map_err(|_| Error::Validation("invalid nonce".to_string()))?;
84 let ciphertext = &data[12..];
85
86 cipher
87 .decrypt(&nonce, ciphertext)
88 .map_err(|_| Error::Validation("decryption tag mismatch".to_string()))
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn test_encryption_round_trip() {
98 let key = [0x42u8; 32];
99 let enc = ContentEncryption::new(key);
100
101 let plaintext = b"Hello, encrypted world!";
102 let encrypted = enc.encrypt(plaintext).unwrap();
103
104 assert_ne!(&encrypted[12..encrypted.len() - 16], plaintext);
105
106 let decrypted = enc.decrypt(&encrypted).unwrap();
107 assert_eq!(decrypted, plaintext);
108 }
109
110 #[test]
111 fn test_encryption_from_hex() {
112 let hex_key = "42".repeat(32);
113 let enc = ContentEncryption::from_hex(&hex_key).unwrap();
114
115 let plaintext = b"test";
116 let encrypted = enc.encrypt(plaintext).unwrap();
117 let decrypted = enc.decrypt(&encrypted).unwrap();
118 assert_eq!(decrypted, plaintext);
119 }
120
121 #[test]
122 fn test_invalid_hex_key_length() {
123 let result = ContentEncryption::from_hex("abcd");
124 assert!(result.is_err());
125 }
126
127 #[test]
128 fn test_tampered_ciphertext_fails() {
129 let key = [0x42u8; 32];
130 let enc = ContentEncryption::new(key);
131
132 let encrypted = enc.encrypt(b"secret data").unwrap();
133 let mut tampered = encrypted.clone();
134 tampered[15] ^= 0xff; let result = enc.decrypt(&tampered);
137 assert!(result.is_err());
138 }
139
140 #[test]
141 fn test_aes_gcm_round_trip() {
142 let key = [0xABu8; 32];
143 let enc = ContentEncryption::new(key);
144
145 for size in [0, 1, 16, 100, 1024, 65536] {
147 let plaintext: Vec<u8> = (0..size).map(|i| (i % 256) as u8).collect();
148 let encrypted = enc.encrypt(&plaintext).unwrap();
149 let decrypted = enc.decrypt(&encrypted).unwrap();
150 assert_eq!(decrypted, plaintext, "round-trip failed for size {size}");
151 }
152 }
153
154 #[test]
155 fn test_aes_gcm_tamper_detection() {
156 let key = [0xCDu8; 32];
157 let enc = ContentEncryption::new(key);
158 let encrypted = enc.encrypt(b"sensitive data").unwrap();
159
160 let mut tampered = encrypted.clone();
162 tampered[0] ^= 0x01;
163 assert!(enc.decrypt(&tampered).is_err());
164
165 let mut tampered = encrypted.clone();
167 tampered[14] ^= 0x01;
168 assert!(enc.decrypt(&tampered).is_err());
169
170 let mut tampered = encrypted.clone();
172 let last = tampered.len() - 1;
173 tampered[last] ^= 0x01;
174 assert!(enc.decrypt(&tampered).is_err());
175 }
176}