Skip to main content

rns_crypto/
token.rs

1use alloc::vec::Vec;
2use core::fmt;
3
4use crate::aes128::Aes128;
5use crate::aes256::Aes256;
6use aes::cipher::{array::Array, BlockModeDecrypt};
7
8use crate::hmac::HmacSha256;
9use crate::pkcs7;
10use crate::sha256::Sha256;
11use crate::Rng;
12
13pub const TOKEN_OVERHEAD: usize = 48; // 16 IV + 32 HMAC
14
15#[derive(Debug, PartialEq)]
16pub enum TokenError {
17    InvalidKeyLength,
18    InvalidToken,
19    HmacMismatch,
20    DecryptionFailed,
21}
22
23impl fmt::Display for TokenError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            TokenError::InvalidKeyLength => write!(f, "Token key must be 32 or 64 bytes"),
27            TokenError::InvalidToken => write!(f, "Token too short"),
28            TokenError::HmacMismatch => write!(f, "Token HMAC was invalid"),
29            TokenError::DecryptionFailed => write!(f, "Could not decrypt token"),
30        }
31    }
32}
33
34#[derive(Clone)]
35enum AesMode {
36    Aes128(Aes128),
37    Aes256(Aes256),
38}
39
40/// CBC chaining state carried across the pieces of one ciphertext. It lives
41/// on the stack for one call, so the variants' size difference is harmless.
42#[allow(clippy::large_enum_variant)]
43enum CbcDecryptor {
44    Aes128(cbc::Decryptor<aes::Aes128>),
45    Aes256(cbc::Decryptor<aes::Aes256>),
46}
47
48impl CbcDecryptor {
49    /// Decrypt whole blocks in place, continuing the chain.
50    fn decrypt_blocks(&mut self, buf: &mut [u8]) {
51        let (blocks, rest) = Array::slice_as_chunks_mut(buf);
52        debug_assert!(rest.is_empty());
53        match self {
54            CbcDecryptor::Aes128(dec) => dec.decrypt_blocks(blocks),
55            CbcDecryptor::Aes256(dec) => dec.decrypt_blocks(blocks),
56        }
57    }
58}
59
60pub struct Token {
61    /// HMAC keyed once with the signing key: each tag clones it instead of
62    /// hashing the key's inner and outer pads again.
63    mac: HmacSha256,
64    mode: AesMode,
65}
66
67impl Token {
68    pub fn new(key: &[u8]) -> Result<Self, TokenError> {
69        match key.len() {
70            32 => {
71                let signing_key = &key[..16];
72                let encryption_key: [u8; 16] = key[16..32].try_into().unwrap();
73                Ok(Token {
74                    mac: HmacSha256::new(signing_key),
75                    mode: AesMode::Aes128(Aes128::new(&encryption_key)),
76                })
77            }
78            64 => {
79                let signing_key = &key[..32];
80                let encryption_key: [u8; 32] = key[32..64].try_into().unwrap();
81                Ok(Token {
82                    mac: HmacSha256::new(signing_key),
83                    mode: AesMode::Aes256(Aes256::new(&encryption_key)),
84                })
85            }
86            _ => Err(TokenError::InvalidKeyLength),
87        }
88    }
89
90    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Vec<u8> {
91        let mut iv = [0u8; 16];
92        rng.fill_bytes(&mut iv);
93        self.encrypt_with_iv(plaintext, &iv)
94    }
95
96    /// Length of the token [`Token::encrypt`] produces for a plaintext of
97    /// `plaintext_len` bytes.
98    pub fn token_len(plaintext_len: usize) -> usize {
99        16 + plaintext_len + (pkcs7::BLOCK_SIZE - plaintext_len % pkcs7::BLOCK_SIZE) + 32
100    }
101
102    /// [`Token::encrypt`] into `out`, which must be exactly
103    /// [`Token::token_len`]`(plaintext.len())` bytes long.
104    pub fn encrypt_into(&self, plaintext: &[u8], rng: &mut dyn Rng, out: &mut [u8]) {
105        let mut iv = [0u8; 16];
106        rng.fill_bytes(&mut iv);
107        self.encrypt_with_iv_into(plaintext, &iv, out)
108    }
109
110    /// [`Token::encrypt_with_iv`] into `out`, which must be exactly
111    /// [`Token::token_len`]`(plaintext.len())` bytes long.
112    pub fn encrypt_with_iv_into(&self, plaintext: &[u8], iv: &[u8; 16], out: &mut [u8]) {
113        assert_eq!(
114            out.len(),
115            Self::token_len(plaintext.len()),
116            "token buffer length"
117        );
118        let (body, tag) = out.split_at_mut(out.len() - 32);
119        let padding = body.len() - 16 - plaintext.len();
120        body[..16].copy_from_slice(iv);
121        body[16..16 + plaintext.len()].copy_from_slice(plaintext);
122        body[16 + plaintext.len()..].fill(padding as u8);
123        match &self.mode {
124            AesMode::Aes128(aes) => aes.encrypt_cbc_in_place(&mut body[16..], iv),
125            AesMode::Aes256(aes) => aes.encrypt_cbc_in_place(&mut body[16..], iv),
126        }
127        .expect("PKCS7 padding always produces block-aligned plaintext");
128        tag.copy_from_slice(&self.tag(body));
129    }
130
131    pub fn encrypt_with_iv(&self, plaintext: &[u8], iv: &[u8; 16]) -> Vec<u8> {
132        // Build the whole token in one buffer: the plaintext is copied once,
133        // padded and encrypted in place, then signed and the tag appended.
134        let padding = pkcs7::BLOCK_SIZE - plaintext.len() % pkcs7::BLOCK_SIZE;
135        let mut token = Vec::with_capacity(16 + plaintext.len() + padding + 32);
136        token.extend_from_slice(iv);
137        token.extend_from_slice(plaintext);
138        token.resize(token.len() + padding, padding as u8);
139        match &self.mode {
140            AesMode::Aes128(aes) => aes.encrypt_cbc_in_place(&mut token[16..], iv),
141            AesMode::Aes256(aes) => aes.encrypt_cbc_in_place(&mut token[16..], iv),
142        }
143        .expect("PKCS7 padding always produces block-aligned plaintext");
144
145        let mac = self.tag(&token);
146        token.extend_from_slice(&mac);
147        token
148    }
149
150    /// [`Token::encrypt`] with the tag left to the caller: the token's last
151    /// 32 bytes are zero, and the returned MAC, fed every byte before them,
152    /// finalizes to the tag. The caller can hash those bytes together with
153    /// other data before writing it.
154    pub fn encrypt_untagged(&self, plaintext: &[u8], rng: &mut dyn Rng) -> (Vec<u8>, HmacSha256) {
155        let mut iv = [0u8; 16];
156        rng.fill_bytes(&mut iv);
157        let mut token = alloc::vec![0u8; Self::token_len(plaintext.len())];
158        let signed = token.len() - 32;
159        let body = &mut token[..signed];
160        let padding = body.len() - 16 - plaintext.len();
161        body[..16].copy_from_slice(&iv);
162        body[16..16 + plaintext.len()].copy_from_slice(plaintext);
163        body[16 + plaintext.len()..].fill(padding as u8);
164        match &self.mode {
165            AesMode::Aes128(aes) => aes.encrypt_cbc_in_place(&mut body[16..], &iv),
166            AesMode::Aes256(aes) => aes.encrypt_cbc_in_place(&mut body[16..], &iv),
167        }
168        .expect("PKCS7 padding always produces block-aligned plaintext");
169        (token, self.mac.clone())
170    }
171
172    /// HMAC-SHA256 of `data` under the signing key.
173    fn tag(&self, data: &[u8]) -> [u8; 32] {
174        let mut mac = self.mac.clone();
175        mac.update(data);
176        mac.finalize()
177    }
178
179    pub fn verify_hmac(&self, token: &[u8]) -> Result<bool, TokenError> {
180        if token.len() <= 32 {
181            return Err(TokenError::InvalidToken);
182        }
183        let received_hmac = &token[token.len() - 32..];
184        let mut mac = self.mac.clone();
185        mac.update(&token[..token.len() - 32]);
186        Ok(mac.verify(received_hmac))
187    }
188
189    /// [`Token::decrypt`] into `out`, replacing its contents and reusing
190    /// its capacity. On error `out` holds no plaintext.
191    pub fn decrypt_into(&self, token: &[u8], out: &mut Vec<u8>) -> Result<(), TokenError> {
192        out.clear();
193        if token.len() <= TOKEN_OVERHEAD {
194            return Err(TokenError::InvalidToken);
195        }
196        if !self
197            .verify_hmac(token)
198            .map_err(|_| TokenError::InvalidToken)?
199        {
200            return Err(TokenError::HmacMismatch);
201        }
202        let iv: [u8; 16] = token[..16].try_into().unwrap();
203        let ciphertext = &token[16..token.len() - 32];
204        if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(16) {
205            return Err(TokenError::DecryptionFailed);
206        }
207        out.extend_from_slice(ciphertext);
208        let decrypted = match &self.mode {
209            AesMode::Aes128(aes) => aes.decrypt_cbc_in_place(out, &iv),
210            AesMode::Aes256(aes) => aes.decrypt_cbc_in_place(out, &iv),
211        };
212        let unpadded_len = decrypted
213            .ok()
214            .and_then(|()| pkcs7::unpad(out, 16).ok().map(<[u8]>::len));
215        match unpadded_len {
216            Some(len) => {
217                out.truncate(len);
218                Ok(())
219            }
220            None => {
221                out.clear();
222                Err(TokenError::DecryptionFailed)
223            }
224        }
225    }
226
227    pub fn decrypt(&self, token: &[u8]) -> Result<Vec<u8>, TokenError> {
228        if token.len() <= TOKEN_OVERHEAD {
229            return Err(TokenError::InvalidToken);
230        }
231
232        if !self
233            .verify_hmac(token)
234            .map_err(|_| TokenError::InvalidToken)?
235        {
236            return Err(TokenError::HmacMismatch);
237        }
238
239        let iv: [u8; 16] = token[..16].try_into().unwrap();
240        let ciphertext = &token[16..token.len() - 32];
241
242        // CBC ciphertext must be a non-empty whole number of 16-byte blocks.
243        if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(16) {
244            return Err(TokenError::DecryptionFailed);
245        }
246
247        let mut decrypted = match &self.mode {
248            AesMode::Aes128(aes) => aes.decrypt_cbc(ciphertext, &iv),
249            AesMode::Aes256(aes) => aes.decrypt_cbc(ciphertext, &iv),
250        }
251        .map_err(|_| TokenError::DecryptionFailed)?;
252
253        let unpadded_len = pkcs7::unpad(&decrypted, 16)
254            .map_err(|_| TokenError::DecryptionFailed)?
255            .len();
256        decrypted.truncate(unpadded_len);
257        Ok(decrypted)
258    }
259
260    /// Decrypt a token that arrives as consecutive chunks, such as the parts
261    /// of a resource, without joining them first. Each chunk is authenticated
262    /// and decrypted while it is still in cache; nothing is returned unless
263    /// the tag matches. The result is [`Token::decrypt`] of the joined bytes
264    /// with its first `skip` bytes (at most one block) dropped, and the errors
265    /// are the same, except that a plaintext shorter than `skip` is
266    /// [`TokenError::DecryptionFailed`].
267    pub fn decrypt_chunks(&self, chunks: &[&[u8]], skip: usize) -> Result<Vec<u8>, TokenError> {
268        let total: usize = chunks.iter().map(|chunk| chunk.len()).sum();
269        let mut stream = self.decrypt_stream(skip);
270        stream.reserve(total.saturating_sub(TOKEN_OVERHEAD));
271        for chunk in chunks {
272            stream.update(chunk);
273        }
274        stream.finish()
275    }
276
277    /// Start a [`Token::decrypt_chunks`] whose chunks are fed one at a time,
278    /// as they become available.
279    pub fn decrypt_stream(&self, skip: usize) -> TokenDecryptStream {
280        assert!(skip <= 16, "decrypt_chunks skips at most one block");
281        TokenDecryptStream {
282            mac: self.mac.clone(),
283            mode: self.mode.clone(),
284            decryptor: None,
285            skip,
286            signed: 0,
287            iv: [0; 16],
288            tail: [0; 32],
289            tail_len: 0,
290            block: [0; 16],
291            block_len: 0,
292            first_block: true,
293            last_byte: 0,
294            plaintext: Vec::new(),
295            plain_hash: None,
296        }
297    }
298}
299
300/// An incremental [`Token::decrypt_chunks`]: the token's bytes are fed in
301/// order with [`TokenDecryptStream::update`], and [`TokenDecryptStream::finish`]
302/// checks the tag and returns the plaintext. The last 32 bytes seen are held
303/// back as the candidate tag; everything before them is authenticated and
304/// decrypted as it arrives. Decrypted bytes are not authentic until `finish`
305/// accepts the tag.
306pub struct TokenDecryptStream {
307    mac: HmacSha256,
308    mode: AesMode,
309    decryptor: Option<CbcDecryptor>,
310    skip: usize,
311    /// Bytes released to the MAC: the IV and the ciphertext so far.
312    signed: usize,
313    iv: [u8; 16],
314    /// The last bytes seen, up to 32: the tag if the token ends here.
315    tail: [u8; 32],
316    tail_len: usize,
317    /// A ciphertext block that straddles updates, and always the first
318    /// block, which carries the skipped bytes.
319    block: [u8; 16],
320    block_len: usize,
321    first_block: bool,
322    last_byte: u8,
323    plaintext: Vec<u8>,
324    /// SHA-256 of the settled plaintext, and how many bytes it covers.
325    plain_hash: Option<(Sha256, usize)>,
326}
327
328impl TokenDecryptStream {
329    /// Also hash the plaintext, together with the tag's MAC, as it settles;
330    /// [`Self::finish_hashed`] returns the hash. Call before any update.
331    pub fn hash_plaintext(&mut self) {
332        debug_assert!(self.signed == 0 && self.tail_len == 0);
333        self.plain_hash = Some((Sha256::new(), 0));
334    }
335
336    /// Reserve room for `additional` more plaintext bytes.
337    pub fn reserve(&mut self, additional: usize) {
338        self.plaintext.reserve(additional);
339    }
340
341    /// The plaintext decrypted so far, without the skipped bytes. Its last
342    /// block may still hold the padding, and none of it is authentic until
343    /// [`Self::finish`] succeeds.
344    pub fn plaintext(&self) -> &[u8] {
345        &self.plaintext
346    }
347
348    /// Feed the token's next bytes.
349    pub fn update(&mut self, data: &[u8]) {
350        let total = self.tail_len + data.len();
351        if total <= 32 {
352            self.tail[self.tail_len..total].copy_from_slice(data);
353            self.tail_len = total;
354            return;
355        }
356        // Everything but the last 32 bytes is signed data.
357        let release = total - 32;
358        let from_tail = release.min(self.tail_len);
359        if from_tail > 0 {
360            let tail = self.tail;
361            self.absorb_signed(&tail[..from_tail]);
362        }
363        let from_data = release - from_tail;
364        self.absorb_signed(&data[..from_data]);
365        let kept = self.tail_len - from_tail;
366        self.tail.copy_within(from_tail..self.tail_len, 0);
367        self.tail[kept..].copy_from_slice(&data[from_data..]);
368        self.tail_len = 32;
369    }
370
371    fn absorb_signed(&mut self, data: &[u8]) {
372        if data.is_empty() {
373            return;
374        }
375        self.decrypt_signed(data);
376        match self.plain_hash.as_mut() {
377            Some((hash, hashed)) => {
378                // The last block may still lose its padding.
379                let settled = self.plaintext.len().saturating_sub(16).max(*hashed);
380                self.mac
381                    .update_paired(data, hash, &self.plaintext[*hashed..settled]);
382                *hashed = settled;
383            }
384            None => self.mac.update(data),
385        }
386    }
387
388    fn decrypt_signed(&mut self, mut data: &[u8]) {
389        if self.signed < 16 {
390            let n = (16 - self.signed).min(data.len());
391            self.iv[self.signed..self.signed + n].copy_from_slice(&data[..n]);
392            self.signed += n;
393            data = &data[n..];
394            if data.is_empty() {
395                return;
396            }
397        }
398        self.signed += data.len();
399        let (mode, iv) = (&self.mode, &self.iv);
400        let decryptor = self.decryptor.get_or_insert_with(|| match mode {
401            AesMode::Aes128(aes) => CbcDecryptor::Aes128(aes.cbc_decryptor(iv)),
402            AesMode::Aes256(aes) => CbcDecryptor::Aes256(aes.cbc_decryptor(iv)),
403        });
404        while !data.is_empty() && (self.block_len > 0 || self.first_block) {
405            let n = (16 - self.block_len).min(data.len());
406            self.block[self.block_len..self.block_len + n].copy_from_slice(&data[..n]);
407            self.block_len += n;
408            data = &data[n..];
409            if self.block_len == 16 {
410                decryptor.decrypt_blocks(&mut self.block);
411                let from = if self.first_block { self.skip } else { 0 };
412                self.plaintext.extend_from_slice(&self.block[from..]);
413                self.last_byte = self.block[15];
414                self.first_block = false;
415                self.block_len = 0;
416            }
417        }
418        if !data.is_empty() {
419            let whole = data.len() - data.len() % 16;
420            if whole > 0 {
421                let at = self.plaintext.len();
422                self.plaintext.extend_from_slice(&data[..whole]);
423                decryptor.decrypt_blocks(&mut self.plaintext[at..]);
424                self.last_byte = self.plaintext[self.plaintext.len() - 1];
425            }
426            let rest = &data[whole..];
427            self.block[..rest.len()].copy_from_slice(rest);
428            self.block_len = rest.len();
429        }
430    }
431
432    /// [`Self::finish`], with the SHA-256 state over the whole plaintext
433    /// when [`Self::hash_plaintext`] was called.
434    pub fn finish_hashed(mut self) -> Result<(Vec<u8>, Option<Sha256>), TokenError> {
435        let plain_hash = self.plain_hash.take();
436        let plaintext = self.finish()?;
437        Ok(match plain_hash {
438            Some((mut hash, hashed)) => {
439                hash.update(&plaintext[hashed..]);
440                (plaintext, Some(hash))
441            }
442            None => (plaintext, None),
443        })
444    }
445
446    /// Check the tag and return the plaintext, as [`Token::decrypt_chunks`]
447    /// of every byte fed.
448    pub fn finish(self) -> Result<Vec<u8>, TokenError> {
449        if self.signed + self.tail_len <= TOKEN_OVERHEAD {
450            return Err(TokenError::InvalidToken);
451        }
452        if !self.mac.verify(&self.tail) {
453            return Err(TokenError::HmacMismatch);
454        }
455        // A misaligned ciphertext is rejected after the tag check, as in
456        // `decrypt`.
457        if self.first_block || self.block_len != 0 {
458            return Err(TokenError::DecryptionFailed);
459        }
460        // The same padding check as `pkcs7::unpad`.
461        let mut plaintext = self.plaintext;
462        let padding = usize::from(self.last_byte);
463        if padding > 16 || padding > plaintext.len() {
464            return Err(TokenError::DecryptionFailed);
465        }
466        plaintext.truncate(plaintext.len() - padding);
467        Ok(plaintext)
468    }
469}
470
471#[cfg(test)]
472mod tests {
473    use super::*;
474    use crate::hmac::hmac_sha256;
475
476    #[test]
477    fn decrypting_into_a_reused_buffer_matches_decrypt() {
478        for key in [[7u8; 32].as_slice(), [9u8; 64].as_slice()] {
479            let token = Token::new(key).unwrap();
480            let other = Token::new(&[1u8; 32]).unwrap();
481            let mut out = vec![0x55; 3];
482            for len in [0usize, 1, 15, 16, 17, 100, 384] {
483                let plaintext: Vec<u8> = (0..len).map(|i| i as u8).collect();
484                let good = token.encrypt_with_iv(&plaintext, &[len as u8; 16]);
485                let mut tampered = good.clone();
486                tampered[20] ^= 1;
487                let foreign = other.encrypt_with_iv(&plaintext, &[0; 16]);
488                for input in [&good[..], &tampered[..], &foreign[..], &good[..40], &[][..]] {
489                    let expected = token.decrypt(input);
490                    let got = token.decrypt_into(input, &mut out);
491                    assert_eq!(
492                        format!("{:?}", got.map(|()| out.clone())),
493                        format!("{expected:?}")
494                    );
495                    if expected.is_err() {
496                        assert!(out.is_empty());
497                    }
498                }
499            }
500        }
501    }
502
503    #[test]
504    fn encrypting_in_place_matches_encrypting_into_a_new_buffer() {
505        for key in [[7u8; 32].as_slice(), [9u8; 64].as_slice()] {
506            let token = Token::new(key).unwrap();
507            for len in [0usize, 1, 15, 16, 17, 31, 32, 33, 100, 383, 384] {
508                let plaintext: Vec<u8> = (0..len).map(|i| i as u8).collect();
509                let iv = [len as u8; 16];
510                let mut out = vec![0xaa; Token::token_len(len)];
511                token.encrypt_with_iv_into(&plaintext, &iv, &mut out);
512                assert_eq!(out, token.encrypt_with_iv(&plaintext, &iv));
513                assert_eq!(token.decrypt(&out).unwrap(), plaintext);
514            }
515        }
516    }
517    use crate::FixedRng;
518
519    #[test]
520    fn test_token_new_32byte_key() {
521        let key = [0u8; 32];
522        assert!(Token::new(&key).is_ok());
523    }
524
525    #[test]
526    fn test_token_new_64byte_key() {
527        let key = [0u8; 64];
528        assert!(Token::new(&key).is_ok());
529    }
530
531    #[test]
532    fn test_token_new_48byte_key() {
533        let key = [0u8; 48];
534        assert!(matches!(
535            Token::new(&key),
536            Err(TokenError::InvalidKeyLength)
537        ));
538    }
539
540    #[test]
541    fn test_token_roundtrip_32() {
542        let key = [0x42u8; 32];
543        let token = Token::new(&key).unwrap();
544        let mut rng = FixedRng::new(&[0xAA; 16]);
545        let plaintext = b"Hello, Reticulum!";
546        let encrypted = token.encrypt(plaintext, &mut rng);
547        let decrypted = token.decrypt(&encrypted).unwrap();
548        assert_eq!(decrypted, plaintext);
549    }
550
551    #[test]
552    fn test_token_roundtrip_64() {
553        let key = [0x42u8; 64];
554        let token = Token::new(&key).unwrap();
555        let mut rng = FixedRng::new(&[0xBB; 16]);
556        let plaintext = b"Hello, Reticulum!";
557        let encrypted = token.encrypt(plaintext, &mut rng);
558        let decrypted = token.decrypt(&encrypted).unwrap();
559        assert_eq!(decrypted, plaintext);
560    }
561
562    #[test]
563    fn test_token_hmac_reject_tampered() {
564        let key = [0x42u8; 64];
565        let token = Token::new(&key).unwrap();
566        let mut rng = FixedRng::new(&[0xCC; 16]);
567        let encrypted = token.encrypt(b"test", &mut rng);
568        let mut tampered = encrypted.clone();
569        tampered[20] ^= 0xFF; // flip a bit in ciphertext
570        assert!(token.decrypt(&tampered).is_err());
571    }
572
573    #[test]
574    fn token_hmac_rejects_mismatches_across_the_entire_tag() {
575        let key = [0x42u8; 64];
576        let token = Token::new(&key).unwrap();
577        let mut rng = FixedRng::new(&[0xCC; 16]);
578        let encrypted = token.encrypt(b"timing regression", &mut rng);
579
580        assert_eq!(token.verify_hmac(&encrypted), Ok(true));
581        for tag_index in 0..32 {
582            let mut corrupted = encrypted.clone();
583            let index = corrupted.len() - 32 + tag_index;
584            corrupted[index] ^= 0x01;
585            assert_eq!(
586                token.verify_hmac(&corrupted),
587                Ok(false),
588                "tag mismatch at byte {tag_index} must fail"
589            );
590        }
591    }
592
593    #[test]
594    fn test_token_decrypt_truncated() {
595        let key = [0x42u8; 64];
596        let token = Token::new(&key).unwrap();
597        assert!(matches!(
598            token.decrypt(&[0u8; 10]),
599            Err(TokenError::InvalidToken)
600        ));
601    }
602
603    fn sign_token_parts(key: &[u8], iv_and_ciphertext: &[u8]) -> Vec<u8> {
604        let signing_key = &key[..key.len() / 2];
605        let mut token = iv_and_ciphertext.to_vec();
606        token.extend_from_slice(&hmac_sha256(signing_key, iv_and_ciphertext));
607        token
608    }
609
610    #[test]
611    fn test_token_decrypt_rejects_misaligned_ciphertext_with_valid_hmac() {
612        for key in [&[0x42u8; 32][..], &[0x42u8; 64][..]] {
613            let token = Token::new(key).unwrap();
614            for ciphertext_len in [1usize, 15, 17, 31, 33] {
615                let mut parts = alloc::vec![0xAAu8; 16];
616                parts.extend(core::iter::repeat_n(0x55u8, ciphertext_len));
617                let signed = sign_token_parts(key, &parts);
618                assert_eq!(token.verify_hmac(&signed), Ok(true));
619                assert_eq!(
620                    token.decrypt(&signed),
621                    Err(TokenError::DecryptionFailed),
622                    "ciphertext length {ciphertext_len} must be rejected"
623                );
624            }
625        }
626    }
627
628    #[test]
629    fn test_token_well_formed_roundtrip_after_alignment_check() {
630        for key in [&[0x24u8; 32][..], &[0x24u8; 64][..]] {
631            let token = Token::new(key).unwrap();
632            for plaintext_len in [0usize, 1, 15, 16, 17, 32, 100] {
633                let plaintext = alloc::vec![0x5Au8; plaintext_len];
634                let encrypted = token.encrypt_with_iv(&plaintext, &[0x11; 16]);
635                assert_eq!((encrypted.len() - TOKEN_OVERHEAD) % 16, 0);
636                assert_eq!(token.decrypt(&encrypted).unwrap(), plaintext);
637            }
638        }
639    }
640
641    #[test]
642    fn test_token_overhead() {
643        assert_eq!(TOKEN_OVERHEAD, 48);
644    }
645
646    /// Split `bytes` at the given offsets (clamped, in order).
647    fn split_at_offsets<'a>(bytes: &'a [u8], offsets: &[usize]) -> Vec<&'a [u8]> {
648        let mut chunks = Vec::new();
649        let mut from = 0;
650        for &offset in offsets {
651            let to = offset.clamp(from, bytes.len());
652            chunks.push(&bytes[from..to]);
653            from = to;
654        }
655        chunks.push(&bytes[from..]);
656        chunks
657    }
658
659    fn chunked_as_joined(token: &Token, bytes: &[u8], chunks: &[&[u8]], skip: usize) {
660        let joined = token.decrypt(bytes);
661        let expected = match joined {
662            Ok(plaintext) if plaintext.len() >= skip => Ok(plaintext[skip..].to_vec()),
663            Ok(_) => Err(TokenError::DecryptionFailed),
664            Err(error) => Err(error),
665        };
666        assert_eq!(
667            token.decrypt_chunks(chunks, skip),
668            expected,
669            "{chunks:?} skip {skip}"
670        );
671    }
672
673    #[test]
674    fn chunked_decryption_matches_joined_decryption() {
675        let mut rng = FixedRng::new(&[0x5D; 64]);
676        for key in [[0x21u8; 32].to_vec(), [0x42u8; 64].to_vec()] {
677            let token = Token::new(&key).unwrap();
678            for len in [0usize, 1, 3, 4, 5, 12, 15, 16, 17, 33, 100, 1000] {
679                let plaintext: Vec<u8> = (0..len).map(|i| (i * 7) as u8).collect();
680                let bytes = token.encrypt(&plaintext, &mut rng);
681                let n = bytes.len();
682                let splits: Vec<Vec<usize>> = alloc::vec![
683                    alloc::vec![],
684                    alloc::vec![1],
685                    alloc::vec![8, 16, 17],
686                    alloc::vec![16, 20, 32, 47],
687                    alloc::vec![n - 33, n - 32, n - 31],
688                    alloc::vec![n - 40, n - 10],
689                    (1..n).collect(),
690                    (0..n).step_by(13).collect(),
691                    alloc::vec![0, 0, 5, 5, n],
692                ];
693                for offsets in &splits {
694                    let chunks = split_at_offsets(&bytes, offsets);
695                    for skip in [0, 1, 4, 16] {
696                        chunked_as_joined(&token, &bytes, &chunks, skip);
697                    }
698                }
699                // Tampering anywhere is caught.
700                for at in 0..n {
701                    let mut tampered = bytes.clone();
702                    tampered[at] ^= 0x01;
703                    let chunks = split_at_offsets(&tampered, &[at / 2, at, at + 1]);
704                    assert_eq!(
705                        token.decrypt_chunks(&chunks, 4),
706                        Err(TokenError::HmacMismatch)
707                    );
708                }
709            }
710            // Misaligned ciphertext under a valid tag, and short tokens.
711            for ciphertext_len in [1usize, 15, 17, 31, 33] {
712                let mut parts = alloc::vec![0xAAu8; 16];
713                parts.extend(core::iter::repeat_n(0x55u8, ciphertext_len));
714                let signed = sign_token_parts(&key, &parts);
715                let chunks = split_at_offsets(&signed, &[7, 20, 40]);
716                chunked_as_joined(&token, &signed, &chunks, 4);
717            }
718            // Signed garbage decrypts to arbitrary padding bytes.
719            for fill in 0u8..64 {
720                for ciphertext_len in [16usize, 32] {
721                    let parts: Vec<u8> = (0..16 + ciphertext_len)
722                        .map(|i| (i as u8).wrapping_mul(fill).wrapping_add(fill))
723                        .collect();
724                    let signed = sign_token_parts(&key, &parts);
725                    for offsets in [alloc::vec![], alloc::vec![3, 19, 30]] {
726                        let chunks = split_at_offsets(&signed, &offsets);
727                        for skip in [0, 4, 16] {
728                            chunked_as_joined(&token, &signed, &chunks, skip);
729                        }
730                    }
731                }
732            }
733            for len in [0usize, 1, 48] {
734                let short = alloc::vec![0u8; len];
735                assert_eq!(
736                    token.decrypt_chunks(&split_at_offsets(&short, &[1]), 0),
737                    Err(TokenError::InvalidToken)
738                );
739            }
740        }
741    }
742
743    #[test]
744    fn untagged_encryption_with_its_mac_is_the_token() {
745        for key in [[0x21u8; 32].to_vec(), [0x42u8; 64].to_vec()] {
746            let token = Token::new(&key).unwrap();
747            for len in [0usize, 1, 15, 16, 100, 5000] {
748                let plaintext: Vec<u8> = (0..len).map(|i| (i * 5) as u8).collect();
749                let expected = token.encrypt(&plaintext, &mut FixedRng::new(&[0x77; 16]));
750                let (mut bytes, mut mac) =
751                    token.encrypt_untagged(&plaintext, &mut FixedRng::new(&[0x77; 16]));
752                let signed = bytes.len() - 32;
753                assert!(bytes[signed..].iter().all(|&b| b == 0));
754                mac.update(&bytes[..signed]);
755                bytes[signed..].copy_from_slice(&mac.finalize());
756                assert_eq!(bytes, expected);
757            }
758        }
759    }
760
761    #[test]
762    fn streamed_plaintext_is_final_except_its_last_block() {
763        let mut rng = FixedRng::new(&[0x3C; 64]);
764        let token = Token::new(&[0x42u8; 64]).unwrap();
765        for len in [4usize, 15, 16, 17, 100, 1000, 5000] {
766            let plaintext: Vec<u8> = (0..len).map(|i| (i * 11) as u8).collect();
767            let bytes = token.encrypt(&plaintext, &mut rng);
768            for step in [1usize, 7, 16, 64, 500] {
769                let mut stream = token.decrypt_stream(4);
770                let mut partials = Vec::new();
771                for chunk in bytes.chunks(step) {
772                    stream.update(chunk);
773                    partials.push(stream.plaintext().to_vec());
774                }
775                let result = stream.finish().unwrap();
776                assert_eq!(result, plaintext[4..]);
777                let mut hashed = token.decrypt_stream(4);
778                hashed.hash_plaintext();
779                for chunk in bytes.chunks(step) {
780                    hashed.update(chunk);
781                }
782                let (again, hash) = hashed.finish_hashed().unwrap();
783                assert_eq!(again, result);
784                assert_eq!(hash.unwrap().digest(), crate::sha256::sha256(&result));
785                for partial in partials {
786                    let settled = partial.len().saturating_sub(16);
787                    assert_eq!(partial[..settled], result[..settled]);
788                }
789            }
790        }
791    }
792
793    #[test]
794    fn single_buffer_encryption_matches_pad_encrypt_sign() {
795        for key in [[0x21u8; 32].to_vec(), [0x42u8; 64].to_vec()] {
796            let token = Token::new(&key).unwrap();
797            let iv = [0x17u8; 16];
798            for len in [0usize, 1, 15, 16, 17, 31, 32, 1000] {
799                let plaintext: Vec<u8> = (0..len).map(|i| i as u8).collect();
800                let padded = pkcs7::pad(&plaintext, 16);
801                let ciphertext = match &token.mode {
802                    AesMode::Aes128(aes) => aes.encrypt_cbc(&padded, &iv),
803                    AesMode::Aes256(aes) => aes.encrypt_cbc(&padded, &iv),
804                }
805                .unwrap();
806                let mut expected = iv.to_vec();
807                expected.extend_from_slice(&ciphertext);
808                // Tags from the raw signing key, not the token's keyed state.
809                let mac = hmac_sha256(&key[..key.len() / 2], &expected);
810                expected.extend_from_slice(&mac);
811                let encrypted = token.encrypt_with_iv(&plaintext, &iv);
812                assert_eq!(encrypted, expected, "key {} len {len}", key.len());
813                assert_eq!(encrypted.capacity(), encrypted.len());
814                assert_eq!(token.decrypt(&encrypted).unwrap(), plaintext);
815            }
816        }
817    }
818}