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::{hmac_sha256, hmac_sha256_verify, HmacSha256};
9use crate::pkcs7;
10use crate::Rng;
11
12pub const TOKEN_OVERHEAD: usize = 48; // 16 IV + 32 HMAC
13
14#[derive(Debug, PartialEq)]
15pub enum TokenError {
16    InvalidKeyLength,
17    InvalidToken,
18    HmacMismatch,
19    DecryptionFailed,
20}
21
22impl fmt::Display for TokenError {
23    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
24        match self {
25            TokenError::InvalidKeyLength => write!(f, "Token key must be 32 or 64 bytes"),
26            TokenError::InvalidToken => write!(f, "Token too short"),
27            TokenError::HmacMismatch => write!(f, "Token HMAC was invalid"),
28            TokenError::DecryptionFailed => write!(f, "Could not decrypt token"),
29        }
30    }
31}
32
33enum AesMode {
34    Aes128(Aes128),
35    Aes256(Aes256),
36}
37
38/// CBC chaining state carried across the pieces of one ciphertext. It lives
39/// on the stack for one call, so the variants' size difference is harmless.
40#[allow(clippy::large_enum_variant)]
41enum CbcDecryptor {
42    Aes128(cbc::Decryptor<aes::Aes128>),
43    Aes256(cbc::Decryptor<aes::Aes256>),
44}
45
46impl CbcDecryptor {
47    /// Decrypt whole blocks in place, continuing the chain.
48    fn decrypt_blocks(&mut self, buf: &mut [u8]) {
49        let (blocks, rest) = Array::slice_as_chunks_mut(buf);
50        debug_assert!(rest.is_empty());
51        match self {
52            CbcDecryptor::Aes128(dec) => dec.decrypt_blocks(blocks),
53            CbcDecryptor::Aes256(dec) => dec.decrypt_blocks(blocks),
54        }
55    }
56}
57
58pub struct Token {
59    signing_key: Vec<u8>,
60    mode: AesMode,
61}
62
63impl Token {
64    pub fn new(key: &[u8]) -> Result<Self, TokenError> {
65        match key.len() {
66            32 => {
67                let signing_key = key[..16].to_vec();
68                let encryption_key: [u8; 16] = key[16..32].try_into().unwrap();
69                Ok(Token {
70                    signing_key,
71                    mode: AesMode::Aes128(Aes128::new(&encryption_key)),
72                })
73            }
74            64 => {
75                let signing_key = key[..32].to_vec();
76                let encryption_key: [u8; 32] = key[32..64].try_into().unwrap();
77                Ok(Token {
78                    signing_key,
79                    mode: AesMode::Aes256(Aes256::new(&encryption_key)),
80                })
81            }
82            _ => Err(TokenError::InvalidKeyLength),
83        }
84    }
85
86    pub fn encrypt(&self, plaintext: &[u8], rng: &mut dyn Rng) -> Vec<u8> {
87        let mut iv = [0u8; 16];
88        rng.fill_bytes(&mut iv);
89        self.encrypt_with_iv(plaintext, &iv)
90    }
91
92    pub fn encrypt_with_iv(&self, plaintext: &[u8], iv: &[u8; 16]) -> Vec<u8> {
93        // Build the whole token in one buffer: the plaintext is copied once,
94        // padded and encrypted in place, then signed and the tag appended.
95        let padding = pkcs7::BLOCK_SIZE - plaintext.len() % pkcs7::BLOCK_SIZE;
96        let mut token = Vec::with_capacity(16 + plaintext.len() + padding + 32);
97        token.extend_from_slice(iv);
98        token.extend_from_slice(plaintext);
99        token.resize(token.len() + padding, padding as u8);
100        match &self.mode {
101            AesMode::Aes128(aes) => aes.encrypt_cbc_in_place(&mut token[16..], iv),
102            AesMode::Aes256(aes) => aes.encrypt_cbc_in_place(&mut token[16..], iv),
103        }
104        .expect("PKCS7 padding always produces block-aligned plaintext");
105
106        let mac = hmac_sha256(&self.signing_key, &token);
107        token.extend_from_slice(&mac);
108        token
109    }
110
111    pub fn verify_hmac(&self, token: &[u8]) -> Result<bool, TokenError> {
112        if token.len() <= 32 {
113            return Err(TokenError::InvalidToken);
114        }
115        let received_hmac = &token[token.len() - 32..];
116        Ok(hmac_sha256_verify(
117            &self.signing_key,
118            &token[..token.len() - 32],
119            received_hmac,
120        ))
121    }
122
123    pub fn decrypt(&self, token: &[u8]) -> Result<Vec<u8>, TokenError> {
124        if token.len() <= TOKEN_OVERHEAD {
125            return Err(TokenError::InvalidToken);
126        }
127
128        if !self
129            .verify_hmac(token)
130            .map_err(|_| TokenError::InvalidToken)?
131        {
132            return Err(TokenError::HmacMismatch);
133        }
134
135        let iv: [u8; 16] = token[..16].try_into().unwrap();
136        let ciphertext = &token[16..token.len() - 32];
137
138        // CBC ciphertext must be a non-empty whole number of 16-byte blocks.
139        if ciphertext.is_empty() || !ciphertext.len().is_multiple_of(16) {
140            return Err(TokenError::DecryptionFailed);
141        }
142
143        let mut decrypted = match &self.mode {
144            AesMode::Aes128(aes) => aes.decrypt_cbc(ciphertext, &iv),
145            AesMode::Aes256(aes) => aes.decrypt_cbc(ciphertext, &iv),
146        }
147        .map_err(|_| TokenError::DecryptionFailed)?;
148
149        let unpadded_len = pkcs7::unpad(&decrypted, 16)
150            .map_err(|_| TokenError::DecryptionFailed)?
151            .len();
152        decrypted.truncate(unpadded_len);
153        Ok(decrypted)
154    }
155
156    /// Decrypt a token that arrives as consecutive chunks, such as the parts
157    /// of a resource, without joining them first. Each chunk is authenticated
158    /// and decrypted while it is still in cache; nothing is returned unless
159    /// the tag matches. The result is [`Token::decrypt`] of the joined bytes
160    /// with its first `skip` bytes (at most one block) dropped, and the errors
161    /// are the same, except that a plaintext shorter than `skip` is
162    /// [`TokenError::DecryptionFailed`].
163    pub fn decrypt_chunks(&self, chunks: &[&[u8]], skip: usize) -> Result<Vec<u8>, TokenError> {
164        assert!(skip <= 16, "decrypt_chunks skips at most one block");
165        let total: usize = chunks.iter().map(|chunk| chunk.len()).sum();
166        if total <= TOKEN_OVERHEAD {
167            return Err(TokenError::InvalidToken);
168        }
169        let signed_len = total - 32;
170        let ciphertext_len = signed_len - 16;
171        // A misaligned ciphertext is rejected after the tag check, as in
172        // `decrypt`; it is authenticated but not decrypted.
173        let aligned = ciphertext_len.is_multiple_of(16);
174
175        let mut mac = HmacSha256::new(&self.signing_key);
176        let mut iv = [0u8; 16];
177        let mut tag = [0u8; 32];
178        let mut plaintext = Vec::with_capacity(if aligned { ciphertext_len - skip } else { 0 });
179        let mut decryptor = None;
180        // A block that straddles chunks, and always the first block, which
181        // carries the skipped bytes, are decrypted here.
182        let mut block = [0u8; 16];
183        let mut block_len = 0;
184        let mut first_block = true;
185        let mut last_byte = 0u8;
186        let mut pos = 0;
187        for chunk in chunks {
188            let end = pos + chunk.len();
189            if pos < signed_len {
190                mac.update(&chunk[..signed_len.min(end) - pos]);
191            }
192            if pos < 16 {
193                let n = 16.min(end) - pos;
194                iv[pos..pos + n].copy_from_slice(&chunk[..n]);
195            }
196            let (start, stop) = (pos.max(16), end.min(signed_len));
197            if aligned && start < stop {
198                let decryptor = decryptor.get_or_insert_with(|| match &self.mode {
199                    AesMode::Aes128(aes) => CbcDecryptor::Aes128(aes.cbc_decryptor(&iv)),
200                    AesMode::Aes256(aes) => CbcDecryptor::Aes256(aes.cbc_decryptor(&iv)),
201                });
202                let mut data = &chunk[start - pos..stop - pos];
203                while !data.is_empty() && (block_len > 0 || first_block) {
204                    let n = (16 - block_len).min(data.len());
205                    block[block_len..block_len + n].copy_from_slice(&data[..n]);
206                    block_len += n;
207                    data = &data[n..];
208                    if block_len == 16 {
209                        decryptor.decrypt_blocks(&mut block);
210                        let from = if first_block { skip } else { 0 };
211                        plaintext.extend_from_slice(&block[from..]);
212                        last_byte = block[15];
213                        first_block = false;
214                        block_len = 0;
215                    }
216                }
217                if !data.is_empty() {
218                    let whole = data.len() - data.len() % 16;
219                    if whole > 0 {
220                        let at = plaintext.len();
221                        plaintext.extend_from_slice(&data[..whole]);
222                        decryptor.decrypt_blocks(&mut plaintext[at..]);
223                        last_byte = plaintext[plaintext.len() - 1];
224                    }
225                    let rest = &data[whole..];
226                    block[..rest.len()].copy_from_slice(rest);
227                    block_len = rest.len();
228                }
229            }
230            if end > signed_len {
231                let from = pos.max(signed_len);
232                tag[from - signed_len..end - signed_len].copy_from_slice(&chunk[from - pos..]);
233            }
234            pos = end;
235        }
236
237        if !mac.verify(&tag) {
238            return Err(TokenError::HmacMismatch);
239        }
240        if !aligned {
241            return Err(TokenError::DecryptionFailed);
242        }
243        // The same padding check as `pkcs7::unpad`.
244        let padding = usize::from(last_byte);
245        if padding > 16 || padding > plaintext.len() {
246            return Err(TokenError::DecryptionFailed);
247        }
248        plaintext.truncate(plaintext.len() - padding);
249        Ok(plaintext)
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256    use crate::FixedRng;
257
258    #[test]
259    fn test_token_new_32byte_key() {
260        let key = [0u8; 32];
261        assert!(Token::new(&key).is_ok());
262    }
263
264    #[test]
265    fn test_token_new_64byte_key() {
266        let key = [0u8; 64];
267        assert!(Token::new(&key).is_ok());
268    }
269
270    #[test]
271    fn test_token_new_48byte_key() {
272        let key = [0u8; 48];
273        assert!(matches!(
274            Token::new(&key),
275            Err(TokenError::InvalidKeyLength)
276        ));
277    }
278
279    #[test]
280    fn test_token_roundtrip_32() {
281        let key = [0x42u8; 32];
282        let token = Token::new(&key).unwrap();
283        let mut rng = FixedRng::new(&[0xAA; 16]);
284        let plaintext = b"Hello, Reticulum!";
285        let encrypted = token.encrypt(plaintext, &mut rng);
286        let decrypted = token.decrypt(&encrypted).unwrap();
287        assert_eq!(decrypted, plaintext);
288    }
289
290    #[test]
291    fn test_token_roundtrip_64() {
292        let key = [0x42u8; 64];
293        let token = Token::new(&key).unwrap();
294        let mut rng = FixedRng::new(&[0xBB; 16]);
295        let plaintext = b"Hello, Reticulum!";
296        let encrypted = token.encrypt(plaintext, &mut rng);
297        let decrypted = token.decrypt(&encrypted).unwrap();
298        assert_eq!(decrypted, plaintext);
299    }
300
301    #[test]
302    fn test_token_hmac_reject_tampered() {
303        let key = [0x42u8; 64];
304        let token = Token::new(&key).unwrap();
305        let mut rng = FixedRng::new(&[0xCC; 16]);
306        let encrypted = token.encrypt(b"test", &mut rng);
307        let mut tampered = encrypted.clone();
308        tampered[20] ^= 0xFF; // flip a bit in ciphertext
309        assert!(token.decrypt(&tampered).is_err());
310    }
311
312    #[test]
313    fn token_hmac_rejects_mismatches_across_the_entire_tag() {
314        let key = [0x42u8; 64];
315        let token = Token::new(&key).unwrap();
316        let mut rng = FixedRng::new(&[0xCC; 16]);
317        let encrypted = token.encrypt(b"timing regression", &mut rng);
318
319        assert_eq!(token.verify_hmac(&encrypted), Ok(true));
320        for tag_index in 0..32 {
321            let mut corrupted = encrypted.clone();
322            let index = corrupted.len() - 32 + tag_index;
323            corrupted[index] ^= 0x01;
324            assert_eq!(
325                token.verify_hmac(&corrupted),
326                Ok(false),
327                "tag mismatch at byte {tag_index} must fail"
328            );
329        }
330    }
331
332    #[test]
333    fn test_token_decrypt_truncated() {
334        let key = [0x42u8; 64];
335        let token = Token::new(&key).unwrap();
336        assert!(matches!(
337            token.decrypt(&[0u8; 10]),
338            Err(TokenError::InvalidToken)
339        ));
340    }
341
342    fn sign_token_parts(key: &[u8], iv_and_ciphertext: &[u8]) -> Vec<u8> {
343        let signing_key = &key[..key.len() / 2];
344        let mut token = iv_and_ciphertext.to_vec();
345        token.extend_from_slice(&hmac_sha256(signing_key, iv_and_ciphertext));
346        token
347    }
348
349    #[test]
350    fn test_token_decrypt_rejects_misaligned_ciphertext_with_valid_hmac() {
351        for key in [&[0x42u8; 32][..], &[0x42u8; 64][..]] {
352            let token = Token::new(key).unwrap();
353            for ciphertext_len in [1usize, 15, 17, 31, 33] {
354                let mut parts = alloc::vec![0xAAu8; 16];
355                parts.extend(core::iter::repeat_n(0x55u8, ciphertext_len));
356                let signed = sign_token_parts(key, &parts);
357                assert_eq!(token.verify_hmac(&signed), Ok(true));
358                assert_eq!(
359                    token.decrypt(&signed),
360                    Err(TokenError::DecryptionFailed),
361                    "ciphertext length {ciphertext_len} must be rejected"
362                );
363            }
364        }
365    }
366
367    #[test]
368    fn test_token_well_formed_roundtrip_after_alignment_check() {
369        for key in [&[0x24u8; 32][..], &[0x24u8; 64][..]] {
370            let token = Token::new(key).unwrap();
371            for plaintext_len in [0usize, 1, 15, 16, 17, 32, 100] {
372                let plaintext = alloc::vec![0x5Au8; plaintext_len];
373                let encrypted = token.encrypt_with_iv(&plaintext, &[0x11; 16]);
374                assert_eq!((encrypted.len() - TOKEN_OVERHEAD) % 16, 0);
375                assert_eq!(token.decrypt(&encrypted).unwrap(), plaintext);
376            }
377        }
378    }
379
380    #[test]
381    fn test_token_overhead() {
382        assert_eq!(TOKEN_OVERHEAD, 48);
383    }
384
385    /// Split `bytes` at the given offsets (clamped, in order).
386    fn split_at_offsets<'a>(bytes: &'a [u8], offsets: &[usize]) -> Vec<&'a [u8]> {
387        let mut chunks = Vec::new();
388        let mut from = 0;
389        for &offset in offsets {
390            let to = offset.clamp(from, bytes.len());
391            chunks.push(&bytes[from..to]);
392            from = to;
393        }
394        chunks.push(&bytes[from..]);
395        chunks
396    }
397
398    fn chunked_as_joined(token: &Token, bytes: &[u8], chunks: &[&[u8]], skip: usize) {
399        let joined = token.decrypt(bytes);
400        let expected = match joined {
401            Ok(plaintext) if plaintext.len() >= skip => Ok(plaintext[skip..].to_vec()),
402            Ok(_) => Err(TokenError::DecryptionFailed),
403            Err(error) => Err(error),
404        };
405        assert_eq!(
406            token.decrypt_chunks(chunks, skip),
407            expected,
408            "{chunks:?} skip {skip}"
409        );
410    }
411
412    #[test]
413    fn chunked_decryption_matches_joined_decryption() {
414        let mut rng = FixedRng::new(&[0x5D; 64]);
415        for key in [[0x21u8; 32].to_vec(), [0x42u8; 64].to_vec()] {
416            let token = Token::new(&key).unwrap();
417            for len in [0usize, 1, 3, 4, 5, 12, 15, 16, 17, 33, 100, 1000] {
418                let plaintext: Vec<u8> = (0..len).map(|i| (i * 7) as u8).collect();
419                let bytes = token.encrypt(&plaintext, &mut rng);
420                let n = bytes.len();
421                let splits: Vec<Vec<usize>> = alloc::vec![
422                    alloc::vec![],
423                    alloc::vec![1],
424                    alloc::vec![8, 16, 17],
425                    alloc::vec![16, 20, 32, 47],
426                    alloc::vec![n - 33, n - 32, n - 31],
427                    alloc::vec![n - 40, n - 10],
428                    (1..n).collect(),
429                    (0..n).step_by(13).collect(),
430                    alloc::vec![0, 0, 5, 5, n],
431                ];
432                for offsets in &splits {
433                    let chunks = split_at_offsets(&bytes, offsets);
434                    for skip in [0, 1, 4, 16] {
435                        chunked_as_joined(&token, &bytes, &chunks, skip);
436                    }
437                }
438                // Tampering anywhere is caught.
439                for at in 0..n {
440                    let mut tampered = bytes.clone();
441                    tampered[at] ^= 0x01;
442                    let chunks = split_at_offsets(&tampered, &[at / 2, at, at + 1]);
443                    assert_eq!(
444                        token.decrypt_chunks(&chunks, 4),
445                        Err(TokenError::HmacMismatch)
446                    );
447                }
448            }
449            // Misaligned ciphertext under a valid tag, and short tokens.
450            for ciphertext_len in [1usize, 15, 17, 31, 33] {
451                let mut parts = alloc::vec![0xAAu8; 16];
452                parts.extend(core::iter::repeat_n(0x55u8, ciphertext_len));
453                let signed = sign_token_parts(&key, &parts);
454                let chunks = split_at_offsets(&signed, &[7, 20, 40]);
455                chunked_as_joined(&token, &signed, &chunks, 4);
456            }
457            // Signed garbage decrypts to arbitrary padding bytes.
458            for fill in 0u8..64 {
459                for ciphertext_len in [16usize, 32] {
460                    let parts: Vec<u8> = (0..16 + ciphertext_len)
461                        .map(|i| (i as u8).wrapping_mul(fill).wrapping_add(fill))
462                        .collect();
463                    let signed = sign_token_parts(&key, &parts);
464                    for offsets in [alloc::vec![], alloc::vec![3, 19, 30]] {
465                        let chunks = split_at_offsets(&signed, &offsets);
466                        for skip in [0, 4, 16] {
467                            chunked_as_joined(&token, &signed, &chunks, skip);
468                        }
469                    }
470                }
471            }
472            for len in [0usize, 1, 48] {
473                let short = alloc::vec![0u8; len];
474                assert_eq!(
475                    token.decrypt_chunks(&split_at_offsets(&short, &[1]), 0),
476                    Err(TokenError::InvalidToken)
477                );
478            }
479        }
480    }
481
482    #[test]
483    fn single_buffer_encryption_matches_pad_encrypt_sign() {
484        for key in [[0x21u8; 32].to_vec(), [0x42u8; 64].to_vec()] {
485            let token = Token::new(&key).unwrap();
486            let iv = [0x17u8; 16];
487            for len in [0usize, 1, 15, 16, 17, 31, 32, 1000] {
488                let plaintext: Vec<u8> = (0..len).map(|i| i as u8).collect();
489                let padded = pkcs7::pad(&plaintext, 16);
490                let ciphertext = match &token.mode {
491                    AesMode::Aes128(aes) => aes.encrypt_cbc(&padded, &iv),
492                    AesMode::Aes256(aes) => aes.encrypt_cbc(&padded, &iv),
493                }
494                .unwrap();
495                let mut expected = iv.to_vec();
496                expected.extend_from_slice(&ciphertext);
497                let mac = hmac_sha256(&token.signing_key, &expected);
498                expected.extend_from_slice(&mac);
499                let encrypted = token.encrypt_with_iv(&plaintext, &iv);
500                assert_eq!(encrypted, expected, "key {} len {len}", key.len());
501                assert_eq!(encrypted.capacity(), encrypted.len());
502                assert_eq!(token.decrypt(&encrypted).unwrap(), plaintext);
503            }
504        }
505    }
506}