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 and length parameters. Sealing/opening live
11/// on the [`SealingAead`] subtrait so that export-only configurations cannot
12/// be passed to `seal_*`/`open_*` methods.
13pub trait Aead: Sealed {
14	/// IANA AEAD ID (RFC 9180 §7.3).
15	const ID: u16;
16	/// Key length in bytes (`Nk`).
17	const KEY_LEN: usize;
18	/// Nonce length in bytes (`Nn`).
19	const NONCE_LEN: usize;
20	/// Authentication tag length in bytes (`Nt`).
21	const TAG_LEN: usize;
22}
23
24/// Marker subtrait for AEADs that actually encrypt (i.e. not export-only).
25///
26/// Both `seal` and `open` collapse all underlying-cipher errors into a single
27/// typed error per direction (`SealError` / `OpenError`) — including the
28/// otherwise-unreachable "wrong key length" path. This keeps `open` failures
29/// indistinguishable to an attacker regardless of failure cause.
30pub trait SealingAead: Aead {
31	/// Encrypt `pt` with `key`, `nonce`, and `aad`. Output is `pt.len() + TAG_LEN` bytes.
32	fn seal(key: &[u8], nonce: &[u8], aad: &[u8], pt: &[u8]) -> Result<Vec<u8>, HpkeError>;
33	/// Decrypt `ct` (which includes the tag) with `key`, `nonce`, and `aad`.
34	fn open(key: &[u8], nonce: &[u8], aad: &[u8], ct: &[u8]) -> Result<Vec<u8>, HpkeError>;
35}
36
37/// ChaCha20-Poly1305 (RFC 9180 §7.3, ID `0x0003`).
38#[derive(Debug, Clone, Copy, Default)]
39pub struct ChaCha20Poly1305;
40
41impl Sealed for ChaCha20Poly1305 {}
42impl Aead for ChaCha20Poly1305 {
43	const ID: u16 = 0x0003;
44	const KEY_LEN: usize = 32;
45	const NONCE_LEN: usize = 12;
46	const TAG_LEN: usize = 16;
47}
48impl SealingAead for ChaCha20Poly1305 {
49	fn seal(key: &[u8], nonce: &[u8], aad: &[u8], pt: &[u8]) -> Result<Vec<u8>, HpkeError> {
50		use chacha20poly1305::{
51			KeyInit, Nonce,
52			aead::{Aead as _, Payload},
53		};
54		let cipher = chacha20poly1305::ChaCha20Poly1305::new_from_slice(key)
55			.map_err(|_| HpkeError::SealError)?;
56		let nonce = Nonce::from_slice(nonce);
57		cipher
58			.encrypt(nonce, Payload { msg: pt, aad })
59			.map_err(|_| HpkeError::SealError)
60	}
61	fn open(key: &[u8], nonce: &[u8], aad: &[u8], ct: &[u8]) -> Result<Vec<u8>, HpkeError> {
62		use chacha20poly1305::{
63			KeyInit, Nonce,
64			aead::{Aead as _, Payload},
65		};
66		let cipher = chacha20poly1305::ChaCha20Poly1305::new_from_slice(key)
67			.map_err(|_| HpkeError::OpenError)?;
68		let nonce = Nonce::from_slice(nonce);
69		cipher
70			.decrypt(nonce, Payload { msg: ct, aad })
71			.map_err(|_| HpkeError::OpenError)
72	}
73}
74
75/// AES-128-GCM (RFC 9180 §7.3, ID `0x0001`).
76///
77/// Constant-time only on platforms with hardware AES-NI/PCLMULQDQ. Prefer
78/// [`ChaCha20Poly1305`] on platforms without these instructions.
79#[derive(Debug, Clone, Copy, Default)]
80pub struct Aes128Gcm;
81
82impl Sealed for Aes128Gcm {}
83impl Aead for Aes128Gcm {
84	const ID: u16 = 0x0001;
85	const KEY_LEN: usize = 16;
86	const NONCE_LEN: usize = 12;
87	const TAG_LEN: usize = 16;
88}
89impl SealingAead for Aes128Gcm {
90	fn seal(key: &[u8], nonce: &[u8], aad: &[u8], pt: &[u8]) -> Result<Vec<u8>, HpkeError> {
91		use aes_gcm::{KeyInit, aead::Aead as _};
92		let cipher = aes_gcm::Aes128Gcm::new_from_slice(key).map_err(|_| HpkeError::SealError)?;
93		let nonce = aes_gcm::Nonce::from_slice(nonce);
94		cipher
95			.encrypt(nonce, aead::Payload { msg: pt, aad })
96			.map_err(|_| HpkeError::SealError)
97	}
98	fn open(key: &[u8], nonce: &[u8], aad: &[u8], ct: &[u8]) -> Result<Vec<u8>, HpkeError> {
99		use aes_gcm::{KeyInit, aead::Aead as _};
100		let cipher = aes_gcm::Aes128Gcm::new_from_slice(key).map_err(|_| HpkeError::OpenError)?;
101		let nonce = aes_gcm::Nonce::from_slice(nonce);
102		cipher
103			.decrypt(nonce, aead::Payload { msg: ct, aad })
104			.map_err(|_| HpkeError::OpenError)
105	}
106}
107
108/// AES-256-GCM (RFC 9180 §7.3, ID `0x0002`).
109///
110/// Constant-time only on platforms with hardware AES-NI/PCLMULQDQ.
111#[derive(Debug, Clone, Copy, Default)]
112pub struct Aes256Gcm;
113
114impl Sealed for Aes256Gcm {}
115impl Aead for Aes256Gcm {
116	const ID: u16 = 0x0002;
117	const KEY_LEN: usize = 32;
118	const NONCE_LEN: usize = 12;
119	const TAG_LEN: usize = 16;
120}
121impl SealingAead for Aes256Gcm {
122	fn seal(key: &[u8], nonce: &[u8], aad: &[u8], pt: &[u8]) -> Result<Vec<u8>, HpkeError> {
123		use aes_gcm::{KeyInit, aead::Aead as _};
124		let cipher = aes_gcm::Aes256Gcm::new_from_slice(key).map_err(|_| HpkeError::SealError)?;
125		let nonce = aes_gcm::Nonce::from_slice(nonce);
126		cipher
127			.encrypt(nonce, aead::Payload { msg: pt, aad })
128			.map_err(|_| HpkeError::SealError)
129	}
130	fn open(key: &[u8], nonce: &[u8], aad: &[u8], ct: &[u8]) -> Result<Vec<u8>, HpkeError> {
131		use aes_gcm::{KeyInit, aead::Aead as _};
132		let cipher = aes_gcm::Aes256Gcm::new_from_slice(key).map_err(|_| HpkeError::OpenError)?;
133		let nonce = aes_gcm::Nonce::from_slice(nonce);
134		cipher
135			.decrypt(nonce, aead::Payload { msg: ct, aad })
136			.map_err(|_| HpkeError::OpenError)
137	}
138}
139
140/// Export-only "AEAD" marker (RFC 9180 §7.3, ID `0xFFFF`).
141///
142/// Configurations parameterized over `ExportOnly` cannot encrypt or decrypt;
143/// only the export methods are available, because `ExportOnly` does not
144/// implement [`SealingAead`].
145#[derive(Debug, Clone, Copy, Default)]
146pub struct ExportOnly;
147
148impl Sealed for ExportOnly {}
149impl Aead for ExportOnly {
150	const ID: u16 = 0xFFFF;
151	const KEY_LEN: usize = 0;
152	const NONCE_LEN: usize = 0;
153	const TAG_LEN: usize = 0;
154}
155
156#[cfg(test)]
157mod tests {
158	use super::*;
159	use hex::FromHex;
160
161	/// RFC 8439 §2.8.2 test vector.
162	#[test]
163	fn rfc8439_chacha20poly1305_test_vector() {
164		let key = Vec::from_hex("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f")
165			.unwrap();
166		let nonce = Vec::from_hex("070000004041424344454647").unwrap();
167		let aad = Vec::from_hex("50515253c0c1c2c3c4c5c6c7").unwrap();
168		let pt = b"Ladies and Gentlemen of the class of '99: \
169                   If I could offer you only one tip for the future, sunscreen would be it.";
170
171		let ct = ChaCha20Poly1305::seal(&key, &nonce, &aad, pt).unwrap();
172		let recovered = ChaCha20Poly1305::open(&key, &nonce, &aad, &ct).unwrap();
173		assert_eq!(recovered, pt);
174
175		let mut bad = ct.clone();
176		bad[0] ^= 1;
177		assert_eq!(
178			ChaCha20Poly1305::open(&key, &nonce, &aad, &bad),
179			Err(HpkeError::OpenError)
180		);
181	}
182
183	#[test]
184	fn aes128gcm_roundtrip_short() {
185		let key = [0u8; 16];
186		let nonce = [0u8; 12];
187		let aad = b"";
188		let pt = b"hello";
189		let ct = Aes128Gcm::seal(&key, &nonce, aad, pt).unwrap();
190		assert_eq!(ct.len(), pt.len() + 16);
191		assert_eq!(Aes128Gcm::open(&key, &nonce, aad, &ct).unwrap(), pt);
192	}
193
194	#[test]
195	fn aes256gcm_roundtrip_short() {
196		let key = [0u8; 32];
197		let nonce = [0u8; 12];
198		let pt = b"world";
199		let ct = Aes256Gcm::seal(&key, &nonce, b"aad", pt).unwrap();
200		assert_eq!(Aes256Gcm::open(&key, &nonce, b"aad", &ct).unwrap(), pt);
201	}
202
203	#[test]
204	fn aes128gcm_rejects_bad_key_len() {
205		let r = Aes128Gcm::seal(&[0u8; 15], &[0u8; 12], b"", b"");
206		assert!(r.is_err());
207	}
208
209	#[test]
210	fn export_only_implements_aead_only() {
211		fn assert_aead<A: Aead>() {}
212		assert_aead::<ExportOnly>();
213		assert_eq!(ExportOnly::ID, 0xFFFF);
214		assert_eq!(ExportOnly::KEY_LEN, 0);
215		assert_eq!(ExportOnly::NONCE_LEN, 0);
216		assert_eq!(ExportOnly::TAG_LEN, 0);
217	}
218}