1use std::io::{Read, Write};
6
7use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10use tls_codec::{
11 Deserialize as TlsDeserializeTrait, DeserializeBytes, Error, Serialize as TlsSerializeTrait,
12 Size, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize, VLBytes,
13};
14
15use crate::{
16 binary_tree::array_representation::LeafNodeIndex,
17 ciphersuite::hash_ref::{make_proposal_ref, KeyPackageRef, ProposalRef},
18 error::LibraryError,
19 extensions::Extensions,
20 framing::{
21 mls_auth_content::AuthenticatedContent, mls_content::FramedContentBody, ContentType,
22 },
23 group::{GroupContext, GroupId},
24 key_packages::*,
25 schedule::psk::*,
26 treesync::LeafNode,
27 versions::ProtocolVersion,
28};
29
30#[cfg(feature = "extensions-draft")]
31use crate::component::ComponentId;
32
33#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug, Hash)]
76#[cfg_attr(
77 feature = "0-8-1-storage-format",
78 derive(serde::Serialize, serde::Deserialize)
79)]
80#[cfg_attr(
81 not(feature = "0-8-1-storage-format"),
82 derive(
83 openmls_serialization_helpers::Serialize,
84 openmls_serialization_helpers::Deserialize,
85 )
86)]
87#[allow(missing_docs)]
88#[repr(u16)]
89pub enum ProposalType {
90 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
91 Add,
92 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
93 Update,
94 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
95 Remove,
96 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
97 PreSharedKey,
98 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
99 Reinit,
100 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
101 ExternalInit,
102 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
103 GroupContextExtensions,
104 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
106 SelfRemove,
107 #[cfg(feature = "extensions-draft")]
108 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
109 AppEphemeral,
110 #[cfg(feature = "extensions-draft")]
111 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 12)]
112 AppDataUpdate,
113 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
114 Grease(u16),
115 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
116 Custom(u16),
117}
118
119impl ProposalType {
120 pub(crate) fn is_default(self) -> bool {
123 match self {
124 ProposalType::Add
125 | ProposalType::Update
126 | ProposalType::Remove
127 | ProposalType::PreSharedKey
128 | ProposalType::Reinit
129 | ProposalType::ExternalInit
130 | ProposalType::GroupContextExtensions => true,
131 ProposalType::SelfRemove | ProposalType::Grease(_) | ProposalType::Custom(_) => false,
132 #[cfg(feature = "extensions-draft")]
133 ProposalType::AppEphemeral | ProposalType::AppDataUpdate => false,
134 }
135 }
136
137 pub fn is_grease(&self) -> bool {
142 matches!(self, ProposalType::Grease(_))
143 }
144}
145
146impl Size for ProposalType {
147 fn tls_serialized_len(&self) -> usize {
148 2
149 }
150}
151
152impl TlsDeserializeTrait for ProposalType {
153 fn tls_deserialize<R: Read>(bytes: &mut R) -> Result<Self, Error>
154 where
155 Self: Sized,
156 {
157 let mut proposal_type = [0u8; 2];
158 bytes.read_exact(&mut proposal_type)?;
159
160 Ok(ProposalType::from(u16::from_be_bytes(proposal_type)))
161 }
162}
163
164impl TlsSerializeTrait for ProposalType {
165 fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
166 writer.write_all(&u16::from(*self).to_be_bytes())?;
167
168 Ok(2)
169 }
170}
171
172impl DeserializeBytes for ProposalType {
173 fn tls_deserialize_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), Error>
174 where
175 Self: Sized,
176 {
177 let mut bytes_ref = bytes;
178 let proposal_type = ProposalType::tls_deserialize(&mut bytes_ref)?;
179 Ok((proposal_type, bytes_ref))
180 }
181}
182
183impl ProposalType {
184 pub fn is_path_required(&self) -> bool {
186 matches!(
187 self,
188 Self::Update
189 | Self::Remove
190 | Self::ExternalInit
191 | Self::GroupContextExtensions
192 | Self::SelfRemove
193 )
194 }
195}
196
197impl From<u16> for ProposalType {
198 fn from(value: u16) -> Self {
199 match value {
200 1 => ProposalType::Add,
201 2 => ProposalType::Update,
202 3 => ProposalType::Remove,
203 4 => ProposalType::PreSharedKey,
204 5 => ProposalType::Reinit,
205 6 => ProposalType::ExternalInit,
206 7 => ProposalType::GroupContextExtensions,
207 #[cfg(feature = "extensions-draft")]
208 8 => ProposalType::AppDataUpdate,
209 #[cfg(feature = "extensions-draft")]
210 0x0009 => ProposalType::AppEphemeral,
211 0x000a => ProposalType::SelfRemove,
212 other if crate::grease::is_grease_value(other) => ProposalType::Grease(other),
213 other => ProposalType::Custom(other),
214 }
215 }
216}
217
218impl From<ProposalType> for u16 {
219 fn from(value: ProposalType) -> Self {
220 match value {
221 ProposalType::Add => 1,
222 ProposalType::Update => 2,
223 ProposalType::Remove => 3,
224 ProposalType::PreSharedKey => 4,
225 ProposalType::Reinit => 5,
226 ProposalType::ExternalInit => 6,
227 ProposalType::GroupContextExtensions => 7,
228 #[cfg(feature = "extensions-draft")]
229 ProposalType::AppDataUpdate => 8,
230 #[cfg(feature = "extensions-draft")]
231 ProposalType::AppEphemeral => 0x0009,
232 ProposalType::SelfRemove => 0x000a,
233 ProposalType::Grease(id) => id,
234 ProposalType::Custom(id) => id,
235 }
236 }
237}
238
239#[derive(Debug, PartialEq, Clone)]
259#[cfg_attr(
260 feature = "0-8-1-storage-format",
261 derive(serde::Serialize, serde::Deserialize)
262)]
263#[cfg_attr(
264 not(feature = "0-8-1-storage-format"),
265 derive(
266 openmls_serialization_helpers::Serialize,
267 openmls_serialization_helpers::Deserialize,
268 )
269)]
270#[allow(missing_docs)]
271pub enum Proposal {
272 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
273 Add(Box<AddProposal>),
274 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
275 Update(Box<UpdateProposal>),
276 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
277 Remove(Box<RemoveProposal>),
278 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
279 PreSharedKey(Box<PreSharedKeyProposal>),
280 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
281 ReInit(Box<ReInitProposal>),
282 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
283 ExternalInit(Box<ExternalInitProposal>),
284 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
285 GroupContextExtensions(Box<GroupContextExtensionProposal>),
286 #[cfg(feature = "extensions-draft")]
289 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
290 AppDataUpdate(Box<AppDataUpdateProposal>),
291 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
293 SelfRemove,
294 #[cfg(feature = "extensions-draft")]
295 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
296 AppEphemeral(Box<AppEphemeralProposal>),
297 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
298 Custom(Box<CustomProposal>),
299}
300
301impl Proposal {
302 pub(crate) fn remove(r: RemoveProposal) -> Self {
304 Self::Remove(Box::new(r))
305 }
306
307 pub(crate) fn add(a: AddProposal) -> Self {
309 Self::Add(Box::new(a))
310 }
311
312 pub(crate) fn custom(c: CustomProposal) -> Self {
314 Self::Custom(Box::new(c))
315 }
316
317 pub(crate) fn psk(p: PreSharedKeyProposal) -> Self {
319 Self::PreSharedKey(Box::new(p))
320 }
321
322 pub(crate) fn update(p: UpdateProposal) -> Self {
324 Self::Update(Box::new(p))
325 }
326
327 pub(crate) fn group_context_extensions(p: GroupContextExtensionProposal) -> Self {
329 Self::GroupContextExtensions(Box::new(p))
330 }
331
332 pub(crate) fn external_init(p: ExternalInitProposal) -> Self {
334 Self::ExternalInit(Box::new(p))
335 }
336
337 #[cfg(test)]
338 pub(crate) fn re_init(p: ReInitProposal) -> Self {
340 Self::ReInit(Box::new(p))
341 }
342
343 pub fn proposal_type(&self) -> ProposalType {
345 match self {
346 Proposal::Add(_) => ProposalType::Add,
347 Proposal::Update(_) => ProposalType::Update,
348 Proposal::Remove(_) => ProposalType::Remove,
349 Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
350 Proposal::ReInit(_) => ProposalType::Reinit,
351 Proposal::ExternalInit(_) => ProposalType::ExternalInit,
352 Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
353 #[cfg(feature = "extensions-draft")]
354 Proposal::AppDataUpdate(_) => ProposalType::AppDataUpdate,
355 Proposal::SelfRemove => ProposalType::SelfRemove,
356 #[cfg(feature = "extensions-draft")]
357 Proposal::AppEphemeral(_) => ProposalType::AppEphemeral,
358 Proposal::Custom(custom) => ProposalType::Custom(custom.proposal_type.to_owned()),
359 }
360 }
361
362 pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
363 self.proposal_type() == proposal_type
364 }
365
366 pub fn is_path_required(&self) -> bool {
368 self.proposal_type().is_path_required()
369 }
370
371 pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
372 match (self, new_proposal) {
373 (Proposal::Update(_), _) => true,
375 (Proposal::Remove(_), Proposal::Update(_)) => false,
377 (Proposal::Remove(_), Proposal::Remove(_)) => true,
379 (_, Proposal::SelfRemove) => true,
381 (Proposal::SelfRemove, Proposal::Update(_) | Proposal::Remove(_)) => false,
382 _ => {
383 debug_assert!(false);
384 false
385 }
386 }
387 }
388
389 pub(crate) fn as_remove(&self) -> Option<&RemoveProposal> {
391 if let Self::Remove(v) = self {
392 Some(v)
393 } else {
394 None
395 }
396 }
397
398 #[must_use]
402 pub fn is_remove(&self) -> bool {
403 matches!(self, Self::Remove(..))
404 }
405}
406
407#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
419pub struct AddProposal {
420 pub(crate) key_package: KeyPackage,
421}
422
423impl AddProposal {
424 pub fn key_package(&self) -> &KeyPackage {
426 &self.key_package
427 }
428}
429
430impl From<KeyPackage> for AddProposal {
431 fn from(key_package: KeyPackage) -> AddProposal {
432 AddProposal { key_package }
433 }
434}
435
436#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
449pub struct UpdateProposal {
450 pub(crate) leaf_node: LeafNode,
451}
452
453impl UpdateProposal {
454 pub fn leaf_node(&self) -> &LeafNode {
456 &self.leaf_node
457 }
458}
459
460#[derive(
472 Debug,
473 PartialEq,
474 Eq,
475 Clone,
476 Serialize,
477 Deserialize,
478 TlsDeserialize,
479 TlsDeserializeBytes,
480 TlsSerialize,
481 TlsSize,
482)]
483pub struct RemoveProposal {
484 pub(crate) removed: LeafNodeIndex,
485}
486
487impl RemoveProposal {
488 pub fn removed(&self) -> LeafNodeIndex {
490 self.removed
491 }
492}
493
494#[derive(
506 Debug,
507 PartialEq,
508 Eq,
509 Clone,
510 Serialize,
511 Deserialize,
512 TlsDeserialize,
513 TlsDeserializeBytes,
514 TlsSerialize,
515 TlsSize,
516)]
517pub struct PreSharedKeyProposal {
518 psk: PreSharedKeyId,
519}
520
521impl PreSharedKeyProposal {
522 pub fn new(psk: PreSharedKeyId) -> Self {
524 Self { psk }
525 }
526
527 pub fn psk(&self) -> &PreSharedKeyId {
529 &self.psk
530 }
531
532 pub(crate) fn into_psk_id(self) -> PreSharedKeyId {
534 self.psk
535 }
536}
537
538#[derive(
555 Debug,
556 PartialEq,
557 Eq,
558 Clone,
559 Serialize,
560 Deserialize,
561 TlsDeserialize,
562 TlsDeserializeBytes,
563 TlsSerialize,
564 TlsSize,
565)]
566pub struct ReInitProposal {
567 pub(crate) group_id: GroupId,
568 pub(crate) version: ProtocolVersion,
569 pub(crate) ciphersuite: Ciphersuite,
570 pub(crate) extensions: Extensions<GroupContext>,
571}
572
573#[derive(
585 Debug,
586 PartialEq,
587 Eq,
588 Clone,
589 Serialize,
590 Deserialize,
591 TlsDeserialize,
592 TlsDeserializeBytes,
593 TlsSerialize,
594 TlsSize,
595)]
596pub struct ExternalInitProposal {
597 kem_output: VLBytes,
598}
599
600impl ExternalInitProposal {
601 pub(crate) fn kem_output(&self) -> &[u8] {
603 self.kem_output.as_slice()
604 }
605}
606
607impl From<Vec<u8>> for ExternalInitProposal {
608 fn from(kem_output: Vec<u8>) -> Self {
609 ExternalInitProposal {
610 kem_output: kem_output.into(),
611 }
612 }
613}
614
615#[cfg(feature = "extensions-draft")]
616#[derive(
620 Debug,
621 PartialEq,
622 Clone,
623 Serialize,
624 Deserialize,
625 TlsDeserialize,
626 TlsDeserializeBytes,
627 TlsSerialize,
628 TlsSize,
629)]
630pub struct AppAck {
631 received_ranges: Vec<MessageRange>,
632}
633
634#[cfg(feature = "extensions-draft")]
635#[derive(
637 Debug,
638 PartialEq,
639 Clone,
640 Serialize,
641 Deserialize,
642 TlsDeserialize,
643 TlsDeserializeBytes,
644 TlsSerialize,
645 TlsSize,
646)]
647pub struct AppEphemeralProposal {
648 component_id: ComponentId,
650 data: VLBytes,
652}
653
654#[cfg(feature = "extensions-draft")]
655impl AppEphemeralProposal {
656 pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
658 Self {
659 component_id,
660 data: data.into(),
661 }
662 }
663 pub fn component_id(&self) -> ComponentId {
665 self.component_id
666 }
667
668 pub fn data(&self) -> &[u8] {
670 self.data.as_slice()
671 }
672}
673
674#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
686pub struct GroupContextExtensionProposal {
687 extensions: Extensions<GroupContext>,
688}
689
690impl Size for GroupContextExtensionProposal {
691 fn tls_serialized_len(&self) -> usize {
692 self.extensions.tls_serialized_len()
693 }
694}
695
696impl TlsSerializeTrait for GroupContextExtensionProposal {
697 fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
698 self.extensions.tls_serialize(writer)
699 }
700}
701
702impl GroupContextExtensionProposal {
703 pub(crate) fn new(extensions: Extensions<GroupContext>) -> Self {
705 Self { extensions }
706 }
707
708 pub fn extensions(&self) -> &Extensions<GroupContext> {
710 &self.extensions
711 }
712
713 pub fn into_extensions(self) -> Extensions<GroupContext> {
715 self.extensions
716 }
717}
718
719#[derive(
742 PartialEq,
743 Clone,
744 Copy,
745 Debug,
746 TlsSerialize,
747 TlsDeserialize,
748 TlsDeserializeBytes,
749 TlsSize,
750 Serialize,
751 Deserialize,
752)]
753#[repr(u8)]
754pub enum ProposalOrRefType {
755 Proposal = 1,
757 Reference = 2,
759}
760
761#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
763#[repr(u8)]
764#[allow(missing_docs)]
765pub enum ProposalOrRef {
766 #[tls_codec(discriminant = 1)]
767 Proposal(Box<Proposal>),
768 Reference(Box<ProposalRef>),
769}
770
771impl ProposalOrRef {
772 pub(crate) fn proposal(p: Proposal) -> Self {
774 Self::Proposal(Box::new(p))
775 }
776
777 pub(crate) fn reference(p: ProposalRef) -> Self {
779 Self::Reference(Box::new(p))
780 }
781
782 pub(crate) fn as_proposal(&self) -> Option<&Proposal> {
783 if let Self::Proposal(v) = self {
784 Some(v)
785 } else {
786 None
787 }
788 }
789
790 pub(crate) fn as_reference(&self) -> Option<&ProposalRef> {
791 if let Self::Reference(v) = self {
792 Some(v)
793 } else {
794 None
795 }
796 }
797}
798
799impl From<Proposal> for ProposalOrRef {
800 fn from(value: Proposal) -> Self {
801 Self::proposal(value)
802 }
803}
804
805impl From<ProposalRef> for ProposalOrRef {
806 fn from(value: ProposalRef) -> Self {
807 Self::reference(value)
808 }
809}
810
811#[derive(Error, Debug)]
812pub(crate) enum ProposalRefError {
813 #[error("Expected `Proposal`, got `{wrong:?}`.")]
814 AuthenticatedContentHasWrongType { wrong: ContentType },
815 #[error(transparent)]
816 Other(#[from] LibraryError),
817}
818
819impl ProposalRef {
820 pub(crate) fn from_authenticated_content_by_ref(
821 crypto: &impl OpenMlsCrypto,
822 ciphersuite: Ciphersuite,
823 authenticated_content: &AuthenticatedContent,
824 ) -> Result<Self, ProposalRefError> {
825 if !matches!(
826 authenticated_content.content(),
827 FramedContentBody::Proposal(_)
828 ) {
829 return Err(ProposalRefError::AuthenticatedContentHasWrongType {
830 wrong: authenticated_content.content().content_type(),
831 });
832 };
833
834 let encoded = authenticated_content
835 .tls_serialize_detached()
836 .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
837
838 make_proposal_ref(&encoded, ciphersuite, crypto)
839 .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
840 }
841
842 pub(crate) fn from_raw_proposal(
848 ciphersuite: Ciphersuite,
849 crypto: &impl OpenMlsCrypto,
850 proposal: &Proposal,
851 ) -> Result<Self, LibraryError> {
852 let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
854
855 let mut encoded = proposal
856 .tls_serialize_detached()
857 .map_err(LibraryError::missing_bound_check)?;
858
859 data.append(&mut encoded);
860
861 make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
862 }
863}
864
865#[derive(
873 Debug,
874 PartialEq,
875 Clone,
876 Serialize,
877 Deserialize,
878 TlsDeserialize,
879 TlsDeserializeBytes,
880 TlsSerialize,
881 TlsSize,
882)]
883pub(crate) struct MessageRange {
884 sender: KeyPackageRef,
885 first_generation: u32,
886 last_generation: u32,
887}
888
889#[cfg(feature = "extensions-draft")]
890mod app_data_update;
891#[cfg(feature = "extensions-draft")]
892pub use app_data_update::*;
893
894#[derive(
896 Debug,
897 PartialEq,
898 Clone,
899 Serialize,
900 Deserialize,
901 TlsSize,
902 TlsSerialize,
903 TlsDeserialize,
904 TlsDeserializeBytes,
905)]
906pub struct CustomProposal {
907 proposal_type: u16,
908 payload: Vec<u8>,
909}
910
911impl CustomProposal {
912 pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
914 Self {
915 proposal_type,
916 payload,
917 }
918 }
919
920 pub fn proposal_type(&self) -> u16 {
922 self.proposal_type
923 }
924
925 pub fn payload(&self) -> &[u8] {
927 &self.payload
928 }
929}
930
931#[cfg(test)]
932mod tests {
933 use tls_codec::{Deserialize, Serialize};
934
935 use super::ProposalType;
936
937 #[test]
938 fn that_unknown_proposal_types_are_de_serialized_correctly() {
939 let proposal_types = [0x0000u16, 0x0B0B, 0x7C7C, 0xF000, 0xFFFF];
941
942 for proposal_type in proposal_types.into_iter() {
943 let test = proposal_type.to_be_bytes().to_vec();
945
946 let got = ProposalType::tls_deserialize_exact(&test).unwrap();
948
949 match got {
950 ProposalType::Custom(got_proposal_type) => {
951 assert_eq!(proposal_type, got_proposal_type);
952 }
953 other => panic!("Expected `ProposalType::Unknown`, got `{other:?}`."),
954 }
955
956 let got_serialized = got.tls_serialize_detached().unwrap();
958 assert_eq!(test, got_serialized);
959 }
960 }
961}