Skip to main content

openmls/ciphersuite/
hpke.rs

1//! ### Public-Key Encryption
2//!
3//! As with signing, MLS includes a label and context in encryption operations to
4//! avoid confusion between ciphertexts produced for different purposes.  Encryption
5//! and decryption including this label and context are done as follows:
6//!
7//! ```text
8//! EncryptWithLabel(PublicKey, Label, Context, Plaintext) =
9//!   SealBase(PublicKey, EncryptContext, "", Plaintext)
10//!
11//! DecryptWithLabel(PrivateKey, Label, Context, KEMOutput, Ciphertext) =
12//!   OpenBase(KEMOutput, PrivateKey, EncryptContext, "", Ciphertext)
13//! ```
14//!
15//! Where EncryptContext is specified as:
16//!
17//! ```text
18//! struct {
19//!   opaque label<V>;
20//!   opaque context<V>;
21//! } EncryptContext;
22//! ```
23//!
24//! And its fields set to:
25//!
26//! ```text
27//! label = "MLS 1.0 " + Label;
28//! context = Context;
29//! ```
30//!
31//! Here, the functions `SealBase` and `OpenBase` are defined RFC9180, using the
32//! HPKE algorithms specified by the group's ciphersuite.  If MLS extensions
33//! require HPKE encryption operations, they should re-use the EncryptWithLabel
34//! construction, using a distinct label.  To avoid collisions in these labels, an
35//! IANA registry is defined in mls-public-key-encryption-labels.
36
37use openmls_traits::{
38    crypto::OpenMlsCrypto,
39    types::{Ciphersuite, CryptoError, HpkeCiphertext},
40};
41use thiserror::Error;
42use tls_codec::{Serialize, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize, VLBytes};
43
44use super::LABEL_PREFIX;
45
46#[cfg(feature = "targeted-messages-draft")]
47use crate::error::LibraryError;
48
49#[cfg(feature = "extensions-draft")]
50use crate::component::{ComponentId, ComponentOperationLabel};
51
52/// HPKE labeled encryption errors.
53#[derive(Error, Debug, PartialEq, Clone)]
54pub enum Error {
55    /// Error while serializing content. This should only happen if a bounds check was missing.
56    #[error(
57        "Error while serializing content. This should only happen if a bounds check was missing."
58    )]
59    MissingBoundCheck,
60
61    /// Decryption failed.
62    #[error("Decryption failed.")]
63    DecryptionFailed,
64}
65
66impl From<tls_codec::Error> for Error {
67    fn from(_: tls_codec::Error) -> Self {
68        Self::MissingBoundCheck
69    }
70}
71
72impl From<CryptoError> for Error {
73    fn from(_: CryptoError) -> Self {
74        Self::DecryptionFailed
75    }
76}
77
78/// Context for HPKE encryption
79#[derive(Debug, Clone, TlsSerialize, TlsDeserialize, TlsDeserializeBytes, TlsSize)]
80pub struct EncryptContext {
81    /// Prefixed with LABEL_PREFIX
82    label: VLBytes,
83    context: VLBytes,
84}
85
86impl EncryptContext {
87    /// Create a new [`EncryptContext`] from a string label and the content bytes.
88    /// Ensures that the prefix LABEL_PREFIX is prepended to the label.
89    pub(crate) fn new(label: &str, context: VLBytes) -> Self {
90        let label_string = LABEL_PREFIX.to_owned() + label;
91        let label = label_string.as_bytes().into();
92        Self { label, context }
93    }
94
95    #[cfg(feature = "extensions-draft")]
96    pub(crate) fn new_from_component_operation_label(
97        label: ComponentOperationLabel,
98        context: VLBytes,
99    ) -> Result<Self, Error> {
100        let serialized_label = label.tls_serialize_detached()?;
101
102        // Prefix the serialized label with the LABEL_PREFIX bytes
103        // Note that the spec isn't precise here. There are different ways to
104        // combine these. https://github.com/mlswg/mls-extensions/issues/79
105        let mut label = LABEL_PREFIX.as_bytes().to_vec();
106        label.extend(serialized_label);
107
108        Ok(Self {
109            label: label.into(),
110            context,
111        })
112    }
113}
114
115impl From<(&str, &[u8])> for EncryptContext {
116    fn from((label, context): (&str, &[u8])) -> Self {
117        Self::new(label, context.into())
118    }
119}
120
121/// Encrypt to an HPKE key with a label.
122pub(crate) fn encrypt_with_label(
123    public_key: &[u8],
124    label: &str,
125    context: &[u8],
126    plaintext: &[u8],
127    ciphersuite: Ciphersuite,
128    crypto: &impl OpenMlsCrypto,
129) -> Result<HpkeCiphertext, Error> {
130    let context: EncryptContext = (label, context).into();
131
132    log_crypto!(
133        debug,
134        "HPKE Encrypt with label `{label}` and ciphersuite `{ciphersuite:?}`:"
135    );
136
137    encrypt_with_label_internal(public_key, context, plaintext, ciphersuite, crypto)
138}
139
140fn encrypt_with_label_internal(
141    public_key: &[u8],
142    context: EncryptContext,
143    plaintext: &[u8],
144    ciphersuite: Ciphersuite,
145    crypto: &impl OpenMlsCrypto,
146) -> Result<HpkeCiphertext, Error> {
147    let context = context.tls_serialize_detached()?;
148
149    log_crypto!(debug, "* context:     {context:x?}");
150    log_crypto!(debug, "* public key:  {public_key:x?}");
151    log_crypto!(debug, "* plaintext:   {plaintext:x?}");
152
153    let cipher = crypto.hpke_seal(
154        ciphersuite.hpke_config(),
155        public_key,
156        &context,
157        &[],
158        plaintext,
159    )?;
160
161    log_crypto!(debug, "* ciphertext:  {:x?}", cipher);
162
163    Ok(cipher)
164}
165
166/// Context for [`safe_encrypt_with_label`] and [`safe_decrypt_with_label`].
167#[cfg(feature = "extensions-draft")]
168pub struct SafeEncryptionContext<'a> {
169    /// The [`ComponentId`] to use.
170    pub component_id: ComponentId,
171
172    /// A label
173    pub label: &'a str,
174
175    /// An optional context.
176    pub context: &'a [u8],
177}
178
179/// Encrypt the provided `plaintext` for the `public_key`.
180/// The [`SafeEncryptionContext`] is used to set the [`ComponentId`], `label`,
181/// and optional `context`.
182///
183/// Returns an [`HpkeCiphertext`] or an [`enum@Error`].
184#[cfg(feature = "extensions-draft")]
185pub fn safe_encrypt_with_label(
186    public_key: &[u8],
187    plaintext: &[u8],
188    ciphersuite: Ciphersuite,
189    context: SafeEncryptionContext,
190    crypto: &impl OpenMlsCrypto,
191) -> Result<HpkeCiphertext, Error> {
192    let component_operation_label =
193        ComponentOperationLabel::new(context.component_id, context.label);
194
195    let context = EncryptContext::new_from_component_operation_label(
196        component_operation_label,
197        context.context.into(),
198    )?;
199
200    encrypt_with_label_internal(public_key, context, plaintext, ciphersuite, crypto)
201}
202
203/// Decrypt with HPKE and label.
204pub(crate) fn decrypt_with_label(
205    private_key: &[u8],
206    label: &str,
207    context: &[u8],
208    ciphertext: &HpkeCiphertext,
209    ciphersuite: Ciphersuite,
210    crypto: &impl OpenMlsCrypto,
211) -> Result<Vec<u8>, Error> {
212    log_crypto!(
213        debug,
214        "HPKE Decrypt with label `{label}` and `ciphersuite` {ciphersuite:?}:"
215    );
216
217    let context: EncryptContext = (label, context).into();
218
219    decrypt_with_label_internal(private_key, context, ciphertext, ciphersuite, crypto)
220}
221
222fn decrypt_with_label_internal(
223    private_key: &[u8],
224    context: EncryptContext,
225    ciphertext: &HpkeCiphertext,
226    ciphersuite: Ciphersuite,
227    crypto: &impl OpenMlsCrypto,
228) -> Result<Vec<u8>, Error> {
229    let context = context.tls_serialize_detached()?;
230
231    log_crypto!(debug, "* context:     {context:x?}");
232    log_crypto!(debug, "* private key: {private_key:x?}");
233    log_crypto!(debug, "* ciphertext:  {ciphertext:x?}");
234
235    let plaintext = crypto
236        .hpke_open(
237            ciphersuite.hpke_config(),
238            ciphertext,
239            private_key,
240            &context,
241            &[],
242        )
243        .map_err(|e| e.into());
244
245    log_crypto!(debug, "* plaintext:   {plaintext:x?}");
246
247    plaintext
248}
249
250#[cfg(feature = "extensions-draft")]
251/// Decrypt the provided `ciphertext` with the `private_key`.
252/// The [`SafeEncryptionContext`] is used to set the [`ComponentId`], `label`,
253/// and optional `context`.
254///
255/// Returns an [`HpkeCiphertext`] or an [`enum@Error`].
256pub fn safe_decrypt_with_label(
257    private_key: &[u8],
258    ciphertext: &HpkeCiphertext,
259    ciphersuite: Ciphersuite,
260    context: SafeEncryptionContext,
261    crypto: &impl OpenMlsCrypto,
262) -> Result<Vec<u8>, Error> {
263    let component_operation_label =
264        ComponentOperationLabel::new(context.component_id, context.label);
265
266    let context: EncryptContext = EncryptContext::new_from_component_operation_label(
267        component_operation_label,
268        context.context.into(),
269    )?;
270
271    decrypt_with_label_internal(private_key, context, ciphertext, ciphersuite, crypto)
272}
273
274/// Parameters shared by [`seal_psk`] and [`open_psk`]: the serialized HPKE
275/// info and the PSK material for the HPKE PSK mode. Callers own the info
276/// layout, so they serialize it before calling these functions.
277#[cfg(feature = "targeted-messages-draft")]
278pub(crate) struct PskEncryptParams<'a> {
279    pub info: &'a [u8],
280    pub psk: &'a [u8],
281    pub psk_id: &'a [u8],
282    pub ciphersuite: Ciphersuite,
283}
284
285#[cfg(feature = "targeted-messages-draft")]
286pub(crate) fn seal_psk(
287    public_key: &[u8],
288    params: PskEncryptParams,
289    aad: &[u8],
290    plaintext: &[u8],
291    crypto: &impl OpenMlsCrypto,
292) -> Result<HpkeCiphertext, LibraryError> {
293    crypto
294        .hpke_seal_psk(
295            params.ciphersuite.hpke_config(),
296            public_key,
297            params.info,
298            aad,
299            plaintext,
300            params.psk,
301            params.psk_id,
302        )
303        .map_err(LibraryError::unexpected_crypto_error)
304}
305
306#[cfg(feature = "targeted-messages-draft")]
307pub(crate) fn open_psk(
308    private_key: &[u8],
309    params: PskEncryptParams,
310    aad: &[u8],
311    ciphertext: &HpkeCiphertext,
312    crypto: &impl OpenMlsCrypto,
313) -> Result<Vec<u8>, Error> {
314    let content_bytes = crypto.hpke_open_psk(
315        params.ciphersuite.hpke_config(),
316        ciphertext,
317        private_key,
318        params.info,
319        aad,
320        params.psk,
321        params.psk_id,
322    )?;
323
324    Ok(content_bytes)
325}