hpke/aead.rs
1//! AEAD algorithms for HPKE ciphersuites (`draft-ietf-hpke-hpke-05` Section 5.2
2//! and Section 7.3).
3//!
4//! The AEADs registered by the specification are owned by the `crypto` crate and
5//! re-exported here, so users of this crate never need to depend on `crypto`
6//! directly:
7//!
8//! - [`Aes256Gcm`] (AEAD id `0x0002`)
9//! - [`ChaCha20Poly1305`] (AEAD id `0x0003`)
10//! - [`ExportOnly`] (AEAD id `0xffff`), the local pseudo-AEAD
11//!
12//! HPKE-specific metadata (the AEAD identifier, message length limits, and the
13//! keyed constructor) is added through the [`Aead`] extension trait.
14
15use crypto::Hash;
16pub use crypto::{aes::Aes256Gcm, chacha::ChaCha20Poly1305};
17
18use super::HpkeError;
19
20/// Maximum plaintext length for AES-GCM (`P_MAX`), 2^36 - 31 bytes ([RFC 5116]
21/// Section 5.2.1.1).
22const AES_GCM_MAX_PLAINTEXT_SIZE: u64 = (1u64 << 36) - 31;
23/// Maximum ciphertext length for AES-GCM (`C_MAX`), 2^36 - 15 bytes ([RFC 5116]
24/// Section 5.2.1.1).
25const AES_GCM_MAX_CIPHERTEXT_SIZE: u64 = (1u64 << 36) - 15;
26/// Maximum plaintext length for ChaCha20-Poly1305 (`P_MAX`), 2^38 - 64 bytes
27/// ([RFC 8439] Section 2.8).
28const CHACHA20_POLY1305_MAX_PLAINTEXT_SIZE: u64 = (1u64 << 38) - 64;
29/// Maximum ciphertext length for ChaCha20-Poly1305 (`C_MAX`), 2^38 - 48 bytes
30/// ([RFC 8439] Section 2.8).
31const CHACHA20_POLY1305_MAX_CIPHERTEXT_SIZE: u64 = (1u64 << 38) - 48;
32
33/// HPKE ciphersuite binding for an AEAD algorithm.
34///
35/// An AEAD is the symmetric cipher used to seal and open messages. This trait
36/// only adds what HPKE itself needs on top of [`crypto::Aead`] (from which the
37/// nonce size, tag size, and the underlying cipher operations are inherited):
38///
39/// - [`Aead::HPKE_AEAD_ID`], the IANA "HPKE AEAD Identifiers" value used to build
40/// the ciphersuite `suite_id`,
41/// - [`Aead::MAX_PLAINTEXT_SIZE`] and [`Aead::MAX_CIPHERTEXT_SIZE`], the message
42/// length limits `P_MAX` and `C_MAX` defined by [RFC 5116],
43/// - [`Aead::new`], the construction of a ready-to-use cipher from the
44/// raw key schedule output.
45///
46/// # Example
47///
48/// ```
49/// use hpke::aead::{Aead, Aes256Gcm, ExportOnly};
50///
51/// let cipher = <Aes256Gcm as Aead>::new(&[0x42u8; 32]).unwrap();
52/// assert_eq!(Aes256Gcm::HPKE_AEAD_ID, 0x0002);
53/// assert_eq!(ExportOnly::HPKE_AEAD_ID, 0xffff);
54/// # let _ = cipher;
55/// ```
56///
57/// # Custom AEADs
58///
59/// Implement [`crypto::Aead`] for the cipher's operations, then implement this
60/// trait to give it an HPKE identifier, key size, and message length limits. Any
61/// nonce and tag size is supported as long as `KEY_SIZE` is at most 64 bytes and
62/// `NONCE_SIZE` is between 8 and 64 bytes; larger values fail to compile when
63/// the cipher is used with [`new_sender`](crate::new_sender) or
64/// [`new_recipient`](crate::new_recipient).
65pub trait Aead: Sized + crypto::Aead {
66 /// The HPKE AEAD identifier (`draft-ietf-hpke-hpke-05` Section 7.3, IANA
67 /// "HPKE AEAD Identifiers"), used to build the ciphersuite `suite_id`.
68 const HPKE_AEAD_ID: u16;
69
70 /// Maximum length, in bytes, of a plaintext passed to [`Aead::seal`], i.e.
71 /// `P_MAX` ([RFC 5116]).
72 ///
73 /// HPKE refuses to encrypt longer plaintexts with
74 /// [`HpkeError::MessageLimitReached`]: exceeding `P_MAX` would void the
75 /// confidentiality and integrity guarantees of the AEAD
76 /// (`draft-ietf-hpke-hpke-05` Section 5.2). There is no default; every AEAD
77 /// must state its limit.
78 const MAX_PLAINTEXT_SIZE: u64;
79
80 /// Maximum length, in bytes, of a ciphertext passed to
81 /// [`Aead::open`], i.e. `C_MAX` ([RFC 5116]).
82 ///
83 /// HPKE refuses to open longer ciphertexts with
84 /// [`HpkeError::MessageLimitReached`] (`draft-ietf-hpke-hpke-05`
85 /// Section 5.2). There is no default; every AEAD must state its limit.
86 const MAX_CIPHERTEXT_SIZE: u64;
87
88 /// Instantiates the cipher from `KEY_SIZE` raw key bytes, as produced by
89 /// the HPKE key schedule.
90 ///
91 /// # Errors
92 ///
93 /// Returns [`HpkeError::InvalidKey`] when `key` does not have exactly
94 /// [`crypto::Aead::KEY_SIZE`] bytes.
95 fn new(key: &[u8]) -> Result<Self, HpkeError>;
96
97 /// Encrypts `in_out` in place and returns the detached authentication tag,
98 /// forwarding to [`crypto::Aead::encrypt_in_place`].
99 ///
100 /// # Errors
101 ///
102 /// Returns [`HpkeError::NotSupported`] when the cipher is the
103 /// [`ExportOnly`] pseudo-AEAD.
104 fn seal(&self, in_out: &mut [u8], nonce: &[u8], associated_data: &[u8]) -> Result<Hash, HpkeError> {
105 return Ok(crypto::Aead::encrypt_in_place(self, in_out, nonce, associated_data));
106 }
107
108 /// Decrypts `in_out` in place using the detached authentication `tag`,
109 /// forwarding to [`crypto::Aead::decrypt_in_place`].
110 ///
111 /// # Errors
112 ///
113 /// Returns [`HpkeError::OpenError`] when authentication fails, or
114 /// [`HpkeError::NotSupported`] when the cipher is the [`ExportOnly`]
115 /// pseudo-AEAD.
116 fn open(&self, in_out: &mut [u8], nonce: &[u8], associated_data: &[u8], tag: &[u8]) -> Result<(), HpkeError> {
117 return crypto::Aead::decrypt_in_place(self, in_out, nonce, associated_data, tag)
118 .map_err(|_| HpkeError::OpenError);
119 }
120}
121
122/// AES-256-GCM (AEAD id `0x0002`).
123impl Aead for Aes256Gcm {
124 const HPKE_AEAD_ID: u16 = 0x0002;
125 const MAX_PLAINTEXT_SIZE: u64 = AES_GCM_MAX_PLAINTEXT_SIZE;
126 const MAX_CIPHERTEXT_SIZE: u64 = AES_GCM_MAX_CIPHERTEXT_SIZE;
127
128 fn new(key: &[u8]) -> Result<Self, HpkeError> {
129 let key: &[u8; 32] = key.try_into().map_err(|_| HpkeError::InvalidKey)?;
130 return Ok(Aes256Gcm::new(key));
131 }
132}
133
134/// ChaCha20-Poly1305 (AEAD id `0x0003`).
135impl Aead for ChaCha20Poly1305 {
136 const HPKE_AEAD_ID: u16 = 0x0003;
137 const MAX_PLAINTEXT_SIZE: u64 = CHACHA20_POLY1305_MAX_PLAINTEXT_SIZE;
138 const MAX_CIPHERTEXT_SIZE: u64 = CHACHA20_POLY1305_MAX_CIPHERTEXT_SIZE;
139
140 fn new(key: &[u8]) -> Result<Self, HpkeError> {
141 let key: &[u8; 32] = key.try_into().map_err(|_| HpkeError::InvalidKey)?;
142 return Ok(ChaCha20Poly1305::new(key));
143 }
144}
145
146/// Export-Only pseudo-AEAD (AEAD id `0xffff`).
147///
148/// Ciphersuites using this "AEAD" can only derive secrets through
149/// [`SenderContext::export`](crate::SenderContext::export) and
150/// [`RecipientContext::export`](crate::RecipientContext::export); [`Aead::seal`]
151/// and [`Aead::open`] return [`HpkeError::NotSupported`].
152///
153/// The [`crypto::Aead`] implementation below only exists to satisfy the
154/// [`Aead`] supertrait bound and must not be called directly: HPKE never
155/// invokes it, and it does not perform any encryption.
156#[derive(Clone, Copy, Debug, PartialEq, Eq)]
157pub struct ExportOnly;
158
159impl crypto::Aead for ExportOnly {
160 const KEY_SIZE: usize = 0;
161 const TAG_SIZE: usize = 0;
162 const NONCE_SIZE: usize = 0;
163
164 fn encrypt_in_place(&self, _in_out: &mut [u8], _nonce: &[u8], _associated_data: &[u8]) -> Hash {
165 return Hash::from([0u8; Self::TAG_SIZE]);
166 }
167
168 fn decrypt_in_place(
169 &self,
170 _in_out: &mut [u8],
171 _nonce: &[u8],
172 _associated_data: &[u8],
173 _tag: &[u8],
174 ) -> Result<(), crypto::AeadError> {
175 return Err(crypto::AeadError::Unsupported);
176 }
177}
178
179impl Aead for ExportOnly {
180 const HPKE_AEAD_ID: u16 = 0xffff;
181 // The Export-Only pseudo-AEAD never encrypts or decrypts, so it has no
182 // message length limits.
183 const MAX_PLAINTEXT_SIZE: u64 = 0;
184 const MAX_CIPHERTEXT_SIZE: u64 = 0;
185
186 fn new(key: &[u8]) -> Result<Self, HpkeError> {
187 if key.is_empty() {
188 return Ok(ExportOnly);
189 }
190 return Err(HpkeError::InvalidKey);
191 }
192
193 fn seal(&self, _in_out: &mut [u8], _nonce: &[u8], _associated_data: &[u8]) -> Result<Hash, HpkeError> {
194 return Err(HpkeError::NotSupported);
195 }
196
197 fn open(&self, _in_out: &mut [u8], _nonce: &[u8], _associated_data: &[u8], _tag: &[u8]) -> Result<(), HpkeError> {
198 return Err(HpkeError::NotSupported);
199 }
200}