1 // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 // Copyright by contributors to this project.
3 // SPDX-License-Identifier: (Apache-2.0 OR MIT)
4 
5 use core::ops::Deref;
6 
7 use crate::{client::MlsError, tree_kem::node::LeafIndex, KeyPackage, KeyPackageRef};
8 
9 use super::{Commit, FramedContentAuthData, GroupInfo, MembershipTag, Welcome};
10 
11 #[cfg(feature = "by_ref_proposal")]
12 use crate::{group::Proposal, mls_rules::ProposalRef};
13 
14 use alloc::vec::Vec;
15 use core::fmt::{self, Debug};
16 use mls_rs_codec::{MlsDecode, MlsEncode, MlsSize};
17 use mls_rs_core::{
18     crypto::{CipherSuite, CipherSuiteProvider},
19     protocol_version::ProtocolVersion,
20 };
21 use zeroize::ZeroizeOnDrop;
22 
23 #[cfg(feature = "private_message")]
24 use alloc::boxed::Box;
25 
26 #[cfg(feature = "custom_proposal")]
27 use crate::group::proposal::{CustomProposal, ProposalOrRef};
28 
29 #[derive(Copy, Clone, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
30 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
31 #[repr(u8)]
32 pub enum ContentType {
33     #[cfg(feature = "private_message")]
34     Application = 1u8,
35     #[cfg(feature = "by_ref_proposal")]
36     Proposal = 2u8,
37     Commit = 3u8,
38 }
39 
40 impl From<&Content> for ContentType {
from(content: &Content) -> Self41     fn from(content: &Content) -> Self {
42         match content {
43             #[cfg(feature = "private_message")]
44             Content::Application(_) => ContentType::Application,
45             #[cfg(feature = "by_ref_proposal")]
46             Content::Proposal(_) => ContentType::Proposal,
47             Content::Commit(_) => ContentType::Commit,
48         }
49     }
50 }
51 
52 #[cfg_attr(
53     all(feature = "ffi", not(test)),
54     safer_ffi_gen::ffi_type(clone, opaque)
55 )]
56 #[derive(Clone, Copy, Debug, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
57 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
58 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
59 #[repr(u8)]
60 #[non_exhaustive]
61 /// Description of a [`MlsMessage`] sender
62 pub enum Sender {
63     /// Current group member index.
64     Member(u32) = 1u8,
65     /// An external entity sending a proposal proposal identified by an index
66     /// in the current
67     /// [`ExternalSendersExt`](crate::extension::ExternalSendersExt) stored in
68     /// group context extensions.
69     #[cfg(feature = "by_ref_proposal")]
70     External(u32) = 2u8,
71     /// A new member proposing their own addition to the group.
72     #[cfg(feature = "by_ref_proposal")]
73     NewMemberProposal = 3u8,
74     /// A member sending an external commit.
75     NewMemberCommit = 4u8,
76 }
77 
78 impl From<LeafIndex> for Sender {
from(leaf_index: LeafIndex) -> Self79     fn from(leaf_index: LeafIndex) -> Self {
80         Sender::Member(*leaf_index)
81     }
82 }
83 
84 impl From<u32> for Sender {
from(leaf_index: u32) -> Self85     fn from(leaf_index: u32) -> Self {
86         Sender::Member(leaf_index)
87     }
88 }
89 
90 #[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode, ZeroizeOnDrop)]
91 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
92 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
93 pub struct ApplicationData(
94     #[mls_codec(with = "mls_rs_codec::byte_vec")]
95     #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))]
96     Vec<u8>,
97 );
98 
99 impl Debug for ApplicationData {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result100     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
101         mls_rs_core::debug::pretty_bytes(&self.0)
102             .named("ApplicationData")
103             .fmt(f)
104     }
105 }
106 
107 impl From<Vec<u8>> for ApplicationData {
from(data: Vec<u8>) -> Self108     fn from(data: Vec<u8>) -> Self {
109         Self(data)
110     }
111 }
112 
113 impl Deref for ApplicationData {
114     type Target = [u8];
115 
deref(&self) -> &Self::Target116     fn deref(&self) -> &Self::Target {
117         &self.0
118     }
119 }
120 
121 impl ApplicationData {
122     /// Underlying message content.
as_bytes(&self) -> &[u8]123     pub fn as_bytes(&self) -> &[u8] {
124         &self.0
125     }
126 }
127 
128 #[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
129 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
130 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
131 #[repr(u8)]
132 pub(crate) enum Content {
133     #[cfg(feature = "private_message")]
134     Application(ApplicationData) = 1u8,
135     #[cfg(feature = "by_ref_proposal")]
136     Proposal(alloc::boxed::Box<Proposal>) = 2u8,
137     Commit(alloc::boxed::Box<Commit>) = 3u8,
138 }
139 
140 impl Content {
content_type(&self) -> ContentType141     pub fn content_type(&self) -> ContentType {
142         self.into()
143     }
144 }
145 
146 #[derive(Clone, Debug, PartialEq)]
147 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
148 pub(crate) struct PublicMessage {
149     pub content: FramedContent,
150     pub auth: FramedContentAuthData,
151     pub membership_tag: Option<MembershipTag>,
152 }
153 
154 impl MlsSize for PublicMessage {
mls_encoded_len(&self) -> usize155     fn mls_encoded_len(&self) -> usize {
156         self.content.mls_encoded_len()
157             + self.auth.mls_encoded_len()
158             + self
159                 .membership_tag
160                 .as_ref()
161                 .map_or(0, |tag| tag.mls_encoded_len())
162     }
163 }
164 
165 impl MlsEncode for PublicMessage {
mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error>166     fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> {
167         self.content.mls_encode(writer)?;
168         self.auth.mls_encode(writer)?;
169 
170         self.membership_tag
171             .as_ref()
172             .map_or(Ok(()), |tag| tag.mls_encode(writer))
173     }
174 }
175 
176 impl MlsDecode for PublicMessage {
mls_decode(reader: &mut &[u8]) -> Result<Self, mls_rs_codec::Error>177     fn mls_decode(reader: &mut &[u8]) -> Result<Self, mls_rs_codec::Error> {
178         let content = FramedContent::mls_decode(reader)?;
179         let auth = FramedContentAuthData::mls_decode(reader, content.content_type())?;
180 
181         let membership_tag = match content.sender {
182             Sender::Member(_) => Some(MembershipTag::mls_decode(reader)?),
183             _ => None,
184         };
185 
186         Ok(Self {
187             content,
188             auth,
189             membership_tag,
190         })
191     }
192 }
193 
194 #[cfg(feature = "private_message")]
195 #[derive(Clone, Debug, PartialEq)]
196 pub(crate) struct PrivateMessageContent {
197     pub content: Content,
198     pub auth: FramedContentAuthData,
199 }
200 
201 #[cfg(feature = "private_message")]
202 impl MlsSize for PrivateMessageContent {
mls_encoded_len(&self) -> usize203     fn mls_encoded_len(&self) -> usize {
204         let content_len_without_type = match &self.content {
205             Content::Application(c) => c.mls_encoded_len(),
206             #[cfg(feature = "by_ref_proposal")]
207             Content::Proposal(c) => c.mls_encoded_len(),
208             Content::Commit(c) => c.mls_encoded_len(),
209         };
210 
211         content_len_without_type + self.auth.mls_encoded_len()
212     }
213 }
214 
215 #[cfg(feature = "private_message")]
216 impl MlsEncode for PrivateMessageContent {
mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error>217     fn mls_encode(&self, writer: &mut Vec<u8>) -> Result<(), mls_rs_codec::Error> {
218         match &self.content {
219             Content::Application(c) => c.mls_encode(writer),
220             #[cfg(feature = "by_ref_proposal")]
221             Content::Proposal(c) => c.mls_encode(writer),
222             Content::Commit(c) => c.mls_encode(writer),
223         }?;
224 
225         self.auth.mls_encode(writer)?;
226 
227         Ok(())
228     }
229 }
230 
231 #[cfg(feature = "private_message")]
232 impl PrivateMessageContent {
mls_decode( reader: &mut &[u8], content_type: ContentType, ) -> Result<Self, mls_rs_codec::Error>233     pub(crate) fn mls_decode(
234         reader: &mut &[u8],
235         content_type: ContentType,
236     ) -> Result<Self, mls_rs_codec::Error> {
237         let content = match content_type {
238             ContentType::Application => Content::Application(ApplicationData::mls_decode(reader)?),
239             #[cfg(feature = "by_ref_proposal")]
240             ContentType::Proposal => Content::Proposal(Box::new(Proposal::mls_decode(reader)?)),
241             ContentType::Commit => {
242                 Content::Commit(alloc::boxed::Box::new(Commit::mls_decode(reader)?))
243             }
244         };
245 
246         let auth = FramedContentAuthData::mls_decode(reader, content.content_type())?;
247 
248         if reader.iter().any(|&i| i != 0u8) {
249             // #[cfg(feature = "std")]
250             // return Err(mls_rs_codec::Error::Custom(
251             //    "non-zero padding bytes discovered".to_string(),
252             // ));
253 
254             // #[cfg(not(feature = "std"))]
255             return Err(mls_rs_codec::Error::Custom(5));
256         }
257 
258         Ok(Self { content, auth })
259     }
260 }
261 
262 #[cfg(feature = "private_message")]
263 #[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
264 pub struct PrivateContentAAD {
265     #[mls_codec(with = "mls_rs_codec::byte_vec")]
266     pub group_id: Vec<u8>,
267     pub epoch: u64,
268     pub content_type: ContentType,
269     #[mls_codec(with = "mls_rs_codec::byte_vec")]
270     pub authenticated_data: Vec<u8>,
271 }
272 
273 #[cfg(feature = "private_message")]
274 impl Debug for PrivateContentAAD {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result275     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
276         f.debug_struct("PrivateContentAAD")
277             .field(
278                 "group_id",
279                 &mls_rs_core::debug::pretty_group_id(&self.group_id),
280             )
281             .field("epoch", &self.epoch)
282             .field("content_type", &self.content_type)
283             .field(
284                 "authenticated_data",
285                 &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
286             )
287             .finish()
288     }
289 }
290 
291 #[cfg(feature = "private_message")]
292 #[derive(Clone, PartialEq, Eq, MlsSize, MlsEncode, MlsDecode)]
293 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
294 pub struct PrivateMessage {
295     #[mls_codec(with = "mls_rs_codec::byte_vec")]
296     pub group_id: Vec<u8>,
297     pub epoch: u64,
298     pub content_type: ContentType,
299     #[mls_codec(with = "mls_rs_codec::byte_vec")]
300     pub authenticated_data: Vec<u8>,
301     #[mls_codec(with = "mls_rs_codec::byte_vec")]
302     pub encrypted_sender_data: Vec<u8>,
303     #[mls_codec(with = "mls_rs_codec::byte_vec")]
304     pub ciphertext: Vec<u8>,
305 }
306 
307 #[cfg(feature = "private_message")]
308 impl Debug for PrivateMessage {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result309     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310         f.debug_struct("PrivateMessage")
311             .field(
312                 "group_id",
313                 &mls_rs_core::debug::pretty_group_id(&self.group_id),
314             )
315             .field("epoch", &self.epoch)
316             .field("content_type", &self.content_type)
317             .field(
318                 "authenticated_data",
319                 &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
320             )
321             .field(
322                 "encrypted_sender_data",
323                 &mls_rs_core::debug::pretty_bytes(&self.encrypted_sender_data),
324             )
325             .field(
326                 "ciphertext",
327                 &mls_rs_core::debug::pretty_bytes(&self.ciphertext),
328             )
329             .finish()
330     }
331 }
332 
333 #[cfg(feature = "private_message")]
334 impl From<&PrivateMessage> for PrivateContentAAD {
from(ciphertext: &PrivateMessage) -> Self335     fn from(ciphertext: &PrivateMessage) -> Self {
336         Self {
337             group_id: ciphertext.group_id.clone(),
338             epoch: ciphertext.epoch,
339             content_type: ciphertext.content_type,
340             authenticated_data: ciphertext.authenticated_data.clone(),
341         }
342     }
343 }
344 
345 #[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
346 #[cfg_attr(
347     all(feature = "ffi", not(test)),
348     ::safer_ffi_gen::ffi_type(clone, opaque)
349 )]
350 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
351 /// A MLS protocol message for sending data over the wire.
352 pub struct MlsMessage {
353     pub(crate) version: ProtocolVersion,
354     pub(crate) payload: MlsMessagePayload,
355 }
356 
357 #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen)]
358 #[allow(dead_code)]
359 impl MlsMessage {
new(version: ProtocolVersion, payload: MlsMessagePayload) -> MlsMessage360     pub(crate) fn new(version: ProtocolVersion, payload: MlsMessagePayload) -> MlsMessage {
361         Self { version, payload }
362     }
363 
364     #[inline(always)]
into_plaintext(self) -> Option<PublicMessage>365     pub(crate) fn into_plaintext(self) -> Option<PublicMessage> {
366         match self.payload {
367             MlsMessagePayload::Plain(plaintext) => Some(plaintext),
368             _ => None,
369         }
370     }
371 
372     #[cfg(feature = "private_message")]
373     #[inline(always)]
into_ciphertext(self) -> Option<PrivateMessage>374     pub(crate) fn into_ciphertext(self) -> Option<PrivateMessage> {
375         match self.payload {
376             MlsMessagePayload::Cipher(ciphertext) => Some(ciphertext),
377             _ => None,
378         }
379     }
380 
381     #[inline(always)]
into_welcome(self) -> Option<Welcome>382     pub(crate) fn into_welcome(self) -> Option<Welcome> {
383         match self.payload {
384             MlsMessagePayload::Welcome(welcome) => Some(welcome),
385             _ => None,
386         }
387     }
388 
389     #[inline(always)]
into_group_info(self) -> Option<GroupInfo>390     pub fn into_group_info(self) -> Option<GroupInfo> {
391         match self.payload {
392             MlsMessagePayload::GroupInfo(info) => Some(info),
393             _ => None,
394         }
395     }
396 
397     #[inline(always)]
as_group_info(&self) -> Option<&GroupInfo>398     pub fn as_group_info(&self) -> Option<&GroupInfo> {
399         match &self.payload {
400             MlsMessagePayload::GroupInfo(info) => Some(info),
401             _ => None,
402         }
403     }
404 
405     #[inline(always)]
into_key_package(self) -> Option<KeyPackage>406     pub fn into_key_package(self) -> Option<KeyPackage> {
407         match self.payload {
408             MlsMessagePayload::KeyPackage(kp) => Some(kp),
409             _ => None,
410         }
411     }
412 
413     /// The wire format value describing the contents of this message.
wire_format(&self) -> WireFormat414     pub fn wire_format(&self) -> WireFormat {
415         match self.payload {
416             MlsMessagePayload::Plain(_) => WireFormat::PublicMessage,
417             #[cfg(feature = "private_message")]
418             MlsMessagePayload::Cipher(_) => WireFormat::PrivateMessage,
419             MlsMessagePayload::Welcome(_) => WireFormat::Welcome,
420             MlsMessagePayload::GroupInfo(_) => WireFormat::GroupInfo,
421             MlsMessagePayload::KeyPackage(_) => WireFormat::KeyPackage,
422         }
423     }
424 
425     /// The epoch that this message belongs to.
426     ///
427     /// Returns `None` if the message is [`WireFormat::KeyPackage`]
428     /// or [`WireFormat::Welcome`]
429     #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen_ignore)]
epoch(&self) -> Option<u64>430     pub fn epoch(&self) -> Option<u64> {
431         match &self.payload {
432             MlsMessagePayload::Plain(p) => Some(p.content.epoch),
433             #[cfg(feature = "private_message")]
434             MlsMessagePayload::Cipher(c) => Some(c.epoch),
435             MlsMessagePayload::GroupInfo(gi) => Some(gi.group_context.epoch),
436             _ => None,
437         }
438     }
439 
440     #[cfg_attr(all(feature = "ffi", not(test)), ::safer_ffi_gen::safer_ffi_gen_ignore)]
cipher_suite(&self) -> Option<CipherSuite>441     pub fn cipher_suite(&self) -> Option<CipherSuite> {
442         match &self.payload {
443             MlsMessagePayload::GroupInfo(i) => Some(i.group_context.cipher_suite),
444             MlsMessagePayload::Welcome(w) => Some(w.cipher_suite),
445             MlsMessagePayload::KeyPackage(k) => Some(k.cipher_suite),
446             _ => None,
447         }
448     }
449 
group_id(&self) -> Option<&[u8]>450     pub fn group_id(&self) -> Option<&[u8]> {
451         match &self.payload {
452             MlsMessagePayload::Plain(p) => Some(&p.content.group_id),
453             #[cfg(feature = "private_message")]
454             MlsMessagePayload::Cipher(p) => Some(&p.group_id),
455             MlsMessagePayload::GroupInfo(p) => Some(&p.group_context.group_id),
456             MlsMessagePayload::KeyPackage(_) | MlsMessagePayload::Welcome(_) => None,
457         }
458     }
459 
460     /// Deserialize a message from transport.
461     #[inline(never)]
from_bytes(bytes: &[u8]) -> Result<Self, MlsError>462     pub fn from_bytes(bytes: &[u8]) -> Result<Self, MlsError> {
463         Self::mls_decode(&mut &*bytes).map_err(Into::into)
464     }
465 
466     /// Serialize a message for transport.
to_bytes(&self) -> Result<Vec<u8>, MlsError>467     pub fn to_bytes(&self) -> Result<Vec<u8>, MlsError> {
468         self.mls_encode_to_vec().map_err(Into::into)
469     }
470 
471     /// If this is a plaintext commit message, return all custom proposals committed by value.
472     /// If this is not a plaintext or not a commit, this returns an empty list.
473     #[cfg(feature = "custom_proposal")]
custom_proposals_by_value(&self) -> Vec<&CustomProposal>474     pub fn custom_proposals_by_value(&self) -> Vec<&CustomProposal> {
475         match &self.payload {
476             MlsMessagePayload::Plain(plaintext) => match &plaintext.content.content {
477                 Content::Commit(commit) => Self::find_custom_proposals(commit),
478                 _ => Vec::new(),
479             },
480             _ => Vec::new(),
481         }
482     }
483 
484     /// If this is a welcome message, return key package references of all members who can
485     /// join using this message.
welcome_key_package_references(&self) -> Vec<&KeyPackageRef>486     pub fn welcome_key_package_references(&self) -> Vec<&KeyPackageRef> {
487         let MlsMessagePayload::Welcome(welcome) = &self.payload else {
488             return Vec::new();
489         };
490 
491         welcome.secrets.iter().map(|s| &s.new_member).collect()
492     }
493 
494     /// If this is a key package, return its key package reference.
495     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
key_package_reference<C: CipherSuiteProvider>( &self, cipher_suite: &C, ) -> Result<Option<KeyPackageRef>, MlsError>496     pub async fn key_package_reference<C: CipherSuiteProvider>(
497         &self,
498         cipher_suite: &C,
499     ) -> Result<Option<KeyPackageRef>, MlsError> {
500         let MlsMessagePayload::KeyPackage(kp) = &self.payload else {
501             return Ok(None);
502         };
503 
504         kp.to_reference(cipher_suite).await.map(Some)
505     }
506 
507     /// If this is a plaintext proposal, return the proposal reference that can be matched e.g. with
508     /// [`StateUpdate::unused_proposals`](super::StateUpdate::unused_proposals).
509     #[cfg(feature = "by_ref_proposal")]
510     #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)]
into_proposal_reference<C: CipherSuiteProvider>( self, cipher_suite: &C, ) -> Result<Option<Vec<u8>>, MlsError>511     pub async fn into_proposal_reference<C: CipherSuiteProvider>(
512         self,
513         cipher_suite: &C,
514     ) -> Result<Option<Vec<u8>>, MlsError> {
515         let MlsMessagePayload::Plain(public_message) = self.payload else {
516             return Ok(None);
517         };
518 
519         ProposalRef::from_content(cipher_suite, &public_message.into())
520             .await
521             .map(|r| Some(r.to_vec()))
522     }
523 }
524 
525 #[cfg(feature = "custom_proposal")]
526 impl MlsMessage {
find_custom_proposals(commit: &Commit) -> Vec<&CustomProposal>527     fn find_custom_proposals(commit: &Commit) -> Vec<&CustomProposal> {
528         commit
529             .proposals
530             .iter()
531             .filter_map(|p| match p {
532                 ProposalOrRef::Proposal(p) => match p.as_ref() {
533                     crate::group::Proposal::Custom(p) => Some(p),
534                     _ => None,
535                 },
536                 _ => None,
537             })
538             .collect()
539     }
540 }
541 
542 #[allow(clippy::large_enum_variant)]
543 #[derive(Clone, Debug, PartialEq, MlsSize, MlsEncode, MlsDecode)]
544 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
545 #[repr(u16)]
546 pub(crate) enum MlsMessagePayload {
547     Plain(PublicMessage) = 1u16,
548     #[cfg(feature = "private_message")]
549     Cipher(PrivateMessage) = 2u16,
550     Welcome(Welcome) = 3u16,
551     GroupInfo(GroupInfo) = 4u16,
552     KeyPackage(KeyPackage) = 5u16,
553 }
554 
555 impl From<PublicMessage> for MlsMessagePayload {
from(m: PublicMessage) -> Self556     fn from(m: PublicMessage) -> Self {
557         Self::Plain(m)
558     }
559 }
560 
561 #[cfg_attr(all(feature = "ffi", not(test)), safer_ffi_gen::ffi_type)]
562 #[derive(
563     Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, MlsSize, MlsEncode, MlsDecode,
564 )]
565 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
566 #[repr(u16)]
567 #[non_exhaustive]
568 /// Content description of an [`MlsMessage`]
569 pub enum WireFormat {
570     PublicMessage = 1u16,
571     PrivateMessage = 2u16,
572     Welcome = 3u16,
573     GroupInfo = 4u16,
574     KeyPackage = 5u16,
575 }
576 
577 #[derive(Clone, PartialEq, MlsSize, MlsEncode, MlsDecode)]
578 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
579 #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
580 pub(crate) struct FramedContent {
581     #[mls_codec(with = "mls_rs_codec::byte_vec")]
582     #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))]
583     pub group_id: Vec<u8>,
584     pub epoch: u64,
585     pub sender: Sender,
586     #[mls_codec(with = "mls_rs_codec::byte_vec")]
587     #[cfg_attr(feature = "serde", serde(with = "mls_rs_core::vec_serde"))]
588     pub authenticated_data: Vec<u8>,
589     pub content: Content,
590 }
591 
592 impl Debug for FramedContent {
fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result593     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
594         f.debug_struct("FramedContent")
595             .field(
596                 "group_id",
597                 &mls_rs_core::debug::pretty_group_id(&self.group_id),
598             )
599             .field("epoch", &self.epoch)
600             .field("sender", &self.sender)
601             .field(
602                 "authenticated_data",
603                 &mls_rs_core::debug::pretty_bytes(&self.authenticated_data),
604             )
605             .field("content", &self.content)
606             .finish()
607     }
608 }
609 
610 impl FramedContent {
content_type(&self) -> ContentType611     pub fn content_type(&self) -> ContentType {
612         self.content.content_type()
613     }
614 }
615 
616 #[cfg(test)]
617 pub(crate) mod test_utils {
618     #[cfg(feature = "private_message")]
619     use crate::group::test_utils::random_bytes;
620 
621     use crate::group::{AuthenticatedContent, MessageSignature};
622 
623     use super::*;
624 
625     use alloc::boxed::Box;
626 
get_test_auth_content() -> AuthenticatedContent627     pub(crate) fn get_test_auth_content() -> AuthenticatedContent {
628         // This is not a valid commit and should not be validated
629         let commit = Commit {
630             proposals: Default::default(),
631             path: None,
632         };
633 
634         AuthenticatedContent {
635             wire_format: WireFormat::PublicMessage,
636             content: FramedContent {
637                 group_id: Vec::new(),
638                 epoch: 0,
639                 sender: Sender::Member(1),
640                 authenticated_data: Vec::new(),
641                 content: Content::Commit(Box::new(commit)),
642             },
643             auth: FramedContentAuthData {
644                 signature: MessageSignature::empty(),
645                 confirmation_tag: None,
646             },
647         }
648     }
649 
650     #[cfg(feature = "private_message")]
get_test_ciphertext_content() -> PrivateMessageContent651     pub(crate) fn get_test_ciphertext_content() -> PrivateMessageContent {
652         PrivateMessageContent {
653             content: Content::Application(random_bytes(1024).into()),
654             auth: FramedContentAuthData {
655                 signature: MessageSignature::from(random_bytes(128)),
656                 confirmation_tag: None,
657             },
658         }
659     }
660 
661     impl AsRef<[u8]> for ApplicationData {
as_ref(&self) -> &[u8]662         fn as_ref(&self) -> &[u8] {
663             &self.0
664         }
665     }
666 }
667 
668 #[cfg(feature = "private_message")]
669 #[cfg(test)]
670 mod tests {
671     use assert_matches::assert_matches;
672 
673     use crate::{
674         client::test_utils::{TEST_CIPHER_SUITE, TEST_PROTOCOL_VERSION},
675         crypto::test_utils::test_cipher_suite_provider,
676         group::{
677             framing::test_utils::get_test_ciphertext_content,
678             proposal_ref::test_utils::auth_content_from_proposal, RemoveProposal,
679         },
680     };
681 
682     use super::*;
683 
684     #[test]
test_mls_ciphertext_content_mls_encoding()685     fn test_mls_ciphertext_content_mls_encoding() {
686         let ciphertext_content = get_test_ciphertext_content();
687 
688         let mut encoded = ciphertext_content.mls_encode_to_vec().unwrap();
689         encoded.extend_from_slice(&[0u8; 128]);
690 
691         let decoded =
692             PrivateMessageContent::mls_decode(&mut &*encoded, (&ciphertext_content.content).into())
693                 .unwrap();
694 
695         assert_eq!(ciphertext_content, decoded);
696     }
697 
698     #[test]
test_mls_ciphertext_content_non_zero_padding_error()699     fn test_mls_ciphertext_content_non_zero_padding_error() {
700         let ciphertext_content = get_test_ciphertext_content();
701 
702         let mut encoded = ciphertext_content.mls_encode_to_vec().unwrap();
703         encoded.extend_from_slice(&[1u8; 128]);
704 
705         let decoded =
706             PrivateMessageContent::mls_decode(&mut &*encoded, (&ciphertext_content.content).into());
707 
708         assert_matches!(decoded, Err(mls_rs_codec::Error::Custom(_)));
709     }
710 
711     #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))]
proposal_ref()712     async fn proposal_ref() {
713         let cs = test_cipher_suite_provider(TEST_CIPHER_SUITE);
714 
715         let test_auth = auth_content_from_proposal(
716             Proposal::Remove(RemoveProposal {
717                 to_remove: LeafIndex(0),
718             }),
719             Sender::External(0),
720         );
721 
722         let expected_ref = ProposalRef::from_content(&cs, &test_auth).await.unwrap();
723 
724         let test_message = MlsMessage {
725             version: TEST_PROTOCOL_VERSION,
726             payload: MlsMessagePayload::Plain(PublicMessage {
727                 content: test_auth.content,
728                 auth: test_auth.auth,
729                 membership_tag: Some(cs.mac(&[1, 2, 3], &[1, 2, 3]).await.unwrap().into()),
730             }),
731         };
732 
733         let computed_ref = test_message
734             .into_proposal_reference(&cs)
735             .await
736             .unwrap()
737             .unwrap();
738 
739         assert_eq!(computed_ref, expected_ref.to_vec());
740     }
741 }
742