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 for (j, chunk) in padded.chunks_exact(BLOCK).enumerate() {
170 let tweak = Self::ad_tweak(j as u64);
171 let mut block = [0u8; BLOCK];
172 block.copy_from_slice(chunk);
173 self.ad.encrypt_block(key, &tweak, &mut block)?;
174 for i in 0..BLOCK {
175 auth[i] ^= block[i];
176 }
177 block.zeroize();
178 }
179 Ok(auth)
180 }
181
182 fn compute_tag(
184 &self,
185 key: &[u8; 32],
186 nonce16: &[u8; 16],
187 checksum: &[u8; BLOCK],
188 last_index: u64,
189 ad_auth: &[u8; BLOCK],
190 ) -> Result<Zeroizing<[u8; BLOCK]>> {
191 let mut tag = Zeroizing::new(*checksum);
192 let tweak = Self::tweak(nonce16, last_index);
193 self.tag.encrypt_block(key, &tweak, &mut tag)?;
194 for i in 0..BLOCK {
195 tag[i] ^= ad_auth[i];
196 }
197 Ok(tag)
198 }
199
200 fn validate_lengths(key: &AeadKey, nonce: &Nonce) -> Result<()> {
201 if key.as_bytes().len() != Self::key_size() {
202 return Err(Error::InvalidKeySize {
203 expected: Self::key_size(),
204 actual: key.as_bytes().len(),
205 });
206 }
207 if nonce.as_bytes().len() != Self::nonce_size() {
208 return Err(Error::InvalidNonceSize {
209 expected: Self::nonce_size(),
210 actual: nonce.as_bytes().len(),
211 });
212 }
213 Ok(())
214 }
215
216 fn decrypt_core(
220 &self,
221 key: &AeadKey,
222 nonce: &Nonce,
223 ciphertext: &[u8],
224 associated_data: Option<&[u8]>,
225 ) -> Result<DecryptSemanticOutcome> {
226 Self::validate_lengths(key, nonce)?;
227
228 if ciphertext.len() < 2 * BLOCK {
230 return Err(Error::aead_ciphertext_shorter_than_tag(
231 2 * BLOCK,
232 ciphertext.len(),
233 ));
234 }
235 if !ciphertext.len().is_multiple_of(BLOCK) {
237 return Err(Error::InvalidCiphertextSize {
238 expected: (ciphertext.len() / BLOCK + 1) * BLOCK,
239 actual: ciphertext.len(),
240 });
241 }
242
243 let body_len = ciphertext.len() - BLOCK;
244 let body = &ciphertext[..body_len];
245 let received_tag = &ciphertext[body_len..];
246 let m = body_len / BLOCK;
247
248 let mut key_staged = Zeroizing::new([0u8; 32]);
249 key_staged.copy_from_slice(key.as_bytes());
250 let mut nonce16 = Zeroizing::new([0u8; 16]);
251 nonce16.copy_from_slice(nonce.as_bytes());
252 let ad = associated_data.unwrap_or(&[]);
253
254 let mut plain = Zeroizing::new(Vec::with_capacity(body_len));
256 let mut checksum = Zeroizing::new([0u8; BLOCK]);
257 for (i, chunk) in body.chunks_exact(BLOCK).enumerate() {
258 let tweak = Self::tweak(&nonce16, i as u64);
259 let mut block = [0u8; BLOCK];
260 block.copy_from_slice(chunk);
261 self.msg.decrypt_block(&key_staged, &tweak, &mut block)?;
262 for k in 0..BLOCK {
263 checksum[k] ^= block[k];
264 }
265 plain.extend_from_slice(&block);
266 block.zeroize();
267 }
268
269 let ad_auth = self.absorb_ad(&key_staged, ad)?;
270 let expected_tag =
271 self.compute_tag(&key_staged, &nonce16, &checksum, (m - 1) as u64, &ad_auth)?;
272
273 let tag_valid = lib_q_core::Utils::constant_time_compare(&*expected_tag, received_tag);
274
275 if !tag_valid {
276 return Ok(DecryptSemanticOutcome::AuthenticationFailed);
277 }
278
279 let plaintext_len = match unpad_len(&plain) {
281 Some(len) => len,
282 None => return Ok(DecryptSemanticOutcome::AuthenticationFailed),
285 };
286 let mut out = Vec::with_capacity(plaintext_len);
287 out.extend_from_slice(&plain[..plaintext_len]);
288 Ok(DecryptSemanticOutcome::Success(Zeroizing::new(out)))
289 }
290}
291
292fn unpad_len(padded: &[u8]) -> Option<usize> {
294 let mut idx = padded.len();
295 while idx > 0 && padded[idx - 1] == 0 {
296 idx -= 1;
297 }
298 if idx == 0 || padded[idx - 1] != 0x80 {
299 return None;
300 }
301 Some(idx - 1)
302}
303
304impl Aead for SaturninQcb {
305 fn encrypt(
306 &self,
307 key: &AeadKey,
308 nonce: &Nonce,
309 plaintext: &[u8],
310 associated_data: Option<&[u8]>,
311 ) -> Result<Vec<u8>> {
312 Self::validate_lengths(key, nonce)?;
313
314 let mut key_staged = Zeroizing::new([0u8; 32]);
315 key_staged.copy_from_slice(key.as_bytes());
316 let mut nonce16 = Zeroizing::new([0u8; 16]);
317 nonce16.copy_from_slice(nonce.as_bytes());
318 let ad = associated_data.unwrap_or(&[]);
319
320 let padded = Self::pad(plaintext);
321 let m = padded.len() / BLOCK;
322
323 let mut output = Vec::with_capacity(padded.len() + BLOCK);
324 let mut checksum = Zeroizing::new([0u8; BLOCK]);
325 for (i, chunk) in padded.chunks_exact(BLOCK).enumerate() {
326 for k in 0..BLOCK {
327 checksum[k] ^= chunk[k];
328 }
329 let tweak = Self::tweak(&nonce16, i as u64);
330 let mut block = [0u8; BLOCK];
331 block.copy_from_slice(chunk);
332 self.msg.encrypt_block(&key_staged, &tweak, &mut block)?;
333 output.extend_from_slice(&block);
334 block.zeroize();
335 }
336
337 let ad_auth = self.absorb_ad(&key_staged, ad)?;
338 let tag = self.compute_tag(&key_staged, &nonce16, &checksum, (m - 1) as u64, &ad_auth)?;
339 output.extend_from_slice(&*tag);
340 Ok(output)
341 }
342
343 fn decrypt(
344 &self,
345 key: &AeadKey,
346 nonce: &Nonce,
347 ciphertext: &[u8],
348 associated_data: Option<&[u8]>,
349 ) -> Result<Vec<u8>> {
350 match self.decrypt_core(key, nonce, ciphertext, associated_data)? {
351 DecryptSemanticOutcome::Success(p) => Ok(Vec::clone(&*p)),
352 DecryptSemanticOutcome::AuthenticationFailed => Err(Error::VerificationFailed {
353 operation: "Saturnin-QCB tag verification".to_string(),
354 }),
355 }
356 }
357}
358
359impl AeadDecryptSemantic for SaturninQcb {
360 fn decrypt_semantic(
361 &self,
362 key: &AeadKey,
363 nonce: &Nonce,
364 ciphertext: &[u8],
365 associated_data: Option<&[u8]>,
366 ) -> Result<DecryptSemanticOutcome> {
367 self.decrypt_core(key, nonce, ciphertext, associated_data)
368 }
369}
370
371impl Default for SaturninQcb {
372 fn default() -> Self {
373 Self::new()
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use alloc::vec;
380
381 use super::*;
382
383 fn key() -> AeadKey {
384 AeadKey::new((0..32u8).collect::<Vec<_>>())
385 }
386
387 fn nonce() -> Nonce {
388 Nonce::new((0..16u8).collect::<Vec<_>>())
389 }
390
391 #[test]
392 fn constants() {
393 assert_eq!(SaturninQcb::key_size(), 32);
394 assert_eq!(SaturninQcb::nonce_size(), 16);
395 assert_eq!(SaturninQcb::tag_size(), 32);
396 }
397
398 #[test]
399 fn round_trip_various_lengths() -> Result<()> {
400 let aead = SaturninQcb::new();
401 for len in [0usize, 1, 15, 31, 32, 33, 64, 100, 256] {
402 let pt: Vec<u8> = (0..len).map(|i| i as u8).collect();
403 let ct = aead.encrypt(&key(), &nonce(), &pt, Some(b"hdr"))?;
404 let expected_body = (len / 32 + 1) * 32;
406 assert_eq!(ct.len(), expected_body + 32, "len={len}");
407 let dec = aead.decrypt(&key(), &nonce(), &ct, Some(b"hdr"))?;
408 assert_eq!(dec, pt, "len={len}");
409 }
410 Ok(())
411 }
412
413 #[test]
414 fn empty_message_and_ad() -> Result<()> {
415 let aead = SaturninQcb::new();
416 let ct = aead.encrypt(&key(), &nonce(), b"", None)?;
417 assert_eq!(ct.len(), 64); assert_eq!(aead.decrypt(&key(), &nonce(), &ct, None)?, b"");
419 Ok(())
420 }
421
422 #[test]
423 fn tampered_tag_fails() -> Result<()> {
424 let aead = SaturninQcb::new();
425 let ct = aead.encrypt(&key(), &nonce(), b"hello world", Some(b"ad"))?;
426 let mut bad = ct.clone();
427 *bad.last_mut().unwrap() ^= 0x01;
428 assert!(matches!(
429 aead.decrypt(&key(), &nonce(), &bad, Some(b"ad")),
430 Err(Error::VerificationFailed { .. })
431 ));
432 assert_eq!(
433 aead.decrypt_semantic(&key(), &nonce(), &bad, Some(b"ad"))?,
434 DecryptSemanticOutcome::AuthenticationFailed
435 );
436 Ok(())
437 }
438
439 #[test]
440 fn tampered_body_fails() -> Result<()> {
441 let aead = SaturninQcb::new();
442 let ct = aead.encrypt(&key(), &nonce(), b"hello world", None)?;
443 let mut bad = ct.clone();
444 bad[0] ^= 0x80;
445 assert!(aead.decrypt(&key(), &nonce(), &bad, None).is_err());
446 Ok(())
447 }
448
449 #[test]
450 fn ad_is_authenticated() -> Result<()> {
451 let aead = SaturninQcb::new();
452 let ct = aead.encrypt(&key(), &nonce(), b"msg", Some(b"header-A"))?;
453 assert!(
455 aead.decrypt(&key(), &nonce(), &ct, Some(b"header-B"))
456 .is_err()
457 );
458 assert!(aead.decrypt(&key(), &nonce(), &ct, None).is_err());
460 Ok(())
461 }
462
463 #[test]
464 fn nonce_binding() -> Result<()> {
465 let aead = SaturninQcb::new();
466 let ct = aead.encrypt(&key(), &nonce(), b"msg", None)?;
467 let other = Nonce::new(vec![0xFFu8; 16]);
468 assert!(aead.decrypt(&key(), &other, &ct, None).is_err());
469 Ok(())
470 }
471
472 fn from_hex(s: &str) -> Vec<u8> {
473 (0..s.len())
474 .step_by(2)
475 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
476 .collect()
477 }
478
479 #[test]
486 fn pinned_kat_vectors() -> Result<()> {
487 let aead = SaturninQcb::new();
488 let cases: &[(&str, &str, &str)] = &[
489 (
490 "",
491 "",
492 "bd0abd723c4149718b458ac68f3a0a1e9e84e1e33c830a5894e48e6591a43a33718cd938614ad4c64e971ae1df9a657e290f3d862e5429088a7066642b07b29a",
493 ),
494 (
495 "",
496 "6173736f636961746564",
497 "bd0abd723c4149718b458ac68f3a0a1e9e84e1e33c830a5894e48e6591a43a33a40976d18060823323aa163b2ab7bf306cbbaff29aa86a0a31b6ba5d826c9dca",
498 ),
499 (
500 "616263",
501 "",
502 "52d715efbd6e430e4be8c2b682527e349a26fa62c69de5da978299c475f41c6df4620482177e4946c61ae01ff424a467ab76d31a63e75d045d3daaad64909edf",
503 ),
504 (
505 "0000000000000000000000000000000000000000000000000000000000000000",
506 "686472",
507 "16e51991ae3cb7cb92f3847c326188cb007267ece8153d03aeb98d4f161c84a730c8e81de51c9573d449dada58a211595a47a6f72f9776fd21347d45696e7f6743f9d93a4663c3f210ee1e99333007d9ceebd632ac2d5dacb2c9251499caddf2",
508 ),
509 (
510 "54686520717569636b2062726f776e20666f78206a756d7073206f76657220746865206c617a7920646f672121",
511 "61642d31",
512 "fe81caa8f1ee16e54fd7b3df31247e7ccd4295382cff4f9f7efefb5e970c6880c10b857de55d457eff7ea96f9e4c0dc2f30180b2c037d52565e8895d48ae701ebd4ceb39dbeece08aafae995d41998ea656e1cedb4326176717a42d8b92693e4",
513 ),
514 ];
515 for (pt_hex, ad_hex, ct_hex) in cases {
516 let pt = from_hex(pt_hex);
517 let ad = from_hex(ad_hex);
518 let ad_opt = if ad.is_empty() {
519 None
520 } else {
521 Some(ad.as_slice())
522 };
523 let ct = aead.encrypt(&key(), &nonce(), &pt, ad_opt)?;
524 assert_eq!(
525 ct,
526 from_hex(ct_hex),
527 "encrypt mismatch for pt={pt_hex} ad={ad_hex}"
528 );
529 let dec = aead.decrypt(&key(), &nonce(), &ct, ad_opt)?;
530 assert_eq!(dec, pt, "decrypt mismatch for pt={pt_hex} ad={ad_hex}");
531 }
532 Ok(())
533 }
534
535 #[test]
536 fn parallel_block_independence() -> Result<()> {
537 let aead = SaturninQcb::new();
541 let mut a = vec![0u8; 96]; let mut b = a.clone();
543 b[40] ^= 0xFF; let ca = aead.encrypt(&key(), &nonce(), &a, None)?;
545 let cb = aead.encrypt(&key(), &nonce(), &b, None)?;
546 assert_eq!(ca[0..32], cb[0..32]);
548 assert_ne!(ca[32..64], cb[32..64]);
549 assert_eq!(ca[64..96], cb[64..96]); a.zeroize();
551 b.zeroize();
552 Ok(())
553 }
554
555 #[test]
556 fn unpad_len_handles_valid_and_malformed() {
557 assert_eq!(unpad_len(&[1, 2, 3, 0x80, 0, 0]), Some(3));
559 assert_eq!(unpad_len(&[0x80]), Some(0));
560 assert_eq!(unpad_len(&[0, 0, 0]), None);
562 assert_eq!(unpad_len(&[]), None);
563 assert_eq!(unpad_len(&[1, 2, 3]), None);
564 }
565
566 #[test]
567 fn default_matches_new() -> Result<()> {
568 let a = SaturninQcb::default();
569 let b = SaturninQcb::new();
570 let pt = b"compare";
571 assert_eq!(
572 a.encrypt(&key(), &nonce(), pt, None)?,
573 b.encrypt(&key(), &nonce(), pt, None)?
574 );
575 Ok(())
576 }
577
578 #[test]
579 fn wrong_size_inputs_rejected() {
580 let aead = SaturninQcb::new();
581 assert!(
582 aead.encrypt(&AeadKey::new(vec![0u8; 16]), &nonce(), b"x", None)
583 .is_err()
584 );
585 assert!(
586 aead.encrypt(&key(), &Nonce::new(vec![0u8; 8]), b"x", None)
587 .is_err()
588 );
589 assert!(aead.decrypt(&key(), &nonce(), &[0u8; 40], None).is_err());
591 assert!(aead.decrypt(&key(), &nonce(), &[0u8; 65], None).is_err());
593 }
594}