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; #[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#[allow(clippy::large_enum_variant)]
41enum CbcDecryptor {
42 Aes128(cbc::Decryptor<aes::Aes128>),
43 Aes256(cbc::Decryptor<aes::Aes256>),
44}
45
46impl CbcDecryptor {
47 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 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 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 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 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 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 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; 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 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 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 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 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}