1#[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
91const DOMAIN_MESSAGE: u8 = 9;
93const DOMAIN_TAG: u8 = 10;
95const DOMAIN_AD: u8 = 11;
97
98const BLOCK: usize = TBC_BLOCK_BYTES;
100
101pub struct SaturninQcb {
106 msg: SaturninTbc,
107 tag: SaturninTbc,
108 ad: SaturninTbc,
109}
110
111impl SaturninQcb {
112 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 pub const fn key_size() -> usize {
123 32
124 }
125
126 pub const fn nonce_size() -> usize {
128 16
129 }
130
131 pub const fn tag_size() -> usize {
133 BLOCK
134 }
135
136 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 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 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 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 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 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 if ciphertext.len() < 2 * BLOCK {
231 return Err(Error::aead_ciphertext_shorter_than_tag(
232 2 * BLOCK,
233 ciphertext.len(),
234 ));
235 }
236 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 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 let plaintext_len = match unpad_len(&plain) {
283 Some(len) => len,
284 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
294fn 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 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); 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 assert!(
458 aead.decrypt(&key(), &nonce(), &ct, Some(b"header-B"))
459 .is_err()
460 );
461 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 #[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 let aead = SaturninQcb::new();
544 let mut a = vec![0u8; 96]; let mut b = a.clone();
546 b[40] ^= 0xFF; let ca = aead.encrypt(&key(), &nonce(), &a, None)?;
548 let cb = aead.encrypt(&key(), &nonce(), &b, None)?;
549 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]); a.zeroize();
554 b.zeroize();
555 Ok(())
556 }
557
558 #[test]
559 fn unpad_len_handles_valid_and_malformed() {
560 assert_eq!(unpad_len(&[1, 2, 3, 0x80, 0, 0]), Some(3));
562 assert_eq!(unpad_len(&[0x80]), Some(0));
563 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 assert!(aead.decrypt(&key(), &nonce(), &[0u8; 40], None).is_err());
594 assert!(aead.decrypt(&key(), &nonce(), &[0u8; 65], None).is_err());
596 }
597}