Skip to main content

hpke_ng/
aead.rs

1//! HPKE AEAD primitives (RFC 9180 §7.3).
2
3use alloc::vec::Vec;
4
5use crate::HpkeError;
6use crate::sealed::Sealed;
7
8/// Sealed trait for HPKE-supported AEAD ciphersuite components.
9///
10/// Implementors expose the IANA ID, length parameters, and a cached
11/// `Cipher` state. The cipher is materialized once at key-schedule time
12/// (see [`Aead::init`]) and reused for every subsequent
13/// [`SealingAead::seal`] / [`SealingAead::open`] call — eliminating the
14/// AES key-schedule expansion + `GHash` precompute cost on each AEAD
15/// operation. Sealing/opening live on the [`SealingAead`] subtrait so
16/// export-only configurations cannot be passed to `seal_*`/`open_*`
17/// methods.
18pub trait Aead: Sealed {
19	/// IANA AEAD ID (RFC 9180 §7.3).
20	const ID: u16;
21	/// Key length in bytes (`Nk`).
22	const KEY_LEN: usize;
23	/// Nonce length in bytes (`Nn`).
24	const NONCE_LEN: usize;
25	/// Authentication tag length in bytes (`Nt`).
26	const TAG_LEN: usize;
27
28	/// Cached cipher state, derived from the key once at key-schedule
29	/// time. For ChaCha20-Poly1305 this stores the key (state is
30	/// re-initialized per call by the underlying primitive). For AES-GCM
31	/// this stores the expanded round keys + the precomputed `GHash` table —
32	/// the expensive part of every per-message call when the cipher is
33	/// reconstructed from raw bytes.
34	type Cipher;
35
36	/// Initialize the cached cipher state from a `KEY_LEN`-byte key.
37	fn init(key: &[u8]) -> Result<Self::Cipher, HpkeError>;
38}
39
40/// Marker subtrait for AEADs that actually encrypt (i.e. not export-only).
41///
42/// Both `seal` and `open` collapse all underlying-cipher errors into a single
43/// typed error per direction (`SealError` / `OpenError`) — including the
44/// otherwise-unreachable "wrong key length" path. This keeps `open` failures
45/// indistinguishable to an attacker regardless of failure cause.
46pub trait SealingAead: Aead {
47	/// Encrypt `pt` with the cached `cipher` state, `nonce`, and `aad`.
48	/// Output is `pt.len() + TAG_LEN` bytes.
49	fn seal(
50		cipher: &Self::Cipher,
51		nonce: &[u8],
52		aad: &[u8],
53		pt: &[u8],
54	) -> Result<Vec<u8>, HpkeError>;
55	/// Decrypt `ct` (which includes the tag) with the cached `cipher`
56	/// state, `nonce`, and `aad`.
57	fn open(
58		cipher: &Self::Cipher,
59		nonce: &[u8],
60		aad: &[u8],
61		ct: &[u8],
62	) -> Result<Vec<u8>, HpkeError>;
63}
64
65/// ChaCha20-Poly1305 (RFC 9180 §7.3, ID `0x0003`).
66#[derive(Debug, Clone, Copy, Default)]
67pub struct ChaCha20Poly1305;
68
69impl Sealed for ChaCha20Poly1305 {}
70impl Aead for ChaCha20Poly1305 {
71	const ID: u16 = 0x0003;
72	const KEY_LEN: usize = 32;
73	const NONCE_LEN: usize = 12;
74	const TAG_LEN: usize = 16;
75
76	type Cipher = chacha20poly1305::ChaCha20Poly1305;
77
78	fn init(key: &[u8]) -> Result<Self::Cipher, HpkeError> {
79		use chacha20poly1305::KeyInit;
80		chacha20poly1305::ChaCha20Poly1305::new_from_slice(key)
81			.map_err(|_| HpkeError::AeadInitError)
82	}
83}
84impl SealingAead for ChaCha20Poly1305 {
85	fn seal(
86		cipher: &Self::Cipher,
87		nonce: &[u8],
88		aad: &[u8],
89		pt: &[u8],
90	) -> Result<Vec<u8>, HpkeError> {
91		use chacha20poly1305::{
92			Nonce,
93			aead::{Aead as _, Payload},
94		};
95		cipher
96			.encrypt(Nonce::from_slice(nonce), Payload { msg: pt, aad })
97			.map_err(|_| HpkeError::SealError)
98	}
99	fn open(
100		cipher: &Self::Cipher,
101		nonce: &[u8],
102		aad: &[u8],
103		ct: &[u8],
104	) -> Result<Vec<u8>, HpkeError> {
105		use chacha20poly1305::{
106			Nonce,
107			aead::{Aead as _, Payload},
108		};
109		cipher
110			.decrypt(Nonce::from_slice(nonce), Payload { msg: ct, aad })
111			.map_err(|_| HpkeError::OpenError)
112	}
113}
114
115/// AES-128-GCM (RFC 9180 §7.3, ID `0x0001`).
116///
117/// Constant-time only on platforms with hardware AES-NI/PCLMULQDQ. Prefer
118/// [`ChaCha20Poly1305`] on platforms without these instructions.
119#[derive(Debug, Clone, Copy, Default)]
120pub struct Aes128Gcm;
121
122impl Sealed for Aes128Gcm {}
123impl Aead for Aes128Gcm {
124	const ID: u16 = 0x0001;
125	const KEY_LEN: usize = 16;
126	const NONCE_LEN: usize = 12;
127	const TAG_LEN: usize = 16;
128
129	type Cipher = aes_gcm::Aes128Gcm;
130
131	fn init(key: &[u8]) -> Result<Self::Cipher, HpkeError> {
132		use aes_gcm::KeyInit;
133		aes_gcm::Aes128Gcm::new_from_slice(key).map_err(|_| HpkeError::AeadInitError)
134	}
135}
136impl SealingAead for Aes128Gcm {
137	fn seal(
138		cipher: &Self::Cipher,
139		nonce: &[u8],
140		aad: &[u8],
141		pt: &[u8],
142	) -> Result<Vec<u8>, HpkeError> {
143		use aes_gcm::aead::Aead as _;
144		cipher
145			.encrypt(
146				aes_gcm::Nonce::from_slice(nonce),
147				aead::Payload { msg: pt, aad },
148			)
149			.map_err(|_| HpkeError::SealError)
150	}
151	fn open(
152		cipher: &Self::Cipher,
153		nonce: &[u8],
154		aad: &[u8],
155		ct: &[u8],
156	) -> Result<Vec<u8>, HpkeError> {
157		use aes_gcm::aead::Aead as _;
158		cipher
159			.decrypt(
160				aes_gcm::Nonce::from_slice(nonce),
161				aead::Payload { msg: ct, aad },
162			)
163			.map_err(|_| HpkeError::OpenError)
164	}
165}
166
167/// AES-256-GCM (RFC 9180 §7.3, ID `0x0002`).
168///
169/// Constant-time only on platforms with hardware AES-NI/PCLMULQDQ.
170#[derive(Debug, Clone, Copy, Default)]
171pub struct Aes256Gcm;
172
173impl Sealed for Aes256Gcm {}
174impl Aead for Aes256Gcm {
175	const ID: u16 = 0x0002;
176	const KEY_LEN: usize = 32;
177	const NONCE_LEN: usize = 12;
178	const TAG_LEN: usize = 16;
179
180	type Cipher = aes_gcm::Aes256Gcm;
181
182	fn init(key: &[u8]) -> Result<Self::Cipher, HpkeError> {
183		use aes_gcm::KeyInit;
184		aes_gcm::Aes256Gcm::new_from_slice(key).map_err(|_| HpkeError::AeadInitError)
185	}
186}
187impl SealingAead for Aes256Gcm {
188	fn seal(
189		cipher: &Self::Cipher,
190		nonce: &[u8],
191		aad: &[u8],
192		pt: &[u8],
193	) -> Result<Vec<u8>, HpkeError> {
194		use aes_gcm::aead::Aead as _;
195		cipher
196			.encrypt(
197				aes_gcm::Nonce::from_slice(nonce),
198				aead::Payload { msg: pt, aad },
199			)
200			.map_err(|_| HpkeError::SealError)
201	}
202	fn open(
203		cipher: &Self::Cipher,
204		nonce: &[u8],
205		aad: &[u8],
206		ct: &[u8],
207	) -> Result<Vec<u8>, HpkeError> {
208		use aes_gcm::aead::Aead as _;
209		cipher
210			.decrypt(
211				aes_gcm::Nonce::from_slice(nonce),
212				aead::Payload { msg: ct, aad },
213			)
214			.map_err(|_| HpkeError::OpenError)
215	}
216}
217
218/// Export-only "AEAD" marker (RFC 9180 §7.3, ID `0xFFFF`).
219///
220/// Configurations parameterized over `ExportOnly` cannot encrypt or decrypt;
221/// only the export methods are available, because `ExportOnly` does not
222/// implement [`SealingAead`].
223#[derive(Debug, Clone, Copy, Default)]
224pub struct ExportOnly;
225
226impl Sealed for ExportOnly {}
227impl Aead for ExportOnly {
228	const ID: u16 = 0xFFFF;
229	const KEY_LEN: usize = 0;
230	const NONCE_LEN: usize = 0;
231	const TAG_LEN: usize = 0;
232
233	type Cipher = ();
234
235	fn init(_key: &[u8]) -> Result<Self::Cipher, HpkeError> {
236		Ok(())
237	}
238}
239
240#[cfg(test)]
241mod tests {
242	use super::*;
243	use hex::FromHex;
244
245	/// RFC 8439 §2.8.2 test vector.
246	#[test]
247	fn rfc8439_chacha20poly1305_test_vector() {
248		let key = Vec::from_hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
249			.unwrap();
250		let nonce = Vec::from_hex("070000004041424344454647").unwrap();
251		let aad = Vec::from_hex("50515253c0c1c2c3c4c5c6c7").unwrap();
252		let pt = b"Ladies and Gentlemen of the class of '99: \
253                   If I could offer you only one tip for the future, sunscreen would be it.";
254
255		let cipher = ChaCha20Poly1305::init(&key).unwrap();
256		let ct = ChaCha20Poly1305::seal(&cipher, &nonce, &aad, pt).unwrap();
257		let recovered = ChaCha20Poly1305::open(&cipher, &nonce, &aad, &ct).unwrap();
258		assert_eq!(recovered, pt);
259
260		let mut bad = ct.clone();
261		bad[0] ^= 1;
262		assert_eq!(
263			ChaCha20Poly1305::open(&cipher, &nonce, &aad, &bad),
264			Err(HpkeError::OpenError)
265		);
266	}
267
268	#[test]
269	fn aes128gcm_roundtrip_short() {
270		let key = [0u8; 16];
271		let nonce = [0u8; 12];
272		let aad = b"";
273		let pt = b"hello";
274		let cipher = Aes128Gcm::init(&key).unwrap();
275		let ct = Aes128Gcm::seal(&cipher, &nonce, aad, pt).unwrap();
276		assert_eq!(ct.len(), pt.len() + 16);
277		assert_eq!(Aes128Gcm::open(&cipher, &nonce, aad, &ct).unwrap(), pt);
278	}
279
280	#[test]
281	fn aes256gcm_roundtrip_short() {
282		let key = [0u8; 32];
283		let nonce = [0u8; 12];
284		let pt = b"world";
285		let cipher = Aes256Gcm::init(&key).unwrap();
286		let ct = Aes256Gcm::seal(&cipher, &nonce, b"aad", pt).unwrap();
287		assert_eq!(Aes256Gcm::open(&cipher, &nonce, b"aad", &ct).unwrap(), pt);
288	}
289
290	#[test]
291	fn aes128gcm_rejects_bad_key_len() {
292		let r = Aes128Gcm::init(&[0u8; 15]);
293		assert_eq!(r.err(), Some(HpkeError::AeadInitError));
294	}
295
296	#[test]
297	fn export_only_implements_aead_only() {
298		fn assert_aead<A: Aead>() {}
299		assert_aead::<ExportOnly>();
300		assert_eq!(ExportOnly::ID, 0xFFFF);
301		assert_eq!(ExportOnly::KEY_LEN, 0);
302		assert_eq!(ExportOnly::NONCE_LEN, 0);
303		assert_eq!(ExportOnly::TAG_LEN, 0);
304		// `init` accepts the empty key cleanly.
305		assert!(ExportOnly::init(&[]).is_ok());
306	}
307}