Skip to main content

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}