Skip to main content

vitaminc_encrypt/
cipher.rs

1use crate::backend::{CipherKey, NONCE_LEN};
2use crate::Key;
3use std::any::Any;
4use vitaminc_aead::{
5    Cipher, CipherTextBuilder, Decipher, DecipherVisitor, Decrypt, Encrypt, IntoAad,
6    LocalCipherText, MapAccess, MapCipher, NonceGenerator, RandomNonceGenerator, SeqAccess,
7    SeqCipher, Unspecified,
8};
9use vitaminc_protected::Protected;
10
11/// The recursive ciphertext container produced by [`Aes256Cipher`].
12///
13/// The shape mirrors the structure of the plaintext that was encrypted: a
14/// single value yields [`Single`](AesCipherText::Single), a `Vec` yields
15/// [`Sequence`](AesCipherText::Sequence), a `HashMap` yields
16/// [`Map`](AesCipherText::Map). Nested structures are represented recursively.
17#[derive(Debug)]
18pub enum AesCipherText {
19    /// A single sealed value (nonce + ciphertext + tag).
20    Single(LocalCipherText),
21    /// A sequence of ciphertexts produced from a `Vec`-shaped plaintext.
22    Sequence(Vec<AesCipherText>),
23    /// A map of (cleartext key, ciphertext value) pairs produced from a
24    /// `HashMap`-shaped plaintext. Keys are not encrypted.
25    Map(Vec<(String, AesCipherText)>),
26    /// The authenticated absent marker produced by [`Cipher::encrypt_none`].
27    /// Stores a sealed empty plaintext whose tag binds the supplied AAD.
28    None(LocalCipherText),
29    /// A typed value passed through unencrypted via [`Cipher::passthrough`].
30    /// Not serializable to bytes — see
31    /// `packages/aead/src/cipher.rs` for the API-level type bound and the
32    /// runtime downcast performed by
33    /// [`Decipher::decrypt_passthrough`](vitaminc_aead::Decipher::decrypt_passthrough).
34    Passthrough(Box<dyn Any + Send + 'static>),
35}
36
37/// Implements AES-256-GCM. Backend is selected at compile time:
38/// `aws-lc-rs` on native targets, `aes-gcm` (RustCrypto) on `wasm32`.
39pub struct Aes256Cipher {
40    pub(crate) nonce_generator: RandomNonceGenerator<NONCE_LEN>,
41    pub(crate) key: CipherKey,
42}
43
44impl Aes256Cipher {
45    /// Construct a new AES-256-GCM cipher bound to the given [`Key`].
46    ///
47    /// A fresh random nonce generator is initialised for each cipher instance.
48    /// Returns an error if the platform RNG cannot be seeded or the key cannot
49    /// be loaded into the backend.
50    pub fn new(key: &Key) -> Result<Self, Unspecified> {
51        Ok(Self {
52            nonce_generator: RandomNonceGenerator::init()?,
53            key: key.cipher_key()?,
54        })
55    }
56}
57
58impl<'c> Cipher for &'c Aes256Cipher {
59    type Ok = AesCipherText;
60    type Error = Unspecified;
61    type SeqCipher = AesSeqCipher<'c>;
62    type MapCipher = AesMapCipher<'c>;
63
64    fn encrypt_bytes_vec<'a, A>(
65        self,
66        data: Protected<Vec<u8>>,
67        aad: A,
68    ) -> Result<Self::Ok, Self::Error>
69    where
70        A: IntoAad<'a>,
71    {
72        let nonce = self.nonce_generator.generate()?;
73        let nonce_bytes: [u8; NONCE_LEN] = nonce.as_ref().try_into().map_err(|_| Unspecified)?;
74        let aad = aad.into_aad();
75
76        CipherTextBuilder::new()
77            .append_nonce(nonce)
78            .append_target_plaintext(data)
79            .accepts_ciphertext_and_tag_ok(|mut buf| {
80                self.key
81                    .seal(&nonce_bytes, aad.as_bytes(), &mut buf)
82                    .map(|()| buf)
83            })
84            .build()
85            .map(AesCipherText::Single)
86    }
87
88    fn encrypt_seq(self, size_hint: Option<usize>) -> Self::SeqCipher {
89        AesSeqCipher {
90            cipher: self,
91            items: Vec::with_capacity(size_hint.unwrap_or(0)),
92        }
93    }
94
95    fn encrypt_map(self) -> Self::MapCipher {
96        AesMapCipher {
97            cipher: self,
98            entries: Vec::new(),
99            current_key: None,
100        }
101    }
102
103    fn encrypt_none<'a, A>(self, aad: A) -> Result<Self::Ok, Self::Error>
104    where
105        A: IntoAad<'a>,
106    {
107        let nonce = self.nonce_generator.generate()?;
108        let nonce_bytes: [u8; NONCE_LEN] = nonce.as_ref().try_into().map_err(|_| Unspecified)?;
109        let aad = aad.into_aad();
110
111        CipherTextBuilder::new()
112            .append_nonce(nonce)
113            .append_target_plaintext(Vec::<u8>::new())
114            .accepts_ciphertext_and_tag_ok(|mut buf| {
115                self.key
116                    .seal(&nonce_bytes, aad.as_bytes(), &mut buf)
117                    .map(|()| buf)
118            })
119            .build()
120            .map(AesCipherText::None)
121    }
122
123    fn passthrough<T>(self, value: T) -> Result<Self::Ok, Self::Error>
124    where
125        T: Any + Send + 'static,
126    {
127        Ok(AesCipherText::Passthrough(Box::new(value)))
128    }
129}
130
131/// [`SeqCipher`] driver for [`Aes256Cipher`]. Encrypts each element under its
132/// own fresh nonce and accumulates the results into an
133/// [`AesCipherText::Sequence`].
134pub struct AesSeqCipher<'c> {
135    cipher: &'c Aes256Cipher,
136    items: Vec<AesCipherText>,
137}
138
139impl<'c> SeqCipher for AesSeqCipher<'c> {
140    type Ok = AesCipherText;
141    type Error = Unspecified;
142
143    fn encrypt_next<'a, T, A>(mut self, data: T, aad: A) -> Result<Self, Self::Error>
144    where
145        T: Encrypt,
146        A: IntoAad<'a>,
147    {
148        let encrypted = data.encrypt_with_aad(self.cipher, aad)?;
149        self.items.push(encrypted);
150        Ok(self)
151    }
152
153    fn passthrough_next<T>(mut self, value: T) -> Result<Self, Self::Error>
154    where
155        T: Any + Send + 'static,
156    {
157        self.items.push(AesCipherText::Passthrough(Box::new(value)));
158        Ok(self)
159    }
160
161    fn end(self) -> Result<Self::Ok, Self::Error> {
162        Ok(AesCipherText::Sequence(self.items))
163    }
164}
165
166/// [`MapCipher`] driver for [`Aes256Cipher`]. Keys are stored in the clear;
167/// values are encrypted under their own fresh nonce and accumulated into an
168/// [`AesCipherText::Map`].
169///
170/// This driver is intended for encrypting sources that already enforce key
171/// uniqueness themselves — `HashMap`s and structs (whose field names are
172/// unique by construction). It therefore stores `entries` as a *positional
173/// list* and performs **no duplicate-key checks** of its own.
174///
175/// Implementors should be aware: if the same key is pushed by two completed
176/// `encrypt_value` / `passthrough_entry` calls, both are kept, and on decrypt
177/// the `HashMap`-shaped visitor is **last-wins**. The built-in
178/// `Encrypt for HashMap` impl never does this (the source map already dedups),
179/// so it is unreachable today; a custom encoder driving this trait directly is
180/// responsible for not emitting duplicate keys.
181pub struct AesMapCipher<'c> {
182    cipher: &'c Aes256Cipher,
183    entries: Vec<(String, AesCipherText)>,
184    current_key: Option<&'static str>,
185}
186
187impl<'c> MapCipher for AesMapCipher<'c> {
188    type Ok = AesCipherText;
189    type Error = Unspecified;
190
191    fn encrypt_key(mut self, key: &'static str) -> Result<Self, Self::Error> {
192        // A key already pending means `encrypt_key` was called twice with no
193        // intervening `encrypt_value` — a trait-contract violation. Fail rather
194        // than silently drop the first key.
195        if self.current_key.is_some() {
196            return Err(Unspecified);
197        }
198        self.current_key = Some(key);
199        Ok(self)
200    }
201
202    fn encrypt_value<'a, U, A>(mut self, value: U, aad: A) -> Result<Self, Self::Error>
203    where
204        U: Encrypt,
205        A: IntoAad<'a>,
206    {
207        let key = self.current_key.take().ok_or(Unspecified)?;
208        let encrypted = value.encrypt_with_aad(self.cipher, aad)?;
209        self.entries.push((key.to_string(), encrypted));
210        Ok(self)
211    }
212
213    fn passthrough_entry<T>(mut self, key: &'static str, value: T) -> Result<Self, Self::Error>
214    where
215        T: Any + Send + 'static,
216    {
217        // A key already pending means `encrypt_key` ran without a matching
218        // `encrypt_value` — adopting it here would silently drop the pending
219        // key, which is the same trait-contract violation `encrypt_key`
220        // rejects.
221        if self.current_key.is_some() {
222            return Err(Unspecified);
223        }
224        self.entries
225            .push((key.to_string(), AesCipherText::Passthrough(Box::new(value))));
226        Ok(self)
227    }
228
229    fn end(self) -> Result<Self::Ok, Self::Error> {
230        // Finalising with a pending key would silently drop the entry.
231        if self.current_key.is_some() {
232            return Err(Unspecified);
233        }
234        Ok(AesCipherText::Map(self.entries))
235    }
236}
237
238impl Aes256Cipher {
239    /// Decrypt an [`AesCipherText`] into `T` with no associated data.
240    /// `T` is inferred from the call site.
241    pub fn decrypt<'c, T: Decrypt<'c> + 'c>(
242        &'c self,
243        ciphertext: AesCipherText,
244    ) -> Result<T, Unspecified> {
245        self.decrypt_with_aad(ciphertext, ())
246    }
247
248    /// Decrypt an [`AesCipherText`] into `T` using the supplied associated data.
249    /// Returns [`Unspecified`] if the AAD does not match the value used at
250    /// encryption time, or if the structural shape of the ciphertext does not
251    /// match what `T` expects.
252    pub fn decrypt_with_aad<'c, 'a, T, A>(
253        &'c self,
254        ciphertext: AesCipherText,
255        aad: A,
256    ) -> Result<T, Unspecified>
257    where
258        T: Decrypt<'c> + 'c,
259        A: IntoAad<'a>,
260    {
261        let aad = aad.into_aad();
262        T::decrypt(AesDecipher {
263            cipher: self,
264            ciphertext,
265            aad: aad.as_bytes().to_vec(),
266        })
267    }
268}
269
270struct AesDecipher<'c> {
271    cipher: &'c Aes256Cipher,
272    ciphertext: AesCipherText,
273    aad: Vec<u8>,
274}
275
276impl AesDecipher<'_> {
277    fn decrypt_local_ciphertext(
278        cipher: &Aes256Cipher,
279        ct: LocalCipherText,
280        aad: &[u8],
281    ) -> Result<Protected<Vec<u8>>, Unspecified> {
282        let (nonce, reader) = ct.into_reader().read_nonce::<NONCE_LEN>()?;
283        let nonce_bytes = nonce.into_inner();
284
285        reader
286            .accepts_plaintext_ok(|data| cipher.key.open(&nonce_bytes, aad, data))
287            .read()
288    }
289}
290
291impl<'c> Decipher<'c> for AesDecipher<'c> {
292    type Ok<T>
293        = Result<T, Unspecified>
294    where
295        T: Send + 'c;
296
297    fn map_ok<T, U, F>(ok: Self::Ok<T>, f: F) -> Self::Ok<U>
298    where
299        T: Send + 'c,
300        U: Send + 'c,
301        F: FnOnce(T) -> U,
302    {
303        ok.map(f)
304    }
305
306    fn decrypt_bytes<V: DecipherVisitor<'c> + Send + 'c>(self, visitor: V) -> Self::Ok<V::Value> {
307        match self.ciphertext {
308            AesCipherText::Single(ct) => {
309                let bytes = Self::decrypt_local_ciphertext(self.cipher, ct, &self.aad)?;
310                visitor.visit_bytes_vec(bytes)
311            }
312            _ => Err(Unspecified),
313        }
314    }
315
316    fn decrypt_seq<V: DecipherVisitor<'c> + Send + 'c>(self, visitor: V) -> Self::Ok<V::Value> {
317        match self.ciphertext {
318            AesCipherText::Sequence(items) => {
319                let seq_access = AesSeqAccess {
320                    cipher: self.cipher,
321                    items: items.into_iter(),
322                    aad: self.aad,
323                };
324                visitor.visit_seq(seq_access)
325            }
326            _ => Err(Unspecified),
327        }
328    }
329
330    fn decrypt_map<V: DecipherVisitor<'c> + Send + 'c>(self, visitor: V) -> Self::Ok<V::Value> {
331        match self.ciphertext {
332            AesCipherText::Map(entries) => {
333                let map_access = AesMapAccess {
334                    cipher: self.cipher,
335                    entries: entries.into_iter(),
336                    aad: self.aad,
337                };
338                visitor.visit_map(map_access)
339            }
340            _ => Err(Unspecified),
341        }
342    }
343
344    fn decrypt_passthrough<T>(self) -> Self::Ok<T>
345    where
346        T: Any + Send + 'static,
347    {
348        match self.ciphertext {
349            AesCipherText::Passthrough(boxed) => {
350                boxed.downcast::<T>().map(|b| *b).map_err(|_| Unspecified)
351            }
352            _ => Err(Unspecified),
353        }
354    }
355
356    fn decrypt_option<T>(self) -> Self::Ok<Option<T>>
357    where
358        T: Decrypt<'c> + 'c,
359    {
360        match self.ciphertext {
361            AesCipherText::None(ct) => {
362                // Verify the AAD-bound tag over the empty plaintext.
363                Self::decrypt_local_ciphertext(self.cipher, ct, &self.aad)?;
364                Ok(None)
365            }
366            // Passthrough must never be decoded as an Option payload.
367            AesCipherText::Passthrough(_) => Err(Unspecified),
368            // Any other variant is the `Some` payload: recurse into `T`. This is
369            // what lets `Option<Vec<T>>`, `Option<HashMap<K, V>>`,
370            // `Option<Protected<T>>` compose naturally.
371            //
372            // Note: there is no depth tag in the ciphertext, so the *shape* of
373            // nested options is decided by `T` at the call site, not by the
374            // bytes — `Some(Some(x))` and `Some(x)` seal to identical
375            // `Single(_)` ciphertexts. Decoding the same ciphertext as
376            // `Option<String>` yields `Some("x")` and as `Option<Option<String>>`
377            // yields `Some(Some("x"))`; both succeed. This mirrors serde's
378            // treatment of `Option` and is intentional — every caller fixes a
379            // concrete type at the call site. See `nested_option_shape_is_caller_decided`.
380            other => {
381                let inner = AesDecipher {
382                    cipher: self.cipher,
383                    ciphertext: other,
384                    aad: self.aad,
385                };
386                T::decrypt(inner).map(Some)
387            }
388        }
389    }
390}
391
392struct AesSeqAccess<'c> {
393    cipher: &'c Aes256Cipher,
394    items: std::vec::IntoIter<AesCipherText>,
395    aad: Vec<u8>,
396}
397
398impl<'c> SeqAccess<'c> for AesSeqAccess<'c> {
399    type Error = Unspecified;
400
401    fn next_element<T: Decrypt<'c> + 'c>(&mut self) -> Result<Option<T>, Self::Error> {
402        let ct = match self.items.next() {
403            Some(ct) => ct,
404            None => return Ok(None),
405        };
406        let decipher = AesDecipher {
407            cipher: self.cipher,
408            ciphertext: ct,
409            aad: self.aad.clone(),
410        };
411        T::decrypt(decipher).map(Some)
412    }
413}
414
415struct AesMapAccess<'c> {
416    cipher: &'c Aes256Cipher,
417    entries: std::vec::IntoIter<(String, AesCipherText)>,
418    aad: Vec<u8>,
419}
420
421impl<'c> MapAccess<'c> for AesMapAccess<'c> {
422    type Error = Unspecified;
423
424    fn next_entry<T: Decrypt<'c> + 'c>(&mut self) -> Result<Option<(String, T)>, Self::Error> {
425        let (key, ct) = match self.entries.next() {
426            Some(entry) => entry,
427            None => return Ok(None),
428        };
429        let decipher = AesDecipher {
430            cipher: self.cipher,
431            ciphertext: ct,
432            aad: self.aad.clone(),
433        };
434        let value = T::decrypt(decipher)?;
435        Ok(Some((key, value)))
436    }
437}
438
439// quickcheck doesn't run under `wasm-pack test --node` (it shells out to
440// std::thread, which isn't available on wasm32-unknown-unknown). These property
441// tests already cover both backends on native via the
442// `_test-rust-crypto-backend` feature; the wasm32 codegen path is gated by the
443// KAT in `crate::backend::tests`.
444#[cfg(all(test, not(target_arch = "wasm32")))]
445#[allow(clippy::unwrap_used)]
446mod test {
447    use super::*;
448    use crate::key::tests::DifferingKeyPair;
449    use quickcheck_macros::quickcheck;
450    use std::collections::HashMap;
451    use vitaminc_aead::Encrypt;
452
453    #[quickcheck]
454    fn roundtrip_byte_array(key: Key, plaintext: [u8; 16]) -> bool {
455        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
456        let ciphertext = plaintext.encrypt(&cipher).expect("Encryption failed");
457        let decrypted: [u8; 16] = cipher.decrypt(ciphertext).expect("Decryption failed");
458        decrypted == plaintext
459    }
460
461    #[quickcheck]
462    fn roundtrip_string(key: Key, plaintext: String) -> bool {
463        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
464        let ciphertext = plaintext
465            .clone()
466            .encrypt(&cipher)
467            .expect("Encryption failed");
468        let decrypted: String = cipher.decrypt(ciphertext).expect("Decryption failed");
469        decrypted == plaintext
470    }
471
472    #[quickcheck]
473    fn roundtrip_str(key: Key, plaintext: String) -> bool {
474        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
475        let ciphertext = plaintext
476            .as_str()
477            .encrypt(&cipher)
478            .expect("Encryption failed");
479        let decrypted: String = cipher.decrypt(ciphertext).expect("Decryption failed");
480        decrypted == plaintext
481    }
482
483    #[quickcheck]
484    fn roundtrip_u32(key: Key, plaintext: u32) -> bool {
485        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
486        let ciphertext = plaintext.encrypt(&cipher).expect("Encryption failed");
487        let decrypted: u32 = cipher.decrypt(ciphertext).expect("Decryption failed");
488        decrypted == plaintext
489    }
490
491    #[quickcheck]
492    fn roundtrip_vec_of_strings(key: Key, plaintext: Vec<String>) -> bool {
493        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
494        let ciphertext = plaintext
495            .clone()
496            .encrypt(&cipher)
497            .expect("Encryption failed");
498        let decrypted: Vec<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
499        decrypted == plaintext
500    }
501
502    #[quickcheck]
503    fn roundtrip_string_with_aad(key: Key, plaintext: String) -> bool {
504        let aad = "test-aad";
505        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
506        let ciphertext = plaintext
507            .clone()
508            .encrypt_with_aad(&cipher, aad)
509            .expect("Encryption failed");
510        let decrypted: String = cipher
511            .decrypt_with_aad(ciphertext, aad)
512            .expect("Decryption failed");
513        decrypted == plaintext
514    }
515
516    #[quickcheck]
517    fn decrypt_fails_with_wrong_aad(key: Key, plaintext: String) -> bool {
518        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
519        let ciphertext = plaintext
520            .encrypt_with_aad(&cipher, "correct-aad")
521            .expect("Encryption failed");
522        cipher
523            .decrypt_with_aad::<String, _>(ciphertext, "wrong-aad")
524            .is_err()
525    }
526
527    #[quickcheck]
528    fn decrypt_fails_with_wrong_key(keys: DifferingKeyPair, plaintext: String) -> bool {
529        // `DifferingKeyPair` guarantees the two keys are distinct, so this test
530        // is deterministic — a key collision cannot make it spuriously fail.
531        let DifferingKeyPair(key_a, key_b) = keys;
532        let cipher_a = Aes256Cipher::new(&key_a).expect("Failed to create cipher A");
533        let cipher_b = Aes256Cipher::new(&key_b).expect("Failed to create cipher B");
534        let ciphertext = plaintext.encrypt(&cipher_a).expect("Encryption failed");
535        cipher_b.decrypt::<String>(ciphertext).is_err()
536    }
537
538    #[test]
539    fn roundtrip_hashmap() {
540        let key = Key::from([42u8; 32]);
541        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
542
543        let mut plaintext = HashMap::new();
544        plaintext.insert("name", "Alice");
545        plaintext.insert("city", "Sydney");
546
547        let ciphertext = plaintext.encrypt(&cipher).expect("Encryption failed");
548        let decrypted: HashMap<String, String> =
549            cipher.decrypt(ciphertext).expect("Decryption failed");
550
551        assert_eq!(decrypted.len(), 2);
552        assert_eq!(decrypted.get("name").unwrap(), "Alice");
553        assert_eq!(decrypted.get("city").unwrap(), "Sydney");
554    }
555
556    #[test]
557    fn roundtrip_hashmap_with_aad() {
558        let key = Key::from([42u8; 32]);
559        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
560        let aad = "map-context";
561
562        let mut plaintext = HashMap::new();
563        plaintext.insert("name", "Alice");
564        plaintext.insert("city", "Sydney");
565
566        let ciphertext = plaintext
567            .encrypt_with_aad(&cipher, aad)
568            .expect("Encryption failed");
569        let decrypted: HashMap<String, String> = cipher
570            .decrypt_with_aad(ciphertext, aad)
571            .expect("Decryption failed");
572
573        assert_eq!(decrypted.len(), 2);
574        assert_eq!(decrypted.get("name").unwrap(), "Alice");
575        assert_eq!(decrypted.get("city").unwrap(), "Sydney");
576    }
577
578    #[test]
579    fn decrypt_hashmap_fails_with_wrong_aad() {
580        let key = Key::from([42u8; 32]);
581        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
582
583        let mut plaintext = HashMap::new();
584        plaintext.insert("name", "Alice");
585
586        let ciphertext = plaintext
587            .encrypt_with_aad(&cipher, "correct-aad")
588            .expect("Encryption failed");
589        assert!(cipher
590            .decrypt_with_aad::<HashMap<String, String>, _>(ciphertext, "wrong-aad")
591            .is_err());
592    }
593
594    #[quickcheck]
595    fn roundtrip_protected_string(key: Key, plaintext: String) -> bool {
596        use vitaminc_protected::{Controlled, Protected};
597
598        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
599        let protected = Protected::new(plaintext.clone());
600        let ciphertext = protected.encrypt(&cipher).expect("Encryption failed");
601        let decrypted: Protected<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
602        decrypted.risky_unwrap() == plaintext
603    }
604
605    #[quickcheck]
606    fn roundtrip_option_some_string(key: Key, plaintext: String) -> bool {
607        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
608        let value = Some(plaintext.clone());
609        let ciphertext = value.encrypt(&cipher).expect("Encryption failed");
610        let decrypted: Option<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
611        decrypted == Some(plaintext)
612    }
613
614    #[test]
615    fn roundtrip_option_none_string() {
616        let key = Key::from([7u8; 32]);
617        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
618        let value: Option<String> = None;
619        let ciphertext = value.encrypt(&cipher).expect("Encryption failed");
620        let decrypted: Option<String> = cipher.decrypt(ciphertext).expect("Decryption failed");
621        assert_eq!(decrypted, None);
622    }
623
624    #[quickcheck]
625    fn roundtrip_option_some_with_aad(key: Key, plaintext: String) -> bool {
626        let aad = "opt-aad";
627        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
628        let ciphertext = Some(plaintext.clone())
629            .encrypt_with_aad(&cipher, aad)
630            .expect("Encryption failed");
631        let decrypted: Option<String> = cipher
632            .decrypt_with_aad(ciphertext, aad)
633            .expect("Decryption failed");
634        decrypted == Some(plaintext)
635    }
636
637    #[quickcheck]
638    fn roundtrip_option_none_with_aad(key: Key) -> bool {
639        let aad = "opt-none-aad";
640        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
641        let ciphertext = None::<String>
642            .encrypt_with_aad(&cipher, aad)
643            .expect("Encryption failed");
644        let decrypted: Option<String> = cipher
645            .decrypt_with_aad(ciphertext, aad)
646            .expect("Decryption failed");
647        decrypted.is_none()
648    }
649
650    #[quickcheck]
651    fn decrypt_option_none_fails_with_wrong_aad(key: Key) -> bool {
652        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
653        let ciphertext = None::<String>
654            .encrypt_with_aad(&cipher, "correct")
655            .expect("Encryption failed");
656        cipher
657            .decrypt_with_aad::<Option<String>, _>(ciphertext, "wrong")
658            .is_err()
659    }
660
661    #[test]
662    fn decrypt_string_rejects_none_ciphertext() {
663        // Closes the empty-plaintext masking concern: an authenticated `None`
664        // marker must not be decodable as `String("")`.
665        let key = Key::from([9u8; 32]);
666        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
667        let ciphertext = None::<String>.encrypt(&cipher).expect("Encryption failed");
668        assert!(cipher.decrypt::<String>(ciphertext).is_err());
669    }
670
671    #[test]
672    fn decrypt_option_rejects_passthrough_ciphertext() {
673        // Type-laundering guard: a passthrough must not satisfy Option<T>.
674        let key = Key::from([11u8; 32]);
675        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
676        let ciphertext = (&cipher).passthrough(42u32).expect("passthrough failed");
677        assert!(cipher.decrypt::<Option<u32>>(ciphertext).is_err());
678    }
679
680    #[test]
681    fn roundtrip_passthrough_u32() {
682        let key = Key::from([1u8; 32]);
683        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
684        let ciphertext = (&cipher).passthrough(12345u32).expect("passthrough failed");
685        let decrypted: u32 = decrypt_passthrough_via(&cipher, ciphertext).expect("decode failed");
686        assert_eq!(decrypted, 12345u32);
687    }
688
689    #[test]
690    fn roundtrip_passthrough_string() {
691        let key = Key::from([2u8; 32]);
692        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
693        let ciphertext = (&cipher)
694            .passthrough(String::from("version-tag"))
695            .expect("passthrough failed");
696        let decrypted: String =
697            decrypt_passthrough_via(&cipher, ciphertext).expect("decode failed");
698        assert_eq!(decrypted, "version-tag");
699    }
700
701    #[test]
702    fn passthrough_type_mismatch_returns_err() {
703        let key = Key::from([3u8; 32]);
704        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
705        let ciphertext = (&cipher).passthrough(42u32).expect("passthrough failed");
706        let result: Result<String, _> = decrypt_passthrough_via(&cipher, ciphertext);
707        assert!(result.is_err());
708    }
709
710    // Convenience: drive `Decipher::decrypt_passthrough` from a known
711    // ciphertext. Mirrors what a derive-generated `Decrypt` impl would do
712    // for a `#[encrypt(passthrough)]` field.
713    fn decrypt_passthrough_via<T>(
714        cipher: &Aes256Cipher,
715        ciphertext: AesCipherText,
716    ) -> Result<T, Unspecified>
717    where
718        T: Any + Send + 'static,
719    {
720        let decipher = AesDecipher {
721            cipher,
722            ciphertext,
723            aad: Vec::new(),
724        };
725        decipher.decrypt_passthrough::<T>()
726    }
727
728    #[quickcheck]
729    fn roundtrip_vec_of_option_string(key: Key, items: Vec<Option<String>>) -> bool {
730        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
731        let ciphertext = items.clone().encrypt(&cipher).expect("Encryption failed");
732        let decrypted: Vec<Option<String>> = cipher.decrypt(ciphertext).expect("Decryption failed");
733        decrypted == items
734    }
735
736    // --- MapCipher state-machine contract ---
737    //
738    // `AesMapCipher` enforces a strict key→value pairing: every `encrypt_key`
739    // must be followed by exactly one `encrypt_value` (or `passthrough_entry`
740    // for the unencrypted variant) before the next key or `end`. The four
741    // tests below pin down each branch where the contract can be violated.
742
743    #[test]
744    fn encrypt_key_twice_without_value_fails() {
745        let key = Key::from([42u8; 32]);
746        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
747        let map = (&cipher).encrypt_map().encrypt_key("first").unwrap();
748        assert!(map.encrypt_key("second").is_err());
749    }
750
751    #[test]
752    fn encrypt_value_without_pending_key_fails() {
753        let key = Key::from([42u8; 32]);
754        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
755        let map = (&cipher).encrypt_map();
756        assert!(map.encrypt_value("orphan-value", ()).is_err());
757    }
758
759    #[test]
760    fn passthrough_entry_with_pending_key_fails() {
761        let key = Key::from([42u8; 32]);
762        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
763        let map = (&cipher).encrypt_map().encrypt_key("pending").unwrap();
764        // Adopting the new key here would silently drop "pending".
765        assert!(map.passthrough_entry("other", 42u32).is_err());
766    }
767
768    #[test]
769    fn end_with_pending_key_fails() {
770        let key = Key::from([42u8; 32]);
771        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
772        let map = (&cipher).encrypt_map().encrypt_key("pending").unwrap();
773        assert!(map.end().is_err());
774    }
775
776    #[test]
777    fn passthrough_entry_succeeds_with_no_pending_key() {
778        let key = Key::from([42u8; 32]);
779        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
780        // Sanity: the new guard does not break the happy path.
781        let ciphertext = (&cipher)
782            .encrypt_map()
783            .passthrough_entry("version", 1u32)
784            .and_then(|m| m.end())
785            .expect("passthrough_entry should succeed without a pending key");
786        match ciphertext {
787            AesCipherText::Map(entries) => {
788                assert_eq!(entries.len(), 1);
789                assert_eq!(entries[0].0, "version");
790                assert!(matches!(entries[0].1, AesCipherText::Passthrough(_)));
791            }
792            _ => panic!("expected Map ciphertext"),
793        }
794    }
795
796    // --- Nonce uniqueness (fundamental AEAD property) ---
797
798    #[quickcheck]
799    fn nonce_is_unique_across_encryptions(key: Key, plaintext: String) -> bool {
800        // Two encryptions of the same plaintext under the same cipher must
801        // produce different ciphertexts — each `encrypt_*` call draws a fresh
802        // nonce. The plaintext is incidental (the nonce is drawn independently),
803        // but running this as a property samples a fresh nonce pair per case,
804        // exercising many more of the generator's outputs than a single fixed
805        // run would. Catches a future RNG / nonce-reuse regression before it
806        // becomes a catastrophic AEAD failure.
807        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
808        let ct1 = plaintext
809            .clone()
810            .encrypt(&cipher)
811            .expect("Encryption failed");
812        let ct2 = plaintext.encrypt(&cipher).expect("Encryption failed");
813        match (ct1, ct2) {
814            (AesCipherText::Single(a), AesCipherText::Single(b)) => a.as_ref() != b.as_ref(),
815            _ => panic!("expected Single ciphertexts"),
816        }
817    }
818
819    // --- Variant-rejection catch-alls (FC.1 cluster) ---
820    //
821    // Each `decrypt_*` method accepts exactly one `AesCipherText` variant and
822    // routes every other variant to `Err(Unspecified)`. These are the
823    // structural type-laundering guards the design relies on. The four tests
824    // below pin every catch-all sub-path (the `None`/`Passthrough` cases
825    // already pinned individually above are re-asserted here for completeness).
826    // `AesCipherText` is not `Clone`, so each rejected variant is rebuilt fresh.
827
828    fn single_ct(cipher: &Aes256Cipher) -> AesCipherText {
829        "single".to_string().encrypt(cipher).expect("encrypt")
830    }
831    fn sequence_ct(cipher: &Aes256Cipher) -> AesCipherText {
832        vec!["a".to_string(), "b".to_string()]
833            .encrypt(cipher)
834            .expect("encrypt")
835    }
836    fn map_ct(cipher: &Aes256Cipher) -> AesCipherText {
837        let mut m = HashMap::new();
838        m.insert("k", "v");
839        m.encrypt(cipher).expect("encrypt")
840    }
841    fn none_ct(cipher: &Aes256Cipher) -> AesCipherText {
842        None::<String>.encrypt(cipher).expect("encrypt")
843    }
844    fn passthrough_ct(cipher: &Aes256Cipher) -> AesCipherText {
845        cipher.passthrough(7u32).expect("passthrough")
846    }
847
848    #[test]
849    fn decrypt_bytes_rejects_non_single_variants() {
850        let cipher = Aes256Cipher::new(&Key::from([20u8; 32])).expect("Failed to create cipher");
851        // `decrypt_bytes` (driving e.g. `String`) accepts only `Single`.
852        assert!(
853            cipher.decrypt::<String>(sequence_ct(&cipher)).is_err(),
854            "Sequence"
855        );
856        assert!(cipher.decrypt::<String>(map_ct(&cipher)).is_err(), "Map");
857        assert!(cipher.decrypt::<String>(none_ct(&cipher)).is_err(), "None");
858        assert!(
859            cipher.decrypt::<String>(passthrough_ct(&cipher)).is_err(),
860            "Passthrough"
861        );
862    }
863
864    #[test]
865    fn decrypt_seq_rejects_non_sequence_variants() {
866        let cipher = Aes256Cipher::new(&Key::from([21u8; 32])).expect("Failed to create cipher");
867        // `decrypt_seq` (driving `Vec<T>`) accepts only `Sequence`.
868        assert!(
869            cipher.decrypt::<Vec<String>>(single_ct(&cipher)).is_err(),
870            "Single"
871        );
872        assert!(
873            cipher.decrypt::<Vec<String>>(map_ct(&cipher)).is_err(),
874            "Map"
875        );
876        assert!(
877            cipher.decrypt::<Vec<String>>(none_ct(&cipher)).is_err(),
878            "None"
879        );
880        assert!(
881            cipher
882                .decrypt::<Vec<String>>(passthrough_ct(&cipher))
883                .is_err(),
884            "Passthrough"
885        );
886    }
887
888    #[test]
889    fn decrypt_map_rejects_non_map_variants() {
890        let cipher = Aes256Cipher::new(&Key::from([22u8; 32])).expect("Failed to create cipher");
891        // `decrypt_map` (driving `HashMap<String, T>`) accepts only `Map`.
892        assert!(
893            cipher
894                .decrypt::<HashMap<String, String>>(single_ct(&cipher))
895                .is_err(),
896            "Single"
897        );
898        assert!(
899            cipher
900                .decrypt::<HashMap<String, String>>(sequence_ct(&cipher))
901                .is_err(),
902            "Sequence"
903        );
904        assert!(
905            cipher
906                .decrypt::<HashMap<String, String>>(none_ct(&cipher))
907                .is_err(),
908            "None"
909        );
910        assert!(
911            cipher
912                .decrypt::<HashMap<String, String>>(passthrough_ct(&cipher))
913                .is_err(),
914            "Passthrough"
915        );
916    }
917
918    #[test]
919    fn decrypt_passthrough_rejects_non_passthrough_variants() {
920        let cipher = Aes256Cipher::new(&Key::from([23u8; 32])).expect("Failed to create cipher");
921        // `decrypt_passthrough` accepts only `Passthrough`.
922        assert!(
923            decrypt_passthrough_via::<u32>(&cipher, single_ct(&cipher)).is_err(),
924            "Single"
925        );
926        assert!(
927            decrypt_passthrough_via::<u32>(&cipher, sequence_ct(&cipher)).is_err(),
928            "Sequence"
929        );
930        assert!(
931            decrypt_passthrough_via::<u32>(&cipher, map_ct(&cipher)).is_err(),
932            "Map"
933        );
934        assert!(
935            decrypt_passthrough_via::<u32>(&cipher, none_ct(&cipher)).is_err(),
936            "None"
937        );
938    }
939
940    // --- Option<T> type variation: exercise the `other`-arm sub-paths ---
941    //
942    // `roundtrip_option_some_string` only lands the `Single` sub-path with a
943    // `String` leaf. These cover a non-String `Single` leaf (u32), the
944    // `Sequence` sub-path (Vec), the `Map` sub-path (HashMap), and the
945    // `Protected`-rewrap path.
946
947    #[quickcheck]
948    fn roundtrip_option_some_u32(key: Key, value: u32) -> bool {
949        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
950        let ct = Some(value).encrypt(&cipher).expect("Encryption failed");
951        cipher
952            .decrypt::<Option<u32>>(ct)
953            .expect("Decryption failed")
954            == Some(value)
955    }
956
957    #[quickcheck]
958    fn roundtrip_option_some_vec_of_strings(items: Vec<String>) -> bool {
959        let cipher = Aes256Cipher::new(&Key::from([31u8; 32])).expect("Failed to create cipher");
960        let ct = Some(items.clone())
961            .encrypt(&cipher)
962            .expect("Encryption failed");
963        cipher
964            .decrypt::<Option<Vec<String>>>(ct)
965            .expect("Decryption failed")
966            == Some(items)
967    }
968
969    #[test]
970    fn roundtrip_option_some_hashmap() {
971        let cipher = Aes256Cipher::new(&Key::from([32u8; 32])).expect("Failed to create cipher");
972        let mut m = HashMap::new();
973        m.insert("name", "Alice");
974        let ct = Some(m).encrypt(&cipher).expect("Encryption failed");
975        let decoded: Option<HashMap<String, String>> =
976            cipher.decrypt(ct).expect("Decryption failed");
977        assert_eq!(
978            decoded.unwrap().get("name").map(String::as_str),
979            Some("Alice")
980        );
981    }
982
983    #[quickcheck]
984    fn roundtrip_option_some_protected_string(plaintext: String) -> bool {
985        use vitaminc_protected::{Controlled, Protected};
986        let cipher = Aes256Cipher::new(&Key::from([33u8; 32])).expect("Failed to create cipher");
987        let ct = Some(Protected::new(plaintext.clone()))
988            .encrypt(&cipher)
989            .expect("Encryption failed");
990        let decoded: Option<Protected<String>> = cipher.decrypt(ct).expect("Decryption failed");
991        decoded.map(Controlled::risky_unwrap) == Some(plaintext)
992    }
993
994    #[quickcheck]
995    fn decrypt_option_some_fails_with_wrong_aad(plaintext: String) -> bool {
996        // Wrong-AAD rejection threaded through the `Option` type itself (not just
997        // the inner leaf).
998        let cipher = Aes256Cipher::new(&Key::from([34u8; 32])).expect("Failed to create cipher");
999        let ct = Some(plaintext)
1000            .encrypt_with_aad(&cipher, "correct")
1001            .expect("Encryption failed");
1002        cipher
1003            .decrypt_with_aad::<Option<String>, _>(ct, "wrong")
1004            .is_err()
1005    }
1006
1007    #[test]
1008    fn nested_option_shape_is_caller_decided() {
1009        // `Some(Some(x))` seals to the same `Single(_)` as `Some(x)`; the
1010        // call-site type decides the decoded shape. Pins the documented
1011        // serde-style property so an accidental future "fix" can't close the
1012        // recursion door. See the note on `decrypt_option`.
1013        let cipher = Aes256Cipher::new(&Key::from([35u8; 32])).expect("Failed to create cipher");
1014        let outer: Option<Option<String>> = Some(Some("x".into()));
1015        let ct = outer.encrypt(&cipher).expect("Encryption failed");
1016        let decoded: Option<String> = cipher.decrypt(ct).expect("Decryption failed");
1017        assert_eq!(decoded, Some("x".into()));
1018    }
1019
1020    // --- Mixed encrypted / passthrough entries + Any-TypeId ---
1021
1022    #[test]
1023    fn seq_cipher_accepts_mixed_encrypt_next_and_passthrough_next() {
1024        let cipher = Aes256Cipher::new(&Key::from([40u8; 32])).expect("Failed to create cipher");
1025        let ct = (&cipher)
1026            .encrypt_seq(Some(3))
1027            .encrypt_next("first", ())
1028            .unwrap()
1029            .passthrough_next(99u32)
1030            .unwrap()
1031            .encrypt_next("third", ())
1032            .unwrap()
1033            .end()
1034            .unwrap();
1035        match ct {
1036            AesCipherText::Sequence(items) => {
1037                assert_eq!(items.len(), 3);
1038                assert!(matches!(items[0], AesCipherText::Single(_)));
1039                assert!(matches!(items[1], AesCipherText::Passthrough(_)));
1040                assert!(matches!(items[2], AesCipherText::Single(_)));
1041            }
1042            _ => panic!("expected Sequence"),
1043        }
1044    }
1045
1046    #[test]
1047    fn map_with_mixed_entries_cannot_decode_as_uniform_hashmap() {
1048        // A passthrough value in a map cannot satisfy a uniform `HashMap<_, T>`
1049        // decode: `T`'s bytes visitor hits the FC.1 catch-all on the
1050        // Passthrough variant, failing the whole decode.
1051        let cipher = Aes256Cipher::new(&Key::from([41u8; 32])).expect("Failed to create cipher");
1052        let ct = (&cipher)
1053            .encrypt_map()
1054            .encrypt_key("name")
1055            .unwrap()
1056            .encrypt_value("Alice".to_string(), ())
1057            .unwrap()
1058            .passthrough_entry("schema_version", 1u32)
1059            .unwrap()
1060            .end()
1061            .unwrap();
1062        assert!(cipher.decrypt::<HashMap<String, String>>(ct).is_err());
1063    }
1064
1065    #[test]
1066    fn roundtrip_hashmap_of_option_values() {
1067        // The companion happy case: mixed Some/None values decode correctly iff
1068        // the decode type explicitly accepts the variation (`Option<T>`).
1069        let cipher = Aes256Cipher::new(&Key::from([42u8; 32])).expect("Failed to create cipher");
1070        let mut m: HashMap<&'static str, Option<String>> = HashMap::new();
1071        m.insert("present", Some("Alice".to_string()));
1072        m.insert("absent", None);
1073        let ct = m.encrypt(&cipher).expect("Encryption failed");
1074        let decoded: HashMap<String, Option<String>> =
1075            cipher.decrypt(ct).expect("Decryption failed");
1076        assert_eq!(decoded.get("present"), Some(&Some("Alice".to_string())));
1077        assert_eq!(decoded.get("absent"), Some(&None));
1078    }
1079
1080    #[test]
1081    fn passthrough_option_is_distinct_type_from_inner() {
1082        // `Any`/`TypeId` pin: `Option<u32>` and `u32` are distinct types even
1083        // when the value is `Some(n)`. A passthrough sealed as `Option<u32>`
1084        // must only downcast back to `Option<u32>`.
1085        let cipher = Aes256Cipher::new(&Key::from([43u8; 32])).expect("Failed to create cipher");
1086        let ct = cipher.passthrough(Some(42u32)).expect("passthrough failed");
1087        let decoded: Option<u32> =
1088            decrypt_passthrough_via(&cipher, ct).expect("decode as Option<u32> should succeed");
1089        assert_eq!(decoded, Some(42u32));
1090
1091        let ct2 = cipher.passthrough(Some(42u32)).expect("passthrough failed");
1092        assert!(
1093            decrypt_passthrough_via::<u32>(&cipher, ct2).is_err(),
1094            "Option<u32> passthrough must not downcast to u32"
1095        );
1096    }
1097
1098    #[quickcheck]
1099    fn decrypt_byte_array_ciphertext_as_vec_u8(key: Key, bytes: [u8; 16]) -> bool {
1100        // `Decrypt for Vec<u8>` reads the bytes pipeline (`Single`), not a
1101        // `Sequence`. There is no `Encrypt for Vec<u8>` (no `Encrypt for u8`),
1102        // so a `[u8; N]` ciphertext is the canonical `Single` producer
1103        // decodable as `Vec<u8>`. Pins the otherwise-unexercised impl across
1104        // arbitrary byte payloads.
1105        let cipher = Aes256Cipher::new(&key).expect("Failed to create cipher");
1106        let ct = bytes.encrypt(&cipher).expect("Encryption failed");
1107        let decoded: Vec<u8> = cipher.decrypt(ct).expect("Decryption failed");
1108        decoded == bytes.to_vec()
1109    }
1110}