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