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 let remainder = &bytes[proposal_type.tls_serialized_len()..];
180 Ok((proposal_type, remainder))
181 }
182}
183
184impl ProposalType {
185 pub fn is_path_required(&self) -> bool {
187 matches!(
188 self,
189 Self::Update
190 | Self::Remove
191 | Self::ExternalInit
192 | Self::GroupContextExtensions
193 | Self::SelfRemove
194 )
195 }
196}
197
198impl From<u16> for ProposalType {
199 fn from(value: u16) -> Self {
200 match value {
201 1 => ProposalType::Add,
202 2 => ProposalType::Update,
203 3 => ProposalType::Remove,
204 4 => ProposalType::PreSharedKey,
205 5 => ProposalType::Reinit,
206 6 => ProposalType::ExternalInit,
207 7 => ProposalType::GroupContextExtensions,
208 #[cfg(feature = "extensions-draft")]
209 8 => ProposalType::AppDataUpdate,
210 #[cfg(feature = "extensions-draft")]
211 0x0009 => ProposalType::AppEphemeral,
212 0x000a => ProposalType::SelfRemove,
213 other if crate::grease::is_grease_value(other) => ProposalType::Grease(other),
214 other => ProposalType::Custom(other),
215 }
216 }
217}
218
219impl From<ProposalType> for u16 {
220 fn from(value: ProposalType) -> Self {
221 match value {
222 ProposalType::Add => 1,
223 ProposalType::Update => 2,
224 ProposalType::Remove => 3,
225 ProposalType::PreSharedKey => 4,
226 ProposalType::Reinit => 5,
227 ProposalType::ExternalInit => 6,
228 ProposalType::GroupContextExtensions => 7,
229 #[cfg(feature = "extensions-draft")]
230 ProposalType::AppDataUpdate => 8,
231 #[cfg(feature = "extensions-draft")]
232 ProposalType::AppEphemeral => 0x0009,
233 ProposalType::SelfRemove => 0x000a,
234 ProposalType::Grease(id) => id,
235 ProposalType::Custom(id) => id,
236 }
237 }
238}
239
240#[derive(Debug, PartialEq, Clone)]
260#[cfg_attr(
261 feature = "0-8-1-storage-format",
262 derive(serde::Serialize, serde::Deserialize)
263)]
264#[cfg_attr(
265 not(feature = "0-8-1-storage-format"),
266 derive(
267 openmls_serialization_helpers::Serialize,
268 openmls_serialization_helpers::Deserialize,
269 )
270)]
271#[allow(missing_docs)]
272pub enum Proposal {
273 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 0)]
274 Add(Box<AddProposal>),
275 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 1)]
276 Update(Box<UpdateProposal>),
277 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 2)]
278 Remove(Box<RemoveProposal>),
279 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 3)]
280 PreSharedKey(Box<PreSharedKeyProposal>),
281 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 4)]
282 ReInit(Box<ReInitProposal>),
283 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 5)]
284 ExternalInit(Box<ExternalInitProposal>),
285 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 6)]
286 GroupContextExtensions(Box<GroupContextExtensionProposal>),
287 #[cfg(feature = "extensions-draft")]
290 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 10)]
291 AppDataUpdate(Box<AppDataUpdateProposal>),
292 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 8)]
294 SelfRemove,
295 #[cfg(feature = "extensions-draft")]
296 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 11)]
297 AppEphemeral(Box<AppEphemeralProposal>),
298 #[cfg_attr(not(feature = "0-8-1-storage-format"), storage_tag = 9)]
299 Custom(Box<CustomProposal>),
300}
301
302impl Proposal {
303 pub(crate) fn remove(r: RemoveProposal) -> Self {
305 Self::Remove(Box::new(r))
306 }
307
308 pub(crate) fn add(a: AddProposal) -> Self {
310 Self::Add(Box::new(a))
311 }
312
313 pub(crate) fn custom(c: CustomProposal) -> Self {
315 Self::Custom(Box::new(c))
316 }
317
318 pub(crate) fn psk(p: PreSharedKeyProposal) -> Self {
320 Self::PreSharedKey(Box::new(p))
321 }
322
323 pub(crate) fn update(p: UpdateProposal) -> Self {
325 Self::Update(Box::new(p))
326 }
327
328 pub(crate) fn group_context_extensions(p: GroupContextExtensionProposal) -> Self {
330 Self::GroupContextExtensions(Box::new(p))
331 }
332
333 pub(crate) fn external_init(p: ExternalInitProposal) -> Self {
335 Self::ExternalInit(Box::new(p))
336 }
337
338 #[cfg(test)]
339 pub(crate) fn re_init(p: ReInitProposal) -> Self {
341 Self::ReInit(Box::new(p))
342 }
343
344 pub fn proposal_type(&self) -> ProposalType {
346 match self {
347 Proposal::Add(_) => ProposalType::Add,
348 Proposal::Update(_) => ProposalType::Update,
349 Proposal::Remove(_) => ProposalType::Remove,
350 Proposal::PreSharedKey(_) => ProposalType::PreSharedKey,
351 Proposal::ReInit(_) => ProposalType::Reinit,
352 Proposal::ExternalInit(_) => ProposalType::ExternalInit,
353 Proposal::GroupContextExtensions(_) => ProposalType::GroupContextExtensions,
354 #[cfg(feature = "extensions-draft")]
355 Proposal::AppDataUpdate(_) => ProposalType::AppDataUpdate,
356 Proposal::SelfRemove => ProposalType::SelfRemove,
357 #[cfg(feature = "extensions-draft")]
358 Proposal::AppEphemeral(_) => ProposalType::AppEphemeral,
359 Proposal::Custom(custom) => ProposalType::Custom(custom.proposal_type.to_owned()),
360 }
361 }
362
363 pub(crate) fn is_type(&self, proposal_type: ProposalType) -> bool {
364 self.proposal_type() == proposal_type
365 }
366
367 pub fn is_path_required(&self) -> bool {
369 self.proposal_type().is_path_required()
370 }
371
372 pub(crate) fn has_lower_priority_than(&self, new_proposal: &Proposal) -> bool {
373 match (self, new_proposal) {
374 (Proposal::Update(_), _) => true,
376 (Proposal::Remove(_), Proposal::Update(_)) => false,
378 (Proposal::Remove(_), Proposal::Remove(_)) => true,
380 (_, Proposal::SelfRemove) => true,
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(crate) fn into_psk_id(self) -> PreSharedKeyId {
524 self.psk
525 }
526}
527
528impl PreSharedKeyProposal {
529 pub fn new(psk: PreSharedKeyId) -> Self {
531 Self { psk }
532 }
533}
534
535#[derive(
552 Debug,
553 PartialEq,
554 Eq,
555 Clone,
556 Serialize,
557 Deserialize,
558 TlsDeserialize,
559 TlsDeserializeBytes,
560 TlsSerialize,
561 TlsSize,
562)]
563pub struct ReInitProposal {
564 pub(crate) group_id: GroupId,
565 pub(crate) version: ProtocolVersion,
566 pub(crate) ciphersuite: Ciphersuite,
567 pub(crate) extensions: Extensions<GroupContext>,
568}
569
570#[derive(
582 Debug,
583 PartialEq,
584 Eq,
585 Clone,
586 Serialize,
587 Deserialize,
588 TlsDeserialize,
589 TlsDeserializeBytes,
590 TlsSerialize,
591 TlsSize,
592)]
593pub struct ExternalInitProposal {
594 kem_output: VLBytes,
595}
596
597impl ExternalInitProposal {
598 pub(crate) fn kem_output(&self) -> &[u8] {
600 self.kem_output.as_slice()
601 }
602}
603
604impl From<Vec<u8>> for ExternalInitProposal {
605 fn from(kem_output: Vec<u8>) -> Self {
606 ExternalInitProposal {
607 kem_output: kem_output.into(),
608 }
609 }
610}
611
612#[cfg(feature = "extensions-draft")]
613#[derive(
617 Debug,
618 PartialEq,
619 Clone,
620 Serialize,
621 Deserialize,
622 TlsDeserialize,
623 TlsDeserializeBytes,
624 TlsSerialize,
625 TlsSize,
626)]
627pub struct AppAck {
628 received_ranges: Vec<MessageRange>,
629}
630
631#[cfg(feature = "extensions-draft")]
632#[derive(
634 Debug,
635 PartialEq,
636 Clone,
637 Serialize,
638 Deserialize,
639 TlsDeserialize,
640 TlsDeserializeBytes,
641 TlsSerialize,
642 TlsSize,
643)]
644pub struct AppEphemeralProposal {
645 component_id: ComponentId,
647 data: VLBytes,
649}
650
651#[cfg(feature = "extensions-draft")]
652impl AppEphemeralProposal {
653 pub fn new(component_id: ComponentId, data: Vec<u8>) -> Self {
655 Self {
656 component_id,
657 data: data.into(),
658 }
659 }
660 pub fn component_id(&self) -> ComponentId {
662 self.component_id
663 }
664
665 pub fn data(&self) -> &[u8] {
667 self.data.as_slice()
668 }
669}
670
671#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
683pub struct GroupContextExtensionProposal {
684 extensions: Extensions<GroupContext>,
685}
686
687impl Size for GroupContextExtensionProposal {
688 fn tls_serialized_len(&self) -> usize {
689 self.extensions.tls_serialized_len()
690 }
691}
692
693impl TlsSerializeTrait for GroupContextExtensionProposal {
694 fn tls_serialize<W: Write>(&self, writer: &mut W) -> Result<usize, Error> {
695 self.extensions.tls_serialize(writer)
696 }
697}
698
699impl GroupContextExtensionProposal {
700 pub(crate) fn new(extensions: Extensions<GroupContext>) -> Self {
702 Self { extensions }
703 }
704
705 pub fn extensions(&self) -> &Extensions<GroupContext> {
707 &self.extensions
708 }
709
710 pub fn into_extensions(self) -> Extensions<GroupContext> {
712 self.extensions
713 }
714}
715
716#[derive(
739 PartialEq,
740 Clone,
741 Copy,
742 Debug,
743 TlsSerialize,
744 TlsDeserialize,
745 TlsDeserializeBytes,
746 TlsSize,
747 Serialize,
748 Deserialize,
749)]
750#[repr(u8)]
751pub enum ProposalOrRefType {
752 Proposal = 1,
754 Reference = 2,
756}
757
758#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, TlsSerialize, TlsSize)]
760#[repr(u8)]
761#[allow(missing_docs)]
762pub enum ProposalOrRef {
763 #[tls_codec(discriminant = 1)]
764 Proposal(Box<Proposal>),
765 Reference(Box<ProposalRef>),
766}
767
768impl ProposalOrRef {
769 pub(crate) fn proposal(p: Proposal) -> Self {
771 Self::Proposal(Box::new(p))
772 }
773
774 pub(crate) fn reference(p: ProposalRef) -> Self {
776 Self::Reference(Box::new(p))
777 }
778
779 pub(crate) fn as_proposal(&self) -> Option<&Proposal> {
780 if let Self::Proposal(v) = self {
781 Some(v)
782 } else {
783 None
784 }
785 }
786
787 pub(crate) fn as_reference(&self) -> Option<&ProposalRef> {
788 if let Self::Reference(v) = self {
789 Some(v)
790 } else {
791 None
792 }
793 }
794}
795
796impl From<Proposal> for ProposalOrRef {
797 fn from(value: Proposal) -> Self {
798 Self::proposal(value)
799 }
800}
801
802impl From<ProposalRef> for ProposalOrRef {
803 fn from(value: ProposalRef) -> Self {
804 Self::reference(value)
805 }
806}
807
808#[derive(Error, Debug)]
809pub(crate) enum ProposalRefError {
810 #[error("Expected `Proposal`, got `{wrong:?}`.")]
811 AuthenticatedContentHasWrongType { wrong: ContentType },
812 #[error(transparent)]
813 Other(#[from] LibraryError),
814}
815
816impl ProposalRef {
817 pub(crate) fn from_authenticated_content_by_ref(
818 crypto: &impl OpenMlsCrypto,
819 ciphersuite: Ciphersuite,
820 authenticated_content: &AuthenticatedContent,
821 ) -> Result<Self, ProposalRefError> {
822 if !matches!(
823 authenticated_content.content(),
824 FramedContentBody::Proposal(_)
825 ) {
826 return Err(ProposalRefError::AuthenticatedContentHasWrongType {
827 wrong: authenticated_content.content().content_type(),
828 });
829 };
830
831 let encoded = authenticated_content
832 .tls_serialize_detached()
833 .map_err(|error| ProposalRefError::Other(LibraryError::missing_bound_check(error)))?;
834
835 make_proposal_ref(&encoded, ciphersuite, crypto)
836 .map_err(|error| ProposalRefError::Other(LibraryError::unexpected_crypto_error(error)))
837 }
838
839 pub(crate) fn from_raw_proposal(
845 ciphersuite: Ciphersuite,
846 crypto: &impl OpenMlsCrypto,
847 proposal: &Proposal,
848 ) -> Result<Self, LibraryError> {
849 let mut data = b"Internal OpenMLS ProposalRef Label".to_vec();
851
852 let mut encoded = proposal
853 .tls_serialize_detached()
854 .map_err(LibraryError::missing_bound_check)?;
855
856 data.append(&mut encoded);
857
858 make_proposal_ref(&data, ciphersuite, crypto).map_err(LibraryError::unexpected_crypto_error)
859 }
860}
861
862#[derive(
870 Debug,
871 PartialEq,
872 Clone,
873 Serialize,
874 Deserialize,
875 TlsDeserialize,
876 TlsDeserializeBytes,
877 TlsSerialize,
878 TlsSize,
879)]
880pub(crate) struct MessageRange {
881 sender: KeyPackageRef,
882 first_generation: u32,
883 last_generation: u32,
884}
885
886#[cfg(feature = "extensions-draft")]
887mod app_data_update;
888#[cfg(feature = "extensions-draft")]
889pub use app_data_update::*;
890
891#[derive(
893 Debug,
894 PartialEq,
895 Clone,
896 Serialize,
897 Deserialize,
898 TlsSize,
899 TlsSerialize,
900 TlsDeserialize,
901 TlsDeserializeBytes,
902)]
903pub struct CustomProposal {
904 proposal_type: u16,
905 payload: Vec<u8>,
906}
907
908impl CustomProposal {
909 pub fn new(proposal_type: u16, payload: Vec<u8>) -> Self {
911 Self {
912 proposal_type,
913 payload,
914 }
915 }
916
917 pub fn proposal_type(&self) -> u16 {
919 self.proposal_type
920 }
921
922 pub fn payload(&self) -> &[u8] {
924 &self.payload
925 }
926}
927
928#[cfg(test)]
929mod tests {
930 use tls_codec::{Deserialize, Serialize};
931
932 use super::ProposalType;
933
934 #[test]
935 fn that_unknown_proposal_types_are_de_serialized_correctly() {
936 let proposal_types = [0x0000u16, 0x0B0B, 0x7C7C, 0xF000, 0xFFFF];
938
939 for proposal_type in proposal_types.into_iter() {
940 let test = proposal_type.to_be_bytes().to_vec();
942
943 let got = ProposalType::tls_deserialize_exact(&test).unwrap();
945
946 match got {
947 ProposalType::Custom(got_proposal_type) => {
948 assert_eq!(proposal_type, got_proposal_type);
949 }
950 other => panic!("Expected `ProposalType::Unknown`, got `{other:?}`."),
951 }
952
953 let got_serialized = got.tls_serialize_detached().unwrap();
955 assert_eq!(test, got_serialized);
956 }
957 }
958}