1use std::collections::BTreeMap;
4use std::fmt;
5
6use ring::aead::{AES_256_GCM, Aad, LessSafeKey, Nonce, UnboundKey};
7use ring::rand::{SecureRandom, SystemRandom};
8use serde::{Serialize, de::DeserializeOwned};
9
10use crate::error::{Error, ErrorKind, Result, ResultExt as _};
11
12const NONCE_LENGTH: usize = 12;
14const TAG_LENGTH: usize = 16;
16const FORMAT_VERSION: u8 = 0;
18
19#[derive(Debug, thiserror::Error)]
21pub(crate) enum CipherError {
22 #[error("encrypted value is malformed")]
23 MalformedEnvelope,
24 #[error("unsupported encrypted value format version {0}")]
25 UnsupportedVersion(u8),
26 #[error("encrypted value has an invalid key ID")]
27 InvalidKeyId,
28 #[error("encrypted value could not be authenticated")]
29 Authentication,
30 #[error("encrypted value could not be deserialized")]
31 Deserialization(#[source] serde_json::Error),
32}
33
34pub struct Cipher {
39 active_key_id: String,
40 active_key: LessSafeKey,
41 decryption_keys: BTreeMap<String, LessSafeKey>,
42 random: SystemRandom,
43}
44
45impl Cipher {
46 pub fn ephemeral() -> anyhow::Result<Self> {
53 let key_id = "ephemeral";
54 let random = SystemRandom::new();
55 let mut key = [0; 32];
56 random
57 .fill(&mut key)
58 .map_err(|_| anyhow::anyhow!("failed to generate encryption key"))?;
59 Self::new(key_id, BTreeMap::from([(key_id.to_owned(), key.to_vec())]))
60 }
61
62 pub fn new(
66 active_key_id: impl Into<String>,
67 keys: BTreeMap<String, Vec<u8>>,
68 ) -> anyhow::Result<Self> {
69 let active_key_id = active_key_id.into();
70 validate_key_id(&active_key_id)?;
71
72 let mut validated = BTreeMap::new();
73 for (key_id, key) in keys {
74 validate_key_id(&key_id)?;
75 anyhow::ensure!(
76 key.len() == AES_256_GCM.key_len(),
77 "encryption key {key_id:?} must contain exactly 32 bytes, got {}",
78 key.len()
79 );
80 let key = UnboundKey::new(&AES_256_GCM, &key)
81 .map(LessSafeKey::new)
82 .map_err(|_| anyhow::anyhow!("invalid encryption key material"))?;
83 validated.insert(key_id, key);
84 }
85
86 let active_key = validated.remove(&active_key_id).ok_or_else(|| {
87 anyhow::anyhow!("active encryption key {active_key_id:?} is not configured")
88 })?;
89
90 Ok(Self {
91 active_key_id,
92 active_key,
93 decryption_keys: validated,
94 random: SystemRandom::new(),
95 })
96 }
97
98 pub(crate) fn encrypt<T>(&self, value: &T) -> Result<Vec<u8>>
111 where
112 T: Serialize + ?Sized,
113 {
114 let key_id = self.active_key_id.as_bytes();
115 let key_id_length = u8::try_from(key_id.len()).context(
116 ErrorKind::Internal,
117 "encryption key ID exceeds maximum length",
118 )?;
119
120 let mut header = Vec::with_capacity(2 + key_id.len());
122 header.push(FORMAT_VERSION);
123 header.push(key_id_length);
124 header.extend_from_slice(key_id);
125
126 let mut ciphertext = serde_json::to_vec(value)
127 .context(ErrorKind::Internal, "failed to serialize encrypted value")?;
128
129 let mut nonce = [0; NONCE_LENGTH];
130 self.random
131 .fill(&mut nonce)
132 .map_err(|_| Error::new(ErrorKind::Internal, "failed to generate encryption nonce"))?;
133
134 self.active_key
137 .seal_in_place_append_tag(
138 Nonce::assume_unique_for_key(nonce),
139 Aad::from(&header),
140 &mut ciphertext,
141 )
142 .map_err(|_| Error::new(ErrorKind::Internal, "failed to encrypt value"))?;
143
144 let mut envelope = Vec::with_capacity(header.len() + nonce.len() + ciphertext.len());
145 envelope.extend_from_slice(&header);
146 envelope.extend_from_slice(&nonce);
147 envelope.extend_from_slice(&ciphertext);
148 Ok(envelope)
149 }
150
151 pub(crate) fn decrypt<T>(&self, envelope: &[u8]) -> std::result::Result<T, CipherError>
153 where
154 T: DeserializeOwned,
155 {
156 let (&version, rest) = envelope
157 .split_first()
158 .ok_or(CipherError::MalformedEnvelope)?;
159 if version != FORMAT_VERSION {
160 return Err(CipherError::UnsupportedVersion(version));
161 }
162
163 let (&key_id_length, rest) = rest.split_first().ok_or(CipherError::MalformedEnvelope)?;
166 let key_id_length = usize::from(key_id_length);
167 if key_id_length == 0 {
168 return Err(CipherError::MalformedEnvelope);
169 }
170 let (key_id, rest) = rest
171 .split_at_checked(key_id_length)
172 .ok_or(CipherError::MalformedEnvelope)?;
173 let (nonce, ciphertext) = rest
174 .split_at_checked(NONCE_LENGTH)
175 .ok_or(CipherError::MalformedEnvelope)?;
176
177 let key_id = std::str::from_utf8(key_id).map_err(|_| CipherError::InvalidKeyId)?;
179 let key = if key_id == self.active_key_id {
180 &self.active_key
181 } else {
182 self.decryption_keys
183 .get(key_id)
184 .ok_or(CipherError::InvalidKeyId)?
185 };
186 let header_length = 2 + key_id_length;
187 let header = &envelope[..header_length];
188 let nonce: [u8; NONCE_LENGTH] = nonce.try_into().expect("nonce length was checked");
189 if ciphertext.len() < TAG_LENGTH {
190 return Err(CipherError::MalformedEnvelope);
191 }
192
193 let mut ciphertext = ciphertext.to_vec();
195 let plaintext = key
196 .open_in_place(
197 Nonce::assume_unique_for_key(nonce),
198 Aad::from(header),
199 &mut ciphertext,
200 )
201 .map_err(|_| CipherError::Authentication)?;
202
203 serde_json::from_slice(plaintext).map_err(CipherError::Deserialization)
204 }
205}
206
207impl fmt::Debug for Cipher {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 let mut key_ids = self.decryption_keys.keys().collect::<Vec<_>>();
210 key_ids.push(&self.active_key_id);
211 key_ids.sort_unstable();
212
213 f.debug_struct("Cipher")
214 .field("active_key_id", &self.active_key_id)
215 .field("key_ids", &key_ids)
216 .finish_non_exhaustive()
217 }
218}
219
220fn validate_key_id(key_id: &str) -> anyhow::Result<()> {
221 let key_id_length = key_id.len();
222 u8::try_from(key_id_length).map_err(|_| {
223 anyhow::anyhow!(
224 "encryption key ID must be at most {} bytes, got {key_id_length}",
225 u8::MAX
226 )
227 })?;
228 anyhow::ensure!(
229 !key_id.is_empty()
230 && key_id
231 .bytes()
232 .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.')),
233 "invalid encryption key ID {key_id:?}"
234 );
235 Ok(())
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 fn cipher(active: &str, keys: &[(&str, u8)]) -> Cipher {
243 Cipher::new(
244 active,
245 keys.iter()
246 .map(|(key_id, byte)| (key_id.to_string(), vec![*byte; 32]))
247 .collect(),
248 )
249 .unwrap()
250 }
251
252 #[test]
253 fn encryption_is_randomized_and_supports_multiple_types() {
254 let cipher = cipher("v1", &[("v1", 7)]);
255 let value = vec!["first".to_owned(), "second".to_owned()];
256
257 let first = cipher.encrypt(&value).unwrap();
258 let second = cipher.encrypt(&value).unwrap();
259
260 assert_ne!(first, second);
261 assert_eq!(first[0], FORMAT_VERSION);
262 assert_eq!(cipher.decrypt::<Vec<String>>(&first).unwrap(), value);
263
264 let number = cipher.encrypt(&42_u64).unwrap();
265 assert_eq!(cipher.decrypt::<u64>(&number).unwrap(), 42);
266 }
267
268 #[test]
269 fn encryption_rejects_tampering_plaintext_and_wrong_types() {
270 let cipher = cipher("v1", &[("v1", 7)]);
271 let token = cipher.encrypt("value").unwrap();
272
273 let mut tampered = token.clone();
274 *tampered.last_mut().unwrap() ^= 1;
275 assert!(matches!(
276 cipher.decrypt::<String>(&tampered),
277 Err(CipherError::Authentication)
278 ));
279
280 let mut unsupported_version = token.clone();
281 unsupported_version[0] = FORMAT_VERSION + 1;
282 assert!(matches!(
283 cipher.decrypt::<String>(&unsupported_version),
284 Err(CipherError::UnsupportedVersion(_))
285 ));
286 assert!(cipher.decrypt::<String>(b"plaintext").is_err());
287 assert!(matches!(
288 cipher.decrypt::<u64>(&token),
289 Err(CipherError::Deserialization(_))
290 ));
291 }
292
293 #[test]
294 fn rotation_decrypts_old_keys_and_removal_invalidates_them() {
295 let old = cipher("v1", &[("v1", 1)]);
296 let old_value = old.encrypt("old value").unwrap();
297
298 let rotated = cipher("v2", &[("v1", 1), ("v2", 2)]);
299 assert_eq!(rotated.decrypt::<String>(&old_value).unwrap(), "old value");
300 let new_value = rotated.encrypt("new value").unwrap();
301 assert_eq!(new_value[2..4], *b"v2");
302
303 let removed = cipher("v2", &[("v2", 2)]);
304 assert!(matches!(
305 removed.decrypt::<String>(&old_value),
306 Err(CipherError::InvalidKeyId)
307 ));
308 }
309
310 #[test]
311 fn configuration_validates_ids_lengths_and_active_key() {
312 let error = Cipher::new("missing", BTreeMap::new()).unwrap_err();
313 assert_eq!(
314 error.to_string(),
315 "active encryption key \"missing\" is not configured"
316 );
317
318 let error =
319 Cipher::new("bad key", BTreeMap::from([("bad key".into(), vec![0; 32])])).unwrap_err();
320 assert_eq!(error.to_string(), "invalid encryption key ID \"bad key\"");
321
322 let error = Cipher::new("v1", BTreeMap::from([("v1".into(), vec![0; 31])])).unwrap_err();
323 assert_eq!(
324 error.to_string(),
325 "encryption key \"v1\" must contain exactly 32 bytes, got 31"
326 );
327 }
328}