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#[cfg(feature = "targeted-messages-draft")]
49use openmls_traits::crypto::HpkeSealPskResolvedAadError;
50
51#[cfg(feature = "extensions-draft")]
52use crate::component::{ComponentId, ComponentOperationLabel};
53
54/// HPKE labeled encryption errors.
55#[derive(Error, Debug, PartialEq, Clone)]
56pub enum Error {
57    /// Error while serializing content. This should only happen if a bounds check was missing.
58    #[error(
59        "Error while serializing content. This should only happen if a bounds check was missing."
60    )]
61    MissingBoundCheck,
62
63    /// Decryption failed.
64    #[error("Decryption failed.")]
65    DecryptionFailed,
66}
67
68impl From<tls_codec::Error> for Error {
69    fn from(_: tls_codec::Error) -> Self {
70        Self::MissingBoundCheck
71    }
72}
73
74impl From<CryptoError> for Error {
75    fn from(_: CryptoError) -> Self {
76        Self::DecryptionFailed
77    }
78}
79
80/// Context for HPKE encryption
81#[derive(Debug, Clone, TlsSerialize, TlsDeserialize, TlsDeserializeBytes, TlsSize)]
82pub struct EncryptContext {
83    /// Prefixed with LABEL_PREFIX
84    label: VLBytes,
85    context: VLBytes,
86}
87
88impl EncryptContext {
89    /// Create a new [`EncryptContext`] from a string label and the content bytes.
90    /// Ensures that the prefix LABEL_PREFIX is prepended to the label.
91    pub(crate) fn new(label: &str, context: VLBytes) -> Self {
92        let label_string = LABEL_PREFIX.to_owned() + label;
93        let label = label_string.as_bytes().into();
94        Self { label, context }
95    }
96
97    #[cfg(feature = "extensions-draft")]
98    pub(crate) fn new_from_component_operation_label(
99        label: ComponentOperationLabel,
100        context: VLBytes,
101    ) -> Result<Self, Error> {
102        let serialized_label = label.tls_serialize_detached()?;
103
104        // Prefix the serialized label with the LABEL_PREFIX bytes
105        // Note that the spec isn't precise here. There are different ways to
106        // combine these. https://github.com/mlswg/mls-extensions/issues/79
107        let mut label = LABEL_PREFIX.as_bytes().to_vec();
108        label.extend(serialized_label);
109
110        Ok(Self {
111            label: label.into(),
112            context,
113        })
114    }
115}
116
117impl From<(&str, &[u8])> for EncryptContext {
118    fn from((label, context): (&str, &[u8])) -> Self {
119        Self::new(label, context.into())
120    }
121}
122
123/// Encrypt to an HPKE key with a label.
124pub(crate) fn encrypt_with_label(
125    public_key: &[u8],
126    label: &str,
127    context: &[u8],
128    plaintext: &[u8],
129    ciphersuite: Ciphersuite,
130    crypto: &impl OpenMlsCrypto,
131) -> Result<HpkeCiphertext, Error> {
132    let context: EncryptContext = (label, context).into();
133
134    log_crypto!(
135        debug,
136        "HPKE Encrypt with label `{label}` and ciphersuite `{ciphersuite:?}`:"
137    );
138
139    encrypt_with_label_internal(public_key, context, plaintext, ciphersuite, crypto)
140}
141
142fn encrypt_with_label_internal(
143    public_key: &[u8],
144    context: EncryptContext,
145    plaintext: &[u8],
146    ciphersuite: Ciphersuite,
147    crypto: &impl OpenMlsCrypto,
148) -> Result<HpkeCiphertext, Error> {
149    let context = context.tls_serialize_detached()?;
150
151    log_crypto!(debug, "* context:     {context:x?}");
152    log_crypto!(debug, "* public key:  {public_key:x?}");
153    log_crypto!(debug, "* plaintext:   {plaintext:x?}");
154
155    let cipher = crypto.hpke_seal(
156        ciphersuite.hpke_config(),
157        public_key,
158        &context,
159        &[],
160        plaintext,
161    )?;
162
163    log_crypto!(debug, "* ciphertext:  {:x?}", cipher);
164
165    Ok(cipher)
166}
167
168/// Context for [`safe_encrypt_with_label`] and [`safe_decrypt_with_label`].
169#[cfg(feature = "extensions-draft")]
170pub struct SafeEncryptionContext<'a> {
171    /// The [`ComponentId`] to use.
172    pub component_id: ComponentId,
173
174    /// A label
175    pub label: &'a str,
176
177    /// An optional context.
178    pub context: &'a [u8],
179}
180
181/// Encrypt the provided `plaintext` for the `public_key`.
182/// The [`SafeEncryptionContext`] is used to set the [`ComponentId`], `label`,
183/// and optional `context`.
184///
185/// Returns an [`HpkeCiphertext`] or an [`enum@Error`].
186#[cfg(feature = "extensions-draft")]
187pub fn safe_encrypt_with_label(
188    public_key: &[u8],
189    plaintext: &[u8],
190    ciphersuite: Ciphersuite,
191    context: SafeEncryptionContext,
192    crypto: &impl OpenMlsCrypto,
193) -> Result<HpkeCiphertext, Error> {
194    let component_operation_label =
195        ComponentOperationLabel::new(context.component_id, context.label);
196
197    let context = EncryptContext::new_from_component_operation_label(
198        component_operation_label,
199        context.context.into(),
200    )?;
201
202    encrypt_with_label_internal(public_key, context, plaintext, ciphersuite, crypto)
203}
204
205/// Decrypt with HPKE and label.
206pub(crate) fn decrypt_with_label(
207    private_key: &[u8],
208    label: &str,
209    context: &[u8],
210    ciphertext: &HpkeCiphertext,
211    ciphersuite: Ciphersuite,
212    crypto: &impl OpenMlsCrypto,
213) -> Result<Vec<u8>, Error> {
214    log_crypto!(
215        debug,
216        "HPKE Decrypt with label `{label}` and `ciphersuite` {ciphersuite:?}:"
217    );
218
219    let context: EncryptContext = (label, context).into();
220
221    decrypt_with_label_internal(private_key, context, ciphertext, ciphersuite, crypto)
222}
223
224fn decrypt_with_label_internal(
225    private_key: &[u8],
226    context: EncryptContext,
227    ciphertext: &HpkeCiphertext,
228    ciphersuite: Ciphersuite,
229    crypto: &impl OpenMlsCrypto,
230) -> Result<Vec<u8>, Error> {
231    let context = context.tls_serialize_detached()?;
232
233    log_crypto!(debug, "* context:     {context:x?}");
234    log_crypto!(debug, "* private key: {private_key:x?}");
235    log_crypto!(debug, "* ciphertext:  {ciphertext:x?}");
236
237    let plaintext = crypto
238        .hpke_open(
239            ciphersuite.hpke_config(),
240            ciphertext,
241            private_key,
242            &context,
243            &[],
244        )
245        .map_err(|e| e.into());
246
247    log_crypto!(debug, "* plaintext:   {plaintext:x?}");
248
249    plaintext
250}
251
252#[cfg(feature = "extensions-draft")]
253/// Decrypt the provided `ciphertext` with the `private_key`.
254/// The [`SafeEncryptionContext`] is used to set the [`ComponentId`], `label`,
255/// and optional `context`.
256///
257/// Returns an [`HpkeCiphertext`] or an [`enum@Error`].
258pub fn safe_decrypt_with_label(
259    private_key: &[u8],
260    ciphertext: &HpkeCiphertext,
261    ciphersuite: Ciphersuite,
262    context: SafeEncryptionContext,
263    crypto: &impl OpenMlsCrypto,
264) -> Result<Vec<u8>, Error> {
265    let component_operation_label =
266        ComponentOperationLabel::new(context.component_id, context.label);
267
268    let context: EncryptContext = EncryptContext::new_from_component_operation_label(
269        component_operation_label,
270        context.context.into(),
271    )?;
272
273    decrypt_with_label_internal(private_key, context, ciphertext, ciphersuite, crypto)
274}
275
276/// Parameters shared by [`encrypt_with_label_psk_resolved_aad`] and
277/// [`decrypt_with_label_psk_aad`]: the label and context that make up the HPKE
278/// info, and the PSK material for the HPKE PSK mode.
279#[cfg(feature = "targeted-messages-draft")]
280pub(crate) struct PskEncryptParams<'a> {
281    pub label: &'a str,
282    pub context: &'a [u8],
283    pub psk: &'a [u8],
284    pub psk_id: &'a [u8],
285    pub ciphersuite: Ciphersuite,
286}
287
288#[cfg(feature = "targeted-messages-draft")]
289pub(crate) fn encrypt_with_label_psk_resolved_aad<F>(
290    public_key: &[u8],
291    params: PskEncryptParams,
292    plaintext: &[u8],
293    crypto: &impl OpenMlsCrypto,
294    aad_builder: F,
295) -> Result<HpkeCiphertext, LibraryError>
296where
297    F: FnOnce(&[u8]) -> Result<Vec<u8>, LibraryError>,
298{
299    let info = EncryptContext::new(params.label, params.context.into())
300        .tls_serialize_detached()
301        .map_err(LibraryError::missing_bound_check)?;
302    crypto
303        .hpke_seal_psk_resolved_aad(
304            params.ciphersuite.hpke_config(),
305            public_key,
306            &info,
307            plaintext,
308            params.psk,
309            params.psk_id,
310            aad_builder,
311        )
312        .map_err(|e| match e {
313            HpkeSealPskResolvedAadError::CryptoError(e) => LibraryError::unexpected_crypto_error(e),
314            HpkeSealPskResolvedAadError::AadBuildError(e) => e,
315        })
316}
317
318#[cfg(feature = "targeted-messages-draft")]
319pub(crate) fn decrypt_with_label_psk_aad(
320    private_key: &[u8],
321    params: PskEncryptParams,
322    aad: &[u8],
323    ciphertext: &HpkeCiphertext,
324    crypto: &impl OpenMlsCrypto,
325) -> Result<Vec<u8>, Error> {
326    let info = EncryptContext::new(params.label, params.context.into()).tls_serialize_detached()?;
327    let content_bytes = crypto.hpke_open_psk(
328        params.ciphersuite.hpke_config(),
329        ciphertext,
330        private_key,
331        &info,
332        aad,
333        params.psk,
334        params.psk_id,
335    )?;
336
337    Ok(content_bytes)
338}