1#[cfg(feature = "alloc")]
83use alloc::{
84 string::ToString,
85 vec::Vec,
86};
87
88use lib_q_core::{
89 Aead,
90 AeadDecryptSemantic,
91 AeadKey,
92 DecryptSemanticOutcome,
93 Error,
94 Nonce,
95 Result,
96};
97use zeroize::{
98 Zeroize,
99 Zeroizing,
100};
101
102use crate::core::SaturninCore;
103#[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
104use crate::simd::{
105 encrypt_blocks8_dispatch,
106 simd_xor,
107};
108
109struct SaturninAeadCores {
114 d1: SaturninCore,
115 d2: SaturninCore,
116 d3: SaturninCore,
117 d4: SaturninCore,
118 d5: SaturninCore,
119}
120
121impl SaturninAeadCores {
122 fn new() -> Result<Self> {
123 Ok(Self {
124 d1: SaturninCore::new(10, 1)?,
125 d2: SaturninCore::new(10, 2)?,
126 d3: SaturninCore::new(10, 3)?,
127 d4: SaturninCore::new(10, 4)?,
128 d5: SaturninCore::new(10, 5)?,
129 })
130 }
131
132 #[inline]
133 fn domain(&self, d: u8) -> &SaturninCore {
134 match d {
135 1 => &self.d1,
136 2 => &self.d2,
137 3 => &self.d3,
138 4 => &self.d4,
139 5 => &self.d5,
140 _ => unreachable!("AEAD CTR/cascade only uses domains 1–5"),
141 }
142 }
143}
144
145pub struct SaturninAead {
151 cores: SaturninAeadCores,
152}
153
154impl SaturninAead {
155 pub fn new() -> Self {
157 Self {
158 cores: SaturninAeadCores::new().expect("Saturnin AEAD uses fixed valid domains"),
159 }
160 }
161
162 pub const fn key_size() -> usize {
164 32
165 }
166
167 pub const fn nonce_size() -> usize {
169 16
170 }
171
172 pub const fn tag_size() -> usize {
174 32
175 }
176
177 fn cascade_init(&self, key: &[u8], nonce: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
179 let key32: &[u8; 32] = key.try_into().map_err(|_| Error::InvalidKeySize {
180 expected: 32,
181 actual: key.len(),
182 })?;
183
184 let mut r = Zeroizing::new([0u8; 32]);
185
186 r[0..16].copy_from_slice(nonce);
188 r[16] = 0x80;
189 self.cores.d2.encrypt_block_32(key32, &mut r)?;
193
194 for i in 0..16 {
196 r[i] ^= nonce[i];
197 }
198 r[16] ^= 0x80;
199
200 Ok(r)
201 }
202
203 fn cascade(&self, r: &mut [u8; 32], d1: u8, d2: u8, data: &[u8]) -> Result<()> {
205 let core_d1 = self.cores.domain(d1);
206 let core_d2 = self.cores.domain(d2);
207
208 let mut offset = 0;
209
210 loop {
211 let mut t: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
212 let mut m: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
213 let remaining = data.len() - offset;
214
215 if remaining >= 32 {
216 t.copy_from_slice(&data[offset..offset + 32]);
217 offset += 32;
218
219 m.copy_from_slice(&*t);
221 core_d1.encrypt_block_32(&*r, &mut m)?;
222 } else {
223 t[0..remaining].copy_from_slice(&data[offset..]);
224 t[remaining] = 0x80;
225 m.copy_from_slice(&*t);
229 core_d2.encrypt_block_32(&*r, &mut m)?;
230 }
231
232 #[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
233 {
234 let mut out: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
235 simd_xor::xor_blocks_32(&m, &t, &mut out);
236 r.copy_from_slice(&*out);
237 }
238
239 #[cfg(not(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon")))]
240 {
241 for i in 0..32 {
242 r[i] = m[i] ^ t[i];
243 }
244 }
245
246 if remaining < 32 {
247 break;
248 }
249 }
250
251 Ok(())
252 }
253
254 #[cfg(feature = "hash")]
273 pub(crate) fn base_tag_over(
274 &self,
275 key: &[u8],
276 nonce: &[u8],
277 ad: &[u8],
278 ct_body: &[u8],
279 ) -> Result<Zeroizing<[u8; 32]>> {
280 let mut tag = self.cascade_init(key, nonce)?;
281 self.cascade(&mut tag, 2, 3, ad)?;
282 self.cascade(&mut tag, 4, 5, ct_body)?;
283 Ok(tag)
284 }
285
286 pub(crate) fn ctr_encrypt(&self, key: &[u8], nonce: &[u8], data: &mut [u8]) -> Result<()> {
292 let key32: &[u8; 32] = key.try_into().map_err(|_| Error::InvalidKeySize {
293 expected: 32,
294 actual: key.len(),
295 })?;
296
297 let core = &self.cores.d1;
298
299 let mut counter = 1u32; let mut offset = 0;
301
302 while offset < data.len() {
303 #[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
304 if data.len() - offset >= 32 * 8 {
305 let mut keystream_blocks = [[0u8; 32]; 8];
306 for (lane, block) in keystream_blocks.iter_mut().enumerate() {
307 let c = counter.wrapping_add(lane as u32);
308 block[0..16].copy_from_slice(nonce);
309 block[16] = 0x80;
310 block[28] = (c >> 24) as u8;
311 block[29] = (c >> 16) as u8;
312 block[30] = (c >> 8) as u8;
313 block[31] = c as u8;
314 }
315
316 encrypt_blocks8_dispatch(10, 1, key, &mut keystream_blocks, Some(core))?;
317
318 for (lane, ks) in keystream_blocks.iter().enumerate() {
319 let start = offset + (lane * 32);
320 let mut input = [0u8; 32];
321 input.copy_from_slice(&data[start..start + 32]);
322 let mut out = [0u8; 32];
323 simd_xor::xor_blocks_32(&input, ks, &mut out);
324 data[start..start + 32].copy_from_slice(&out);
325 }
326
327 offset += 32 * 8;
328 let (next_counter, overflowed) = counter.overflowing_add(8);
329 if overflowed {
330 return Err(Error::InvalidMessageSize {
331 max: usize::MAX,
332 actual: data.len(),
333 });
334 }
335 counter = next_counter;
336 continue;
337 }
338
339 let mut keystream = [0u8; 32];
340
341 keystream[0..16].copy_from_slice(nonce);
343 keystream[16] = 0x80;
344 keystream[28] = (counter >> 24) as u8;
346 keystream[29] = (counter >> 16) as u8;
347 keystream[30] = (counter >> 8) as u8;
348 keystream[31] = counter as u8;
349
350 core.encrypt_block_32(key32, &mut keystream)?;
352
353 let remaining = data.len() - offset;
354 let block_len = remaining.min(32);
355 #[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
356 {
357 if block_len == 32 {
358 let mut input = [0u8; 32];
359 input.copy_from_slice(&data[offset..offset + 32]);
360 let mut out = [0u8; 32];
361 simd_xor::xor_blocks_32(&input, &keystream, &mut out);
362 data[offset..offset + 32].copy_from_slice(&out);
363 } else {
364 for i in 0..block_len {
365 data[offset + i] ^= keystream[i];
366 }
367 }
368 }
369
370 #[cfg(not(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon")))]
371 {
372 for i in 0..block_len {
373 data[offset + i] ^= keystream[i];
374 }
375 }
376
377 offset += block_len;
378 counter = counter.wrapping_add(1);
379 }
380
381 Ok(())
382 }
383
384 fn decrypt_core(
391 &self,
392 key: &[u8],
393 nonce: &[u8],
394 ciphertext: &[u8],
395 associated_data: Option<&[u8]>,
396 ) -> Result<DecryptSemanticOutcome> {
397 if key.len() != Self::key_size() {
398 return Err(Error::InvalidKeySize {
399 expected: Self::key_size(),
400 actual: key.len(),
401 });
402 }
403
404 if nonce.len() != Self::nonce_size() {
405 return Err(Error::InvalidNonceSize {
406 expected: Self::nonce_size(),
407 actual: nonce.len(),
408 });
409 }
410
411 if (ciphertext.len() >> 5) >= 0xFFFFFFFE {
412 return Err(Error::InvalidMessageSize {
413 max: 0xFFFFFFFE << 5,
414 actual: ciphertext.len(),
415 });
416 }
417
418 if ciphertext.len() < Self::tag_size() {
419 return Err(Error::aead_ciphertext_shorter_than_tag(
420 Self::tag_size(),
421 ciphertext.len(),
422 ));
423 }
424
425 let ad = associated_data.unwrap_or(&[]);
426 let plaintext_len = ciphertext.len() - 32;
427 let ciphertext_data = &ciphertext[0..plaintext_len];
428 let received_tag = &ciphertext[plaintext_len..];
429
430 let mut key_staged = Zeroizing::new([0u8; 32]);
431 key_staged.copy_from_slice(key);
432 let mut nonce_staged = Zeroizing::new([0u8; 16]);
433 nonce_staged.copy_from_slice(nonce);
434 let kb = key_staged.as_slice();
435 let nb = nonce_staged.as_slice();
436
437 let mut tag = self.cascade_init(kb, nb)?;
438 self.cascade(&mut tag, 2, 3, ad)?;
439 self.cascade(&mut tag, 4, 5, ciphertext_data)?;
440
441 let tag_valid = lib_q_core::Utils::constant_time_compare(&*tag, received_tag);
442
443 let mut plaintext = ciphertext_data.to_vec();
444 if let Err(e) = self.ctr_encrypt(kb, nb, &mut plaintext) {
445 plaintext.zeroize();
446 return Err(e);
447 }
448
449 if tag_valid {
450 Ok(DecryptSemanticOutcome::Success(Zeroizing::new(plaintext)))
451 } else {
452 plaintext.zeroize();
453 Ok(DecryptSemanticOutcome::AuthenticationFailed)
454 }
455 }
456
457 pub fn encrypt_bytes(
461 &self,
462 key: &[u8],
463 nonce: &[u8],
464 plaintext: &[u8],
465 associated_data: Option<&[u8]>,
466 ) -> Result<Vec<u8>> {
467 if key.len() != Self::key_size() {
468 return Err(Error::InvalidKeySize {
469 expected: Self::key_size(),
470 actual: key.len(),
471 });
472 }
473
474 if nonce.len() != Self::nonce_size() {
475 return Err(Error::InvalidNonceSize {
476 expected: Self::nonce_size(),
477 actual: nonce.len(),
478 });
479 }
480
481 if (plaintext.len() >> 5) >= 0xFFFFFFFD {
483 return Err(Error::InvalidMessageSize {
484 max: 0xFFFFFFFD << 5,
485 actual: plaintext.len(),
486 });
487 }
488
489 let ad = associated_data.unwrap_or(&[]);
490
491 let mut key_staged = Zeroizing::new([0u8; 32]);
492 key_staged.copy_from_slice(key);
493 let mut nonce_staged = Zeroizing::new([0u8; 16]);
494 nonce_staged.copy_from_slice(nonce);
495 let kb = key_staged.as_slice();
496 let nb = nonce_staged.as_slice();
497
498 let mut tag = self.cascade_init(kb, nb)?;
500
501 self.cascade(&mut tag, 2, 3, ad)?;
503
504 let mut ciphertext = plaintext.to_vec();
506 if let Err(e) = self.ctr_encrypt(kb, nb, &mut ciphertext) {
507 ciphertext.zeroize();
508 return Err(e);
509 }
510
511 self.cascade(&mut tag, 4, 5, &ciphertext)?;
513
514 ciphertext.extend_from_slice(&*tag);
516
517 Ok(ciphertext)
518 }
519
520 pub fn decrypt_bytes(
523 &self,
524 key: &[u8],
525 nonce: &[u8],
526 ciphertext: &[u8],
527 associated_data: Option<&[u8]>,
528 ) -> Result<Vec<u8>> {
529 match self.decrypt_core(key, nonce, ciphertext, associated_data) {
530 Ok(DecryptSemanticOutcome::Success(p)) => Ok(Vec::clone(&*p)),
531 Ok(DecryptSemanticOutcome::AuthenticationFailed) => Err(Error::VerificationFailed {
532 operation: "AEAD tag verification".to_string(),
533 }),
534 Err(e) => Err(e),
535 }
536 }
537}
538
539impl Aead for SaturninAead {
540 fn encrypt(
551 &self,
552 key: &AeadKey,
553 nonce: &Nonce,
554 plaintext: &[u8],
555 associated_data: Option<&[u8]>,
556 ) -> Result<Vec<u8>> {
557 self.encrypt_bytes(key.as_bytes(), nonce.as_bytes(), plaintext, associated_data)
558 }
559
560 fn decrypt(
562 &self,
563 key: &AeadKey,
564 nonce: &Nonce,
565 ciphertext: &[u8],
566 associated_data: Option<&[u8]>,
567 ) -> Result<Vec<u8>> {
568 self.decrypt_bytes(
569 key.as_bytes(),
570 nonce.as_bytes(),
571 ciphertext,
572 associated_data,
573 )
574 }
575}
576
577impl AeadDecryptSemantic for SaturninAead {
578 fn decrypt_semantic(
580 &self,
581 key: &AeadKey,
582 nonce: &Nonce,
583 ciphertext: &[u8],
584 associated_data: Option<&[u8]>,
585 ) -> Result<DecryptSemanticOutcome> {
586 self.decrypt_core(
587 key.as_bytes(),
588 nonce.as_bytes(),
589 ciphertext,
590 associated_data,
591 )
592 }
593}
594
595impl Default for SaturninAead {
596 fn default() -> Self {
597 Self::new()
598 }
599}
600
601#[cfg(test)]
602mod tests {
603 #[cfg(feature = "alloc")]
604 use alloc::vec;
605
606 use super::*;
607
608 #[test]
609 fn test_saturnin_creation() {
610 let _aead = SaturninAead::new();
611 }
614
615 #[test]
616 fn test_saturnin_constants() {
617 assert_eq!(SaturninAead::key_size(), 32);
618 assert_eq!(SaturninAead::nonce_size(), 16);
619 assert_eq!(SaturninAead::tag_size(), 32);
620 }
621
622 #[test]
623 fn test_saturnin_encrypt_decrypt_round_trip() -> Result<()> {
624 let aead = SaturninAead::new();
625 let key = AeadKey::new(vec![0u8; 32]);
626 let nonce = Nonce::new(vec![0u8; 16]);
627 let plaintext = b"test"; let ad: Option<&[u8]> = None;
629
630 let ciphertext = aead.encrypt(&key, &nonce, plaintext, ad)?;
632 assert_eq!(ciphertext.len(), plaintext.len() + 32); let decrypted = aead.decrypt(&key, &nonce, &ciphertext, ad)?;
636 assert_eq!(decrypted, plaintext);
637
638 Ok(())
639 }
640
641 #[test]
642 fn test_saturnin_decrypt_semantic_bad_tag() -> Result<()> {
643 use lib_q_core::AeadDecryptSemantic;
644
645 let aead = SaturninAead::new();
646 let key = AeadKey::new(vec![7u8; 32]);
647 let nonce = Nonce::new(vec![8u8; 16]);
648 let ad: Option<&[u8]> = Some(b"ad");
649 let ct = aead.encrypt(&key, &nonce, b"m", ad)?;
650 let mut bad = ct.clone();
651 *bad.last_mut().expect("tag") ^= 0x40;
652 let out = aead.decrypt_semantic(&key, &nonce, &bad, ad)?;
653 assert_eq!(out, DecryptSemanticOutcome::AuthenticationFailed);
654 assert!(matches!(
655 aead.decrypt(&key, &nonce, &bad, ad),
656 Err(Error::VerificationFailed { .. })
657 ));
658 match aead.decrypt_semantic(&key, &nonce, &ct, ad)? {
659 DecryptSemanticOutcome::Success(pt) => assert_eq!(pt.as_slice(), b"m"),
660 DecryptSemanticOutcome::AuthenticationFailed => {
661 panic!("unexpected auth failure on good ciphertext")
662 }
663 }
664 Ok(())
665 }
666
667 #[test]
668 fn test_saturnin_default_matches_new() {
669 let aead = SaturninAead::default();
672 let key = AeadKey::new(vec![3u8; 32]);
673 let nonce = Nonce::new(vec![4u8; 16]);
674 let ct = aead
675 .encrypt(&key, &nonce, b"via-default", None)
676 .expect("default-constructed AEAD must encrypt");
677 let pt = aead
678 .decrypt(&key, &nonce, &ct, None)
679 .expect("default-constructed AEAD must decrypt its own ciphertext");
680 assert_eq!(pt, b"via-default");
681 }
682
683 #[test]
684 fn test_encrypt_bytes_rejects_wrong_key_size() {
685 let aead = SaturninAead::new();
686 let err = aead
687 .encrypt_bytes(&[0u8; 31], &[0u8; 16], b"m", None)
688 .expect_err("31-byte key must be rejected");
689 assert!(matches!(
690 err,
691 Error::InvalidKeySize {
692 expected: 32,
693 actual: 31
694 }
695 ));
696 }
697
698 #[test]
699 fn test_encrypt_bytes_rejects_wrong_nonce_size() {
700 let aead = SaturninAead::new();
701 let err = aead
702 .encrypt_bytes(&[0u8; 32], &[0u8; 15], b"m", None)
703 .expect_err("15-byte nonce must be rejected");
704 assert!(matches!(
705 err,
706 Error::InvalidNonceSize {
707 expected: 16,
708 actual: 15
709 }
710 ));
711 }
712
713 #[test]
714 fn test_decrypt_bytes_rejects_wrong_key_size() {
715 let aead = SaturninAead::new();
716 let err = aead
717 .decrypt_bytes(&[0u8; 20], &[0u8; 16], &[0u8; 32], None)
718 .expect_err("20-byte key must be rejected");
719 assert!(matches!(
720 err,
721 Error::InvalidKeySize {
722 expected: 32,
723 actual: 20
724 }
725 ));
726 }
727
728 #[test]
729 fn test_decrypt_bytes_rejects_wrong_nonce_size() {
730 let aead = SaturninAead::new();
731 let err = aead
732 .decrypt_bytes(&[0u8; 32], &[0u8; 4], &[0u8; 32], None)
733 .expect_err("4-byte nonce must be rejected");
734 assert!(matches!(
735 err,
736 Error::InvalidNonceSize {
737 expected: 16,
738 actual: 4
739 }
740 ));
741 }
742
743 #[test]
744 fn test_decrypt_bytes_rejects_ciphertext_shorter_than_tag() {
745 let aead = SaturninAead::new();
746 let err = aead
748 .decrypt_bytes(&[0u8; 32], &[0u8; 16], &[0u8; 10], None)
749 .expect_err("ciphertext shorter than the tag must be rejected");
750 assert!(matches!(err, Error::InvalidCiphertextSize { .. }));
751 }
752
753 #[test]
754 fn test_round_trip_across_block_boundary_sizes() -> Result<()> {
755 let aead = SaturninAead::new();
759 let key = AeadKey::new(vec![9u8; 32]);
760 let nonce = Nonce::new(vec![5u8; 16]);
761 for len in [0usize, 1, 31, 32, 33, 63, 64, 65] {
762 let plaintext = vec![0xAAu8; len];
763 for ad_len in [0usize, 32] {
764 let ad = vec![0x55u8; ad_len];
765 let ad_opt = Some(ad.as_slice());
766 let ct = aead.encrypt(&key, &nonce, &plaintext, ad_opt)?;
767 assert_eq!(ct.len(), len + 32);
768 let pt = aead.decrypt(&key, &nonce, &ct, ad_opt)?;
769 assert_eq!(
770 pt, plaintext,
771 "round trip failed for len={len}, ad_len={ad_len}"
772 );
773 }
774 }
775 Ok(())
776 }
777
778 #[test]
779 fn test_round_trip_crosses_avx2_ctr_batch_threshold() -> Result<()> {
780 let aead = SaturninAead::new();
785 let key = AeadKey::new(vec![0x71u8; 32]);
786 let nonce = Nonce::new(vec![0x62u8; 16]);
787 let plaintext = vec![0xC3u8; 600];
788 let ad = vec![0x5Au8; 17];
789
790 let ct = aead.encrypt(&key, &nonce, &plaintext, Some(&ad))?;
791 assert_eq!(ct.len(), plaintext.len() + 32);
792 let pt = aead.decrypt(&key, &nonce, &ct, Some(&ad))?;
793 assert_eq!(pt, plaintext);
794 Ok(())
795 }
796}