Skip to main content

boring/
aead.rs

1//! Authenticated encryption with detached authentication tags.
2//!
3//! This module wraps BoringSSL's `EVP_AEAD` interface and is intended for
4//! protocols that keep ciphertext and authentication tag in separate buffers.
5//!
6//! # Overview
7//!
8//! [`AeadCtx`] is the main type. Create one with an [`Algorithm`] and a key,
9//! then use it to encrypt and decrypt:
10//!
11//! - [`AeadCtxRef::seal_in_place`] / [`AeadCtxRef::open_in_place`] — encrypt
12//!   or decrypt a buffer in place with a detached tag. These cover the common
13//!   case (TLS record framing, packet formats with explicit tag fields, etc.).
14//!
15//! - [`AeadCtxRef::seal_scatter`] / [`AeadCtxRef::open_gather`] — lower-level
16//!   scatter/gather operations for protocols that split ciphertext output across
17//!   multiple buffers.
18//!
19//! # When to use [`crate::symm`] instead
20//!
21//! If you want one-shot helpers that allocate output buffers or APIs centered
22//! on `EVP_CIPHER`, prefer [`crate::symm`], including
23//! [`crate::symm::encrypt_aead`] and [`crate::symm::decrypt_aead`].
24//!
25//! # Nonce guidance
26//!
27//! Never reuse a nonce with the same key. Nonce reuse can completely undermine
28//! AEAD security.
29//!
30//! Nonces are usually public (not secret). They must either be transmitted with
31//! the message or derived by both sides (for example from a shared sequence
32//! number).
33//!
34//! Different algorithms can have different nonce-length requirements and safety
35//! considerations around nonce generation. The caller is responsible for
36//! following safe nonce practices for the selected algorithm.
37//! [`Algorithm::nonce_len`] returns the required nonce size in bytes.
38//!
39//! # Example
40//!
41//! ```
42//! use boring::aead::{AeadCtx, Algorithm};
43//!
44//! let algorithm = Algorithm::aes_128_gcm();
45//! let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap();
46//! let nonce = [0u8; 12];
47//! let aad = b"record-header";
48//! let mut payload = b"hello world".to_vec();
49//! let mut tag = vec![0u8; algorithm.max_overhead()];
50//!
51//! ctx.seal_in_place(&nonce, payload.as_mut_slice(), &mut tag, aad)
52//!     .unwrap();
53//!
54//! ctx.open_in_place(&nonce, payload.as_mut_slice(), &tag, aad)
55//!     .unwrap();
56//!
57//! assert_eq!(payload.as_slice(), b"hello world");
58//! ```
59
60use std::ptr;
61
62use foreign_types::{ForeignType, ForeignTypeRef};
63use openssl_macros::corresponds;
64
65use crate::error::ErrorStack;
66use crate::ffi;
67use crate::{cvt, cvt_p};
68
69/// Represents a specific AEAD algorithm.
70#[derive(Copy, Clone, Debug, PartialEq, Eq)]
71pub struct Algorithm(*const ffi::EVP_AEAD);
72
73impl Algorithm {
74    /// Creates an [`Algorithm`] from a raw BoringSSL pointer.
75    ///
76    /// # Safety
77    ///
78    /// The caller must ensure that `ptr` is a valid pointer to an
79    /// `EVP_AEAD` with a `'static` lifetime.
80    #[must_use]
81    pub const unsafe fn from_ptr(ptr: *const ffi::EVP_AEAD) -> Self {
82        Self(ptr)
83    }
84
85    /// AES-128 in Galois Counter Mode (GCM).
86    #[corresponds(EVP_aead_aes_128_gcm)]
87    #[must_use]
88    pub fn aes_128_gcm() -> Self {
89        unsafe { Self(ffi::EVP_aead_aes_128_gcm()) }
90    }
91
92    /// AES-256 in Galois Counter Mode (GCM).
93    #[corresponds(EVP_aead_aes_256_gcm)]
94    #[must_use]
95    pub fn aes_256_gcm() -> Self {
96        unsafe { Self(ffi::EVP_aead_aes_256_gcm()) }
97    }
98
99    /// ChaCha20-Poly1305 as described in RFC 8439.
100    #[corresponds(EVP_aead_chacha20_poly1305)]
101    #[must_use]
102    pub fn chacha20_poly1305() -> Self {
103        unsafe { Self(ffi::EVP_aead_chacha20_poly1305()) }
104    }
105
106    /// XChaCha20-Poly1305 with a 24-byte nonce.
107    #[corresponds(EVP_aead_xchacha20_poly1305)]
108    #[must_use]
109    pub fn xchacha20_poly1305() -> Self {
110        unsafe { Self(ffi::EVP_aead_xchacha20_poly1305()) }
111    }
112
113    /// Returns the key length, in bytes, required by this algorithm.
114    #[corresponds(EVP_AEAD_key_length)]
115    #[allow(clippy::trivially_copy_pass_by_ref)]
116    #[must_use]
117    pub fn key_length(&self) -> usize {
118        unsafe { ffi::EVP_AEAD_key_length(self.0) }
119    }
120
121    /// Returns the maximum additional bytes produced when sealing.
122    #[corresponds(EVP_AEAD_max_overhead)]
123    #[allow(clippy::trivially_copy_pass_by_ref)]
124    #[must_use]
125    pub fn max_overhead(&self) -> usize {
126        unsafe { ffi::EVP_AEAD_max_overhead(self.0) }
127    }
128
129    /// Returns the maximum tag length for this algorithm.
130    #[corresponds(EVP_AEAD_max_tag_len)]
131    #[allow(clippy::trivially_copy_pass_by_ref)]
132    #[must_use]
133    pub fn max_tag_len(&self) -> usize {
134        unsafe { ffi::EVP_AEAD_max_tag_len(self.0) }
135    }
136
137    /// Returns the nonce length, in bytes, required by this algorithm.
138    #[corresponds(EVP_AEAD_nonce_length)]
139    #[allow(clippy::trivially_copy_pass_by_ref)]
140    #[must_use]
141    pub fn nonce_len(&self) -> usize {
142        unsafe { ffi::EVP_AEAD_nonce_length(self.0) }
143    }
144
145    /// Returns the raw `EVP_AEAD` pointer.
146    #[allow(clippy::trivially_copy_pass_by_ref)]
147    #[must_use]
148    pub const fn as_ptr(&self) -> *const ffi::EVP_AEAD {
149        self.0
150    }
151}
152
153unsafe impl Send for Algorithm {}
154unsafe impl Sync for Algorithm {}
155
156foreign_type_and_impl_send_sync! {
157    type CType = ffi::EVP_AEAD_CTX;
158    fn drop = ffi::EVP_AEAD_CTX_free;
159
160    /// An AEAD encryption/decryption context wrapping BoringSSL's `EVP_AEAD_CTX`.
161    ///
162    /// Holds the keying material for a specific [`Algorithm`]. Use
163    /// [`AeadCtx::new_default_tag`] for the common case, or [`AeadCtx::new`]
164    /// when you need a custom tag length.
165    ///
166    /// See [`AeadCtxRef::seal_in_place`] and [`AeadCtxRef::open_in_place`] for
167    /// the primary encryption/decryption API.
168    pub struct AeadCtx;
169}
170
171impl AeadCtx {
172    /// Creates a new AEAD context.
173    ///
174    /// `tag_len` controls the default tag length used by the context.
175    #[corresponds(EVP_AEAD_CTX_new)]
176    pub fn new(algorithm: &Algorithm, key: &[u8], tag_len: usize) -> Result<Self, ErrorStack> {
177        ffi::init();
178
179        if key.len() != algorithm.key_length() {
180            return Err(ErrorStack::internal_error_str("invalid key size"));
181        }
182
183        unsafe {
184            cvt_p(ffi::EVP_AEAD_CTX_new(
185                algorithm.as_ptr(),
186                key.as_ptr(),
187                key.len(),
188                tag_len,
189            ))
190            .map(|ptr| AeadCtx::from_ptr(ptr))
191        }
192    }
193
194    /// Creates a new AEAD context using the algorithm's full (maximum) tag
195    /// length.
196    ///
197    /// This is the recommended constructor for most use cases. The full tag
198    /// length provides the strongest authentication guarantee for the algorithm.
199    /// Use [`AeadCtx::new`] instead when your protocol requires a truncated tag.
200    pub fn new_default_tag(algorithm: &Algorithm, key: &[u8]) -> Result<Self, ErrorStack> {
201        Self::new(algorithm, key, ffi::EVP_AEAD_DEFAULT_TAG_LENGTH as usize)
202    }
203}
204
205impl AeadCtxRef {
206    /// Computes the exact tag length for a [`seal_scatter`](AeadCtxRef::seal_scatter)
207    /// call with the given `in_len` and `extra_in_len`.
208    ///
209    /// This is useful for sizing `out_tag` buffers precisely rather than relying
210    /// on the worst-case [`Algorithm::max_overhead`].
211    #[corresponds(EVP_AEAD_CTX_tag_len)]
212    pub fn tag_len(&self, in_len: usize, extra_in_len: usize) -> Result<usize, ErrorStack> {
213        let mut out_tag_len: usize = 0;
214        unsafe {
215            cvt(ffi::EVP_AEAD_CTX_tag_len(
216                self.as_ptr(),
217                &mut out_tag_len,
218                in_len,
219                extra_in_len,
220            ))?;
221        }
222        Ok(out_tag_len)
223    }
224
225    /// Encrypts `in_out` in place and writes the authentication tag to
226    /// `out_tag`.
227    ///
228    /// `extra_in` is optional additional plaintext for protocols that split
229    /// ciphertext output across buffers. When `Some(extra)` is provided, the
230    /// ciphertext for `extra` is written to the start of `out_tag`, followed by
231    /// the detached tag bytes.
232    ///
233    /// In the common case, pass `None` and `out_tag` receives only the tag.
234    ///
235    /// `out_tag` must be large enough for all detached output:
236    /// `extra_in.len() + tag_len` (or conservatively
237    /// `extra_in.len() + Algorithm::max_overhead()`).
238    ///
239    /// # Parameters
240    ///
241    /// - `nonce`: Per-message nonce for this encryption operation.
242    /// - `in_out`: Plaintext input and in-place ciphertext output.
243    /// - `out_tag`: Detached output buffer for `extra_in` ciphertext (if any)
244    ///   and the authentication tag.
245    /// - `extra_in`: Optional extra plaintext chunk written as ciphertext into
246    ///   `out_tag` before the tag.
247    /// - `associated_data`: Additional authenticated data (AAD).
248    ///
249    /// Returns the sub-slice of `out_tag` that was written to.
250    /// This includes any encrypted `extra_in` bytes and the final tag.
251    ///
252    /// # Examples
253    ///
254    /// ```
255    /// use boring::aead::{AeadCtx, Algorithm};
256    ///
257    /// let algorithm = Algorithm::chacha20_poly1305();
258    /// let ctx = AeadCtx::new(&algorithm, &[7u8; 32], algorithm.max_tag_len()).unwrap();
259    ///
260    /// let nonce = [1u8; 12];
261    /// let aad = b"frame-header";
262    ///
263    /// // Main payload is encrypted in-place.
264    /// let mut main = b"hello".to_vec();
265    /// // Extra plaintext is encrypted into the detached buffer.
266    /// let extra = b" world";
267    /// let mut detached = vec![0u8; extra.len() + algorithm.max_overhead()];
268    ///
269    /// let detached_written = ctx
270    ///     .seal_scatter(
271    ///         &nonce,
272    ///         main.as_mut_slice(),
273    ///         detached.as_mut_slice(),
274    ///         Some(extra),
275    ///         aad,
276    ///     )
277    ///     .unwrap();
278    ///
279    /// // `detached_written` contains: extra ciphertext bytes followed by tag bytes.
280    /// let extra_ct_len = extra.len();
281    /// let tag = &detached_written[extra_ct_len..];
282    ///
283    /// // Reconstruct the full ciphertext by appending extra ciphertext bytes.
284    /// let mut full_ciphertext = main.clone();
285    /// full_ciphertext.extend_from_slice(&detached_written[..extra_ct_len]);
286    ///
287    /// // `open_gather` takes ciphertext and detached tag separately.
288    /// ctx.open_gather(&nonce, full_ciphertext.as_mut_slice(), tag, aad)
289    ///     .unwrap();
290    ///
291    /// assert_eq!(full_ciphertext.as_slice(), b"hello world");
292    /// ```
293    #[corresponds(EVP_AEAD_CTX_seal_scatter)]
294    pub fn seal_scatter<'a>(
295        &self,
296        nonce: &[u8],
297        in_out: &mut [u8],
298        out_tag: &'a mut [u8],
299        extra_in: Option<&[u8]>,
300        associated_data: &[u8],
301    ) -> Result<&'a mut [u8], ErrorStack> {
302        let (extra_in_ptr, extra_in_len) = extra_in
303            .map(|buf| (buf.as_ptr(), buf.len()))
304            .unwrap_or((ptr::null(), 0));
305
306        let mut out_tag_len = out_tag.len();
307        unsafe {
308            cvt(ffi::EVP_AEAD_CTX_seal_scatter(
309                self.as_ptr(),
310                in_out.as_mut_ptr(),
311                out_tag.as_mut_ptr(),
312                &mut out_tag_len,
313                out_tag.len(),
314                nonce.as_ptr(),
315                nonce.len(),
316                in_out.as_ptr(),
317                in_out.len(),
318                extra_in_ptr,
319                extra_in_len,
320                associated_data.as_ptr(),
321                associated_data.len(),
322            ))?;
323        }
324
325        Ok(&mut out_tag[..out_tag_len])
326    }
327
328    /// Decrypts `in_out` in place and verifies `in_tag` and
329    /// `associated_data`.
330    ///
331    /// When the corresponding [`seal_scatter`](AeadCtxRef::seal_scatter) call
332    /// used `extra_in`, append the extra ciphertext prefix to `in_out` and pass
333    /// only the tag suffix as `in_tag`. See the [`seal_scatter`](AeadCtxRef::seal_scatter)
334    /// documentation for a full example.
335    ///
336    /// # Parameters
337    ///
338    /// - `nonce`: The same nonce that was used during encryption.
339    /// - `in_out`: Ciphertext input and in-place plaintext output.
340    /// - `in_tag`: Detached tag bytes produced by
341    ///   [`seal_scatter`](AeadCtxRef::seal_scatter).
342    /// - `associated_data`: The same AAD that was passed during encryption.
343    #[corresponds(EVP_AEAD_CTX_open_gather)]
344    pub fn open_gather(
345        &self,
346        nonce: &[u8],
347        in_out: &mut [u8],
348        in_tag: &[u8],
349        associated_data: &[u8],
350    ) -> Result<(), ErrorStack> {
351        unsafe {
352            cvt(ffi::EVP_AEAD_CTX_open_gather(
353                self.as_ptr(),
354                in_out.as_mut_ptr(),
355                nonce.as_ptr(),
356                nonce.len(),
357                in_out.as_ptr(),
358                in_out.len(),
359                in_tag.as_ptr(),
360                in_tag.len(),
361                associated_data.as_ptr(),
362                associated_data.len(),
363            ))
364        }
365    }
366
367    /// Encrypts `buffer` in place and writes the authentication tag into `tag`.
368    ///
369    /// This is a convenience wrapper around [`seal_scatter`](AeadCtxRef::seal_scatter)
370    /// with `extra_in = None`.
371    ///
372    /// # Parameters
373    ///
374    /// - `nonce`: Per-message nonce. Must match the length returned by
375    ///   [`Algorithm::nonce_len`].
376    /// - `buffer`: Plaintext on input, ciphertext on output (encrypted in
377    ///   place).
378    /// - `tag`: Output buffer for the authentication tag. Must be at least
379    ///   [`Algorithm::max_overhead`] bytes; use [`AeadCtxRef::tag_len`] for
380    ///   the exact size.
381    /// - `associated_data`: Additional authenticated data (AAD) that is
382    ///   authenticated but not encrypted.
383    ///
384    /// Returns the sub-slice of `tag` that was written to.
385    pub fn seal_in_place<'a>(
386        &self,
387        nonce: &[u8],
388        buffer: &mut [u8],
389        tag: &'a mut [u8],
390        associated_data: &[u8],
391    ) -> Result<&'a mut [u8], ErrorStack> {
392        self.seal_scatter(nonce, buffer, tag, None, associated_data)
393    }
394
395    /// Decrypts `buffer` in place, verifying the authentication `tag` and
396    /// `associated_data`.
397    ///
398    /// This is a convenience wrapper around [`open_gather`](AeadCtxRef::open_gather).
399    ///
400    /// # Parameters
401    ///
402    /// - `nonce`: The same nonce that was used during encryption.
403    /// - `buffer`: Ciphertext on input, plaintext on output (decrypted in
404    ///   place).
405    /// - `tag`: The authentication tag produced by
406    ///   [`seal_in_place`](AeadCtxRef::seal_in_place).
407    /// - `associated_data`: The same AAD that was passed during encryption.
408    pub fn open_in_place(
409        &self,
410        nonce: &[u8],
411        buffer: &mut [u8],
412        tag: &[u8],
413        associated_data: &[u8],
414    ) -> Result<(), ErrorStack> {
415        self.open_gather(nonce, buffer, tag, associated_data)
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use super::{AeadCtx, Algorithm};
422
423    #[test]
424    fn in_out() {
425        let algorithm = Algorithm::aes_128_gcm();
426        let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap();
427        let nonce = [0u8; 12];
428        let associated_data = b"this is authenticated";
429        let mut buffer = b"ABCDE".to_vec();
430
431        let mut tag = [0u8; 16];
432        ctx.seal_in_place(&nonce, buffer.as_mut_slice(), &mut tag, associated_data)
433            .unwrap();
434
435        ctx.open_in_place(&nonce, buffer.as_mut_slice(), &tag, associated_data)
436            .unwrap();
437
438        assert_eq!(b"ABCDE", buffer.as_slice());
439    }
440
441    #[test]
442    fn xchacha_in_out() {
443        let algorithm = Algorithm::xchacha20_poly1305();
444        let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap();
445        let nonce = [0u8; 24];
446        let associated_data = b"xchacha";
447        let mut buffer = b"payload".to_vec();
448
449        let mut tag = [0u8; 16];
450        let tag_written = ctx
451            .seal_in_place(&nonce, buffer.as_mut_slice(), &mut tag, associated_data)
452            .unwrap();
453        let tag_len = tag_written.len();
454
455        ctx.open_in_place(
456            &nonce,
457            buffer.as_mut_slice(),
458            &tag[..tag_len],
459            associated_data,
460        )
461        .unwrap();
462
463        assert_eq!(b"payload", buffer.as_slice());
464    }
465
466    #[test]
467    fn seal_scatter_with_extra_in() {
468        let algorithm = Algorithm::chacha20_poly1305();
469        let ctx = AeadCtx::new(&algorithm, &[7u8; 32], algorithm.max_tag_len()).unwrap();
470
471        let nonce = [1u8; 12];
472        let aad = b"frame-header";
473        let mut main = b"hello".to_vec();
474        let extra = b" world";
475        let mut detached = vec![0u8; extra.len() + algorithm.max_overhead()];
476
477        let detached_written = ctx
478            .seal_scatter(
479                &nonce,
480                main.as_mut_slice(),
481                detached.as_mut_slice(),
482                Some(extra),
483                aad,
484            )
485            .unwrap();
486
487        let extra_ct_len = extra.len();
488        let tag = &detached_written[extra_ct_len..];
489        let mut full_ciphertext = main;
490        full_ciphertext.extend_from_slice(&detached_written[..extra_ct_len]);
491
492        ctx.open_gather(&nonce, full_ciphertext.as_mut_slice(), tag, aad)
493            .unwrap();
494
495        assert_eq!(full_ciphertext.as_slice(), b"hello world");
496    }
497
498    #[test]
499    fn new_rejects_invalid_key_length() {
500        let result = AeadCtx::new_default_tag(&Algorithm::aes_128_gcm(), &[0u8; 15]);
501        assert!(result.is_err());
502    }
503
504    #[test]
505    fn tag_len_returns_expected_value() {
506        let algorithm = Algorithm::aes_128_gcm();
507        let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap();
508
509        let tag_len = ctx.tag_len(0, 0).unwrap();
510        assert_eq!(tag_len, algorithm.max_overhead());
511    }
512
513    #[test]
514    fn seal_rejects_invalid_nonce_length() {
515        // ChaCha20-Poly1305 strictly requires a 12-byte nonce.
516        // (AES-GCM accepts variable-length nonces per spec, so it is not
517        // suitable for testing nonce-length rejection.)
518        let algorithm = Algorithm::chacha20_poly1305();
519        let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap();
520        let mut payload = [0u8; 8];
521        let mut tag = [0u8; 16];
522
523        let result = ctx.seal_in_place(&[0u8; 11], &mut payload, &mut tag, b"");
524        assert!(result.is_err());
525    }
526
527    #[test]
528    fn seal_rejects_insufficient_tag_buffer() {
529        let algorithm = Algorithm::aes_128_gcm();
530        let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 16]).unwrap();
531        let mut payload = [0u8; 8];
532
533        // AES-128-GCM produces a 16-byte tag; an 8-byte buffer must be rejected.
534        let mut short_tag = [0u8; 8];
535        let result = ctx.seal_in_place(&[0u8; 12], &mut payload, &mut short_tag, b"");
536        assert!(result.is_err());
537    }
538
539    #[test]
540    fn open_rejects_invalid_nonce_length() {
541        let algorithm = Algorithm::chacha20_poly1305();
542        let ctx = AeadCtx::new_default_tag(&algorithm, &[0u8; 32]).unwrap();
543        let mut payload = [0u8; 8];
544        let tag = [0u8; 16];
545
546        let result = ctx.open_in_place(&[0u8; 11], &mut payload, &tag, b"");
547        assert!(result.is_err());
548    }
549}