1#[cfg(feature = "alloc")]
65use alloc::{
66 string::ToString,
67 vec::Vec,
68};
69
70use lib_q_core::{
71 Aead,
72 AeadDecryptSemantic,
73 AeadKey,
74 DecryptSemanticOutcome,
75 Error,
76 Nonce,
77 Result,
78};
79use zeroize::{
80 Zeroize,
81 Zeroizing,
82};
83
84use crate::core::SaturninCore;
85#[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
86use crate::simd::{
87 encrypt_blocks8_dispatch,
88 simd_xor,
89};
90
91struct SaturninAeadCores {
96 d1: SaturninCore,
97 d2: SaturninCore,
98 d3: SaturninCore,
99 d4: SaturninCore,
100 d5: SaturninCore,
101}
102
103impl SaturninAeadCores {
104 fn new() -> Result<Self> {
105 Ok(Self {
106 d1: SaturninCore::new(10, 1)?,
107 d2: SaturninCore::new(10, 2)?,
108 d3: SaturninCore::new(10, 3)?,
109 d4: SaturninCore::new(10, 4)?,
110 d5: SaturninCore::new(10, 5)?,
111 })
112 }
113
114 #[inline]
115 fn domain(&self, d: u8) -> &SaturninCore {
116 match d {
117 1 => &self.d1,
118 2 => &self.d2,
119 3 => &self.d3,
120 4 => &self.d4,
121 5 => &self.d5,
122 _ => unreachable!("AEAD CTR/cascade only uses domains 1–5"),
123 }
124 }
125}
126
127pub struct SaturninAead {
133 cores: SaturninAeadCores,
134}
135
136impl SaturninAead {
137 pub fn new() -> Self {
139 Self {
140 cores: SaturninAeadCores::new().expect("Saturnin AEAD uses fixed valid domains"),
141 }
142 }
143
144 pub const fn key_size() -> usize {
146 32
147 }
148
149 pub const fn nonce_size() -> usize {
151 16
152 }
153
154 pub const fn tag_size() -> usize {
156 32
157 }
158
159 fn cascade_init(&self, key: &[u8], nonce: &[u8]) -> Result<Zeroizing<[u8; 32]>> {
161 let key32: &[u8; 32] = key.try_into().map_err(|_| Error::InvalidKeySize {
162 expected: 32,
163 actual: key.len(),
164 })?;
165
166 let mut r = Zeroizing::new([0u8; 32]);
167
168 r[0..16].copy_from_slice(nonce);
170 r[16] = 0x80;
171 self.cores.d2.encrypt_block_32(key32, &mut r)?;
175
176 for i in 0..16 {
178 r[i] ^= nonce[i];
179 }
180 r[16] ^= 0x80;
181
182 Ok(r)
183 }
184
185 fn cascade(&self, r: &mut [u8; 32], d1: u8, d2: u8, data: &[u8]) -> Result<()> {
187 let core_d1 = self.cores.domain(d1);
188 let core_d2 = self.cores.domain(d2);
189
190 let mut offset = 0;
191
192 loop {
193 let mut t: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
194 let mut m: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
195 let remaining = data.len() - offset;
196
197 if remaining >= 32 {
198 t.copy_from_slice(&data[offset..offset + 32]);
199 offset += 32;
200
201 m.copy_from_slice(&*t);
203 core_d1.encrypt_block_32(&*r, &mut m)?;
204 } else {
205 t[0..remaining].copy_from_slice(&data[offset..]);
206 t[remaining] = 0x80;
207 m.copy_from_slice(&*t);
211 core_d2.encrypt_block_32(&*r, &mut m)?;
212 }
213
214 #[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
215 {
216 let mut out: Zeroizing<[u8; 32]> = Zeroizing::new([0u8; 32]);
217 simd_xor::xor_blocks_32(&m, &t, &mut out);
218 r.copy_from_slice(&*out);
219 }
220
221 #[cfg(not(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon")))]
222 {
223 for i in 0..32 {
224 r[i] = m[i] ^ t[i];
225 }
226 }
227
228 if remaining < 32 {
229 break;
230 }
231 }
232
233 Ok(())
234 }
235
236 fn ctr_encrypt(&self, key: &[u8], nonce: &[u8], data: &mut [u8]) -> Result<()> {
238 let key32: &[u8; 32] = key.try_into().map_err(|_| Error::InvalidKeySize {
239 expected: 32,
240 actual: key.len(),
241 })?;
242
243 let core = &self.cores.d1;
244
245 let mut counter = 1u32; let mut offset = 0;
247
248 while offset < data.len() {
249 #[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
250 if data.len() - offset >= 32 * 8 {
251 let mut keystream_blocks = [[0u8; 32]; 8];
252 for (lane, block) in keystream_blocks.iter_mut().enumerate() {
253 let c = counter.wrapping_add(lane as u32);
254 block[0..16].copy_from_slice(nonce);
255 block[16] = 0x80;
256 block[28] = (c >> 24) as u8;
257 block[29] = (c >> 16) as u8;
258 block[30] = (c >> 8) as u8;
259 block[31] = c as u8;
260 }
261
262 encrypt_blocks8_dispatch(10, 1, key, &mut keystream_blocks, Some(core))?;
263
264 for (lane, ks) in keystream_blocks.iter().enumerate() {
265 let start = offset + (lane * 32);
266 let mut input = [0u8; 32];
267 input.copy_from_slice(&data[start..start + 32]);
268 let mut out = [0u8; 32];
269 simd_xor::xor_blocks_32(&input, ks, &mut out);
270 data[start..start + 32].copy_from_slice(&out);
271 }
272
273 offset += 32 * 8;
274 let (next_counter, overflowed) = counter.overflowing_add(8);
275 if overflowed {
276 return Err(Error::InvalidMessageSize {
277 max: usize::MAX,
278 actual: data.len(),
279 });
280 }
281 counter = next_counter;
282 continue;
283 }
284
285 let mut keystream = [0u8; 32];
286
287 keystream[0..16].copy_from_slice(nonce);
289 keystream[16] = 0x80;
290 keystream[28] = (counter >> 24) as u8;
292 keystream[29] = (counter >> 16) as u8;
293 keystream[30] = (counter >> 8) as u8;
294 keystream[31] = counter as u8;
295
296 core.encrypt_block_32(key32, &mut keystream)?;
298
299 let remaining = data.len() - offset;
300 let block_len = remaining.min(32);
301 #[cfg(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon"))]
302 {
303 if block_len == 32 {
304 let mut input = [0u8; 32];
305 input.copy_from_slice(&data[offset..offset + 32]);
306 let mut out = [0u8; 32];
307 simd_xor::xor_blocks_32(&input, &keystream, &mut out);
308 data[offset..offset + 32].copy_from_slice(&out);
309 } else {
310 for i in 0..block_len {
311 data[offset + i] ^= keystream[i];
312 }
313 }
314 }
315
316 #[cfg(not(any(feature = "simd", feature = "simd-avx2", feature = "simd-neon")))]
317 {
318 for i in 0..block_len {
319 data[offset + i] ^= keystream[i];
320 }
321 }
322
323 offset += block_len;
324 counter = counter.wrapping_add(1);
325 }
326
327 Ok(())
328 }
329
330 fn decrypt_core(
337 &self,
338 key: &[u8],
339 nonce: &[u8],
340 ciphertext: &[u8],
341 associated_data: Option<&[u8]>,
342 ) -> Result<DecryptSemanticOutcome> {
343 if key.len() != Self::key_size() {
344 return Err(Error::InvalidKeySize {
345 expected: Self::key_size(),
346 actual: key.len(),
347 });
348 }
349
350 if nonce.len() != Self::nonce_size() {
351 return Err(Error::InvalidNonceSize {
352 expected: Self::nonce_size(),
353 actual: nonce.len(),
354 });
355 }
356
357 if (ciphertext.len() >> 5) >= 0xFFFFFFFE {
358 return Err(Error::InvalidMessageSize {
359 max: 0xFFFFFFFE << 5,
360 actual: ciphertext.len(),
361 });
362 }
363
364 if ciphertext.len() < Self::tag_size() {
365 return Err(Error::aead_ciphertext_shorter_than_tag(
366 Self::tag_size(),
367 ciphertext.len(),
368 ));
369 }
370
371 let ad = associated_data.unwrap_or(&[]);
372 let plaintext_len = ciphertext.len() - 32;
373 let ciphertext_data = &ciphertext[0..plaintext_len];
374 let received_tag = &ciphertext[plaintext_len..];
375
376 let mut key_staged = Zeroizing::new([0u8; 32]);
377 key_staged.copy_from_slice(key);
378 let mut nonce_staged = Zeroizing::new([0u8; 16]);
379 nonce_staged.copy_from_slice(nonce);
380 let kb = key_staged.as_slice();
381 let nb = nonce_staged.as_slice();
382
383 let mut tag = self.cascade_init(kb, nb)?;
384 self.cascade(&mut tag, 2, 3, ad)?;
385 self.cascade(&mut tag, 4, 5, ciphertext_data)?;
386
387 let tag_valid = lib_q_core::Utils::constant_time_compare(&*tag, received_tag);
388
389 let mut plaintext = ciphertext_data.to_vec();
390 if let Err(e) = self.ctr_encrypt(kb, nb, &mut plaintext) {
391 plaintext.zeroize();
392 return Err(e);
393 }
394
395 if tag_valid {
396 Ok(DecryptSemanticOutcome::Success(Zeroizing::new(plaintext)))
397 } else {
398 plaintext.zeroize();
399 Ok(DecryptSemanticOutcome::AuthenticationFailed)
400 }
401 }
402
403 pub fn encrypt_bytes(
407 &self,
408 key: &[u8],
409 nonce: &[u8],
410 plaintext: &[u8],
411 associated_data: Option<&[u8]>,
412 ) -> Result<Vec<u8>> {
413 if key.len() != Self::key_size() {
414 return Err(Error::InvalidKeySize {
415 expected: Self::key_size(),
416 actual: key.len(),
417 });
418 }
419
420 if nonce.len() != Self::nonce_size() {
421 return Err(Error::InvalidNonceSize {
422 expected: Self::nonce_size(),
423 actual: nonce.len(),
424 });
425 }
426
427 if (plaintext.len() >> 5) >= 0xFFFFFFFD {
429 return Err(Error::InvalidMessageSize {
430 max: 0xFFFFFFFD << 5,
431 actual: plaintext.len(),
432 });
433 }
434
435 let ad = associated_data.unwrap_or(&[]);
436
437 let mut key_staged = Zeroizing::new([0u8; 32]);
438 key_staged.copy_from_slice(key);
439 let mut nonce_staged = Zeroizing::new([0u8; 16]);
440 nonce_staged.copy_from_slice(nonce);
441 let kb = key_staged.as_slice();
442 let nb = nonce_staged.as_slice();
443
444 let mut tag = self.cascade_init(kb, nb)?;
446
447 self.cascade(&mut tag, 2, 3, ad)?;
449
450 let mut ciphertext = plaintext.to_vec();
452 if let Err(e) = self.ctr_encrypt(kb, nb, &mut ciphertext) {
453 ciphertext.zeroize();
454 return Err(e);
455 }
456
457 self.cascade(&mut tag, 4, 5, &ciphertext)?;
459
460 ciphertext.extend_from_slice(&*tag);
462
463 Ok(ciphertext)
464 }
465
466 pub fn decrypt_bytes(
469 &self,
470 key: &[u8],
471 nonce: &[u8],
472 ciphertext: &[u8],
473 associated_data: Option<&[u8]>,
474 ) -> Result<Vec<u8>> {
475 match self.decrypt_core(key, nonce, ciphertext, associated_data) {
476 Ok(DecryptSemanticOutcome::Success(p)) => Ok(Vec::clone(&*p)),
477 Ok(DecryptSemanticOutcome::AuthenticationFailed) => Err(Error::VerificationFailed {
478 operation: "AEAD tag verification".to_string(),
479 }),
480 Err(e) => Err(e),
481 }
482 }
483}
484
485impl Aead for SaturninAead {
486 fn encrypt(
497 &self,
498 key: &AeadKey,
499 nonce: &Nonce,
500 plaintext: &[u8],
501 associated_data: Option<&[u8]>,
502 ) -> Result<Vec<u8>> {
503 self.encrypt_bytes(key.as_bytes(), nonce.as_bytes(), plaintext, associated_data)
504 }
505
506 fn decrypt(
508 &self,
509 key: &AeadKey,
510 nonce: &Nonce,
511 ciphertext: &[u8],
512 associated_data: Option<&[u8]>,
513 ) -> Result<Vec<u8>> {
514 self.decrypt_bytes(
515 key.as_bytes(),
516 nonce.as_bytes(),
517 ciphertext,
518 associated_data,
519 )
520 }
521}
522
523impl AeadDecryptSemantic for SaturninAead {
524 fn decrypt_semantic(
526 &self,
527 key: &AeadKey,
528 nonce: &Nonce,
529 ciphertext: &[u8],
530 associated_data: Option<&[u8]>,
531 ) -> Result<DecryptSemanticOutcome> {
532 self.decrypt_core(
533 key.as_bytes(),
534 nonce.as_bytes(),
535 ciphertext,
536 associated_data,
537 )
538 }
539}
540
541impl Default for SaturninAead {
542 fn default() -> Self {
543 Self::new()
544 }
545}
546
547#[cfg(test)]
548mod tests {
549 #[cfg(feature = "alloc")]
550 use alloc::vec;
551
552 use super::*;
553
554 #[test]
555 fn test_saturnin_creation() {
556 let _aead = SaturninAead::new();
557 }
560
561 #[test]
562 fn test_saturnin_constants() {
563 assert_eq!(SaturninAead::key_size(), 32);
564 assert_eq!(SaturninAead::nonce_size(), 16);
565 assert_eq!(SaturninAead::tag_size(), 32);
566 }
567
568 #[test]
569 fn test_saturnin_encrypt_decrypt_round_trip() -> Result<()> {
570 let aead = SaturninAead::new();
571 let key = AeadKey::new(vec![0u8; 32]);
572 let nonce = Nonce::new(vec![0u8; 16]);
573 let plaintext = b"test"; let ad: Option<&[u8]> = None;
575
576 let ciphertext = aead.encrypt(&key, &nonce, plaintext, ad)?;
578 assert_eq!(ciphertext.len(), plaintext.len() + 32); let decrypted = aead.decrypt(&key, &nonce, &ciphertext, ad)?;
582 assert_eq!(decrypted, plaintext);
583
584 Ok(())
585 }
586
587 #[test]
588 fn test_saturnin_decrypt_semantic_bad_tag() -> Result<()> {
589 use lib_q_core::AeadDecryptSemantic;
590
591 let aead = SaturninAead::new();
592 let key = AeadKey::new(vec![7u8; 32]);
593 let nonce = Nonce::new(vec![8u8; 16]);
594 let ad: Option<&[u8]> = Some(b"ad");
595 let ct = aead.encrypt(&key, &nonce, b"m", ad)?;
596 let mut bad = ct.clone();
597 *bad.last_mut().expect("tag") ^= 0x40;
598 let out = aead.decrypt_semantic(&key, &nonce, &bad, ad)?;
599 assert_eq!(out, DecryptSemanticOutcome::AuthenticationFailed);
600 assert!(matches!(
601 aead.decrypt(&key, &nonce, &bad, ad),
602 Err(Error::VerificationFailed { .. })
603 ));
604 match aead.decrypt_semantic(&key, &nonce, &ct, ad)? {
605 DecryptSemanticOutcome::Success(pt) => assert_eq!(pt.as_slice(), b"m"),
606 DecryptSemanticOutcome::AuthenticationFailed => {
607 panic!("unexpected auth failure on good ciphertext")
608 }
609 }
610 Ok(())
611 }
612}