rainier_crypt/
encrypter.rs1use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
5use base64::Engine as _;
6use rainier_support::{Error, Result};
7use serde::de::DeserializeOwned;
8use serde::Serialize;
9
10use crate::cipher::Cipher;
11use crate::key::KeyRing;
12
13pub trait Encrypter: Send + Sync + 'static {
21 fn encrypt_bytes(&self, plain: &[u8]) -> Result<String>;
23
24 fn decrypt_bytes(&self, payload: &str) -> Result<Vec<u8>>;
30}
31
32pub trait EncrypterExt: Encrypter {
37 fn encrypt(&self, plain: &str) -> Result<String> {
39 self.encrypt_bytes(plain.as_bytes())
40 }
41
42 fn decrypt(&self, payload: &str) -> Result<String> {
44 let bytes = self.decrypt_bytes(payload)?;
45 String::from_utf8(bytes)
46 .map_err(|_| Error::internal("the decrypted value is not valid UTF-8"))
47 }
48
49 fn encrypt_json<T: Serialize>(&self, value: &T) -> Result<String> {
51 let json = serde_json::to_vec(value)?;
52 self.encrypt_bytes(&json)
53 }
54
55 fn decrypt_json<T: DeserializeOwned>(&self, payload: &str) -> Result<T> {
57 let bytes = self.decrypt_bytes(payload)?;
58 serde_json::from_slice(&bytes).map_err(|e| {
59 Error::internal(format!("the decrypted value is not the expected shape: {e}"))
62 })
63 }
64}
65
66impl<E: Encrypter + ?Sized> EncrypterExt for E {}
67
68pub struct AeadEncrypter {
88 keys: KeyRing,
89 cipher: Cipher,
90}
91
92impl AeadEncrypter {
93 pub fn new(keys: KeyRing) -> Self {
95 Self { keys, cipher: Cipher::default() }
96 }
97
98 pub fn with_cipher(mut self, cipher: Cipher) -> Self {
100 self.cipher = cipher;
101 self
102 }
103
104 pub fn keys(&self) -> &KeyRing {
106 &self.keys
107 }
108
109 pub fn cipher(&self) -> Cipher {
111 self.cipher
112 }
113}
114
115impl Encrypter for AeadEncrypter {
116 fn encrypt_bytes(&self, plain: &[u8]) -> Result<String> {
117 let key = self.keys.current();
118 let nonce = self.cipher.nonce();
119
120 let header = format!("{}.{}", self.cipher.id(), key.id());
123 let sealed = self.cipher.encrypt(key, &nonce, header.as_bytes(), plain)?;
124
125 Ok(format!("{header}.{}.{}", B64.encode(&nonce), B64.encode(&sealed)))
126 }
127
128 fn decrypt_bytes(&self, payload: &str) -> Result<Vec<u8>> {
129 let invalid = || Error::bad_request("the payload could not be decrypted");
134
135 let mut parts = payload.split('.');
136 let (Some(algorithm), Some(key_id), Some(nonce), Some(sealed), None) =
137 (parts.next(), parts.next(), parts.next(), parts.next(), parts.next())
138 else {
139 return Err(invalid());
140 };
141
142 let cipher = Cipher::from_id(algorithm).ok_or_else(invalid)?;
143
144 let key = self.keys.find(key_id).ok_or_else(|| {
145 Error::internal(format!(
149 "no key with id `{key_id}` is on the ring; it is still needed to read \
150 payloads written with it"
151 ))
152 })?;
153
154 let nonce = B64.decode(nonce).map_err(|_| invalid())?;
155 let sealed = B64.decode(sealed).map_err(|_| invalid())?;
156
157 let header = format!("{algorithm}.{key_id}");
160 cipher.decrypt(key, &nonce, header.as_bytes(), &sealed)
161 }
162}
163
164impl std::fmt::Debug for AeadEncrypter {
165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
166 f.debug_struct("AeadEncrypter")
167 .field("cipher", &self.cipher.id())
168 .field("keys", &self.keys.ids())
169 .finish()
170 }
171}
172
173#[cfg(test)]
174mod tests {
175 use super::*;
176 use crate::key::Key;
177
178 fn encrypter() -> AeadEncrypter {
179 AeadEncrypter::new(KeyRing::new(Key::generate()))
180 }
181
182 #[test]
183 fn a_value_round_trips() {
184 let crypt = encrypter();
185 let payload = crypt.encrypt("hello, world").unwrap();
186
187 assert_eq!(crypt.decrypt(&payload).unwrap(), "hello, world");
188 }
189
190 #[test]
191 fn the_ciphertext_does_not_contain_the_plaintext() {
192 let payload = encrypter().encrypt("correct-horse-battery-staple").unwrap();
193 assert!(!payload.contains("correct-horse"), "{payload}");
194 }
195
196 #[test]
197 fn the_same_value_encrypts_differently_every_time() {
198 let crypt = encrypter();
199
200 assert_ne!(crypt.encrypt("same").unwrap(), crypt.encrypt("same").unwrap());
204 }
205
206 #[test]
207 fn a_payload_is_url_safe() {
208 let payload = encrypter().encrypt("a value with / and + in mind").unwrap();
209
210 assert!(
211 payload.chars().all(|c| c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '_')),
212 "{payload}"
213 );
214 }
215
216 #[test]
217 fn tampering_with_the_ciphertext_is_detected() {
218 let crypt = encrypter();
219 let payload = crypt.encrypt("transfer 10").unwrap();
220
221 let mut parts: Vec<&str> = payload.split('.').collect();
222 let flipped = {
223 let mut bytes = B64.decode(parts[3]).unwrap();
224 bytes[0] ^= 0x01;
225 B64.encode(bytes)
226 };
227 parts[3] = &flipped;
228
229 assert!(crypt.decrypt(&parts.join(".")).is_err());
230 }
231
232 #[test]
233 fn tampering_with_the_nonce_is_detected() {
234 let crypt = encrypter();
235 let payload = crypt.encrypt("transfer 10").unwrap();
236
237 let mut parts: Vec<&str> = payload.split('.').collect();
238 let other = B64.encode(vec![0u8; Cipher::default().nonce_len()]);
239 parts[2] = &other;
240
241 assert!(crypt.decrypt(&parts.join(".")).is_err());
242 }
243
244 #[test]
245 fn relabelling_the_cipher_is_detected() {
246 let crypt = encrypter();
249 let payload = crypt.encrypt("secret").unwrap();
250
251 let mut parts: Vec<&str> = payload.split('.').collect();
252 parts[0] = "a256gcm";
253
254 assert!(crypt.decrypt(&parts.join(".")).is_err());
255 }
256
257 #[test]
258 fn every_cipher_round_trips_through_the_encrypter() {
259 let keys = KeyRing::new(Key::generate());
260
261 for cipher in Cipher::ALL {
262 let crypt = AeadEncrypter::new(keys.clone()).with_cipher(cipher);
263 let payload = crypt.encrypt("the message").unwrap();
264
265 assert!(payload.starts_with(&format!("{}.", cipher.id())), "{payload}");
266 assert_eq!(crypt.decrypt(&payload).unwrap(), "the message", "{cipher}");
267 }
268 }
269
270 #[test]
271 fn a_payload_written_by_any_cipher_reads_under_any_default() {
272 let keys = KeyRing::new(Key::generate());
275 let written: Vec<String> = Cipher::ALL
276 .iter()
277 .map(|c| AeadEncrypter::new(keys.clone()).with_cipher(*c).encrypt("old").unwrap())
278 .collect();
279
280 for reader in Cipher::ALL {
281 let crypt = AeadEncrypter::new(keys.clone()).with_cipher(reader);
282 for payload in &written {
283 assert_eq!(crypt.decrypt(payload).unwrap(), "old", "{reader} reading {payload}");
284 }
285 }
286 }
287
288 #[test]
289 fn an_unknown_cipher_id_is_refused() {
290 let crypt = encrypter();
291 assert_eq!(crypt.decrypt("rot13.abc.def.ghi").unwrap_err().status(), 400);
292 }
293
294 #[test]
295 fn a_legacy_v1_payload_still_opens() {
296 let key = Key::generate();
299 let cipher = Cipher::XChaCha20Poly1305;
300 let nonce = cipher.nonce();
301 let header = format!("v1.{}", key.id());
302 let sealed = cipher.encrypt(&key, &nonce, header.as_bytes(), b"from before").unwrap();
303 let payload = format!("{header}.{}.{}", B64.encode(&nonce), B64.encode(&sealed));
304
305 let crypt = AeadEncrypter::new(KeyRing::new(key));
306 assert_eq!(crypt.decrypt(&payload).unwrap(), "from before");
307 }
308
309 #[test]
310 fn relabelling_the_header_is_detected() {
311 let old = Key::generate();
314 let current = Key::generate();
315 let crypt = AeadEncrypter::new(KeyRing::new(current).with_previous(old.clone()));
316
317 let payload = crypt.encrypt("secret").unwrap();
318 let mut parts: Vec<&str> = payload.split('.').collect();
319 parts[1] = old.id();
320
321 assert!(crypt.decrypt(&parts.join(".")).is_err());
322 }
323
324 #[test]
325 fn a_malformed_payload_is_a_400_not_a_500() {
326 let crypt = encrypter();
327
328 for rubbish in ["", "nonsense", "v1.abc", "v1.abc.def.ghi.jkl", "v2.abc.def.ghi"] {
329 let err = crypt.decrypt(rubbish).unwrap_err();
330 assert_eq!(err.status(), 400, "{rubbish}");
331 }
332 }
333
334 #[test]
335 fn every_failure_reports_the_same_thing() {
336 let crypt = encrypter();
338 let truncated = crypt.decrypt("v1.abc.def.ghi").unwrap_err();
339 let garbage = crypt.decrypt("v1.abc.!!!.!!!").unwrap_err();
340
341 assert_eq!(truncated.message(), garbage.message());
342 }
343
344 #[test]
345 fn another_applications_key_cannot_read_the_payload() {
346 let payload = encrypter().encrypt("secret").unwrap();
347 assert!(encrypter().decrypt(&payload).is_err());
348 }
349
350 #[test]
351 fn a_retired_key_still_reads_what_it_wrote() {
352 let old = Key::generate();
353 let written = AeadEncrypter::new(KeyRing::new(old.clone())).encrypt("before").unwrap();
354
355 let rotated = AeadEncrypter::new(KeyRing::new(Key::generate()).with_previous(old));
357
358 assert_eq!(rotated.decrypt(&written).unwrap(), "before");
359 assert!(
360 rotated.encrypt("after").unwrap().contains(rotated.keys().current().id()),
361 "new payloads use the new key"
362 );
363 }
364
365 #[test]
366 fn dropping_a_key_that_is_still_needed_says_so() {
367 let written = AeadEncrypter::new(KeyRing::new(Key::generate())).encrypt("x").unwrap();
368 let err = encrypter().decrypt(&written).unwrap_err();
369
370 assert_eq!(err.status(), 500, "an operator error, not a client one");
371 assert!(err.message().contains("still needed"), "{}", err.message());
372 }
373
374 #[test]
375 fn json_round_trips() {
376 #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
377 struct Token {
378 user: u64,
379 scopes: Vec<String>,
380 }
381
382 let crypt = encrypter();
383 let token = Token { user: 7, scopes: vec!["read".into()] };
384 let payload = crypt.encrypt_json(&token).unwrap();
385
386 assert_eq!(crypt.decrypt_json::<Token>(&payload).unwrap(), token);
387 }
388
389 #[test]
390 fn an_empty_value_round_trips() {
391 let crypt = encrypter();
392 assert_eq!(crypt.decrypt(&crypt.encrypt("").unwrap()).unwrap(), "");
393 }
394
395 #[test]
396 fn binary_round_trips() {
397 let crypt = encrypter();
398 let bytes: Vec<u8> = (0..=255u8).collect();
399
400 assert_eq!(crypt.decrypt_bytes(&crypt.encrypt_bytes(&bytes).unwrap()).unwrap(), bytes);
401 }
402
403 #[test]
404 fn the_port_is_object_safe() {
405 let crypt: std::sync::Arc<dyn Encrypter> = std::sync::Arc::new(encrypter());
406 assert_eq!(crypt.decrypt(&crypt.encrypt("via dyn").unwrap()).unwrap(), "via dyn");
409 }
410}