Skip to main content

mls_rs/group/
framing.rs

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