1use aes_gcm::{Aes128Gcm, Aes256Gcm};
4use aes_gcm_siv::Aes256GcmSiv;
5use chacha20poly1305::aead::{Aead, KeyInit, Payload};
6use chacha20poly1305::{ChaCha20Poly1305, XChaCha20Poly1305};
7use hkdf::Hkdf;
8use rainier_support::{Error, Result};
9use sha2::Sha256;
10
11use crate::key::Key;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
20pub enum Cipher {
21 #[default]
27 XChaCha20Poly1305,
28
29 ChaCha20Poly1305,
35
36 Aes256Gcm,
43
44 Aes128Gcm,
46
47 Aes256GcmSiv,
53}
54
55impl Cipher {
56 pub const ALL: [Cipher; 5] = [
58 Cipher::XChaCha20Poly1305,
59 Cipher::ChaCha20Poly1305,
60 Cipher::Aes256Gcm,
61 Cipher::Aes128Gcm,
62 Cipher::Aes256GcmSiv,
63 ];
64
65 pub fn id(self) -> &'static str {
67 match self {
68 Cipher::XChaCha20Poly1305 => "xc20p",
69 Cipher::ChaCha20Poly1305 => "c20p",
70 Cipher::Aes256Gcm => "a256gcm",
71 Cipher::Aes128Gcm => "a128gcm",
72 Cipher::Aes256GcmSiv => "a256siv",
73 }
74 }
75
76 pub fn from_id(id: &str) -> Option<Self> {
82 match id {
83 "xc20p" | "v1" => Some(Cipher::XChaCha20Poly1305),
84 "c20p" => Some(Cipher::ChaCha20Poly1305),
85 "a256gcm" => Some(Cipher::Aes256Gcm),
86 "a128gcm" => Some(Cipher::Aes128Gcm),
87 "a256siv" => Some(Cipher::Aes256GcmSiv),
88 _ => None,
89 }
90 }
91
92 pub fn key_len(self) -> usize {
94 match self {
95 Cipher::Aes128Gcm => 16,
96 _ => 32,
97 }
98 }
99
100 pub fn nonce_len(self) -> usize {
102 match self {
103 Cipher::XChaCha20Poly1305 => 24,
104 _ => 12,
105 }
106 }
107
108 pub fn is_nonce_misuse_resistant(self) -> bool {
110 matches!(self, Cipher::Aes256GcmSiv)
111 }
112
113 fn derive(self, key: &Key) -> Result<Vec<u8>> {
122 let hkdf = Hkdf::<Sha256>::new(Some(b"rainier-cipher"), key.bytes());
123 let mut out = vec![0u8; self.key_len()];
124 hkdf.expand(self.id().as_bytes(), &mut out)
125 .map_err(|_| Error::internal("could not derive a cipher key"))?;
126 Ok(out)
127 }
128
129 pub fn encrypt(self, key: &Key, nonce: &[u8], aad: &[u8], plain: &[u8]) -> Result<Vec<u8>> {
131 self.check_nonce(nonce)?;
132 let derived = self.derive(key)?;
133 let payload = Payload { msg: plain, aad };
134 let failed = || Error::internal("encryption failed");
135
136 match self {
137 Cipher::XChaCha20Poly1305 => XChaCha20Poly1305::new_from_slice(&derived)
138 .map_err(|_| failed())?
139 .encrypt(nonce.into(), payload)
140 .map_err(|_| failed()),
141 Cipher::ChaCha20Poly1305 => ChaCha20Poly1305::new_from_slice(&derived)
142 .map_err(|_| failed())?
143 .encrypt(nonce.into(), payload)
144 .map_err(|_| failed()),
145 Cipher::Aes256Gcm => {
146 use aes_gcm::aead::Aead as _;
147 aes_gcm::KeyInit::new_from_slice(&derived)
148 .map(|cipher: Aes256Gcm| cipher)
149 .map_err(|_| failed())?
150 .encrypt(nonce.into(), payload)
151 .map_err(|_| failed())
152 }
153 Cipher::Aes128Gcm => {
154 use aes_gcm::aead::Aead as _;
155 aes_gcm::KeyInit::new_from_slice(&derived)
156 .map(|cipher: Aes128Gcm| cipher)
157 .map_err(|_| failed())?
158 .encrypt(nonce.into(), payload)
159 .map_err(|_| failed())
160 }
161 Cipher::Aes256GcmSiv => {
162 use aes_gcm_siv::aead::Aead as _;
163 aes_gcm_siv::KeyInit::new_from_slice(&derived)
164 .map(|cipher: Aes256GcmSiv| cipher)
165 .map_err(|_| failed())?
166 .encrypt(nonce.into(), payload)
167 .map_err(|_| failed())
168 }
169 }
170 }
171
172 pub fn decrypt(self, key: &Key, nonce: &[u8], aad: &[u8], sealed: &[u8]) -> Result<Vec<u8>> {
178 self.check_nonce(nonce)?;
179 let derived = self.derive(key)?;
180 let payload = Payload { msg: sealed, aad };
181 let failed = || Error::bad_request("the payload could not be decrypted");
182
183 match self {
184 Cipher::XChaCha20Poly1305 => XChaCha20Poly1305::new_from_slice(&derived)
185 .map_err(|_| failed())?
186 .decrypt(nonce.into(), payload)
187 .map_err(|_| failed()),
188 Cipher::ChaCha20Poly1305 => ChaCha20Poly1305::new_from_slice(&derived)
189 .map_err(|_| failed())?
190 .decrypt(nonce.into(), payload)
191 .map_err(|_| failed()),
192 Cipher::Aes256Gcm => {
193 use aes_gcm::aead::Aead as _;
194 aes_gcm::KeyInit::new_from_slice(&derived)
195 .map(|cipher: Aes256Gcm| cipher)
196 .map_err(|_| failed())?
197 .decrypt(nonce.into(), payload)
198 .map_err(|_| failed())
199 }
200 Cipher::Aes128Gcm => {
201 use aes_gcm::aead::Aead as _;
202 aes_gcm::KeyInit::new_from_slice(&derived)
203 .map(|cipher: Aes128Gcm| cipher)
204 .map_err(|_| failed())?
205 .decrypt(nonce.into(), payload)
206 .map_err(|_| failed())
207 }
208 Cipher::Aes256GcmSiv => {
209 use aes_gcm_siv::aead::Aead as _;
210 aes_gcm_siv::KeyInit::new_from_slice(&derived)
211 .map(|cipher: Aes256GcmSiv| cipher)
212 .map_err(|_| failed())?
213 .decrypt(nonce.into(), payload)
214 .map_err(|_| failed())
215 }
216 }
217 }
218
219 pub fn nonce(self) -> Vec<u8> {
221 use rand::RngCore;
222
223 let mut nonce = vec![0u8; self.nonce_len()];
224 rand::rngs::OsRng.fill_bytes(&mut nonce);
225 nonce
226 }
227
228 fn check_nonce(self, nonce: &[u8]) -> Result<()> {
229 if nonce.len() == self.nonce_len() {
230 Ok(())
231 } else {
232 Err(Error::bad_request("the payload could not be decrypted"))
233 }
234 }
235}
236
237impl std::fmt::Display for Cipher {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 f.write_str(self.id())
240 }
241}
242
243impl std::str::FromStr for Cipher {
244 type Err = Error;
245
246 fn from_str(value: &str) -> Result<Self> {
247 Self::from_id(value).ok_or_else(|| {
248 Error::internal(format!(
249 "`{value}` is not a known cipher; expected one of {}",
250 Cipher::ALL.iter().map(|c| c.id()).collect::<Vec<_>>().join(", ")
251 ))
252 })
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn key() -> Key {
261 Key::generate()
262 }
263
264 #[test]
265 fn every_cipher_round_trips() {
266 let key = key();
267
268 for cipher in Cipher::ALL {
269 let nonce = cipher.nonce();
270 let sealed = cipher.encrypt(&key, &nonce, b"aad", b"the message").unwrap();
271 let opened = cipher.decrypt(&key, &nonce, b"aad", &sealed).unwrap();
272
273 assert_eq!(opened, b"the message", "{cipher}");
274 assert_ne!(sealed, b"the message", "{cipher}");
275 }
276 }
277
278 #[test]
279 fn every_cipher_detects_tampering() {
280 let key = key();
281
282 for cipher in Cipher::ALL {
283 let nonce = cipher.nonce();
284 let mut sealed = cipher.encrypt(&key, &nonce, b"aad", b"transfer 10").unwrap();
285 sealed[0] ^= 0x01;
286
287 assert!(cipher.decrypt(&key, &nonce, b"aad", &sealed).is_err(), "{cipher}");
288 }
289 }
290
291 #[test]
292 fn every_cipher_authenticates_its_aad() {
293 let key = key();
294
295 for cipher in Cipher::ALL {
296 let nonce = cipher.nonce();
297 let sealed = cipher.encrypt(&key, &nonce, b"header-a", b"message").unwrap();
298
299 assert!(
300 cipher.decrypt(&key, &nonce, b"header-b", &sealed).is_err(),
301 "{cipher} must not open with different associated data"
302 );
303 }
304 }
305
306 #[test]
307 fn a_wrong_nonce_length_is_refused() {
308 let key = key();
309
310 for cipher in Cipher::ALL {
311 assert!(cipher.encrypt(&key, &[0u8; 7], b"", b"x").is_err(), "{cipher}");
312 assert!(cipher.decrypt(&key, &[0u8; 7], b"", b"x").is_err(), "{cipher}");
313 }
314 }
315
316 #[test]
317 fn ids_round_trip_and_are_distinct() {
318 let mut seen = std::collections::HashSet::new();
319
320 for cipher in Cipher::ALL {
321 assert_eq!(Cipher::from_id(cipher.id()), Some(cipher));
322 assert!(seen.insert(cipher.id()), "duplicate id: {cipher}");
323 }
324 }
325
326 #[test]
327 fn the_legacy_v1_id_still_names_xchacha() {
328 assert_eq!(Cipher::from_id("v1"), Some(Cipher::XChaCha20Poly1305));
330 }
331
332 #[test]
333 fn an_unknown_id_is_none_and_parses_to_a_helpful_error() {
334 assert_eq!(Cipher::from_id("rot13"), None);
335
336 let err = "rot13".parse::<Cipher>().unwrap_err();
337 assert!(err.message().contains("xc20p"), "{}", err.message());
338 }
339
340 #[test]
341 fn the_default_is_xchacha() {
342 assert_eq!(Cipher::default(), Cipher::XChaCha20Poly1305);
343 }
344
345 #[test]
346 fn each_cipher_derives_a_different_key_from_the_same_material() {
347 let key = key();
350 let mut derived = std::collections::HashSet::new();
351
352 for cipher in Cipher::ALL {
353 assert!(derived.insert(cipher.derive(&key).unwrap()), "{cipher} shares a key");
354 }
355 }
356
357 #[test]
358 fn derivation_is_deterministic() {
359 let key = key();
360 assert_eq!(
361 Cipher::Aes256Gcm.derive(&key).unwrap(),
362 Cipher::Aes256Gcm.derive(&key).unwrap()
363 );
364 }
365
366 #[test]
367 fn aes_128_gets_a_short_key_and_the_rest_a_long_one() {
368 let key = key();
369
370 assert_eq!(Cipher::Aes128Gcm.derive(&key).unwrap().len(), 16);
371 for cipher in Cipher::ALL.iter().filter(|c| **c != Cipher::Aes128Gcm) {
372 assert_eq!(cipher.derive(&key).unwrap().len(), 32, "{cipher}");
373 }
374 }
375
376 #[test]
377 fn one_cipher_cannot_open_anothers_payload() {
378 let key = key();
379 let nonce = Cipher::Aes256Gcm.nonce();
380 let sealed = Cipher::Aes256Gcm.encrypt(&key, &nonce, b"", b"secret").unwrap();
381
382 assert!(Cipher::ChaCha20Poly1305.decrypt(&key, &nonce, b"", &sealed).is_err());
385 }
386
387 #[test]
388 fn nonces_are_the_right_length_and_not_repeated() {
389 for cipher in Cipher::ALL {
390 let a = cipher.nonce();
391 let b = cipher.nonce();
392
393 assert_eq!(a.len(), cipher.nonce_len(), "{cipher}");
394 assert_ne!(a, b, "{cipher}");
395 }
396 }
397
398 #[test]
399 fn only_siv_claims_misuse_resistance() {
400 assert!(Cipher::Aes256GcmSiv.is_nonce_misuse_resistant());
401 for cipher in Cipher::ALL.iter().filter(|c| **c != Cipher::Aes256GcmSiv) {
402 assert!(!cipher.is_nonce_misuse_resistant(), "{cipher}");
403 }
404 }
405
406 #[test]
407 fn a_repeated_nonce_under_siv_reveals_only_equality() {
408 let key = key();
410 let nonce = Cipher::Aes256GcmSiv.nonce();
411
412 let one = Cipher::Aes256GcmSiv.encrypt(&key, &nonce, b"", b"same").unwrap();
413 let two = Cipher::Aes256GcmSiv.encrypt(&key, &nonce, b"", b"same").unwrap();
414 let other = Cipher::Aes256GcmSiv.encrypt(&key, &nonce, b"", b"different").unwrap();
415
416 assert_eq!(one, two, "SIV is deterministic for a given key, nonce and message");
417 assert_ne!(one, other);
418 assert_eq!(Cipher::Aes256GcmSiv.decrypt(&key, &nonce, b"", &one).unwrap(), b"same");
419 }
420
421 #[test]
422 fn empty_and_large_messages_round_trip() {
423 let key = key();
424 let big = vec![7u8; 100_000];
425
426 for cipher in Cipher::ALL {
427 let nonce = cipher.nonce();
428
429 assert_eq!(
430 cipher
431 .decrypt(&key, &nonce, b"", &cipher.encrypt(&key, &nonce, b"", b"").unwrap())
432 .unwrap(),
433 Vec::<u8>::new(),
434 "{cipher}"
435 );
436 assert_eq!(
437 cipher
438 .decrypt(&key, &nonce, b"", &cipher.encrypt(&key, &nonce, b"", &big).unwrap())
439 .unwrap(),
440 big,
441 "{cipher}"
442 );
443 }
444 }
445}