Skip to main content

lib_q_saturnin/
qcb.rs

1//! Saturnin-QCB authenticated encryption
2//!
3//! Saturnin-QCB is the one-pass, parallelizable AEAD proposed in "An Update on Saturnin". It is
4//! a `ΘCB`/`TAE`-style mode built on the Saturnin [tweakable block cipher](crate::tbc): each
5//! block of plaintext is encrypted by one TBC call whose tweak binds a domain separator, the
6//! nonce, and the block number; the tag is produced by encrypting a checksum of the (padded)
7//! message under a distinct domain. Because nonce + block-number give every TBC call a unique
8//! tweak (when nonces are not reused), the mode achieves rate-one encryption with a tighter
9//! quantum-security proof than Saturnin-CTR-Cascade, and every block can be processed
10//! independently (parallelized).
11//!
12//! # ⚠️ Instantiation note — not validated against designer test vectors
13//!
14//! The update note gives only a **high-level** description of Saturnin-QCB (Section 5 and
15//! Figure 1); the full mode definition lives in the separate QCB paper `[BBC+20]`, which is not
16//! bundled with this repository, and **no official QCB known-answer test vectors are
17//! published**. The construction below faithfully follows everything the update note specifies,
18//! and fills the gaps it leaves open with explicit, documented choices:
19//!
20//! - **TBC** (unambiguous, from the note): `TBC_d(K,T)(M) = Saturnin16^d_{K⊕T}(M)`. See
21//!   [`crate::tbc`].
22//! - **Domains** (from Figure 1): message blocks use domain **9**, the tag uses domain **10**.
23//!   Associated-data blocks use domain **11** (the note states AD blocks also cost 16
24//!   super-rounds but Figure 1 omits AD layout — this domain choice is ours).
25//! - **Tweak encoding** (ours): `T = N (16 bytes) ‖ 0x00·8 ‖ block_index_be_u64 (8 bytes)`, a
26//!   256-bit value. AD-block tweaks use the same layout with the nonce field zeroed, so AD
27//!   authentication is nonce-independent (OCB tradition).
28//! - **Padding** (from the note: "the message is always padded with a 01* padding, so the
29//!   ciphertext can be up to 512 bits longer than the plaintext"): `10*` padding (`0x80` then
30//!   zeros) is **always** applied, adding a whole extra block when the input is already a block
31//!   multiple. This trades a little length for a simple, unambiguous, invertible mode.
32//! - **Checksum / AD folding** (ours): `checksum = ⊕ padded_message_blocks`;
33//!   `tag = TBC_10(K, tweak(N, last)) (checksum) ⊕ ⊕_j TBC_11(K, tweak_ad(j)) (A_j)`.
34//!
35//! This module is therefore a **spec-faithful interpretation** suitable for experimentation and
36//! cross-checking, not a byte-compatible reference for an external Saturnin-QCB. It is verified
37//! by round-trip, tamper-detection, parallel-equivalence, and pinned self-consistency vectors —
38//! not by designer KATs. If/when official QCB vectors are published, pin them here.
39//!
40//! ## Usage Example
41//!
42//! ```rust
43//! # #[cfg(feature = "qcb")]
44//! # {
45//! use lib_q_saturnin::{
46//!     Aead,
47//!     AeadKey,
48//!     Nonce,
49//!     SaturninQcb,
50//! };
51//!
52//! let aead = SaturninQcb::new();
53//! let key = AeadKey::new(vec![0u8; 32]);
54//! let nonce = Nonce::new(vec![0u8; 16]);
55//!
56//! let ciphertext = aead
57//!     .encrypt(&key, &nonce, b"Secret message", Some(b"metadata"))
58//!     .unwrap();
59//! let decrypted = aead
60//!     .decrypt(&key, &nonce, &ciphertext, Some(b"metadata"))
61//!     .unwrap();
62//! assert_eq!(decrypted, b"Secret message");
63//! # }
64//! ```
65
66#[cfg(feature = "alloc")]
67use alloc::{
68    string::ToString,
69    vec::Vec,
70};
71
72use lib_q_core::{
73    Aead,
74    AeadDecryptSemantic,
75    AeadKey,
76    DecryptSemanticOutcome,
77    Error,
78    Nonce,
79    Result,
80};
81use zeroize::{
82    Zeroize,
83    Zeroizing,
84};
85
86use crate::tbc::{
87    SaturninTbc,
88    TBC_BLOCK_BYTES,
89};
90
91/// Domain separator for message blocks (Figure 1).
92const DOMAIN_MESSAGE: u8 = 9;
93/// Domain separator for the tag / checksum block (Figure 1).
94const DOMAIN_TAG: u8 = 10;
95/// Domain separator for associated-data blocks (this instantiation's choice).
96const DOMAIN_AD: u8 = 11;
97
98/// Block size in bytes (256-bit Saturnin block).
99const BLOCK: usize = TBC_BLOCK_BYTES;
100
101/// Saturnin-QCB AEAD.
102///
103/// Holds pre-built tweakable block ciphers for the three domains used by the mode so that
104/// per-message work allocates no round constants.
105pub struct SaturninQcb {
106    msg: SaturninTbc,
107    tag: SaturninTbc,
108    ad: SaturninTbc,
109}
110
111impl SaturninQcb {
112    /// Create a new Saturnin-QCB instance.
113    pub fn new() -> Self {
114        Self {
115            msg: SaturninTbc::new(DOMAIN_MESSAGE).expect("domain 9 is valid"),
116            tag: SaturninTbc::new(DOMAIN_TAG).expect("domain 10 is valid"),
117            ad: SaturninTbc::new(DOMAIN_AD).expect("domain 11 is valid"),
118        }
119    }
120
121    /// Key size in bytes (256 bits).
122    pub const fn key_size() -> usize {
123        32
124    }
125
126    /// Nonce size in bytes (128 bits).
127    pub const fn nonce_size() -> usize {
128        16
129    }
130
131    /// Tag size in bytes (256 bits).
132    pub const fn tag_size() -> usize {
133        BLOCK
134    }
135
136    /// Build the 256-bit message/tag tweak `N ‖ 0·8 ‖ block_index_be`.
137    fn tweak(nonce16: &[u8; 16], block_index: u64) -> [u8; BLOCK] {
138        let mut t = [0u8; BLOCK];
139        t[0..16].copy_from_slice(nonce16);
140        t[24..32].copy_from_slice(&block_index.to_be_bytes());
141        t
142    }
143
144    /// Build the 256-bit associated-data tweak (nonce field zeroed; AD is nonce-independent).
145    fn ad_tweak(block_index: u64) -> [u8; BLOCK] {
146        let mut t = [0u8; BLOCK];
147        t[24..32].copy_from_slice(&block_index.to_be_bytes());
148        t
149    }
150
151    /// `10*`-pad `data` to a positive multiple of [`BLOCK`], always appending at least the `0x80`
152    /// marker (a whole extra block when `data` is already a block multiple, or when empty).
153    fn pad(data: &[u8]) -> Zeroizing<Vec<u8>> {
154        let padded_len = (data.len() / BLOCK + 1) * BLOCK;
155        let mut out = Zeroizing::new(Vec::with_capacity(padded_len));
156        out.extend_from_slice(data);
157        out.push(0x80);
158        out.resize(padded_len, 0u8);
159        out
160    }
161
162    /// Authenticate associated data into a 256-bit accumulator (`0` when AD is empty).
163    fn absorb_ad(&self, key: &[u8; 32], ad: &[u8]) -> Result<Zeroizing<[u8; BLOCK]>> {
164        let mut auth = Zeroizing::new([0u8; BLOCK]);
165        if ad.is_empty() {
166            return Ok(auth);
167        }
168        let padded = Self::pad(ad);
169        let (padded_blocks, _rem) = padded.as_chunks::<BLOCK>();
170        for (j, chunk) in padded_blocks.iter().enumerate() {
171            let tweak = Self::ad_tweak(j as u64);
172            let mut block = [0u8; BLOCK];
173            block.copy_from_slice(chunk);
174            self.ad.encrypt_block(key, &tweak, &mut block)?;
175            for i in 0..BLOCK {
176                auth[i] ^= block[i];
177            }
178            block.zeroize();
179        }
180        Ok(auth)
181    }
182
183    /// Compute the 256-bit tag over the padded-message checksum and the AD accumulator.
184    fn compute_tag(
185        &self,
186        key: &[u8; 32],
187        nonce16: &[u8; 16],
188        checksum: &[u8; BLOCK],
189        last_index: u64,
190        ad_auth: &[u8; BLOCK],
191    ) -> Result<Zeroizing<[u8; BLOCK]>> {
192        let mut tag = Zeroizing::new(*checksum);
193        let tweak = Self::tweak(nonce16, last_index);
194        self.tag.encrypt_block(key, &tweak, &mut tag)?;
195        for i in 0..BLOCK {
196            tag[i] ^= ad_auth[i];
197        }
198        Ok(tag)
199    }
200
201    fn validate_lengths(key: &AeadKey, nonce: &Nonce) -> Result<()> {
202        if key.as_bytes().len() != Self::key_size() {
203            return Err(Error::InvalidKeySize {
204                expected: Self::key_size(),
205                actual: key.as_bytes().len(),
206            });
207        }
208        if nonce.as_bytes().len() != Self::nonce_size() {
209            return Err(Error::InvalidNonceSize {
210                expected: Self::nonce_size(),
211                actual: nonce.as_bytes().len(),
212            });
213        }
214        Ok(())
215    }
216
217    /// Shared decrypt core for Layer A ([`Aead::decrypt`]) and Layer B
218    /// ([`AeadDecryptSemantic::decrypt_semantic`]). Always decrypts the full ciphertext body
219    /// before the authentication outcome is allowed to influence the returned plaintext.
220    fn decrypt_core(
221        &self,
222        key: &AeadKey,
223        nonce: &Nonce,
224        ciphertext: &[u8],
225        associated_data: Option<&[u8]>,
226    ) -> Result<DecryptSemanticOutcome> {
227        Self::validate_lengths(key, nonce)?;
228
229        // Need at least one message block plus the tag.
230        if ciphertext.len() < 2 * BLOCK {
231            return Err(Error::aead_ciphertext_shorter_than_tag(
232                2 * BLOCK,
233                ciphertext.len(),
234            ));
235        }
236        // The body (ciphertext minus tag) must be block-aligned.
237        if !ciphertext.len().is_multiple_of(BLOCK) {
238            return Err(Error::InvalidCiphertextSize {
239                expected: (ciphertext.len() / BLOCK + 1) * BLOCK,
240                actual: ciphertext.len(),
241            });
242        }
243
244        let body_len = ciphertext.len() - BLOCK;
245        let body = &ciphertext[..body_len];
246        let received_tag = &ciphertext[body_len..];
247        let m = body_len / BLOCK;
248
249        let mut key_staged = Zeroizing::new([0u8; 32]);
250        key_staged.copy_from_slice(key.as_bytes());
251        let mut nonce16 = Zeroizing::new([0u8; 16]);
252        nonce16.copy_from_slice(nonce.as_bytes());
253        let ad = associated_data.unwrap_or(&[]);
254
255        // Decrypt every block and accumulate the checksum (full work, no early exit).
256        let mut plain = Zeroizing::new(Vec::with_capacity(body_len));
257        let mut checksum = Zeroizing::new([0u8; BLOCK]);
258        let (body_blocks, _rem) = body.as_chunks::<BLOCK>();
259        for (i, chunk) in body_blocks.iter().enumerate() {
260            let tweak = Self::tweak(&nonce16, i as u64);
261            let mut block = [0u8; BLOCK];
262            block.copy_from_slice(chunk);
263            self.msg.decrypt_block(&key_staged, &tweak, &mut block)?;
264            for k in 0..BLOCK {
265                checksum[k] ^= block[k];
266            }
267            plain.extend_from_slice(&block);
268            block.zeroize();
269        }
270
271        let ad_auth = self.absorb_ad(&key_staged, ad)?;
272        let expected_tag =
273            self.compute_tag(&key_staged, &nonce16, &checksum, (m - 1) as u64, &ad_auth)?;
274
275        let tag_valid = lib_q_core::Utils::constant_time_compare(&*expected_tag, received_tag);
276
277        if !tag_valid {
278            return Ok(DecryptSemanticOutcome::AuthenticationFailed);
279        }
280
281        // Authenticated: strip 10* padding (the final block always carries the 0x80 marker).
282        let plaintext_len = match unpad_len(&plain) {
283            Some(len) => len,
284            // Authentic ciphertext we produced always has well-formed padding; treat a malformed
285            // (e.g. truncated/forged-yet-matching) layout as an authentication failure.
286            None => return Ok(DecryptSemanticOutcome::AuthenticationFailed),
287        };
288        let mut out = Vec::with_capacity(plaintext_len);
289        out.extend_from_slice(&plain[..plaintext_len]);
290        Ok(DecryptSemanticOutcome::Success(Zeroizing::new(out)))
291    }
292}
293
294/// Locate the `10*` padding marker; returns the unpadded length, or `None` if malformed.
295fn unpad_len(padded: &[u8]) -> Option<usize> {
296    let mut idx = padded.len();
297    while idx > 0 && padded[idx - 1] == 0 {
298        idx -= 1;
299    }
300    if idx == 0 || padded[idx - 1] != 0x80 {
301        return None;
302    }
303    Some(idx - 1)
304}
305
306impl Aead for SaturninQcb {
307    fn encrypt(
308        &self,
309        key: &AeadKey,
310        nonce: &Nonce,
311        plaintext: &[u8],
312        associated_data: Option<&[u8]>,
313    ) -> Result<Vec<u8>> {
314        Self::validate_lengths(key, nonce)?;
315
316        let mut key_staged = Zeroizing::new([0u8; 32]);
317        key_staged.copy_from_slice(key.as_bytes());
318        let mut nonce16 = Zeroizing::new([0u8; 16]);
319        nonce16.copy_from_slice(nonce.as_bytes());
320        let ad = associated_data.unwrap_or(&[]);
321
322        let padded = Self::pad(plaintext);
323        let m = padded.len() / BLOCK;
324
325        let mut output = Vec::with_capacity(padded.len() + BLOCK);
326        let mut checksum = Zeroizing::new([0u8; BLOCK]);
327        let (padded_blocks, _rem) = padded.as_chunks::<BLOCK>();
328        for (i, chunk) in padded_blocks.iter().enumerate() {
329            for k in 0..BLOCK {
330                checksum[k] ^= chunk[k];
331            }
332            let tweak = Self::tweak(&nonce16, i as u64);
333            let mut block = [0u8; BLOCK];
334            block.copy_from_slice(chunk);
335            self.msg.encrypt_block(&key_staged, &tweak, &mut block)?;
336            output.extend_from_slice(&block);
337            block.zeroize();
338        }
339
340        let ad_auth = self.absorb_ad(&key_staged, ad)?;
341        let tag = self.compute_tag(&key_staged, &nonce16, &checksum, (m - 1) as u64, &ad_auth)?;
342        output.extend_from_slice(&*tag);
343        Ok(output)
344    }
345
346    fn decrypt(
347        &self,
348        key: &AeadKey,
349        nonce: &Nonce,
350        ciphertext: &[u8],
351        associated_data: Option<&[u8]>,
352    ) -> Result<Vec<u8>> {
353        match self.decrypt_core(key, nonce, ciphertext, associated_data)? {
354            DecryptSemanticOutcome::Success(p) => Ok(Vec::clone(&*p)),
355            DecryptSemanticOutcome::AuthenticationFailed => Err(Error::VerificationFailed {
356                operation: "Saturnin-QCB tag verification".to_string(),
357            }),
358        }
359    }
360}
361
362impl AeadDecryptSemantic for SaturninQcb {
363    fn decrypt_semantic(
364        &self,
365        key: &AeadKey,
366        nonce: &Nonce,
367        ciphertext: &[u8],
368        associated_data: Option<&[u8]>,
369    ) -> Result<DecryptSemanticOutcome> {
370        self.decrypt_core(key, nonce, ciphertext, associated_data)
371    }
372}
373
374impl Default for SaturninQcb {
375    fn default() -> Self {
376        Self::new()
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use alloc::vec;
383
384    use super::*;
385
386    fn key() -> AeadKey {
387        AeadKey::new((0..32u8).collect::<Vec<_>>())
388    }
389
390    fn nonce() -> Nonce {
391        Nonce::new((0..16u8).collect::<Vec<_>>())
392    }
393
394    #[test]
395    fn constants() {
396        assert_eq!(SaturninQcb::key_size(), 32);
397        assert_eq!(SaturninQcb::nonce_size(), 16);
398        assert_eq!(SaturninQcb::tag_size(), 32);
399    }
400
401    #[test]
402    fn round_trip_various_lengths() -> Result<()> {
403        let aead = SaturninQcb::new();
404        for len in [0usize, 1, 15, 31, 32, 33, 64, 100, 256] {
405            let pt: Vec<u8> = (0..len).map(|i| i as u8).collect();
406            let ct = aead.encrypt(&key(), &nonce(), &pt, Some(b"hdr"))?;
407            // Always-pad: body is padded message (multiple of 32) plus a 32-byte tag.
408            let expected_body = (len / 32 + 1) * 32;
409            assert_eq!(ct.len(), expected_body + 32, "len={len}");
410            let dec = aead.decrypt(&key(), &nonce(), &ct, Some(b"hdr"))?;
411            assert_eq!(dec, pt, "len={len}");
412        }
413        Ok(())
414    }
415
416    #[test]
417    fn empty_message_and_ad() -> Result<()> {
418        let aead = SaturninQcb::new();
419        let ct = aead.encrypt(&key(), &nonce(), b"", None)?;
420        assert_eq!(ct.len(), 64); // one padding block + tag
421        assert_eq!(aead.decrypt(&key(), &nonce(), &ct, None)?, b"");
422        Ok(())
423    }
424
425    #[test]
426    fn tampered_tag_fails() -> Result<()> {
427        let aead = SaturninQcb::new();
428        let ct = aead.encrypt(&key(), &nonce(), b"hello world", Some(b"ad"))?;
429        let mut bad = ct.clone();
430        *bad.last_mut().unwrap() ^= 0x01;
431        assert!(matches!(
432            aead.decrypt(&key(), &nonce(), &bad, Some(b"ad")),
433            Err(Error::VerificationFailed { .. })
434        ));
435        assert_eq!(
436            aead.decrypt_semantic(&key(), &nonce(), &bad, Some(b"ad"))?,
437            DecryptSemanticOutcome::AuthenticationFailed
438        );
439        Ok(())
440    }
441
442    #[test]
443    fn tampered_body_fails() -> Result<()> {
444        let aead = SaturninQcb::new();
445        let ct = aead.encrypt(&key(), &nonce(), b"hello world", None)?;
446        let mut bad = ct.clone();
447        bad[0] ^= 0x80;
448        assert!(aead.decrypt(&key(), &nonce(), &bad, None).is_err());
449        Ok(())
450    }
451
452    #[test]
453    fn ad_is_authenticated() -> Result<()> {
454        let aead = SaturninQcb::new();
455        let ct = aead.encrypt(&key(), &nonce(), b"msg", Some(b"header-A"))?;
456        // Wrong AD must fail.
457        assert!(
458            aead.decrypt(&key(), &nonce(), &ct, Some(b"header-B"))
459                .is_err()
460        );
461        // Missing AD must fail.
462        assert!(aead.decrypt(&key(), &nonce(), &ct, None).is_err());
463        Ok(())
464    }
465
466    #[test]
467    fn nonce_binding() -> Result<()> {
468        let aead = SaturninQcb::new();
469        let ct = aead.encrypt(&key(), &nonce(), b"msg", None)?;
470        let other = Nonce::new(vec![0xFFu8; 16]);
471        assert!(aead.decrypt(&key(), &other, &ct, None).is_err());
472        Ok(())
473    }
474
475    fn from_hex(s: &str) -> Vec<u8> {
476        (0..s.len())
477            .step_by(2)
478            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
479            .collect()
480    }
481
482    /// Pinned self-consistency vectors for this instantiation (key = 00..1f, nonce = 00..0f).
483    ///
484    /// These are **derived** from the construction in this module (Saturnin TBC + the documented
485    /// QCB instantiation), not official designer KATs — see the module-level instantiation note.
486    /// They lock the byte-level behavior so any accidental change to padding, tweak encoding,
487    /// domains, or AD folding is caught.
488    #[test]
489    fn pinned_kat_vectors() -> Result<()> {
490        let aead = SaturninQcb::new();
491        let cases: &[(&str, &str, &str)] = &[
492            (
493                "",
494                "",
495                "bd0abd723c4149718b458ac68f3a0a1e9e84e1e33c830a5894e48e6591a43a33718cd938614ad4c64e971ae1df9a657e290f3d862e5429088a7066642b07b29a",
496            ),
497            (
498                "",
499                "6173736f636961746564",
500                "bd0abd723c4149718b458ac68f3a0a1e9e84e1e33c830a5894e48e6591a43a33a40976d18060823323aa163b2ab7bf306cbbaff29aa86a0a31b6ba5d826c9dca",
501            ),
502            (
503                "616263",
504                "",
505                "52d715efbd6e430e4be8c2b682527e349a26fa62c69de5da978299c475f41c6df4620482177e4946c61ae01ff424a467ab76d31a63e75d045d3daaad64909edf",
506            ),
507            (
508                "0000000000000000000000000000000000000000000000000000000000000000",
509                "686472",
510                "16e51991ae3cb7cb92f3847c326188cb007267ece8153d03aeb98d4f161c84a730c8e81de51c9573d449dada58a211595a47a6f72f9776fd21347d45696e7f6743f9d93a4663c3f210ee1e99333007d9ceebd632ac2d5dacb2c9251499caddf2",
511            ),
512            (
513                "54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f672121",
514                "61642d31",
515                "fe81caa8f1ee16e54fd7b3df31247e7ccd4295382cff4f9f7efefb5e970c6880c10b857de55d457eff7ea96f9e4c0dc2f30180b2c037d52565e8895d48ae701ebd4ceb39dbeece08aafae995d41998ea656e1cedb4326176717a42d8b92693e4",
516            ),
517        ];
518        for (pt_hex, ad_hex, ct_hex) in cases {
519            let pt = from_hex(pt_hex);
520            let ad = from_hex(ad_hex);
521            let ad_opt = if ad.is_empty() {
522                None
523            } else {
524                Some(ad.as_slice())
525            };
526            let ct = aead.encrypt(&key(), &nonce(), &pt, ad_opt)?;
527            assert_eq!(
528                ct,
529                from_hex(ct_hex),
530                "encrypt mismatch for pt={pt_hex} ad={ad_hex}"
531            );
532            let dec = aead.decrypt(&key(), &nonce(), &ct, ad_opt)?;
533            assert_eq!(dec, pt, "decrypt mismatch for pt={pt_hex} ad={ad_hex}");
534        }
535        Ok(())
536    }
537
538    #[test]
539    fn parallel_block_independence() -> Result<()> {
540        // QCB is rate-one and embarrassingly parallel: each ciphertext block depends only on its
541        // own plaintext block, the key, the nonce, and its index. Changing one plaintext block
542        // must change only that ciphertext block (the tag aside).
543        let aead = SaturninQcb::new();
544        let mut a = vec![0u8; 96]; // 3 blocks
545        let mut b = a.clone();
546        b[40] ^= 0xFF; // flip a byte in block 1
547        let ca = aead.encrypt(&key(), &nonce(), &a, None)?;
548        let cb = aead.encrypt(&key(), &nonce(), &b, None)?;
549        // Block 0 (bytes 0..32) identical; block 1 (32..64) differs.
550        assert_eq!(ca[0..32], cb[0..32]);
551        assert_ne!(ca[32..64], cb[32..64]);
552        assert_eq!(ca[64..96], cb[64..96]); // block 2 unchanged
553        a.zeroize();
554        b.zeroize();
555        Ok(())
556    }
557
558    #[test]
559    fn unpad_len_handles_valid_and_malformed() {
560        // Valid 10* padding: marker then zeros.
561        assert_eq!(unpad_len(&[1, 2, 3, 0x80, 0, 0]), Some(3));
562        assert_eq!(unpad_len(&[0x80]), Some(0));
563        // Malformed: no marker (all zeros, or trailing non-zero that isn't 0x80).
564        assert_eq!(unpad_len(&[0, 0, 0]), None);
565        assert_eq!(unpad_len(&[]), None);
566        assert_eq!(unpad_len(&[1, 2, 3]), None);
567    }
568
569    #[test]
570    fn default_matches_new() -> Result<()> {
571        let a = SaturninQcb::default();
572        let b = SaturninQcb::new();
573        let pt = b"compare";
574        assert_eq!(
575            a.encrypt(&key(), &nonce(), pt, None)?,
576            b.encrypt(&key(), &nonce(), pt, None)?
577        );
578        Ok(())
579    }
580
581    #[test]
582    fn wrong_size_inputs_rejected() {
583        let aead = SaturninQcb::new();
584        assert!(
585            aead.encrypt(&AeadKey::new(vec![0u8; 16]), &nonce(), b"x", None)
586                .is_err()
587        );
588        assert!(
589            aead.encrypt(&key(), &Nonce::new(vec![0u8; 8]), b"x", None)
590                .is_err()
591        );
592        // Ciphertext shorter than one block + tag.
593        assert!(aead.decrypt(&key(), &nonce(), &[0u8; 40], None).is_err());
594        // Non block-aligned body.
595        assert!(aead.decrypt(&key(), &nonce(), &[0u8; 65], None).is_err());
596    }
597}