Skip to main content

hpke/
hpke.rs

1//! HPKE: Hybrid Public Key Encryption, as specified in
2//! `draft-ietf-hpke-hpke-05` (which obsoletes RFC 9180).
3//!
4//! HPKE provides public-key encryption of arbitrary-sized plaintexts for a
5//! recipient public key. It also includes an authenticated variant that
6//! authenticates possession of a pre-shared key (PSK). The two asymmetric
7//! authenticated variants of RFC 9180 (`mode_auth` and `mode_auth_psk`) were
8//! removed from the specification and are not implemented.
9//!
10//! A ciphersuite is a triple `(KEM, KDF, AEAD)` selected at compile time via
11//! type parameters. The crate ships the registered algorithms, the
12//! MLKEM768-X25519 hybrid post-quantum KEM (KEM id `0x647a`) and the SHAKE256
13//! single-stage KDF (KDF id `0x0011`) from `draft-ietf-hpke-pq-05`:
14//!
15//! - KEMs: [`kem::MLKEM768X25519`], [`kem::X25519HkdfSha256`],
16//!   [`kem::P256HkdfSha256`], [`kem::P521HkdfSha512`]
17//! - KDFs: [`kdf::HkdfSha256`], [`kdf::HkdfSha512`], [`kdf::Shake256`],
18//!   [`kdf::Blake3`] (unofficial KDF id `0xFF01`, see the type docs)
19//! - AEADs: [`aead::Aes256Gcm`], [`aead::ChaCha20Poly1305`], [`aead::ExportOnly`]
20//!
21//! Any of the three roles can be replaced by a custom implementation: define a
22//! type and implement [`kem::Kem`], [`kdf::Kdf`] or [`aead::Aead`] for it.
23//!
24//! # Example
25//!
26//! One-shot encryption with the hybrid post-quantum MLKEM768-X25519 +
27//! SHAKE256 + ChaCha20-Poly1305 suite:
28//!
29//! ```
30//! # #[cfg(all(feature = "alloc", feature = "random"))] {
31//! use hpke::{
32//!     self, Mode,
33//!     aead::ChaCha20Poly1305,
34//!     kdf::Shake256,
35//!     kem::Kem,
36//!     kem::MLKEM768X25519,
37//! };
38//!
39//! let (bob_sk, bob_pk) = MLKEM768X25519::generate_keypair().unwrap();
40//!
41//! let info = b"Alice and Bob's weekly chat";
42//! let (encapped_key, mut sender) = hpke::new_sender::<MLKEM768X25519, Shake256, ChaCha20Poly1305>(
43//!     &Mode::Base, &bob_pk, info,
44//! ).unwrap();
45//! let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();
46//!
47//! let mut recipient = hpke::new_recipient::<MLKEM768X25519, Shake256, ChaCha20Poly1305>(
48//!     &Mode::Base, &bob_sk, &encapped_key, info,
49//! ).unwrap();
50//! let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
51//! assert_eq!(plaintext, b"fronthand or backhand?");
52//! # }
53//! ```
54//!
55//! # Pre-shared key mode
56//!
57//! In pre-shared key mode both parties are provisioned with a `pre_shared_key`
58//! (at least 32 bytes of entropy) and a `pre_shared_key_id` that identifies it.
59//! The pre-shared key strengthens confidentiality in certain adversary models
60//! and proves possession of the key, but it does not by itself say *which*
61//! sender produced a message. Use the sender-authentication construction below
62//! when the recipient must attribute a message to a specific sender.
63//!
64//! ```
65//! # #[cfg(all(feature = "alloc", feature = "random"))] {
66//! use hpke::{
67//!     self, Mode,
68//!     aead::Aes256Gcm,
69//!     kdf::HkdfSha256,
70//!     kem::Kem,
71//!     kem::X25519HkdfSha256,
72//! };
73//!
74//! let (bob_sk, bob_pk) = X25519HkdfSha256::generate_keypair().unwrap();
75//! let pre_shared_key = [0x42u8; 32];
76//! let pre_shared_key_id = b"weekly-chat";
77//!
78//! let info = b"Alice and Bob's weekly chat";
79//! let (encapped_key, mut sender) = hpke::new_sender::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
80//!     &Mode::PreSharedKey { pre_shared_key: &pre_shared_key, pre_shared_key_id },
81//!     &bob_pk, info,
82//! ).unwrap();
83//! let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();
84//!
85//! let mut recipient = hpke::new_recipient::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
86//!     &Mode::PreSharedKey { pre_shared_key: &pre_shared_key, pre_shared_key_id },
87//!     &bob_sk, &encapped_key, info,
88//! ).unwrap();
89//! let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
90//! assert_eq!(plaintext, b"fronthand or backhand?");
91//! # }
92//! ```
93//!
94//! # Sender authentication
95//!
96//! `draft-ietf-hpke-hpke-05` removed the asymmetric authenticated modes of
97//! RFC 9180 (`mode_auth` and `mode_auth_psk`), so an HPKE ciphertext does not
98//! carry a sender signature. To let the recipient attribute a message to a
99//! specific sender, combine Base-mode HPKE with a signature scheme over the
100//! transmitted `(encapped_key, ciphertext)` tuple. This example uses ML-DSA-65
101//! (FIPS 204), which is post-quantum; the same construction works with any
102//! signature scheme.
103//!
104//! ```
105//! # #[cfg(all(feature = "alloc", feature = "random"))] {
106//! use crypto::mldsa::MlDsa65SecretKey;
107//! use hpke::{
108//!     self, Mode,
109//!     aead::Aes256Gcm,
110//!     kdf::HkdfSha256,
111//!     kem::{Kem, X25519HkdfSha256},
112//! };
113//!
114//! // Alice's long-term ML-DSA-65 signing key pair; Bob holds the HPKE key pair.
115//! let alice_signing = MlDsa65SecretKey::generate().unwrap();
116//! let alice_signing_public = alice_signing.public_key();
117//! let (bob_sk, bob_pk) = X25519HkdfSha256::generate_keypair().unwrap();
118//!
119//! let info = b"Alice and Bob's weekly chat";
120//! let (encapped_key, mut sender) = hpke::new_sender::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
121//!     &Mode::Base, &bob_pk, info,
122//! ).unwrap();
123//! let ciphertext = sender.seal(b"fronthand or backhand?", b"a gentleman's game").unwrap();
124//!
125//! // Alice signs the message `encapped_key || ciphertext` under a
126//! // domain-separation context.
127//! let mut encapped_key_bytes = [0u8; X25519HkdfSha256::ENCAPPED_KEY_SIZE];
128//! X25519HkdfSha256::encapped_key_to_bytes(&mut encapped_key_bytes, &encapped_key).unwrap();
129//! let mut transcript = encapped_key_bytes.to_vec();
130//! transcript.extend_from_slice(&ciphertext);
131//! let signature = alice_signing.sign(&transcript, b"HPKE sender auth").unwrap();
132//!
133//! // Bob verifies the signature before decrypting and rejects on failure.
134//! assert!(
135//!     alice_signing_public
136//!         .verify(&transcript, &signature, b"HPKE sender auth")
137//!         .is_ok()
138//! );
139//!
140//! let mut recipient = hpke::new_recipient::<X25519HkdfSha256, HkdfSha256, Aes256Gcm>(
141//!     &Mode::Base, &bob_sk, &encapped_key, info,
142//! ).unwrap();
143//! let plaintext = recipient.open(&ciphertext, b"a gentleman's game").unwrap();
144//! assert_eq!(plaintext, b"fronthand or backhand?");
145//! # }
146//! ```
147//!
148//! The signature covers the encapsulated key as well as the ciphertext, so a
149//! replayed or re-encapsulated message does not verify against the same
150//! signature. The recipient must verify (and reject on failure) *before*
151//! treating the plaintext as authenticated.
152//!
153//! # Security
154//!
155//! - Messages **must** be sealed and opened in the same order. There is no
156//!   tolerance for reordering or loss; per-message context (e.g. a counter)
157//!   can be authenticated by passing it as `associated_data`.
158//! - Replaying an `encapped_key` reproduces the same context, so exported
159//!   secrets are not replay protected. Do not derive an AEAD `(key, nonce)`
160//!   pair from an exported secret; mix in fresh recipient-provided randomness
161//!   (`draft-ietf-hpke-hpke-05` Section 9.8).
162//! - Plaintexts larger than the AEAD's `MAX_PLAINTEXT_SIZE` (or ciphertexts larger than
163//!   `MAX_CIPHERTEXT_SIZE`) are rejected rather than processed, as `draft-ietf-hpke-hpke-05`
164//!   Section 5.2 requires. Sealing and opening also take `&mut self`, so nonce
165//!   derivation, encryption, and sequence-number advancement cannot be
166//!   interleaved on a shared context.
167//! - Pre-shared keys must contain at least 32 bytes of entropy. Low-entropy
168//!   pre-shared keys are vulnerable to dictionary attacks.
169//! - There is no forward secrecy against recipient static key compromise:
170//!   anyone with the recipient's secret key can decrypt all past captures.
171
172#![cfg_attr(not(test), no_std)]
173
174#[cfg(feature = "alloc")]
175extern crate alloc;
176
177pub mod aead;
178mod context;
179pub mod kdf;
180pub mod kem;
181
182#[cfg(all(test, feature = "alloc", feature = "random"))]
183mod tests;
184
185pub use aead::Aead;
186#[cfg(feature = "random")]
187pub use context::new_sender;
188pub use context::{Mode, RecipientContext, SenderContext, new_recipient};
189pub use kdf::Kdf;
190pub use kem::Kem;
191
192/// Best-effort wipe of transient secret buffers.
193///
194/// This is a no-op unless the `zeroize` feature is enabled.
195#[inline(always)]
196pub(crate) fn wipe(_bytes: &mut [u8]) {
197    #[cfg(feature = "zeroize")]
198    {
199        use zeroize::Zeroize;
200        _bytes.zeroize();
201    }
202}
203
204////////////////////////////////////////////////////////////////////////////////////////////////////
205// Errors
206
207/// Errors that can occur during HPKE setup, key encapsulation, key derivation
208/// or message encryption/decryption.
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum HpkeError {
211    /// A KEM input or output failed validation (e.g. an invalid or low-order
212    /// public key was provided), or a pre-shared key mode input is missing (an
213    /// empty pre-shared key or pre-shared key identifier).
214    ValidationError,
215    /// A key, encapsulated key or ciphertext could not be deserialized
216    /// (e.g. wrong length or malformed encoding).
217    DeserializeError,
218    /// The encapsulation step failed (e.g. the recipient's public key is
219    /// invalid).
220    EncapError,
221    /// The decapsulation step failed (e.g. the encapsulated key is invalid,
222    /// or the Diffie-Hellman output was rejected).
223    DecapError,
224    /// The AEAD failed to decrypt the ciphertext (wrong key, tampered
225    /// ciphertext or wrong associated data).
226    OpenError,
227    /// Deriving a key pair from input keying material failed after
228    /// exhausting all rejection-sampling candidates.
229    DeriveKeyPairError,
230    /// A message length or sequence-number limit was reached: the plaintext
231    /// exceeds the AEAD's `P_MAX`, the ciphertext exceeds `C_MAX`, or the
232    /// context has exhausted its sequence numbers, so no further messages can
233    /// be sealed or opened with this context (`draft-ietf-hpke-hpke-05`
234    /// Sections 4.3 and 5.2).
235    MessageLimitReached,
236    /// The operation is not supported by this KEM, KDF or AEAD (e.g. an
237    /// authenticated mode with a KEM that does not support it, or sealing a
238    /// message with the Export-Only pseudo-AEAD).
239    NotSupported,
240    /// The pseudorandom key given to the KDF expand step does not have the
241    /// expected length.
242    InvalidPseudorandomKey,
243    /// The key given to the AEAD does not have the expected length.
244    InvalidKey,
245    /// The KDF was asked for more output than it can produce
246    /// (`255 * OUTPUT_SIZE` for two-stage KDFs, `2^16 - 1` bytes for
247    /// single-stage KDFs).
248    KdfOutputTooLong,
249    /// An underlying AEAD error.
250    Aead(crypto::AeadError),
251    /// An underlying elliptic-curve error.
252    EllipticCurve(crypto::EllipticCurveError),
253    /// An underlying ML-KEM error.
254    MlKem(crypto::mlkem::MlKemError),
255    /// The operating system's random number generator failed.
256    Random(crypto::RandomError),
257    /// Something went wrong, without further details.
258    Unspecified,
259}
260
261impl core::fmt::Display for HpkeError {
262    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
263        match self {
264            HpkeError::ValidationError => write!(f, "key validation failed"),
265            HpkeError::DeserializeError => write!(f, "key or ciphertext deserialization failed"),
266            HpkeError::EncapError => write!(f, "encapsulation failed"),
267            HpkeError::DecapError => write!(f, "decapsulation failed"),
268            HpkeError::OpenError => write!(f, "decryption failed"),
269            HpkeError::DeriveKeyPairError => write!(f, "key pair derivation failed"),
270            HpkeError::MessageLimitReached => write!(f, "message limit reached"),
271            HpkeError::NotSupported => write!(f, "operation is not supported"),
272            HpkeError::InvalidPseudorandomKey => write!(f, "pseudorandom key has an invalid length"),
273            HpkeError::InvalidKey => write!(f, "key has an invalid length"),
274            HpkeError::KdfOutputTooLong => write!(f, "KDF output length exceeds the limit"),
275            HpkeError::Aead(err) => write!(f, "{err}"),
276            HpkeError::EllipticCurve(err) => write!(f, "{err}"),
277            HpkeError::MlKem(err) => write!(f, "{err}"),
278            HpkeError::Random(err) => write!(f, "{err}"),
279            HpkeError::Unspecified => write!(f, "unknown error"),
280        }
281    }
282}
283
284impl core::error::Error for HpkeError {}
285
286impl From<crypto::AeadError> for HpkeError {
287    fn from(err: crypto::AeadError) -> Self {
288        HpkeError::Aead(err)
289    }
290}
291
292impl From<crypto::HkdfError> for HpkeError {
293    fn from(err: crypto::HkdfError) -> Self {
294        match err {
295            crypto::HkdfError::PrkIsTooShort(_) => HpkeError::InvalidPseudorandomKey,
296            crypto::HkdfError::OutputIsTooLong => HpkeError::KdfOutputTooLong,
297        }
298    }
299}
300
301impl From<crypto::EllipticCurveError> for HpkeError {
302    fn from(err: crypto::EllipticCurveError) -> Self {
303        HpkeError::EllipticCurve(err)
304    }
305}
306
307impl From<crypto::mlkem::MlKemError> for HpkeError {
308    fn from(err: crypto::mlkem::MlKemError) -> Self {
309        HpkeError::MlKem(err)
310    }
311}
312
313impl From<crypto::RandomError> for HpkeError {
314    fn from(err: crypto::RandomError) -> Self {
315        HpkeError::Random(err)
316    }
317}
318
319impl From<crypto::xwing::XWingError> for HpkeError {
320    fn from(err: crypto::xwing::XWingError) -> Self {
321        match err {
322            crypto::xwing::XWingError::MlKem(err) => HpkeError::MlKem(err),
323            crypto::xwing::XWingError::Random(err) => HpkeError::Random(err),
324        }
325    }
326}