Skip to main content

lib_q_saturnin/
aead.rs

1//! Saturnin AEAD implementation
2//!
3//! Saturnin is a lightweight post-quantum symmetric algorithm suite designed
4//! for IoT and constrained devices, providing authenticated encryption and
5//! hashing modes with superior post-quantum security.
6//!
7//! ## Usage Example
8//!
9//! ```rust
10//! use lib_q_saturnin::{
11//!     Aead,
12//!     AeadKey,
13//!     Nonce,
14//!     SaturninAead,
15//! };
16//!
17//! // Create AEAD instance
18//! let aead = SaturninAead::new();
19//!
20//! // Generate key and nonce (in practice, use secure random generation)
21//! let key = AeadKey::new(vec![0u8; 32]);
22//! let nonce = Nonce::new(vec![0u8; 16]);
23//!
24//! let plaintext = b"Secret message";
25//! let associated_data = b"metadata";
26//!
27//! // Encrypt with associated data
28//! let ciphertext = aead
29//!     .encrypt(&key, &nonce, plaintext, Some(associated_data))
30//!     .unwrap();
31//!
32//! // Decrypt and verify authenticity
33//! let decrypted = aead
34//!     .decrypt(&key, &nonce, &ciphertext, Some(associated_data))
35//!     .unwrap();
36//! assert_eq!(decrypted, plaintext);
37//! ```
38//!
39//! ## Performance Notes
40//!
41//! - **Key size**: 256 bits (32 bytes)
42//! - **Nonce size**: 128 bits (16 bytes)  
43//! - **Tag size**: 256 bits (32 bytes)
44//! - **Throughput**: ~100-500 MB/s on modern hardware
45//! - **Memory usage**: Small fixed state (pre-built cipher cores for domains 1–5); per-message
46//!   key/nonce are staged in zeroizing buffers at the `Aead` boundary, and the cascade running tag
47//!   plus per-iteration cascade blocks (`t`, `m`, and SIMD xor staging) are held in `Zeroizing`
48//!   buffers so they are cleared on drop.
49//!
50//! ## Verification timing
51//!
52//! Decrypt computes the expected tag over AAD and ciphertext (cascade), compares it to the
53//! appended tag with [`lib_q_core::Utils::constant_time_compare`](lib_q_core::Utils::constant_time_compare),
54//! then **always** runs full CTR on the ciphertext body. Only after that does the API return
55//! `Ok(plaintext)` versus `Err(Error::VerificationFailed)` (Layer A) for a failed tag after that
56//! schedule, or `Ok(DecryptSemanticOutcome::AuthenticationFailed)` (Layer B). Ciphertext shorter
57//! than the tag is rejected up front as `Err(Error::InvalidCiphertextSize)` (operational). Failed
58//! plaintext buffers are zeroized. This matches the [`lib_q_core::Aead`] contract in
59//! `lib-q-core`: bulk symmetric work is not skipped on auth failure; the public `Result` / outcome
60//! still discriminates at the boundary. For semantic decrypt without plaintext on authentication
61//! failure, see [`lib_q_core::AeadDecryptSemantic`]. See this crate’s
62//! `SECURITY.md` for Saturnin-Short specifics.
63
64#[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
91/// Pre-built Saturnin cores for CTR-Cascade AEAD (10 super-rounds, domains 1–5).
92///
93/// Building these once per [`SaturninAead`] avoids repeated `Vec` allocation of round constants
94/// on every encrypt/decrypt (domains 1–5 cover CTR and all cascade steps).
95struct 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
127/// Saturnin AEAD implementation
128///
129/// Provides authenticated encryption using the Saturnin CTR-Cascade mode.
130/// This is the full AEAD mode that supports associated data and arbitrary
131/// length plaintexts.
132pub struct SaturninAead {
133    cores: SaturninAeadCores,
134}
135
136impl SaturninAead {
137    /// Create a new Saturnin AEAD instance
138    pub fn new() -> Self {
139        Self {
140            cores: SaturninAeadCores::new().expect("Saturnin AEAD uses fixed valid domains"),
141        }
142    }
143
144    /// Get the key size in bytes (256 bits = 32 bytes)
145    pub const fn key_size() -> usize {
146        32
147    }
148
149    /// Get the nonce size in bytes (128 bits = 16 bytes)
150    pub const fn nonce_size() -> usize {
151        16
152    }
153
154    /// Get the tag size in bytes (256 bits = 32 bytes)
155    pub const fn tag_size() -> usize {
156        32
157    }
158
159    /// Initialize the cascade state
160    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        // Copy nonce to first 16 bytes
169        r[0..16].copy_from_slice(nonce);
170        r[16] = 0x80;
171        // Remaining bytes are already zero
172
173        // Encrypt with cascade parameters: 10 super-rounds, domain 2 (AAD1)
174        self.cores.d2.encrypt_block_32(key32, &mut r)?;
175
176        // XOR with nonce
177        for i in 0..16 {
178            r[i] ^= nonce[i];
179        }
180        r[16] ^= 0x80;
181
182        Ok(r)
183    }
184
185    /// Apply cascade construction to data (optimized)
186    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                // Use pre-allocated core for d1
202                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                // Remaining bytes are already zero
208
209                // Use pre-allocated core for d2
210                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    /// CTR encryption/decryption (optimized)
237    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; // Counter starts at 1
246        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            // Build counter block efficiently
288            keystream[0..16].copy_from_slice(nonce);
289            keystream[16] = 0x80;
290            // Bytes 17-27 are zero
291            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            // Encrypt to get keystream
297            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    /// Shared decrypt core for Layer A ([`Aead::decrypt`](lib_q_core::Aead::decrypt)) and Layer B
331    /// ([`AeadDecryptSemantic::decrypt_semantic`](lib_q_core::AeadDecryptSemantic::decrypt_semantic)).
332    ///
333    /// Takes key/nonce as byte slices: the `Aead`/`AeadDecryptSemantic` trait methods forward
334    /// `key.as_bytes()`/`nonce.as_bytes()` here, and the allocation-free [`Self::decrypt_bytes`]
335    /// passes its slices directly — neither path materializes an `AeadKey`/`Nonce` wrapper.
336    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    /// Allocation-free encrypt: takes key/nonce as byte slices, avoiding the `AeadKey`/`Nonce`
404    /// `Vec` wrappers that [`Aead::encrypt`] requires. Per-packet callers (e.g. per-packet record sealing)
405    /// use this to skip two heap allocations on every record; the trait method forwards here.
406    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        // Check length limits (about 137.4 GB)
428        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        // Initialize cascade state
445        let mut tag = self.cascade_init(kb, nb)?;
446
447        // Process associated data
448        self.cascade(&mut tag, 2, 3, ad)?;
449
450        // Encrypt plaintext with CTR
451        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        // Continue cascade on ciphertext
458        self.cascade(&mut tag, 4, 5, &ciphertext)?;
459
460        // Append tag
461        ciphertext.extend_from_slice(&*tag);
462
463        Ok(ciphertext)
464    }
465
466    /// Allocation-free Layer A decrypt: byte-slice counterpart to [`Aead::decrypt`]. Returns the
467    /// plaintext on success, or [`Error::VerificationFailed`] on tag mismatch.
468    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    /// Encrypt data with authentication
487    ///
488    /// # Arguments
489    /// * `key` - 256-bit encryption key
490    /// * `nonce` - 128-bit nonce
491    /// * `plaintext` - Data to encrypt
492    /// * `associated_data` - Additional authenticated data
493    ///
494    /// # Returns
495    /// Encrypted data with authentication tag appended
496    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    /// Decrypt and verify data (Layer A); shares one decrypt core with [`lib_q_core::AeadDecryptSemantic`].
507    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    /// Layer B semantic decrypt; see `docs/adr/003-aead-decrypt-layers.md`.
525    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        // Saturnin implementation created successfully
558        // Test passes if we reach this point without panicking
559    }
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"; // 4 bytes
574        let ad: Option<&[u8]> = None;
575
576        // Test encryption
577        let ciphertext = aead.encrypt(&key, &nonce, plaintext, ad)?;
578        assert_eq!(ciphertext.len(), plaintext.len() + 32); // plaintext + 32-byte tag
579
580        // Test decryption
581        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}