Skip to main content

oxicrypto_aead/
lib.rs

1#![forbid(unsafe_code)]
2
3//! Pure Rust AEAD implementations for the OxiCrypto stack.
4//!
5//! | Algorithm | Module | Key / Nonce |
6//! |-----------|--------|-------------|
7//! | AES-128-GCM | (inline) | 16 / 12 bytes |
8//! | AES-256-GCM | (inline) | 32 / 12 bytes |
9//! | ChaCha20-Poly1305 | (inline) | 32 / 12 bytes |
10//! | AES-128-GCM-SIV | [`aes_gcm_siv`] | 16 / 12 bytes |
11//! | AES-256-GCM-SIV | [`aes_gcm_siv`] | 32 / 12 bytes |
12//! | XChaCha20-Poly1305 | [`xchacha20`] | 32 / 24 bytes |
13//! | AES-128-CCM | [`ccm`] | 16 / 13 bytes |
14//! | AES-256-CCM | [`ccm`] | 32 / 13 bytes |
15//! | AES-128-OCB3 | [`ocb3_impl`] | 16 / 12 bytes |
16//! | AES-256-OCB3 | [`ocb3_impl`] | 32 / 12 bytes |
17//! | Deoxys-II-128-128 | [`deoxys`] | 16 / 16 bytes |
18//!
19//! # Streaming AEAD (STREAM construction)
20//!
21//! [`stream::Aes256GcmStream`] and [`stream::ChaCha20Poly1305Stream`] implement
22//! the `StreamingAead` trait using the STREAM chunked construction
23//! (Hoang-Reyhanitabar-Rogaway-Vizár 2015).
24//!
25//! # Nonce sequences
26//!
27//! [`nonce_seq::Nonce12`] and [`nonce_seq::Nonce24`] provide monotonic nonce
28//! generators suitable for AES-GCM / XChaCha20 respectively.
29//!
30//! # Key Wrap (RFC 3394)
31//!
32//! [`keywrap`] provides AES-128-KW and AES-256-KW for wrapping key material.
33//! This is a standalone API that does **not** implement the `Aead` trait.
34//!
35//! # SealedBox
36//!
37//! [`sealed_box`] provides `seal_box` / `open_box` helpers that prepend a
38//! randomly-generated nonce to the ciphertext as a single opaque blob.
39//!
40//! # Random-nonce helper
41//!
42//! [`seal_with_random_nonce`] encrypts plaintext with an on-the-fly random
43//! nonce and returns `(nonce, ciphertext_with_tag)` separately.
44
45extern crate alloc;
46
47pub mod aes_gcm_siv;
48pub mod ccm;
49pub mod deoxys;
50pub(crate) mod deoxys_bc;
51pub mod keywrap;
52pub mod nonce_seq;
53pub mod ocb3_impl;
54pub mod sealed_box;
55pub mod stream;
56pub mod xchacha20;
57
58pub use aes_gcm_siv::{AesGcmSiv128, AesGcmSiv256};
59pub use ccm::{Aes128Ccm, Aes256Ccm};
60pub use deoxys::Deoxys2_128;
61pub use keywrap::{aes128_key_unwrap, aes128_key_wrap, aes256_key_unwrap, aes256_key_wrap};
62pub use nonce_seq::{Nonce12, Nonce24, NonceSequence};
63pub use ocb3_impl::{Aes128Ocb3, Aes256Ocb3};
64pub use sealed_box::{open_box, seal_box};
65pub use stream::{Aes256GcmStream, ChaCha20Poly1305Stream};
66pub use xchacha20::XChaCha20Poly1305;
67
68// ── Random-nonce helper ───────────────────────────────────────────────────────
69
70/// Encrypt `plaintext` with a freshly-generated random nonce.
71///
72/// Returns `(nonce, ciphertext_with_tag)` as separate `Vec<u8>` buffers.
73/// Use this when the transport layer carries the nonce and ciphertext in
74/// separate fields.  For an all-in-one wire format, prefer [`seal_box`].
75///
76/// # Arguments
77///
78/// * `aead`      — AEAD algorithm instance.
79/// * `key`       — symmetric key (must have length `aead.key_len()`).
80/// * `aad`       — additional authenticated data (may be empty).
81/// * `plaintext` — message to encrypt.
82/// * `rng`       — cryptographically-secure random source.
83///
84/// # Errors
85///
86/// * Propagates any error from `rng.fill` or `aead.seal_to_vec`.
87pub fn seal_with_random_nonce(
88    aead: &dyn oxicrypto_core::Aead,
89    key: &[u8],
90    aad: &[u8],
91    plaintext: &[u8],
92    rng: &mut dyn oxicrypto_core::Rng,
93) -> Result<(alloc::vec::Vec<u8>, alloc::vec::Vec<u8>), oxicrypto_core::CryptoError> {
94    let nonce_len = aead.nonce_len();
95    let mut nonce = alloc::vec![0u8; nonce_len];
96    rng.fill(&mut nonce)?;
97    let ct = aead.seal_to_vec(key, &nonce, aad, plaintext)?;
98    Ok((nonce, ct))
99}
100
101use aead::{AeadInPlace, KeyInit, KeySizeUser};
102use oxicrypto_core::{Aead, CryptoError};
103
104// ── Internal helpers ──────────────────────────────────────────────────────────
105
106/// Size parameters for a particular AEAD instantiation.
107struct AeadParams {
108    key_len: usize,
109    nonce_len: usize,
110    tag_len: usize,
111}
112
113/// Perform AEAD seal using the `AeadInPlace` interface to avoid heap allocation.
114///
115/// The output `ct_out` must be at least `pt.len() + params.tag_len` bytes.
116/// Returns `pt.len() + tag_len`.
117fn seal_in_place<C: AeadInPlace + KeyInit>(
118    key: &[u8],
119    nonce: &[u8],
120    aad: &[u8],
121    pt: &[u8],
122    ct_out: &mut [u8],
123    params: AeadParams,
124) -> Result<usize, CryptoError> {
125    if key.len() != params.key_len {
126        return Err(CryptoError::InvalidKey);
127    }
128    if nonce.len() != params.nonce_len {
129        return Err(CryptoError::InvalidNonce);
130    }
131    let required = pt
132        .len()
133        .checked_add(params.tag_len)
134        .ok_or(CryptoError::BadInput)?;
135    if ct_out.len() < required {
136        return Err(CryptoError::BufferTooSmall);
137    }
138
139    // Copy plaintext into the output buffer; the tag is appended after.
140    ct_out[..pt.len()].copy_from_slice(pt);
141
142    let cipher = C::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
143    let nonce_arr = aead::generic_array::GenericArray::from_slice(nonce);
144    let tag = cipher
145        .encrypt_in_place_detached(nonce_arr, aad, &mut ct_out[..pt.len()])
146        .map_err(|_| CryptoError::Internal("AEAD encrypt failed"))?;
147    ct_out[pt.len()..required].copy_from_slice(&tag);
148    Ok(required)
149}
150
151/// Perform AEAD open using the `AeadInPlace` interface.
152///
153/// `ct` must be at least `params.tag_len` bytes (ciphertext ‖ tag).
154/// `pt_out` must be at least `ct.len() - params.tag_len` bytes.
155/// Returns the number of plaintext bytes written.
156fn open_in_place<C: AeadInPlace + KeyInit>(
157    key: &[u8],
158    nonce: &[u8],
159    aad: &[u8],
160    ct: &[u8],
161    pt_out: &mut [u8],
162    params: AeadParams,
163) -> Result<usize, CryptoError> {
164    if key.len() != params.key_len {
165        return Err(CryptoError::InvalidKey);
166    }
167    if nonce.len() != params.nonce_len {
168        return Err(CryptoError::InvalidNonce);
169    }
170    if ct.len() < params.tag_len {
171        return Err(CryptoError::BadInput);
172    }
173    let pt_len = ct.len() - params.tag_len;
174    if pt_out.len() < pt_len {
175        return Err(CryptoError::BufferTooSmall);
176    }
177
178    pt_out[..pt_len].copy_from_slice(&ct[..pt_len]);
179
180    let cipher = C::new_from_slice(key).map_err(|_| CryptoError::InvalidKey)?;
181    let nonce_arr = aead::generic_array::GenericArray::from_slice(nonce);
182    let tag_bytes = &ct[pt_len..];
183    if tag_bytes.len() != params.tag_len {
184        return Err(CryptoError::BadInput);
185    }
186    let tag = aead::Tag::<C>::clone_from_slice(tag_bytes);
187
188    cipher
189        .decrypt_in_place_detached(nonce_arr, aad, &mut pt_out[..pt_len], &tag)
190        .map_err(|_| CryptoError::InvalidTag)?;
191
192    Ok(pt_len)
193}
194
195// ── AES-128-GCM ───────────────────────────────────────────────────────────────
196
197/// AES-128-GCM authenticated encryption.
198///
199/// Key: 16 bytes, nonce: 12 bytes, tag: 16 bytes.
200#[derive(Debug, Default, Clone, Copy)]
201pub struct Aes128Gcm;
202
203impl Aead for Aes128Gcm {
204    fn name(&self) -> &'static str {
205        "AES-128-GCM"
206    }
207    fn key_len(&self) -> usize {
208        aes_gcm::Aes128Gcm::key_size()
209    }
210    fn nonce_len(&self) -> usize {
211        12
212    }
213    fn tag_len(&self) -> usize {
214        16
215    }
216    fn seal(
217        &self,
218        key: &[u8],
219        nonce: &[u8],
220        aad: &[u8],
221        pt: &[u8],
222        ct_out: &mut [u8],
223    ) -> Result<usize, CryptoError> {
224        seal_in_place::<aes_gcm::Aes128Gcm>(
225            key,
226            nonce,
227            aad,
228            pt,
229            ct_out,
230            AeadParams {
231                key_len: 16,
232                nonce_len: 12,
233                tag_len: 16,
234            },
235        )
236    }
237    fn open(
238        &self,
239        key: &[u8],
240        nonce: &[u8],
241        aad: &[u8],
242        ct: &[u8],
243        pt_out: &mut [u8],
244    ) -> Result<usize, CryptoError> {
245        open_in_place::<aes_gcm::Aes128Gcm>(
246            key,
247            nonce,
248            aad,
249            ct,
250            pt_out,
251            AeadParams {
252                key_len: 16,
253                nonce_len: 12,
254                tag_len: 16,
255            },
256        )
257    }
258}
259
260// ── AES-256-GCM ───────────────────────────────────────────────────────────────
261
262/// AES-256-GCM authenticated encryption.
263///
264/// Key: 32 bytes, nonce: 12 bytes, tag: 16 bytes.
265#[derive(Debug, Default, Clone, Copy)]
266pub struct Aes256Gcm;
267
268impl Aead for Aes256Gcm {
269    fn name(&self) -> &'static str {
270        "AES-256-GCM"
271    }
272    fn key_len(&self) -> usize {
273        aes_gcm::Aes256Gcm::key_size()
274    }
275    fn nonce_len(&self) -> usize {
276        12
277    }
278    fn tag_len(&self) -> usize {
279        16
280    }
281    fn seal(
282        &self,
283        key: &[u8],
284        nonce: &[u8],
285        aad: &[u8],
286        pt: &[u8],
287        ct_out: &mut [u8],
288    ) -> Result<usize, CryptoError> {
289        seal_in_place::<aes_gcm::Aes256Gcm>(
290            key,
291            nonce,
292            aad,
293            pt,
294            ct_out,
295            AeadParams {
296                key_len: 32,
297                nonce_len: 12,
298                tag_len: 16,
299            },
300        )
301    }
302    fn open(
303        &self,
304        key: &[u8],
305        nonce: &[u8],
306        aad: &[u8],
307        ct: &[u8],
308        pt_out: &mut [u8],
309    ) -> Result<usize, CryptoError> {
310        open_in_place::<aes_gcm::Aes256Gcm>(
311            key,
312            nonce,
313            aad,
314            ct,
315            pt_out,
316            AeadParams {
317                key_len: 32,
318                nonce_len: 12,
319                tag_len: 16,
320            },
321        )
322    }
323}
324
325// ── ChaCha20-Poly1305 ─────────────────────────────────────────────────────────
326
327/// ChaCha20-Poly1305 authenticated encryption.
328///
329/// Key: 32 bytes, nonce: 12 bytes, tag: 16 bytes.
330#[derive(Debug, Default, Clone, Copy)]
331pub struct ChaCha20Poly1305;
332
333impl Aead for ChaCha20Poly1305 {
334    fn name(&self) -> &'static str {
335        "ChaCha20-Poly1305"
336    }
337    fn key_len(&self) -> usize {
338        chacha20poly1305::ChaCha20Poly1305::key_size()
339    }
340    fn nonce_len(&self) -> usize {
341        12
342    }
343    fn tag_len(&self) -> usize {
344        16
345    }
346    fn seal(
347        &self,
348        key: &[u8],
349        nonce: &[u8],
350        aad: &[u8],
351        pt: &[u8],
352        ct_out: &mut [u8],
353    ) -> Result<usize, CryptoError> {
354        seal_in_place::<chacha20poly1305::ChaCha20Poly1305>(
355            key,
356            nonce,
357            aad,
358            pt,
359            ct_out,
360            AeadParams {
361                key_len: 32,
362                nonce_len: 12,
363                tag_len: 16,
364            },
365        )
366    }
367    fn open(
368        &self,
369        key: &[u8],
370        nonce: &[u8],
371        aad: &[u8],
372        ct: &[u8],
373        pt_out: &mut [u8],
374    ) -> Result<usize, CryptoError> {
375        open_in_place::<chacha20poly1305::ChaCha20Poly1305>(
376            key,
377            nonce,
378            aad,
379            ct,
380            pt_out,
381            AeadParams {
382                key_len: 32,
383                nonce_len: 12,
384                tag_len: 16,
385            },
386        )
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    const KEY_128: [u8; 16] = [0x42u8; 16];
395    const KEY_256: [u8; 32] = [0x42u8; 32];
396    const NONCE_12: [u8; 12] = [0x24u8; 12];
397    const AAD: &[u8] = b"additional authenticated data";
398    const PLAINTEXT: &[u8] = b"hello, oxicrypto!";
399
400    fn round_trip<A: Aead>(aead: &A, key: &[u8]) {
401        let mut ct = vec![0u8; PLAINTEXT.len() + aead.tag_len()];
402        let written = aead
403            .seal(key, &NONCE_12, AAD, PLAINTEXT, &mut ct)
404            .expect("seal failed");
405        assert_eq!(written, PLAINTEXT.len() + aead.tag_len());
406
407        let mut pt = vec![0u8; PLAINTEXT.len()];
408        let recovered = aead
409            .open(key, &NONCE_12, AAD, &ct[..written], &mut pt)
410            .expect("open failed");
411        assert_eq!(recovered, PLAINTEXT.len());
412        assert_eq!(&pt[..recovered], PLAINTEXT);
413    }
414
415    fn wrong_key_fails<A: Aead>(aead: &A, good_key: &[u8], wrong_key: &[u8]) {
416        let mut ct = vec![0u8; PLAINTEXT.len() + aead.tag_len()];
417        let written = aead
418            .seal(good_key, &NONCE_12, AAD, PLAINTEXT, &mut ct)
419            .unwrap();
420
421        let mut pt = vec![0u8; PLAINTEXT.len()];
422        let result = aead.open(wrong_key, &NONCE_12, AAD, &ct[..written], &mut pt);
423        assert_eq!(result, Err(CryptoError::InvalidTag));
424    }
425
426    #[test]
427    fn aes128gcm_round_trip() {
428        round_trip(&Aes128Gcm, &KEY_128);
429    }
430
431    #[test]
432    fn aes256gcm_round_trip() {
433        round_trip(&Aes256Gcm, &KEY_256);
434    }
435
436    #[test]
437    fn chacha20poly1305_round_trip() {
438        round_trip(&ChaCha20Poly1305, &KEY_256);
439    }
440
441    #[test]
442    fn aes128gcm_wrong_key_fails() {
443        wrong_key_fails(&Aes128Gcm, &KEY_128, &[0x00u8; 16]);
444    }
445
446    #[test]
447    fn aes256gcm_wrong_key_fails() {
448        wrong_key_fails(&Aes256Gcm, &KEY_256, &[0x00u8; 32]);
449    }
450
451    #[test]
452    fn chacha20poly1305_wrong_key_fails() {
453        wrong_key_fails(&ChaCha20Poly1305, &KEY_256, &[0x00u8; 32]);
454    }
455
456    #[test]
457    fn invalid_key_length() {
458        let aead = Aes256Gcm;
459        let mut ct = vec![0u8; PLAINTEXT.len() + 16];
460        let result = aead.seal(&[0u8; 16], &NONCE_12, AAD, PLAINTEXT, &mut ct);
461        assert_eq!(result, Err(CryptoError::InvalidKey));
462    }
463
464    // ── seal_with_random_nonce tests ─────────────────────────────────────────
465
466    /// Deterministic counter RNG for tests.
467    struct CounterRng {
468        counter: u8,
469    }
470
471    impl CounterRng {
472        fn new() -> Self {
473            Self { counter: 0x11 }
474        }
475    }
476
477    impl oxicrypto_core::Rng for CounterRng {
478        fn fill(&mut self, dst: &mut [u8]) -> Result<(), CryptoError> {
479            for b in dst.iter_mut() {
480                *b = self.counter;
481                self.counter = self.counter.wrapping_add(1);
482            }
483            Ok(())
484        }
485    }
486
487    #[test]
488    fn seal_with_random_nonce_aes128gcm_round_trip() {
489        let aead = Aes128Gcm;
490        let mut rng = CounterRng::new();
491
492        let (nonce, ct) = seal_with_random_nonce(&aead, &KEY_128, AAD, PLAINTEXT, &mut rng)
493            .expect("seal_with_random_nonce failed");
494
495        assert_eq!(
496            nonce.len(),
497            aead.nonce_len(),
498            "nonce length must match aead.nonce_len()"
499        );
500        assert_eq!(
501            ct.len(),
502            PLAINTEXT.len() + aead.tag_len(),
503            "ct length must be pt+tag"
504        );
505
506        // Nonce is produced by our counter RNG — verify it is the expected prefix.
507        let expected_nonce: alloc::vec::Vec<u8> =
508            (0u8..12).map(|i| 0x11_u8.wrapping_add(i)).collect();
509        assert_eq!(nonce, expected_nonce, "nonce must match RNG output");
510
511        // Decrypt with the separately returned nonce.
512        let recovered = aead
513            .open_to_vec(&KEY_128, &nonce, AAD, &ct)
514            .expect("open_to_vec after seal_with_random_nonce failed");
515        assert_eq!(
516            recovered.as_slice(),
517            PLAINTEXT,
518            "round-trip must recover plaintext"
519        );
520    }
521
522    #[test]
523    fn seal_with_random_nonce_returns_separate_nonce_and_ct() {
524        // Verify that (nonce, ct) are distinct buffers — nonce is NOT prepended in ct.
525        let aead = Aes256Gcm;
526        let mut rng = CounterRng::new();
527
528        let (nonce, ct) = seal_with_random_nonce(&aead, &KEY_256, AAD, PLAINTEXT, &mut rng)
529            .expect("seal_with_random_nonce failed");
530
531        // ct starts with ciphertext, NOT nonce bytes
532        assert_eq!(ct.len(), PLAINTEXT.len() + aead.tag_len());
533        // nonce and ct are independent
534        assert_eq!(nonce.len(), aead.nonce_len());
535        assert_ne!(
536            nonce.as_slice(),
537            &ct[..nonce.len()],
538            "nonce must not be embedded in ct"
539        );
540    }
541
542    #[test]
543    fn seal_with_random_nonce_rng_failure_propagates() {
544        struct AlwaysFailRng;
545        impl oxicrypto_core::Rng for AlwaysFailRng {
546            fn fill(&mut self, _dst: &mut [u8]) -> Result<(), CryptoError> {
547                Err(CryptoError::Rng)
548            }
549        }
550        let aead = Aes128Gcm;
551        let result = seal_with_random_nonce(&aead, &KEY_128, AAD, PLAINTEXT, &mut AlwaysFailRng);
552        assert_eq!(result, Err(CryptoError::Rng), "RNG failure must propagate");
553    }
554}