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,
10};
11
12use super::*;
13
14/// `PrivateMessage` is the framing struct for an encrypted `PublicMessage`.
15/// This message format is meant to be sent to and received from the Delivery
16/// Service.
17///
18/// ```c
19/// // draft-ietf-mls-protocol-17
20/// struct {
21///     opaque group_id<V>;
22///     uint64 epoch;
23///     ContentType content_type;
24///     opaque authenticated_data<V>;
25///     opaque encrypted_sender_data<V>;
26///     opaque ciphertext<V>;
27/// } PrivateMessage;
28/// ```
29#[derive(Debug, PartialEq, Eq, Clone, TlsSerialize, TlsSize)]
30pub struct PrivateMessage {
31    pub(crate) group_id: GroupId,
32    pub(crate) epoch: GroupEpoch,
33    pub(crate) content_type: ContentType,
34    pub(crate) authenticated_data: VLBytes,
35    pub(crate) encrypted_sender_data: VLBytes,
36    pub(crate) ciphertext: VLBytes,
37}
38
39pub(crate) struct MlsMessageHeader {
40    pub(crate) group_id: GroupId,
41    pub(crate) epoch: GroupEpoch,
42    pub(crate) sender: LeafNodeIndex,
43}
44
45impl PrivateMessage {
46    #[cfg(test)]
47    pub(crate) fn new(
48        group_id: GroupId,
49        epoch: GroupEpoch,
50        content_type: ContentType,
51        authenticated_data: VLBytes,
52        encrypted_sender_data: VLBytes,
53        ciphertext: VLBytes,
54    ) -> Self {
55        Self {
56            group_id,
57            epoch,
58            content_type,
59            authenticated_data,
60            encrypted_sender_data,
61            ciphertext,
62        }
63    }
64
65    /// Try to create a new `PrivateMessage` from an `AuthenticatedContent`.
66    ///
67    /// TODO #1148: Refactor theses constructors to avoid test code in main and
68    /// to avoid validation using a special feature flag.
69    pub(crate) fn try_from_authenticated_content<T>(
70        crypto: &impl OpenMlsCrypto,
71        rand: &impl OpenMlsRand,
72        public_message: &AuthenticatedContent,
73        ciphersuite: Ciphersuite,
74        message_secrets: &mut MessageSecrets,
75        padding_size: usize,
76    ) -> Result<PrivateMessage, MessageEncryptionError<T>> {
77        log::debug!("PrivateMessage::try_from_authenticated_content");
78        log::trace!("  ciphersuite: {}", ciphersuite);
79        // Check the message has the correct wire format
80        if public_message.wire_format() != WireFormat::PrivateMessage {
81            return Err(MessageEncryptionError::WrongWireFormat);
82        }
83        Self::encrypt_content(
84            crypto,
85            rand,
86            None,
87            public_message,
88            ciphersuite,
89            message_secrets,
90            padding_size,
91        )
92    }
93
94    #[cfg(any(feature = "test-utils", test))]
95    pub(crate) fn encrypt_without_check<T>(
96        crypto: &impl OpenMlsCrypto,
97        rand: &impl OpenMlsRand,
98        public_message: &AuthenticatedContent,
99        ciphersuite: Ciphersuite,
100        message_secrets: &mut MessageSecrets,
101        padding_size: usize,
102    ) -> Result<PrivateMessage, MessageEncryptionError<T>> {
103        Self::encrypt_content(
104            crypto,
105            rand,
106            None,
107            public_message,
108            ciphersuite,
109            message_secrets,
110            padding_size,
111        )
112    }
113
114    #[cfg(test)]
115    pub(crate) fn encrypt_with_different_header<T>(
116        crypto: &impl OpenMlsCrypto,
117        rand: &impl OpenMlsRand,
118        public_message: &AuthenticatedContent,
119        ciphersuite: Ciphersuite,
120        header: MlsMessageHeader,
121        message_secrets: &mut MessageSecrets,
122        padding_size: usize,
123    ) -> Result<PrivateMessage, MessageEncryptionError<T>> {
124        Self::encrypt_content(
125            crypto,
126            rand,
127            Some(header),
128            public_message,
129            ciphersuite,
130            message_secrets,
131            padding_size,
132        )
133    }
134
135    /// Internal function to encrypt content. The extra message header is only used
136    /// for tests. Otherwise, the data from the given `AuthenticatedContent` is used.
137    fn encrypt_content<T>(
138        crypto: &impl OpenMlsCrypto,
139        rand: &impl OpenMlsRand,
140        test_header: Option<MlsMessageHeader>,
141        public_message: &AuthenticatedContent,
142        ciphersuite: Ciphersuite,
143        message_secrets: &mut MessageSecrets,
144        padding_size: usize,
145    ) -> Result<PrivateMessage, MessageEncryptionError<T>> {
146        // https://validation.openmls.tech/#valn1305
147        let sender_index = if let Some(index) = public_message.sender().as_member() {
148            index
149        } else {
150            return Err(LibraryError::custom("Sender is not a member.").into());
151        };
152        // Take the provided header only if one is given and if this is indeed a test.
153        let header = match test_header {
154            Some(header) if cfg!(any(feature = "test-utils", test)) => header,
155            _ => MlsMessageHeader {
156                group_id: public_message.group_id().clone(),
157                epoch: public_message.epoch(),
158                sender: sender_index,
159            },
160        };
161        // Serialize the content AAD
162        let private_message_content_aad = PrivateContentAad {
163            group_id: header.group_id.clone(),
164            epoch: header.epoch,
165            content_type: public_message.content().content_type(),
166            authenticated_data: VLByteSlice(public_message.authenticated_data()),
167        };
168        let private_message_content_aad_bytes = private_message_content_aad
169            .tls_serialize_detached()
170            .map_err(LibraryError::missing_bound_check)?;
171        // Extract generation and key material for encryption
172        let secret_type = SecretType::from(&public_message.content().content_type());
173        let (generation, (ratchet_key, ratchet_nonce)) = message_secrets
174            .secret_tree_mut()
175            // Even in tests we want to use the real sender index, so we have a key to encrypt.
176            .secret_for_encryption(ciphersuite, crypto, sender_index, secret_type)?;
177        // Sample reuse guard uniformly at random.
178        let reuse_guard: ReuseGuard =
179            ReuseGuard::try_from_random(rand).map_err(LibraryError::unexpected_crypto_error)?;
180        // Prepare the nonce by xoring with the reuse guard.
181        let prepared_nonce = ratchet_nonce.xor_with_reuse_guard(&reuse_guard);
182        // Encrypt the payload
183        log_crypto!(
184            trace,
185            "Encryption key for private message: {ratchet_key:x?}"
186        );
187        log_crypto!(trace, "Encryption of private message private_message_content_aad_bytes: {private_message_content_aad_bytes:x?} - ratchet_nonce: {prepared_nonce:x?}");
188        let ciphertext = ratchet_key
189            .aead_seal(
190                crypto,
191                &Self::encode_padded_ciphertext_content_detached(
192                    public_message,
193                    padding_size,
194                    ciphersuite.mac_length(),
195                )
196                .map_err(LibraryError::missing_bound_check)?,
197                &private_message_content_aad_bytes,
198                &prepared_nonce,
199            )
200            .map_err(LibraryError::unexpected_crypto_error)?;
201        log::trace!("Encrypted ciphertext {:x?}", ciphertext);
202        // Derive the sender data key from the key schedule using the ciphertext.
203        let sender_data_key = message_secrets
204            .sender_data_secret()
205            .derive_aead_key(crypto, ciphersuite, &ciphertext)
206            .map_err(LibraryError::unexpected_crypto_error)?;
207        // Derive initial nonce from the key schedule using the ciphertext.
208        let sender_data_nonce = message_secrets
209            .sender_data_secret()
210            .derive_aead_nonce(ciphersuite, crypto, &ciphertext)
211            .map_err(LibraryError::unexpected_crypto_error)?;
212        // Compute sender data nonce by xoring reuse guard and key schedule
213        // nonce as per spec.
214        let mls_sender_data_aad = MlsSenderDataAad::new(
215            header.group_id.clone(),
216            header.epoch,
217            public_message.content().content_type(),
218        );
219        // Serialize the sender data AAD
220        let mls_sender_data_aad_bytes = mls_sender_data_aad
221            .tls_serialize_detached()
222            .map_err(LibraryError::missing_bound_check)?;
223        let sender_data = MlsSenderData::from_sender(
224            // XXX: #106 This will fail for messages with a non-member sender.
225            header.sender,
226            generation,
227            reuse_guard,
228        );
229        // Encrypt the sender data
230        log_crypto!(
231            trace,
232            "Encryption key for sender data: {sender_data_key:x?}"
233        );
234        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?}");
235        let encrypted_sender_data = sender_data_key
236            .aead_seal(
237                crypto,
238                &sender_data
239                    .tls_serialize_detached()
240                    .map_err(LibraryError::missing_bound_check)?,
241                &mls_sender_data_aad_bytes,
242                &sender_data_nonce,
243            )
244            .map_err(LibraryError::unexpected_crypto_error)?;
245        Ok(PrivateMessage {
246            group_id: header.group_id.clone(),
247            epoch: header.epoch,
248            content_type: public_message.content().content_type(),
249            authenticated_data: public_message.authenticated_data().into(),
250            encrypted_sender_data: encrypted_sender_data.into(),
251            ciphertext: ciphertext.into(),
252        })
253    }
254
255    /// Returns `true` if this is a handshake message and `false` otherwise.
256    #[cfg(test)]
257    pub(crate) fn is_handshake_message(&self) -> bool {
258        self.content_type.is_handshake_message()
259    }
260
261    /// Encodes the `PrivateMessageContent` struct with padding.
262    fn encode_padded_ciphertext_content_detached(
263        authenticated_content: &AuthenticatedContent,
264        padding_size: usize,
265        mac_len: usize,
266    ) -> Result<Vec<u8>, tls_codec::Error> {
267        let plaintext_length = authenticated_content
268            .content()
269            .serialized_len_without_type()
270            + authenticated_content.auth.tls_serialized_len();
271
272        let padding_length = if padding_size > 0 {
273            // Calculate padding block size.
274            // Only the AEAD tag is added.
275            let padding_offset = plaintext_length + mac_len;
276            // Return padding block size
277            (padding_size - (padding_offset % padding_size)) % padding_size
278        } else {
279            0
280        };
281
282        // Persist all initial fields manually (avoids cloning them)
283        let buffer = &mut Vec::with_capacity(plaintext_length + padding_length);
284
285        // The `content` field is serialized without the `content_type`, which
286        // is not part of the struct as per MLS spec.
287        authenticated_content
288            .content()
289            .serialize_without_type(buffer)?;
290        authenticated_content.auth.tls_serialize(buffer)?;
291        // Note: The `tls_codec::Serialize` implementation for `&[u8]` prepends the length.
292        // We do not want this here and thus use the "raw" `write_all` method.
293        buffer
294            .write_all(&vec![0u8; padding_length])
295            .map_err(|_| Error::EncodingError("Failed to write padding.".into()))?;
296
297        Ok(buffer.to_vec())
298    }
299
300    /// Get the cipher text bytes as slice.
301    #[cfg(test)]
302    pub(crate) fn ciphertext(&self) -> &[u8] {
303        self.ciphertext.as_slice()
304    }
305}
306
307#[derive(TlsSerialize, TlsSize)]
308pub(crate) struct PrivateContentAad<'a> {
309    pub(crate) group_id: GroupId,
310    pub(crate) epoch: GroupEpoch,
311    pub(crate) content_type: ContentType,
312    pub(crate) authenticated_data: VLByteSlice<'a>,
313}