Skip to main content

openmls/framing/
private_message.rs

1use openmls_traits::{crypto::OpenMlsCrypto, random::OpenMlsRand, types::Ciphersuite};
2use std::io::Write;
3use tls_codec::{Serialize, Size, TlsSerialize, TlsSize};
4
5use super::mls_auth_content::AuthenticatedContent;
6
7use crate::{
8    binary_tree::array_representation::LeafNodeIndex, error::LibraryError,
9    tree::secret_tree::SecretType, tree::sender_ratchet::Generation,
10};
11
12#[cfg(feature = "virtual-clients-draft")]
13use crate::{
14    binary_tree::array_representation::TreeSize, components::vc_derivation_info::ReuseGuardSecret,
15};
16
17use super::*;
18
19/// Inputs the framing layer needs to derive (or invert) a virtual-clients
20/// reuse guard for a single message.
21#[cfg(feature = "virtual-clients-draft")]
22pub(crate) struct EmulatorReuseGuardCtx<'a> {
23    pub(crate) reuse_guard_secret: &'a ReuseGuardSecret,
24    pub(crate) emulation_ciphersuite: Ciphersuite,
25    pub(crate) emulation_group_size: TreeSize,
26    pub(crate) emulation_leaf_index: LeafNodeIndex,
27}
28
29/// The result of encrypting an [`AuthenticatedContent`] into a
30/// [`PrivateMessage`].
31#[derive(Debug)]
32pub(crate) struct EncryptionOutput {
33    /// The generation of the encryption secret used. With the
34    /// `virtual-clients-draft` feature, callers use the generation to confirm
35    /// the message and delete the corresponding encryption secret. Without the
36    /// feature, the generation is unused.
37    pub(crate) generation: Generation,
38    /// The resulting encrypted message.
39    pub(crate) private_message: PrivateMessage,
40    /// The [`GenerationId`] for this message when the group is bound to an
41    /// emulation epoch, `None` otherwise. Derived by [`MlsGroup::encrypt`]
42    /// once the ratchet generation is known, since `encrypt_content` does not
43    /// hold the emulation-epoch state.
44    ///
45    /// [`GenerationId`]: crate::components::vc_derivation_info::GenerationId
46    /// [`MlsGroup::encrypt`]: crate::group::MlsGroup::encrypt
47    #[cfg(feature = "virtual-clients-draft")]
48    pub(crate) generation_id: Option<crate::components::vc_derivation_info::GenerationId>,
49}
50
51/// `PrivateMessage` is the framing struct for an encrypted `PublicMessage`.
52/// This message format is meant to be sent to and received from the Delivery
53/// Service.
54///
55/// ```c
56/// // draft-ietf-mls-protocol-17
57/// struct {
58///     opaque group_id<V>;
59///     uint64 epoch;
60///     ContentType content_type;
61///     opaque authenticated_data<V>;
62///     opaque encrypted_sender_data<V>;
63///     opaque ciphertext<V>;
64/// } PrivateMessage;
65/// ```
66#[derive(
67    Debug, PartialEq, Eq, Clone, TlsSerialize, TlsSize, serde::Serialize, serde::Deserialize,
68)]
69pub struct PrivateMessage {
70    pub(crate) group_id: GroupId,
71    pub(crate) epoch: GroupEpoch,
72    pub(crate) content_type: ContentType,
73    pub(crate) authenticated_data: VLBytes,
74    pub(crate) encrypted_sender_data: VLBytes,
75    pub(crate) ciphertext: VLBytes,
76}
77
78pub(crate) struct MlsMessageHeader {
79    pub(crate) group_id: GroupId,
80    pub(crate) epoch: GroupEpoch,
81    pub(crate) sender: LeafNodeIndex,
82}
83
84impl PrivateMessage {
85    /// Returns the [`GroupId`] of the group this message was sent in
86    pub fn group_id(&self) -> &GroupId {
87        &self.group_id
88    }
89
90    /// Returns the [`GroupEpoch`] of the group this message was sent in
91    pub fn epoch(&self) -> GroupEpoch {
92        self.epoch
93    }
94
95    /// Returns the [`ContentType`] of the payload of this message
96    pub fn content_type(&self) -> ContentType {
97        self.content_type
98    }
99
100    #[cfg(test)]
101    pub(crate) fn new(
102        group_id: GroupId,
103        epoch: GroupEpoch,
104        content_type: ContentType,
105        authenticated_data: VLBytes,
106        encrypted_sender_data: VLBytes,
107        ciphertext: VLBytes,
108    ) -> Self {
109        Self {
110            group_id,
111            epoch,
112            content_type,
113            authenticated_data,
114            encrypted_sender_data,
115            ciphertext,
116        }
117    }
118
119    /// Try to create a new `PrivateMessage` from an `AuthenticatedContent`.
120    ///
121    /// TODO #1148: Refactor theses constructors to avoid test code in main and
122    /// to avoid validation using a special feature flag.
123    pub(crate) fn try_from_authenticated_content<T>(
124        crypto: &impl OpenMlsCrypto,
125        rand: &impl OpenMlsRand,
126        public_message: &AuthenticatedContent,
127        ciphersuite: Ciphersuite,
128        message_secrets: &mut MessageSecrets,
129        padding_size: usize,
130        #[cfg(feature = "virtual-clients-draft")] emulator_ctx: Option<&EmulatorReuseGuardCtx<'_>>,
131    ) -> Result<EncryptionOutput, MessageEncryptionError<T>> {
132        log::debug!("PrivateMessage::try_from_authenticated_content");
133        log::trace!("  ciphersuite: {ciphersuite}");
134        // Check the message has the correct wire format
135        if public_message.wire_format() != WireFormat::PrivateMessage {
136            return Err(MessageEncryptionError::WrongWireFormat);
137        }
138        Self::encrypt_content(
139            crypto,
140            rand,
141            None,
142            public_message,
143            ciphersuite,
144            message_secrets,
145            padding_size,
146            #[cfg(feature = "virtual-clients-draft")]
147            emulator_ctx,
148        )
149    }
150
151    #[cfg(any(feature = "test-utils", test))]
152    pub(crate) fn encrypt_without_check<T>(
153        crypto: &impl OpenMlsCrypto,
154        rand: &impl OpenMlsRand,
155        public_message: &AuthenticatedContent,
156        ciphersuite: Ciphersuite,
157        message_secrets: &mut MessageSecrets,
158        padding_size: usize,
159    ) -> Result<EncryptionOutput, MessageEncryptionError<T>> {
160        Self::encrypt_content(
161            crypto,
162            rand,
163            None,
164            public_message,
165            ciphersuite,
166            message_secrets,
167            padding_size,
168            #[cfg(feature = "virtual-clients-draft")]
169            None,
170        )
171    }
172
173    #[cfg(test)]
174    pub(crate) fn encrypt_with_different_header<T>(
175        crypto: &impl OpenMlsCrypto,
176        rand: &impl OpenMlsRand,
177        public_message: &AuthenticatedContent,
178        ciphersuite: Ciphersuite,
179        header: MlsMessageHeader,
180        message_secrets: &mut MessageSecrets,
181        padding_size: usize,
182    ) -> Result<EncryptionOutput, MessageEncryptionError<T>> {
183        Self::encrypt_content(
184            crypto,
185            rand,
186            Some(header),
187            public_message,
188            ciphersuite,
189            message_secrets,
190            padding_size,
191            #[cfg(feature = "virtual-clients-draft")]
192            None,
193        )
194    }
195
196    /// Internal function to encrypt content. The extra message header is only used
197    /// for tests. Otherwise, the data from the given `AuthenticatedContent` is used.
198    #[allow(clippy::too_many_arguments)]
199    fn encrypt_content<T>(
200        crypto: &impl OpenMlsCrypto,
201        rand: &impl OpenMlsRand,
202        test_header: Option<MlsMessageHeader>,
203        public_message: &AuthenticatedContent,
204        ciphersuite: Ciphersuite,
205        message_secrets: &mut MessageSecrets,
206        padding_size: usize,
207        #[cfg(feature = "virtual-clients-draft")] emulator_ctx: Option<&EmulatorReuseGuardCtx<'_>>,
208    ) -> Result<EncryptionOutput, MessageEncryptionError<T>> {
209        // https://validation.openmls.tech/#valn1305
210        let sender_index = if let Some(index) = public_message.sender().as_member() {
211            index
212        } else {
213            return Err(LibraryError::custom("Sender is not a member.").into());
214        };
215        // Take the provided header only if one is given and if this is indeed a test.
216        let header = match test_header {
217            Some(header) if cfg!(any(feature = "test-utils", test)) => header,
218            _ => MlsMessageHeader {
219                group_id: public_message.group_id().clone(),
220                epoch: public_message.epoch(),
221                sender: sender_index,
222            },
223        };
224        // Serialize the content AAD
225        let private_message_content_aad = PrivateContentAad {
226            group_id: header.group_id.clone(),
227            epoch: header.epoch,
228            content_type: public_message.content().content_type(),
229            authenticated_data: VLByteSlice(public_message.authenticated_data()),
230        };
231        let private_message_content_aad_bytes = private_message_content_aad
232            .tls_serialize_detached()
233            .map_err(LibraryError::missing_bound_check)?;
234        // Extract generation and key material for encryption
235        let secret_type = SecretType::from(&public_message.content().content_type());
236        let (generation, (ratchet_key, ratchet_nonce)) = message_secrets
237            .secret_tree_mut()
238            // Even in tests we want to use the real sender index, so we have a key to encrypt.
239            .secret_for_encryption(ciphersuite, crypto, sender_index, secret_type)?;
240        // Derive the reuse guard deterministically when the group is
241        // bound to an emulation epoch, otherwise sample at random.
242        #[cfg(feature = "virtual-clients-draft")]
243        let reuse_guard: ReuseGuard = if let Some(ctx) = emulator_ctx {
244            ReuseGuard::for_emulator_sender(
245                crypto,
246                rand,
247                ctx.reuse_guard_secret,
248                ctx.emulation_ciphersuite,
249                &ratchet_nonce,
250                ctx.emulation_leaf_index,
251                ctx.emulation_group_size,
252            )
253            .map_err(|e| match e {
254                ReuseGuardDerivationError::VirtualClients(inner) => {
255                    MessageEncryptionError::VirtualClientsError(inner)
256                }
257                ReuseGuardDerivationError::Library(inner) => {
258                    MessageEncryptionError::LibraryError(inner)
259                }
260            })?
261        } else {
262            ReuseGuard::try_from_random(rand).map_err(LibraryError::unexpected_crypto_error)?
263        };
264        #[cfg(not(feature = "virtual-clients-draft"))]
265        let reuse_guard: ReuseGuard =
266            ReuseGuard::try_from_random(rand).map_err(LibraryError::unexpected_crypto_error)?;
267        // Prepare the nonce by xoring with the reuse guard.
268        let prepared_nonce = ratchet_nonce.xor_with_reuse_guard(&reuse_guard);
269        // Encrypt the payload
270        log_crypto!(
271            trace,
272            "Encryption key for private message: {ratchet_key:x?}"
273        );
274        log_crypto!(trace, "Encryption of private message private_message_content_aad_bytes: {private_message_content_aad_bytes:x?} - ratchet_nonce: {prepared_nonce:x?}");
275        let ciphertext = ratchet_key
276            .aead_seal(
277                crypto,
278                &Self::encode_padded_ciphertext_content_detached(
279                    public_message,
280                    padding_size,
281                    ciphersuite.mac_length(),
282                )
283                .map_err(LibraryError::missing_bound_check)?,
284                &private_message_content_aad_bytes,
285                &prepared_nonce,
286            )
287            .map_err(LibraryError::unexpected_crypto_error)?;
288        log::trace!("Encrypted ciphertext {ciphertext:x?}");
289        // Derive the sender data key from the key schedule using the ciphertext.
290        let sender_data_key = message_secrets
291            .sender_data_secret()
292            .derive_aead_key(crypto, ciphersuite, &ciphertext)
293            .map_err(LibraryError::unexpected_crypto_error)?;
294        // Derive initial nonce from the key schedule using the ciphertext.
295        let sender_data_nonce = message_secrets
296            .sender_data_secret()
297            .derive_aead_nonce(ciphersuite, crypto, &ciphertext)
298            .map_err(LibraryError::unexpected_crypto_error)?;
299        // Compute sender data nonce by xoring reuse guard and key schedule
300        // nonce as per spec.
301        let mls_sender_data_aad = MlsSenderDataAad::new(
302            header.group_id.clone(),
303            header.epoch,
304            public_message.content().content_type(),
305        );
306        // Serialize the sender data AAD
307        let mls_sender_data_aad_bytes = mls_sender_data_aad
308            .tls_serialize_detached()
309            .map_err(LibraryError::missing_bound_check)?;
310        let sender_data = MlsSenderData::from_sender(
311            // XXX: #106 This will fail for messages with a non-member sender.
312            header.sender,
313            generation,
314            reuse_guard,
315        );
316        // Encrypt the sender data
317        log_crypto!(
318            trace,
319            "Encryption key for sender data: {sender_data_key:x?}"
320        );
321        log_crypto!(trace, "Encryption of sender data mls_sender_data_aad_bytes: {mls_sender_data_aad_bytes:x?} - sender_data_nonce: {sender_data_nonce:x?}");
322        let encrypted_sender_data = sender_data_key
323            .aead_seal(
324                crypto,
325                &sender_data
326                    .tls_serialize_detached()
327                    .map_err(LibraryError::missing_bound_check)?,
328                &mls_sender_data_aad_bytes,
329                &sender_data_nonce,
330            )
331            .map_err(LibraryError::unexpected_crypto_error)?;
332        let private_message = PrivateMessage {
333            group_id: header.group_id.clone(),
334            epoch: header.epoch,
335            content_type: public_message.content().content_type(),
336            authenticated_data: public_message.authenticated_data().into(),
337            encrypted_sender_data: encrypted_sender_data.into(),
338            ciphertext: ciphertext.into(),
339        };
340        Ok(EncryptionOutput {
341            generation,
342            private_message,
343            #[cfg(feature = "virtual-clients-draft")]
344            generation_id: None,
345        })
346    }
347
348    /// Returns `true` if this is a handshake message and `false` otherwise.
349    #[cfg(test)]
350    pub(crate) fn is_handshake_message(&self) -> bool {
351        self.content_type.is_handshake_message()
352    }
353
354    /// Encodes the `PrivateMessageContent` struct with padding.
355    fn encode_padded_ciphertext_content_detached(
356        authenticated_content: &AuthenticatedContent,
357        padding_size: usize,
358        mac_len: usize,
359    ) -> Result<Vec<u8>, tls_codec::Error> {
360        let plaintext_length = authenticated_content
361            .content()
362            .serialized_len_without_type()
363            + authenticated_content.auth.tls_serialized_len();
364
365        let padding_length = if padding_size > 0 {
366            // Calculate padding block size.
367            // Only the AEAD tag is added.
368            let padding_offset = plaintext_length + mac_len;
369            // Return padding block size
370            (padding_size - (padding_offset % padding_size)) % padding_size
371        } else {
372            0
373        };
374
375        // Persist all initial fields manually (avoids cloning them)
376        let buffer = &mut Vec::with_capacity(plaintext_length + padding_length);
377
378        // The `content` field is serialized without the `content_type`, which
379        // is not part of the struct as per MLS spec.
380        authenticated_content
381            .content()
382            .serialize_without_type(buffer)?;
383        authenticated_content.auth.tls_serialize(buffer)?;
384        // Note: The `tls_codec::Serialize` implementation for `&[u8]` prepends the length.
385        // We do not want this here and thus use the "raw" `write_all` method.
386        buffer
387            .write_all(&vec![0u8; padding_length])
388            .map_err(|_| Error::EncodingError("Failed to write padding.".into()))?;
389
390        Ok(buffer.to_vec())
391    }
392
393    /// Get the cipher text bytes as slice.
394    #[cfg(test)]
395    pub(crate) fn ciphertext(&self) -> &[u8] {
396        self.ciphertext.as_slice()
397    }
398}
399
400#[derive(TlsSerialize, TlsSize)]
401pub(crate) struct PrivateContentAad<'a> {
402    pub(crate) group_id: GroupId,
403    pub(crate) epoch: GroupEpoch,
404    pub(crate) content_type: ContentType,
405    pub(crate) authenticated_data: VLByteSlice<'a>,
406}