hpke/context.rs
1//! HPKE key schedule, modes and encryption contexts
2//! (`draft-ietf-hpke-hpke-05` Section 5).
3
4#[cfg(feature = "alloc")]
5use alloc::vec::Vec;
6use core::marker::PhantomData;
7
8use crypto::{Hash, MAX_HASH_OUTPUT_SIZE};
9
10use super::{HpkeError, aead::Aead, kdf::Kdf, kem::Kem, wipe};
11
12/// Maximum AEAD key size supported by the key schedule. The AEADs defined in
13/// this document use at most 32-byte keys.
14const MAX_AEAD_KEY_SIZE: usize = 64;
15
16/// Maximum AEAD nonce size supported by the contexts. The AEADs defined in this
17/// document use 12-byte nonces.
18const MAX_AEAD_NONCE_SIZE: usize = 64;
19
20/// HPKE mode values (`draft-ietf-hpke-hpke-05` Section 5.1).
21///
22/// The values `0x02` (`mode_auth`) and `0x03` (`mode_auth_psk`) were used by
23/// RFC 9180 and are RESERVED in `draft-ietf-hpke-hpke-05`, which removed the
24/// authenticated modes. They are deliberately not implemented here.
25const MODE_BASE: u8 = 0x00;
26const MODE_PRE_SHARED_KEY: u8 = 0x01;
27
28/// The HPKE session mode, shared by the sender and recipient roles.
29///
30/// The mode selects whether the key schedule is bound to a pre-shared key
31/// ([`Mode::PreSharedKey`]) or not ([`Mode::Base`]).
32///
33/// Pre-shared key bytes and their identifier are carried together, so the
34/// inconsistent inputs rejected by the specification cannot be expressed.
35///
36/// `draft-ietf-hpke-hpke-05` removed the asymmetric authenticated modes of
37/// RFC 9180 (`mode_auth` and `mode_auth_psk`). For sender *identity*
38/// authentication, sign the `(encapped_key, ciphertext)` tuple (see the crate
39/// documentation); a pre-shared key only proves possession of the key.
40pub enum Mode<'a> {
41 /// No sender authentication.
42 Base,
43 /// Pre-shared key mode: the shared key is mixed into the key schedule,
44 /// proving possession of it. This does not authenticate a sender identity.
45 PreSharedKey {
46 /// The pre-shared key. MUST contain at least 32 bytes of entropy.
47 pre_shared_key: &'a [u8],
48 /// A public identifier for the pre-shared key.
49 ///
50 /// It's purpose is let a recipient pick which PSK to use out of several it holds, and
51 /// to be bound into the key schedule so a wrong id yields a different key.
52 ///
53 /// As per the spec, `pre_shared_key_id`
54 /// MUST not be empty in pre-shared key mode. We recommend the Nil UUID (`00000000-0000-0000-0000-000000000000`)
55 /// if you don't have a relevant identifier for the pre-shared key.
56 pre_shared_key_id: &'a [u8],
57 },
58}
59
60impl<'a> Mode<'a> {
61 /// Ensures that in [`Mode::PreSharedKey`] mode both `pre_shared_key` and `pre_shared_key_id`
62 /// are not empty, as per the spec (`draft-ietf-hpke-hpke-05` Section 5.1 `VerifyPSKInputs`).
63 ///
64 /// Then decomposes the mode into the wire `mode` value, `pre_shared_key` and `pre_shared_key_id`.
65 fn validate_and_get_parts(&self) -> Result<(u8, &'a [u8], &'a [u8]), HpkeError> {
66 match self {
67 Mode::Base => Ok((MODE_BASE, &[], &[])),
68 Mode::PreSharedKey {
69 pre_shared_key,
70 pre_shared_key_id,
71 } => {
72 if pre_shared_key.is_empty() || pre_shared_key_id.is_empty() {
73 return Err(HpkeError::ValidationError);
74 }
75 Ok((MODE_PRE_SHARED_KEY, pre_shared_key, pre_shared_key_id))
76 }
77 }
78 }
79}
80
81/// Sets up a sender context (`draft-ietf-hpke-hpke-05` Section 5.1).
82///
83/// The encapsulated key must be transmitted to the recipient alongside the
84/// first message.
85///
86/// # Errors
87///
88/// - [`HpkeError::EncapError`] when the recipient's public key is invalid.
89/// - [`HpkeError::ValidationError`] when the pre-shared key or its identifier
90/// is empty in a pre-shared key mode.
91/// - [`HpkeError::Random`] when the operating system's random number generator
92/// is unavailable or fails.
93#[cfg(feature = "random")]
94pub fn new_sender<K: Kem, D: Kdf, A: Aead>(
95 mode: &Mode<'_>,
96 recipient_public_key: &K::PublicKey,
97 info: &[u8],
98) -> Result<(K::EncappedKey, SenderContext<A, D>), HpkeError> {
99 const { assert!(K::SHARED_SECRET_SIZE <= MAX_HASH_OUTPUT_SIZE) };
100
101 let (mode_id, pre_shared_key, pre_shared_key_id) = mode.validate_and_get_parts()?;
102
103 let (mut shared_secret, encapped_key) = K::encap(recipient_public_key)?;
104
105 let suite_id = ciphersuite_id::<K, D, A>();
106 let scheduled = key_schedule::<D, A>(mode_id, suite_id, &shared_secret, info, pre_shared_key, pre_shared_key_id)?;
107 wipe(shared_secret.as_mut());
108
109 let context = SenderContext(Context {
110 aead: scheduled.aead,
111 suite_id,
112 base_nonce: scheduled.base_nonce,
113 sequence_number: 0,
114 exporter_secret: scheduled.exporter_secret,
115 _kdf: PhantomData,
116 });
117 return Ok((encapped_key, context));
118}
119
120/// Sets up a recipient context (`draft-ietf-hpke-hpke-05` Section 5.1).
121///
122/// # Errors
123///
124/// - [`HpkeError::DecapError`] when the encapsulated key is invalid or the key
125/// exchange fails.
126/// - [`HpkeError::ValidationError`] when the pre-shared key or its identifier
127/// is empty in a pre-shared key mode.
128pub fn new_recipient<K: Kem, D: Kdf, A: Aead>(
129 mode: &Mode<'_>,
130 recipient_secret_key: &K::SecretKey,
131 encapped_key: &K::EncappedKey,
132 info: &[u8],
133) -> Result<RecipientContext<A, D>, HpkeError> {
134 const { assert!(K::SHARED_SECRET_SIZE <= MAX_HASH_OUTPUT_SIZE) };
135
136 let (mode_id, pre_shared_key, pre_shared_key_id) = mode.validate_and_get_parts()?;
137
138 let mut shared_secret = K::decap(encapped_key, recipient_secret_key)?;
139
140 let suite_id = ciphersuite_id::<K, D, A>();
141 let scheduled = key_schedule::<D, A>(mode_id, suite_id, &shared_secret, info, pre_shared_key, pre_shared_key_id)?;
142 wipe(shared_secret.as_mut());
143
144 let context = RecipientContext(Context {
145 aead: scheduled.aead,
146 suite_id,
147 base_nonce: scheduled.base_nonce,
148 sequence_number: 0,
149 exporter_secret: scheduled.exporter_secret,
150 _kdf: PhantomData,
151 });
152 return Ok(context);
153}
154
155/// Builds the HPKE `suite_id`: `"HPKE" || I2OSP(kem_id, 2) || I2OSP(kdf_id, 2)
156/// || I2OSP(aead_id, 2)` (`draft-ietf-hpke-hpke-05` Section 5.1).
157fn ciphersuite_id<K: Kem, D: Kdf, A: Aead>() -> [u8; 10] {
158 let mut suite_id = [0u8; 10];
159 suite_id[..4].copy_from_slice(b"HPKE");
160 suite_id[4..6].copy_from_slice(&K::HPKE_KEM_ID.to_be_bytes());
161 suite_id[6..8].copy_from_slice(&D::HPKE_KDF_ID.to_be_bytes());
162 suite_id[8..10].copy_from_slice(&A::HPKE_AEAD_ID.to_be_bytes());
163 return suite_id;
164}
165
166/// The result of the HPKE key schedule (`draft-ietf-hpke-hpke-05` Section 5.1).
167struct KeySchedule<A: Aead> {
168 aead: A,
169 base_nonce: [u8; MAX_AEAD_NONCE_SIZE],
170 exporter_secret: Hash,
171}
172
173/// `draft-ietf-hpke-hpke-05` Section 5.1 `CombineSecrets_TwoStage` (identical
174/// to the RFC 9180 `KeySchedule`) for two-stage KDFs, or `CombineSecrets_OneStage`
175/// for single-stage KDFs.
176fn key_schedule<D: Kdf, A: Aead>(
177 mode: u8,
178 suite_id: [u8; 10],
179 shared_secret: &Hash,
180 info: &[u8],
181 pre_shared_key: &[u8],
182 pre_shared_key_id: &[u8],
183) -> Result<KeySchedule<A>, HpkeError> {
184 if D::SINGLE_STAGE {
185 return single_stage_key_schedule::<D, A>(
186 mode,
187 suite_id,
188 shared_secret,
189 info,
190 pre_shared_key,
191 pre_shared_key_id,
192 );
193 }
194
195 const {
196 assert!(A::KEY_SIZE <= MAX_AEAD_KEY_SIZE);
197 // `0` is the Export-Only pseudo-AEAD, which has no nonce; every other
198 // AEAD must have the nonce size the specification mandates.
199 assert!(A::NONCE_SIZE == 0 || (A::NONCE_SIZE >= 8 && A::NONCE_SIZE <= MAX_AEAD_NONCE_SIZE));
200 assert!(D::OUTPUT_SIZE <= MAX_HASH_OUTPUT_SIZE);
201 }
202
203 let pre_shared_key_id_hash = D::labeled_extract(b"", &suite_id, b"psk_id_hash", pre_shared_key_id);
204 let info_hash = D::labeled_extract(b"", &suite_id, b"info_hash", info);
205
206 let mut key_schedule_context = [0u8; 1 + 2 * MAX_HASH_OUTPUT_SIZE];
207 key_schedule_context[0] = mode;
208 key_schedule_context[1..1 + D::OUTPUT_SIZE].copy_from_slice(&pre_shared_key_id_hash);
209 key_schedule_context[1 + D::OUTPUT_SIZE..1 + 2 * D::OUTPUT_SIZE].copy_from_slice(&info_hash);
210 let key_schedule_context = &key_schedule_context[..1 + 2 * D::OUTPUT_SIZE];
211
212 let mut secret = D::labeled_extract(shared_secret, &suite_id, b"secret", pre_shared_key);
213
214 let mut key = [0u8; MAX_AEAD_KEY_SIZE];
215 D::labeled_expand(&mut key[..A::KEY_SIZE], &secret, &suite_id, b"key", key_schedule_context)?;
216 let aead = A::new(&key[..A::KEY_SIZE])?;
217 wipe(&mut key);
218
219 let mut base_nonce = [0u8; MAX_AEAD_NONCE_SIZE];
220 D::labeled_expand(
221 &mut base_nonce[..A::NONCE_SIZE],
222 &secret,
223 &suite_id,
224 b"base_nonce",
225 key_schedule_context,
226 )?;
227
228 let mut exporter_secret_bytes = [0u8; MAX_HASH_OUTPUT_SIZE];
229 D::labeled_expand(
230 &mut exporter_secret_bytes[..D::OUTPUT_SIZE],
231 &secret,
232 &suite_id,
233 b"exp",
234 key_schedule_context,
235 )?;
236 let exporter_secret = Hash::try_from(&exporter_secret_bytes[..D::OUTPUT_SIZE]).expect("OUTPUT_SIZE fits in a Hash");
237 wipe(&mut exporter_secret_bytes);
238 wipe(secret.as_mut());
239
240 return Ok(KeySchedule {
241 aead,
242 base_nonce,
243 exporter_secret,
244 });
245}
246
247/// `I2OSP(value, 2)`: the big-endian two-byte encoding of `len`.
248fn length_prefix(len: usize) -> [u8; 2] {
249 return (len as u16).to_be_bytes();
250}
251
252/// `draft-ietf-hpke-hpke-05` Section 5.1 `CombineSecrets_OneStage` for
253/// single-stage KDFs:
254/// `secret = LabeledDerive(concat(lengthPrefixed(pre_shared_key),
255/// lengthPrefixed(shared_secret)), "secret", concat(mode,
256/// lengthPrefixed(pre_shared_key_id), lengthPrefixed(info)), KEY_SIZE + NONCE_SIZE + OUTPUT_SIZE)`,
257/// split into the AEAD key, the base nonce and the exporter secret.
258fn single_stage_key_schedule<D: Kdf, A: Aead>(
259 mode: u8,
260 suite_id: [u8; 10],
261 shared_secret: &Hash,
262 info: &[u8],
263 pre_shared_key: &[u8],
264 pre_shared_key_id: &[u8],
265) -> Result<KeySchedule<A>, HpkeError> {
266 const {
267 assert!(A::KEY_SIZE <= MAX_AEAD_KEY_SIZE);
268 // `0` is the Export-Only pseudo-AEAD, which has no nonce; every other
269 // AEAD must have the nonce size the specification mandates.
270 assert!(A::NONCE_SIZE == 0 || (A::NONCE_SIZE >= 8 && A::NONCE_SIZE <= MAX_AEAD_NONCE_SIZE));
271 assert!(D::OUTPUT_SIZE <= MAX_HASH_OUTPUT_SIZE);
272 }
273
274 // `key_schedule` only dispatches here when `D::SINGLE_STAGE` is `true`, but
275 // the branch is a runtime one, so this function is also monomorphized for
276 // two-stage KDFs. Reject them at runtime instead of failing to compile.
277 if !D::SINGLE_STAGE {
278 return Err(HpkeError::NotSupported);
279 }
280
281 // The single-stage `LabeledDerive` encodes each length in two bytes.
282 if pre_shared_key.len() > u16::MAX as usize
283 || shared_secret.len() > u16::MAX as usize
284 || pre_shared_key_id.len() > u16::MAX as usize
285 || info.len() > u16::MAX as usize
286 {
287 return Err(HpkeError::ValidationError);
288 }
289
290 let mode = [mode];
291 let pre_shared_key_len = length_prefix(pre_shared_key.len());
292 let shared_secret_len = length_prefix(shared_secret.len());
293 let pre_shared_key_id_len = length_prefix(pre_shared_key_id.len());
294 let info_len = length_prefix(info.len());
295
296 let secret_len = A::KEY_SIZE + A::NONCE_SIZE + D::OUTPUT_SIZE;
297 let mut secret_bytes = [0u8; MAX_AEAD_KEY_SIZE + MAX_AEAD_NONCE_SIZE + MAX_HASH_OUTPUT_SIZE];
298 D::labeled_derive(
299 &mut secret_bytes[..secret_len],
300 &[
301 &pre_shared_key_len,
302 pre_shared_key,
303 &shared_secret_len,
304 shared_secret.as_ref(),
305 ],
306 &suite_id,
307 b"secret",
308 &[&mode, &pre_shared_key_id_len, pre_shared_key_id, &info_len, info],
309 )?;
310
311 let aead = A::new(&secret_bytes[..A::KEY_SIZE])?;
312
313 let mut base_nonce = [0u8; MAX_AEAD_NONCE_SIZE];
314 base_nonce[..A::NONCE_SIZE].copy_from_slice(&secret_bytes[A::KEY_SIZE..A::KEY_SIZE + A::NONCE_SIZE]);
315
316 let exporter_secret =
317 Hash::try_from(&secret_bytes[A::KEY_SIZE + A::NONCE_SIZE..secret_len]).expect("OUTPUT_SIZE fits in a Hash");
318 wipe(&mut secret_bytes);
319
320 return Ok(KeySchedule {
321 aead,
322 base_nonce,
323 exporter_secret,
324 });
325}
326
327/// Computes the per-message nonce `base_nonce XOR I2OSP(sequence_number, NONCE_SIZE)`
328/// (`draft-ietf-hpke-hpke-05` Section 5.2).
329fn compute_nonce<A: Aead>(base_nonce: &[u8; MAX_AEAD_NONCE_SIZE], sequence_number: u64) -> [u8; MAX_AEAD_NONCE_SIZE] {
330 let mut nonce = *base_nonce;
331 let sequence_number_bytes = sequence_number.to_be_bytes();
332 let start = A::NONCE_SIZE - sequence_number_bytes.len();
333 for (i, byte) in sequence_number_bytes.iter().enumerate() {
334 nonce[start + i] ^= byte;
335 }
336 return nonce;
337}
338
339/// Shared state of an HPKE encryption context (`draft-ietf-hpke-hpke-05` Section 5.1).
340///
341/// This is the common implementation behind [`SenderContext`] and
342/// [`RecipientContext`]; the two wrappers restrict the exposed operations to
343/// their role.
344#[cfg_attr(feature = "zeroize", derive(zeroize::ZeroizeOnDrop))]
345struct Context<A: Aead, D: Kdf> {
346 #[cfg_attr(feature = "zeroize", zeroize(skip))]
347 aead: A,
348 suite_id: [u8; 10],
349 base_nonce: [u8; MAX_AEAD_NONCE_SIZE],
350 sequence_number: u64,
351 exporter_secret: Hash,
352 #[cfg_attr(feature = "zeroize", zeroize(skip))]
353 _kdf: PhantomData<D>,
354}
355
356impl<A: Aead, D: Kdf> Context<A, D> {
357 fn seal_in_place(&mut self, in_out: &mut [u8], associated_data: &[u8]) -> Result<Hash, HpkeError> {
358 // The Export-Only pseudo-AEAD (`NONCE_SIZE`/`TAG_SIZE` of `0`) cannot
359 // encrypt; report it as unsupported rather than failing to compile. The
360 // `NONCE_SIZE < 8` bound also keeps `compute_nonce` from underflowing.
361 if A::NONCE_SIZE < 8 || A::TAG_SIZE == 0 {
362 return Err(HpkeError::NotSupported);
363 }
364
365 // `draft-ietf-hpke-hpke-05` Section 5.2: implementations MUST NOT
366 // encrypt plaintexts larger than the AEAD's `P_MAX`.
367 if in_out.len() as u64 > A::MAX_PLAINTEXT_SIZE {
368 return Err(HpkeError::MessageLimitReached);
369 }
370
371 if self.sequence_number == u64::MAX {
372 return Err(HpkeError::MessageLimitReached);
373 }
374
375 // Computing the nonce, sealing, and incrementing the sequence number is
376 // a single `&mut self` operation, so it cannot be interleaved with
377 // another `seal`/`open` on the same context. This satisfies the
378 // atomicity requirement of `draft-ietf-hpke-hpke-05` Section 5.2.
379 let nonce = compute_nonce::<A>(&self.base_nonce, self.sequence_number);
380 let tag = self.aead.seal(in_out, &nonce[..A::NONCE_SIZE], associated_data)?;
381 self.sequence_number += 1;
382 return Ok(tag);
383 }
384
385 #[cfg(feature = "alloc")]
386 fn seal(&mut self, plaintext: &[u8], associated_data: &[u8]) -> Result<Vec<u8>, HpkeError> {
387 let mut ciphertext = Vec::with_capacity(plaintext.len() + A::TAG_SIZE);
388 ciphertext.extend_from_slice(plaintext);
389
390 let tag = self.seal_in_place(&mut ciphertext, associated_data)?;
391 ciphertext.extend_from_slice(tag.as_ref());
392 return Ok(ciphertext);
393 }
394
395 fn open_in_place(&mut self, in_out: &mut [u8], associated_data: &[u8], tag: &[u8]) -> Result<(), HpkeError> {
396 // See `seal_in_place`: the Export-Only pseudo-AEAD cannot open, and the
397 // bound keeps `compute_nonce` from underflowing.
398 if A::NONCE_SIZE < 8 || A::TAG_SIZE == 0 {
399 return Err(HpkeError::NotSupported);
400 }
401
402 // `draft-ietf-hpke-hpke-05` Section 5.2: implementations MUST NOT open
403 // ciphertexts larger than the AEAD's `C_MAX`. The ciphertext here is
404 // the plaintext buffer plus the detached tag.
405 if (in_out.len() as u64).saturating_add(tag.len() as u64) > A::MAX_CIPHERTEXT_SIZE {
406 return Err(HpkeError::MessageLimitReached);
407 }
408
409 let nonce = compute_nonce::<A>(&self.base_nonce, self.sequence_number);
410 self.aead.open(in_out, &nonce[..A::NONCE_SIZE], associated_data, tag)?;
411
412 if self.sequence_number == u64::MAX {
413 return Err(HpkeError::MessageLimitReached);
414 }
415 self.sequence_number += 1;
416 return Ok(());
417 }
418
419 #[cfg(feature = "alloc")]
420 fn open(&mut self, ciphertext: &[u8], associated_data: &[u8]) -> Result<Vec<u8>, HpkeError> {
421 if ciphertext.len() < A::TAG_SIZE {
422 return Err(HpkeError::OpenError);
423 }
424
425 let plaintext_len = ciphertext.len() - A::TAG_SIZE;
426 let mut plaintext = Vec::with_capacity(plaintext_len);
427 plaintext.extend_from_slice(&ciphertext[..plaintext_len]);
428
429 self.open_in_place(&mut plaintext, associated_data, &ciphertext[plaintext_len..])?;
430 return Ok(plaintext);
431 }
432
433 fn export(&self, out: &mut [u8], exporter_context: &[u8]) -> Result<(), HpkeError> {
434 if D::SINGLE_STAGE {
435 return D::labeled_derive(
436 out,
437 &[self.exporter_secret.as_ref()],
438 &self.suite_id,
439 b"sec",
440 &[exporter_context],
441 );
442 }
443 return D::labeled_expand(out, &self.exporter_secret, &self.suite_id, b"sec", exporter_context);
444 }
445}
446
447/// The sender's encryption context, returned by [`new_sender`].
448///
449/// A context tracks a monotonically increasing sequence number; messages MUST
450/// be sealed in order, and the matching [`RecipientContext`] MUST open them in
451/// the same order. Sealing more than `2^64 - 1` messages with a single context
452/// is not possible.
453pub struct SenderContext<A: Aead, D: Kdf>(Context<A, D>);
454
455impl<A: Aead, D: Kdf> SenderContext<A, D> {
456 /// Encrypts `in_out` in place and returns the detached authentication tag.
457 ///
458 /// The ciphertext is `in_out` itself; combine it with the returned tag as
459 /// `ciphertext || tag` for transmission.
460 ///
461 /// # Errors
462 ///
463 /// Returns [`HpkeError::MessageLimitReached`] when `in_out` is longer than
464 /// the AEAD's maximum plaintext size (`P_MAX`) or the context's sequence
465 /// number is exhausted, or [`HpkeError::NotSupported`] when the
466 /// ciphersuite uses the [`ExportOnly`](super::aead::ExportOnly) pseudo-AEAD.
467 pub fn seal_in_place(&mut self, in_out: &mut [u8], associated_data: &[u8]) -> Result<Hash, HpkeError> {
468 return self.0.seal_in_place(in_out, associated_data);
469 }
470
471 /// Encrypts `plaintext` and returns `ciphertext || tag`.
472 ///
473 /// # Errors
474 ///
475 /// Returns [`HpkeError::MessageLimitReached`] when `plaintext` is longer
476 /// than the AEAD's maximum plaintext size (`P_MAX`) or the context's
477 /// sequence number is exhausted, or [`HpkeError::NotSupported`] when the
478 /// ciphersuite uses the [`ExportOnly`](super::aead::ExportOnly) pseudo-AEAD.
479 #[cfg(feature = "alloc")]
480 pub fn seal(&mut self, plaintext: &[u8], associated_data: &[u8]) -> Result<Vec<u8>, HpkeError> {
481 return self.0.seal(plaintext, associated_data);
482 }
483
484 /// Derives `out.len()` bytes of exported key material, bound to
485 /// `exporter_context` (`draft-ietf-hpke-hpke-05` Section 5.3).
486 ///
487 /// # Replay
488 ///
489 /// HPKE does not provide replay protection: replaying the encapsulated key
490 /// `enc` produces an identical context and therefore identical exported
491 /// secrets. Applications MUST NOT use an exported secret unless it is safe
492 /// for the same value to be produced more than once, and MUST NOT derive an
493 /// AEAD `(key, nonce)` pair from it (as the example in RFC 9180 Section 9.8
494 /// did). When an exported secret feeds encryption, mix in fresh
495 /// recipient-provided randomness, as in `draft-ietf-hpke-hpke-05`
496 /// Section 9.8.
497 ///
498 /// # Errors
499 ///
500 /// Returns [`HpkeError::KdfOutputTooLong`] when `out` is longer than the
501 /// KDF can produce (`255 * OUTPUT_SIZE` for two-stage KDFs, `2^16 - 1` bytes for
502 /// single-stage KDFs).
503 pub fn export(&self, out: &mut [u8], exporter_context: &[u8]) -> Result<(), HpkeError> {
504 return self.0.export(out, exporter_context);
505 }
506}
507
508/// The recipient's decryption context, returned by [`new_recipient`].
509///
510/// See [`SenderContext`] for the ordering and message-limit requirements.
511pub struct RecipientContext<A: Aead, D: Kdf>(Context<A, D>);
512
513impl<A: Aead, D: Kdf> RecipientContext<A, D> {
514 /// Decrypts `in_out` in place using the detached authentication `tag`.
515 ///
516 /// The sequence number is only advanced when decryption succeeds.
517 ///
518 /// # Errors
519 ///
520 /// Returns [`HpkeError::OpenError`] when the ciphertext is invalid (wrong
521 /// key, tampered data or wrong associated data),
522 /// [`HpkeError::MessageLimitReached`] when the ciphertext is longer than
523 /// the AEAD's maximum (`C_MAX`) or the context's sequence number is
524 /// exhausted, or [`HpkeError::NotSupported`] when the ciphersuite uses
525 /// the [`ExportOnly`](super::aead::ExportOnly) pseudo-AEAD.
526 ///
527 /// # Buffer contents on error
528 ///
529 /// When decryption succeeds but the sequence number is exhausted, the
530 /// plaintext has already been written to `in_out` even though
531 /// [`HpkeError::MessageLimitReached`] is returned (matching
532 /// `draft-ietf-hpke-hpke-05` Section 5.2, which raises the error after
533 /// `Open`). On [`HpkeError::OpenError`] the buffer is left untouched.
534 pub fn open_in_place(&mut self, in_out: &mut [u8], associated_data: &[u8], tag: &[u8]) -> Result<(), HpkeError> {
535 return self.0.open_in_place(in_out, associated_data, tag);
536 }
537
538 /// Decrypts `ciphertext` (which must end with the authentication tag) and
539 /// returns the plaintext.
540 ///
541 /// # Errors
542 ///
543 /// Returns [`HpkeError::OpenError`] when the ciphertext is invalid or too
544 /// short to contain a tag, [`HpkeError::MessageLimitReached`] when the
545 /// ciphertext is longer than the AEAD's maximum (`C_MAX`) or the context's
546 /// sequence number is exhausted, or [`HpkeError::NotSupported`] when the
547 /// ciphersuite uses the
548 /// [`ExportOnly`](super::aead::ExportOnly) pseudo-AEAD.
549 #[cfg(feature = "alloc")]
550 pub fn open(&mut self, ciphertext: &[u8], associated_data: &[u8]) -> Result<Vec<u8>, HpkeError> {
551 return self.0.open(ciphertext, associated_data);
552 }
553
554 /// Derives `out.len()` bytes of exported key material, bound to
555 /// `exporter_context` (`draft-ietf-hpke-hpke-05` Section 5.3).
556 ///
557 /// # Replay
558 ///
559 /// HPKE does not provide replay protection: replaying the encapsulated key
560 /// `enc` produces an identical context and therefore identical exported
561 /// secrets. Applications MUST NOT use an exported secret unless it is safe
562 /// for the same value to be produced more than once, and MUST NOT derive an
563 /// AEAD `(key, nonce)` pair from it (as the example in RFC 9180 Section 9.8
564 /// did). When an exported secret feeds encryption, mix in fresh
565 /// recipient-provided randomness, as in `draft-ietf-hpke-hpke-05`
566 /// Section 9.8.
567 ///
568 /// # Errors
569 ///
570 /// Returns [`HpkeError::KdfOutputTooLong`] when `out` is longer than the
571 /// KDF can produce (`255 * OUTPUT_SIZE` for two-stage KDFs, `2^16 - 1` bytes for
572 /// single-stage KDFs).
573 pub fn export(&self, out: &mut [u8], exporter_context: &[u8]) -> Result<(), HpkeError> {
574 return self.0.export(out, exporter_context);
575 }
576}
577
578/// Builds a sender context from an already-computed KEM `shared_secret`,
579/// skipping encapsulation.
580///
581/// This exists so the test suite can forward-check the vector corpus: given the
582/// `shared_secret` from a test vector, the resulting context must reproduce the
583/// vector's ciphertexts and exported values. It is not part of the public API.
584#[cfg(all(test, feature = "alloc", feature = "random"))]
585pub(crate) fn new_sender_from_shared_secret<K: Kem, D: Kdf, A: Aead>(
586 mode: &Mode<'_>,
587 shared_secret: &Hash,
588 info: &[u8],
589) -> Result<SenderContext<A, D>, HpkeError> {
590 const { assert!(K::SHARED_SECRET_SIZE <= MAX_HASH_OUTPUT_SIZE) };
591
592 let (mode_id, pre_shared_key, pre_shared_key_id) = mode.validate_and_get_parts()?;
593
594 let suite_id = ciphersuite_id::<K, D, A>();
595 let scheduled = key_schedule::<D, A>(mode_id, suite_id, shared_secret, info, pre_shared_key, pre_shared_key_id)?;
596
597 return Ok(SenderContext(Context {
598 aead: scheduled.aead,
599 suite_id,
600 base_nonce: scheduled.base_nonce,
601 sequence_number: 0,
602 exporter_secret: scheduled.exporter_secret,
603 _kdf: PhantomData,
604 }));
605}