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}