1use std::{borrow::BorrowMut, marker::PhantomData};
5
6use openmls_traits::{
7 crypto::OpenMlsCrypto, random::OpenMlsRand, signatures::Signer, storage::StorageProvider as _,
8};
9use tls_codec::Serialize as _;
10
11use crate::{
12 binary_tree::LeafNodeIndex,
13 ciphersuite::{signable::Signable as _, Secret},
14 extensions::Extensions,
15 framing::{FramingParameters, WireFormat},
16 group::{
17 diff::compute_path::{CommitType, PathComputationResult},
18 CommitBuilderStageError, CreateCommitError, Extension, ExternalPubExtension, GroupContext,
19 ProposalQueue, ProposalQueueError, QueuedProposal, RatchetTreeExtension, StagedCommit,
20 WireFormatPolicy,
21 },
22 key_packages::KeyPackage,
23 messages::{
24 group_info::{GroupInfo, GroupInfoTBS},
25 Commit, Welcome,
26 },
27 prelude::{
28 CredentialWithKey, InvalidExtensionError, LeafNodeParameters, LibraryError, NewSignerBundle,
29 },
30 schedule::{
31 psk::{load_psks, PskSecret},
32 EpochSecretsResult, JoinerSecret, KeySchedule, PreSharedKeyId,
33 },
34 storage::{OpenMlsProvider, StorageProvider},
35 treesync::errors::LeafNodeValidationError,
36 versions::ProtocolVersion,
37};
38#[cfg(feature = "extensions-draft-08")]
39use crate::{
40 messages::proposals::AppDataUpdateProposal,
41 prelude::processing::{AppDataDictionaryUpdater, AppDataUpdates},
42 schedule::application_export_tree::ApplicationExportTree,
43};
44
45pub(crate) mod external_commits;
46
47pub use external_commits::{ExternalCommitBuilder, ExternalCommitBuilderError};
48
49#[cfg(doc)]
50use super::MlsGroupJoinConfig;
51
52use super::{
53 mls_auth_content::AuthenticatedContent,
54 staged_commit::{MemberStagedCommitState, StagedCommitState},
55 AddProposal, CreateCommitResult, GroupContextExtensionProposal, MlsGroup, MlsGroupState,
56 MlsMessageOut, PendingCommitState, Proposal, RemoveProposal, Sender,
57};
58
59#[derive(Debug)]
60struct ExternalCommitInfo {
61 aad: Vec<u8>,
62 credential: CredentialWithKey,
63 wire_format_policy: WireFormatPolicy,
64}
65
66#[derive(Debug, Default)]
67struct GroupInfoConfig {
68 create_group_info: bool,
69 use_ratchet_tree_extension: bool,
70 other_extensions: Vec<Extension>,
71}
72
73#[derive(Debug)]
75pub struct Initial {
76 own_proposals: Vec<Proposal>,
77 force_self_update: bool,
78 leaf_node_parameters: LeafNodeParameters,
79 external_commit_info: Option<ExternalCommitInfo>,
80
81 consume_proposal_store: bool,
84}
85
86impl Default for Initial {
87 fn default() -> Self {
88 Initial {
89 consume_proposal_store: true,
90 force_self_update: false,
91 leaf_node_parameters: LeafNodeParameters::default(),
92 own_proposals: vec![],
93 external_commit_info: None,
94 }
95 }
96}
97
98pub struct LoadedPsks {
100 own_proposals: Vec<Proposal>,
101 force_self_update: bool,
102 leaf_node_parameters: LeafNodeParameters,
103 external_commit_info: Option<ExternalCommitInfo>,
104
105 consume_proposal_store: bool,
108 psks: Vec<(PreSharedKeyId, Secret)>,
109
110 group_info_config: GroupInfoConfig,
112
113 #[cfg(feature = "extensions-draft-08")]
114 app_data_dictionary_updates: Option<AppDataUpdates>,
115}
116
117#[derive(Debug)]
119pub struct Complete {
120 result: CreateCommitResult,
121 original_wire_format_policy: Option<WireFormatPolicy>,
123}
124
125#[derive(Debug)]
163pub struct CommitBuilder<'a, T, G: BorrowMut<MlsGroup> = &'a mut MlsGroup> {
164 group: G,
167
168 stage: T,
170
171 pd: PhantomData<&'a ()>,
172}
173
174impl<'a, T, G: BorrowMut<MlsGroup>> CommitBuilder<'a, T, G> {
175 pub(crate) fn replace_stage<NextStage>(
176 self,
177 next_stage: NextStage,
178 ) -> (T, CommitBuilder<'a, NextStage, G>) {
179 self.map_stage(|prev_stage| (prev_stage, next_stage))
180 }
181
182 pub(crate) fn into_stage<NextStage>(
183 self,
184 next_stage: NextStage,
185 ) -> CommitBuilder<'a, NextStage, G> {
186 self.replace_stage(next_stage).1
187 }
188
189 fn take_stage(self) -> (T, CommitBuilder<'a, (), G>) {
190 self.replace_stage(())
191 }
192
193 fn map_stage<NextStage, Aux, F: FnOnce(T) -> (Aux, NextStage)>(
194 self,
195 f: F,
196 ) -> (Aux, CommitBuilder<'a, NextStage, G>) {
197 let Self {
198 group,
199 stage,
200 pd: PhantomData,
201 } = self;
202
203 let (aux, stage) = f(stage);
204
205 (
206 aux,
207 CommitBuilder {
208 group,
209 stage,
210 pd: PhantomData,
211 },
212 )
213 }
214
215 #[cfg(feature = "fork-resolution")]
216 pub(crate) fn stage(&self) -> &T {
217 &self.stage
218 }
219}
220
221impl MlsGroup {
222 pub fn commit_builder(&mut self) -> CommitBuilder<'_, Initial> {
224 CommitBuilder::<'_, Initial, &mut MlsGroup>::new(self)
225 }
226}
227
228impl<'a> CommitBuilder<'a, Initial, &mut MlsGroup> {
230 pub fn consume_proposal_store(mut self, consume_proposal_store: bool) -> Self {
233 self.stage.consume_proposal_store = consume_proposal_store;
234 self
235 }
236
237 pub fn force_self_update(mut self, force_self_update: bool) -> Self {
239 self.stage.force_self_update = force_self_update;
240 self
241 }
242
243 pub fn propose_adds(mut self, key_packages: impl IntoIterator<Item = KeyPackage>) -> Self {
246 self.stage.own_proposals.extend(
247 key_packages
248 .into_iter()
249 .map(|key_package| Proposal::add(AddProposal { key_package })),
250 );
251 self
252 }
253
254 pub fn propose_removals(mut self, removed: impl IntoIterator<Item = LeafNodeIndex>) -> Self {
257 self.stage.own_proposals.extend(
258 removed
259 .into_iter()
260 .map(|removed| Proposal::remove(RemoveProposal { removed })),
261 );
262 self
263 }
264
265 pub fn propose_group_context_extensions(
268 mut self,
269 extensions: Extensions<GroupContext>,
270 ) -> Result<Self, CreateCommitError> {
271 let proposal = GroupContextExtensionProposal::new(extensions);
272 self.stage
273 .own_proposals
274 .push(Proposal::group_context_extensions(proposal));
275 Ok(self)
276 }
277
278 pub fn add_proposal(mut self, proposal: Proposal) -> Self {
281 self.stage.own_proposals.push(proposal);
282 self
283 }
284
285 pub fn add_proposals(mut self, proposals: impl IntoIterator<Item = Proposal>) -> Self {
287 self.stage.own_proposals.extend(proposals);
288 self
289 }
290}
291
292impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, Initial, G> {
294 pub fn new(group: G) -> CommitBuilder<'a, Initial, G> {
296 let stage = Initial {
297 ..Default::default()
298 };
299 CommitBuilder {
300 group,
301 stage,
302 pd: PhantomData,
303 }
304 }
305
306 pub fn leaf_node_parameters(mut self, leaf_node_parameters: LeafNodeParameters) -> Self {
309 self.stage.leaf_node_parameters = leaf_node_parameters;
310 self
311 }
312
313 pub fn load_psks<Storage: StorageProvider>(
315 self,
316 storage: &'a Storage,
317 ) -> Result<CommitBuilder<'a, LoadedPsks, G>, CreateCommitError> {
318 let psk_ids: Vec<_> = self
319 .stage
320 .own_proposals
321 .iter()
322 .chain(
323 self.group
324 .borrow()
325 .proposal_store()
326 .proposals()
327 .map(|queued_proposal| queued_proposal.proposal()),
328 )
329 .filter_map(|proposal| match proposal {
330 Proposal::PreSharedKey(psk_proposal) => Some(psk_proposal.clone().into_psk_id()),
331 _ => None,
332 })
333 .collect();
334
335 let psks = load_psks(storage, &self.group.borrow().resumption_psk_store, &psk_ids)?
337 .into_iter()
338 .map(|(psk_id_ref, key)| (psk_id_ref.clone(), key))
339 .collect();
340
341 let use_ratchet_tree_extension = self
343 .group
344 .borrow()
345 .configuration()
346 .use_ratchet_tree_extension;
347
348 let group_info_config = GroupInfoConfig {
349 use_ratchet_tree_extension,
350 create_group_info: use_ratchet_tree_extension,
351 other_extensions: vec![],
352 };
353
354 Ok(self
355 .map_stage(|stage| {
356 (
357 (),
358 LoadedPsks {
359 own_proposals: stage.own_proposals,
360 psks,
361 force_self_update: stage.force_self_update,
362 leaf_node_parameters: stage.leaf_node_parameters,
363 consume_proposal_store: stage.consume_proposal_store,
364 group_info_config,
365 external_commit_info: stage.external_commit_info,
366 #[cfg(feature = "extensions-draft-08")]
367 app_data_dictionary_updates: None,
368 },
369 )
370 })
371 .1)
372 }
373}
374
375impl<'a, G: BorrowMut<MlsGroup>> CommitBuilder<'a, LoadedPsks, G> {
376 pub fn create_group_info(mut self, create_group_info: bool) -> Self {
379 self.stage.group_info_config.create_group_info = create_group_info;
380 self
381 }
382
383 pub fn use_ratchet_tree_extension(mut self, use_ratchet_tree_extension: bool) -> Self {
386 if use_ratchet_tree_extension {
387 self.stage.group_info_config.create_group_info = true;
388 }
389 self.stage.group_info_config.use_ratchet_tree_extension = use_ratchet_tree_extension;
390 self
391 }
392
393 pub fn create_group_info_with_extensions(
398 mut self,
399 extensions: impl IntoIterator<Item = Extension>,
400 ) -> Result<Self, InvalidExtensionError> {
401 self.stage.group_info_config.create_group_info = true;
402 self.stage.group_info_config.other_extensions = extensions
403 .into_iter()
404 .map(|extension| {
405 if extension.as_ratchet_tree_extension().is_ok()
406 || extension.as_external_pub_extension().is_ok()
407 {
408 Err(InvalidExtensionError::CannotAddDirectlyToGroupInfo)
409 } else {
410 Ok(extension)
411 }
412 })
413 .collect::<Result<Vec<_>, _>>()?;
414
415 Ok(self)
416 }
417 pub fn build<S: Signer>(
421 self,
422 rand: &impl OpenMlsRand,
423 crypto: &impl OpenMlsCrypto,
424 signer: &S,
425 f: impl FnMut(&QueuedProposal) -> bool,
426 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
427 self.build_internal(rand, crypto, signer, None::<NewSignerBundle<'_, S>>, f)
428 }
429
430 pub fn build_with_new_signer<S: Signer>(
438 self,
439 rand: &impl OpenMlsRand,
440 crypto: &impl OpenMlsCrypto,
441 old_signer: &impl Signer,
442 new_signer: NewSignerBundle<'_, S>,
443 f: impl FnMut(&QueuedProposal) -> bool,
444 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
445 self.build_internal(rand, crypto, old_signer, Some(new_signer), f)
446 }
447
448 fn build_internal<S: Signer>(
449 self,
450 rand: &impl OpenMlsRand,
451 crypto: &impl OpenMlsCrypto,
452 old_signer: &impl Signer,
453 new_signer: Option<NewSignerBundle<'_, S>>,
454 f: impl FnMut(&QueuedProposal) -> bool,
455 ) -> Result<CommitBuilder<'a, Complete, G>, CreateCommitError> {
456 let (mut cur_stage, builder) = self.take_stage();
457
458 let GroupInfoConfig {
460 create_group_info,
461 use_ratchet_tree_extension,
462 other_extensions,
463 } = cur_stage.group_info_config;
464
465 let group = builder.group.borrow();
466 let ciphersuite = group.ciphersuite();
467 let own_leaf_index = group.own_leaf_index();
468 let (sender, is_external_commit) = match cur_stage.external_commit_info {
469 None => (Sender::build_member(own_leaf_index), false),
470 Some(_) => (Sender::NewMemberCommit, true),
471 };
472 let psks = cur_stage.psks;
473
474 let own_proposals: Vec<_> = cur_stage
477 .own_proposals
478 .into_iter()
479 .map(|proposal| {
480 QueuedProposal::from_proposal_and_sender(ciphersuite, crypto, proposal, &sender)
481 })
482 .collect::<Result<_, _>>()?;
483
484 let group_proposal_store_queue = group
487 .pending_proposals()
488 .filter(|_| cur_stage.consume_proposal_store)
489 .cloned();
490
491 let proposal_queue = group_proposal_store_queue.chain(own_proposals).filter(f);
495
496 let (proposal_queue, contains_own_updates) =
497 ProposalQueue::filter_proposals(proposal_queue, group.own_leaf_index).map_err(|e| {
498 match e {
499 ProposalQueueError::LibraryError(e) => e.into(),
500 ProposalQueueError::ProposalNotFound => CreateCommitError::MissingProposal,
501 ProposalQueueError::UpdateFromExternalSender
502 | ProposalQueueError::SelfRemoveFromNonMember => {
503 CreateCommitError::WrongProposalSenderType
504 }
505 }
506 })?;
507
508 group
513 .public_group
514 .validate_proposal_type_support(&proposal_queue)?;
515 group
520 .public_group
521 .validate_key_uniqueness(&proposal_queue, None)?;
522 group.public_group.validate_add_proposals(&proposal_queue)?;
524 group.public_group.validate_capabilities(&proposal_queue)?;
527 group
530 .public_group
531 .validate_remove_proposals(&proposal_queue)?;
532 group
533 .public_group
534 .validate_pre_shared_key_proposals(&proposal_queue)?;
535 group
540 .public_group
541 .validate_update_proposals(&proposal_queue, own_leaf_index)?;
542
543 group
546 .public_group
547 .validate_group_context_extensions_proposal(&proposal_queue)?;
548
549 #[cfg(feature = "extensions-draft-08")]
550 group
551 .public_group
552 .validate_app_data_update_proposals_and_group_context(&proposal_queue)?;
553
554 if is_external_commit {
555 group
556 .public_group
557 .validate_external_commit(&proposal_queue)?;
558 }
559
560 let proposal_reference_list = proposal_queue.commit_list();
561
562 let mut diff = group.public_group.empty_diff();
564
565 #[cfg(feature = "extensions-draft-08")]
567 let apply_proposals_values = diff.apply_proposals_with_app_data_updates(
568 &proposal_queue,
569 own_leaf_index,
570 cur_stage.app_data_dictionary_updates,
571 )?;
572 #[cfg(not(feature = "extensions-draft-08"))]
573 let apply_proposals_values = diff.apply_proposals(&proposal_queue, own_leaf_index)?;
574 if apply_proposals_values.self_removed && !is_external_commit {
575 return Err(CreateCommitError::CannotRemoveSelf);
576 }
577
578 let path_computation_result =
579 if apply_proposals_values.path_required
581 || contains_own_updates
582 || cur_stage.force_self_update
583 || !cur_stage.leaf_node_parameters.is_empty()
584 {
585 let commit_type = match &cur_stage.external_commit_info {
586 Some(ExternalCommitInfo { credential , ..}) => {
587 CommitType::External(credential.clone())
588 }
589 None => CommitType::Member,
590 };
591 if let Some(new_signer) = new_signer {
595 if let Some(credential_with_key) =
596 cur_stage.leaf_node_parameters.credential_with_key()
597 {
598 if credential_with_key != &new_signer.credential_with_key {
599 return Err(CreateCommitError::InvalidLeafNodeParameters);
600 }
601 }
602 cur_stage.leaf_node_parameters.set_credential_with_key(
603 new_signer.credential_with_key,
604 );
605
606 diff.compute_path(
607 rand,
608 crypto,
609 own_leaf_index,
610 apply_proposals_values.exclusion_list(),
611 &commit_type,
612 &cur_stage.leaf_node_parameters,
613 new_signer.signer,
614 apply_proposals_values.extensions.clone()
615 )?
616 } else {
617 diff.compute_path(
618 rand,
619 crypto,
620 own_leaf_index,
621 apply_proposals_values.exclusion_list(),
622 &commit_type,
623 &cur_stage.leaf_node_parameters,
624 old_signer,
625 apply_proposals_values.extensions.clone()
626 )?
627 }
628 } else {
629 diff.update_group_context(crypto, apply_proposals_values.extensions.clone())?;
632 PathComputationResult::default()
633 };
634
635 let update_path_leaf_node = path_computation_result
636 .encrypted_path
637 .as_ref()
638 .map(|path| path.leaf_node().clone());
639
640 if let Some(ref leaf_node) = update_path_leaf_node {
642 if !diff
648 .group_context()
649 .extensions()
650 .iter()
651 .map(Extension::extension_type)
652 .all(|ext_type| leaf_node.supports_extension(&ext_type))
653 {
654 return Err(CreateCommitError::LeafNodeValidation(
655 LeafNodeValidationError::UnsupportedExtensions,
656 ));
657 }
658
659 if let Some(required_capabilities) =
662 diff.group_context().extensions().required_capabilities()
663 {
664 leaf_node
665 .capabilities()
666 .supports_required_capabilities(required_capabilities)?
667 }
668 }
669
670 let commit = Commit {
672 proposals: proposal_reference_list,
673 path: path_computation_result.encrypted_path,
674 };
675
676 let framing_parameters =
677 if let Some(ExternalCommitInfo { aad, .. }) = &cur_stage.external_commit_info {
678 FramingParameters::new(aad, WireFormat::PublicMessage)
679 } else {
680 group.framing_parameters()
681 };
682
683 let mut authenticated_content = AuthenticatedContent::commit(
685 framing_parameters,
686 sender,
687 commit,
688 group.public_group.group_context(),
689 old_signer,
690 )?;
691
692 diff.update_confirmed_transcript_hash(crypto, &authenticated_content)?;
694
695 let serialized_provisional_group_context = diff
696 .group_context()
697 .tls_serialize_detached()
698 .map_err(LibraryError::missing_bound_check)?;
699
700 let joiner_secret = JoinerSecret::new(
701 crypto,
702 ciphersuite,
703 path_computation_result.commit_secret,
704 group.group_epoch_secrets().init_secret(),
705 &serialized_provisional_group_context,
706 )
707 .map_err(LibraryError::unexpected_crypto_error)?;
708
709 let psk_secret = { PskSecret::new(crypto, ciphersuite, psks)? };
711
712 let mut key_schedule = KeySchedule::init(ciphersuite, crypto, &joiner_secret, psk_secret)?;
714
715 let serialized_provisional_group_context = diff
716 .group_context()
717 .tls_serialize_detached()
718 .map_err(LibraryError::missing_bound_check)?;
719
720 let welcome_secret = key_schedule
721 .welcome(crypto, ciphersuite)
722 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
723 key_schedule
724 .add_context(crypto, &serialized_provisional_group_context)
725 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
726 let EpochSecretsResult {
727 epoch_secrets: provisional_epoch_secrets,
728 #[cfg(feature = "extensions-draft-08")]
729 application_exporter,
730 } = key_schedule
731 .epoch_secrets(crypto, ciphersuite)
732 .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
733
734 let confirmation_tag = provisional_epoch_secrets
736 .confirmation_key()
737 .tag(
738 crypto,
739 ciphersuite,
740 diff.group_context().confirmed_transcript_hash(),
741 )
742 .map_err(LibraryError::unexpected_crypto_error)?;
743
744 authenticated_content.set_confirmation_tag(confirmation_tag.clone());
746
747 diff.update_interim_transcript_hash(ciphersuite, crypto, confirmation_tag.clone())?;
748
749 let needs_welcome = !apply_proposals_values.invitation_list.is_empty();
751
752 let needs_group_info = needs_welcome || create_group_info;
756
757 let (welcome_option, group_info) = if !needs_group_info {
758 (None, None)
759 } else {
760 let mut extensions_list = vec![];
762 if use_ratchet_tree_extension {
763 extensions_list.push(Extension::RatchetTree(RatchetTreeExtension::new(
764 diff.export_ratchet_tree(),
765 )));
766 };
767 extensions_list.extend(other_extensions);
769
770 let mut extensions = Extensions::from_vec(extensions_list)?;
771
772 let welcome_option = needs_welcome
773 .then(|| -> Result<_, CreateCommitError> {
774 let group_info_tbs = {
775 GroupInfoTBS::new(
776 diff.group_context().clone(),
777 extensions.clone(),
778 confirmation_tag.clone(),
779 own_leaf_index,
780 )?
781 };
782 let group_info = group_info_tbs.sign(old_signer)?;
784
785 let (welcome_key, welcome_nonce) = welcome_secret
787 .derive_welcome_key_nonce(crypto, ciphersuite)
788 .map_err(LibraryError::unexpected_crypto_error)?;
789 let encrypted_group_info = welcome_key
790 .aead_seal(
791 crypto,
792 group_info
793 .tls_serialize_detached()
794 .map_err(LibraryError::missing_bound_check)?
795 .as_slice(),
796 &[],
797 &welcome_nonce,
798 )
799 .map_err(LibraryError::unexpected_crypto_error)?;
800
801 let encrypted_secrets = diff.encrypt_group_secrets(
804 &joiner_secret,
805 apply_proposals_values.invitation_list,
806 path_computation_result.plain_path.as_deref(),
807 &apply_proposals_values.presharedkeys,
808 &encrypted_group_info,
809 crypto,
810 own_leaf_index,
811 )?;
812
813 let welcome =
815 Welcome::new(ciphersuite, encrypted_secrets, encrypted_group_info);
816 Ok(welcome)
817 })
818 .transpose()?;
819
820 let exported_group_info = create_group_info
823 .then(|| -> Result<_, CreateCommitError> {
824 let external_pub = provisional_epoch_secrets
825 .external_secret()
826 .derive_external_keypair(crypto, ciphersuite)
827 .map_err(LibraryError::unexpected_crypto_error)?
828 .public;
829
830 let external_pub_extension =
831 Extension::ExternalPub(ExternalPubExtension::new(external_pub.into()));
832 extensions.add(external_pub_extension)?;
833 let group_info_tbs = {
834 GroupInfoTBS::new(
835 diff.group_context().clone(),
836 extensions,
837 confirmation_tag.clone(),
838 own_leaf_index,
839 )?
840 };
841 Ok(group_info_tbs.sign(old_signer)?)
843 })
844 .transpose()?;
845
846 (welcome_option, exported_group_info)
847 };
848
849 let (provisional_group_epoch_secrets, provisional_message_secrets) =
850 provisional_epoch_secrets.split_secrets(
851 serialized_provisional_group_context,
852 diff.tree_size(),
853 own_leaf_index,
854 );
855
856 #[cfg(feature = "extensions-draft-08")]
857 let application_export_tree = ApplicationExportTree::new(application_exporter);
858 let staged_commit_state = MemberStagedCommitState::new(
859 provisional_group_epoch_secrets,
860 provisional_message_secrets,
861 diff.into_staged_diff(crypto, ciphersuite)?,
862 path_computation_result.new_keypairs,
863 None,
866 update_path_leaf_node,
867 #[cfg(feature = "extensions-draft-08")]
868 application_export_tree,
869 );
870 let staged_commit = StagedCommit::new(
871 proposal_queue,
872 StagedCommitState::GroupMember(Box::new(staged_commit_state)),
873 );
874
875 Ok(builder.into_stage(Complete {
876 result: CreateCommitResult {
877 commit: authenticated_content,
878 welcome_option,
879 staged_commit,
880 group_info: group_info.filter(|_| create_group_info),
881 },
882 original_wire_format_policy: cur_stage
883 .external_commit_info
884 .as_ref()
885 .map(|info| info.wire_format_policy),
886 }))
887 }
888
889 #[cfg(feature = "extensions-draft-08")]
894 pub fn app_data_dictionary_updater(&self) -> AppDataDictionaryUpdater<'_> {
895 AppDataDictionaryUpdater::new(self.group.borrow().context().app_data_dict())
896 }
897
898 #[cfg(feature = "extensions-draft-08")]
900 pub fn with_app_data_dictionary_updates(
901 &mut self,
902 app_data_dictionary_updates: Option<AppDataUpdates>,
903 ) {
904 self.stage.app_data_dictionary_updates = app_data_dictionary_updates;
905 }
906
907 #[cfg(feature = "extensions-draft-08")]
909 pub fn app_data_update_proposals(&self) -> impl Iterator<Item = &AppDataUpdateProposal> {
910 let proposal_store_proposals = self
911 .group
912 .borrow()
913 .proposal_store()
914 .proposals()
915 .map(|queued_proposal| queued_proposal.proposal());
916
917 let all_proposals = proposal_store_proposals.chain(self.stage.own_proposals.iter());
919
920 let mut app_data_update_proposals: Vec<&AppDataUpdateProposal> = all_proposals
922 .filter_map(|proposal| match proposal {
923 Proposal::AppDataUpdate(proposal) => Some(proposal.as_ref()),
924 _ => None,
925 })
926 .collect();
927
928 app_data_update_proposals.sort_by_key(|prop| prop.component_id());
929 app_data_update_proposals.into_iter()
930 }
931}
932
933impl CommitBuilder<'_, Complete, &mut MlsGroup> {
935 #[cfg(test)]
936 pub(crate) fn commit_result(self) -> CreateCommitResult {
937 self.stage.result
938 }
939
940 pub fn stage_commit<Provider: OpenMlsProvider>(
942 self,
943 provider: &Provider,
944 ) -> Result<CommitMessageBundle, CommitBuilderStageError<Provider::StorageError>> {
945 let Self {
946 group,
947 stage:
948 Complete {
949 result: create_commit_result,
950 original_wire_format_policy: _,
951 },
952 ..
953 } = self;
954
955 group.group_state = MlsGroupState::PendingCommit(Box::new(PendingCommitState::Member(
958 create_commit_result.staged_commit,
959 )));
960
961 provider
962 .storage()
963 .write_group_state(group.group_id(), &group.group_state)
964 .map_err(CommitBuilderStageError::KeyStoreError)?;
965
966 group.reset_aad();
967
968 let mls_message = group.content_to_mls_message(create_commit_result.commit, provider)?;
974
975 Ok(CommitMessageBundle {
976 version: group.version(),
977 commit: mls_message,
978 welcome: create_commit_result.welcome_option,
979 group_info: create_commit_result.group_info,
980 })
981 }
982}
983
984#[derive(Debug, Clone)]
987pub struct CommitMessageBundle {
988 version: ProtocolVersion,
989 commit: MlsMessageOut,
990 welcome: Option<Welcome>,
991 group_info: Option<GroupInfo>,
992}
993
994pub struct WelcomeCommitMessages {
999 pub commit: MlsMessageOut,
1001
1002 pub welcome: MlsMessageOut,
1004
1005 pub group_info: Option<MlsMessageOut>,
1007}
1008
1009impl TryFrom<CommitMessageBundle> for WelcomeCommitMessages {
1010 type Error = LibraryError;
1011
1012 fn try_from(value: CommitMessageBundle) -> Result<Self, Self::Error> {
1013 let (commit, welcome_opt, group_info) = value.into_messages();
1014 Ok(Self {
1015 commit,
1016 welcome: welcome_opt.ok_or(LibraryError::custom(
1017 "WelcomeCommitMessages must only be used with commits that produce a welcome.",
1018 ))?,
1019 group_info,
1020 })
1021 }
1022}
1023
1024#[cfg(test)]
1025impl CommitMessageBundle {
1026 pub fn new(
1027 version: ProtocolVersion,
1028 commit: MlsMessageOut,
1029 welcome: Option<Welcome>,
1030 group_info: Option<GroupInfo>,
1031 ) -> Self {
1032 Self {
1033 version,
1034 commit,
1035 welcome,
1036 group_info,
1037 }
1038 }
1039}
1040
1041impl CommitMessageBundle {
1042 pub fn commit(&self) -> &MlsMessageOut {
1046 &self.commit
1047 }
1048
1049 pub fn welcome(&self) -> Option<&Welcome> {
1052 self.welcome.as_ref()
1053 }
1054
1055 pub fn to_welcome_msg(&self) -> Option<MlsMessageOut> {
1058 self.welcome
1059 .as_ref()
1060 .map(|welcome| MlsMessageOut::from_welcome(welcome.clone(), self.version))
1061 }
1062
1063 pub fn group_info(&self) -> Option<&GroupInfo> {
1067 self.group_info.as_ref()
1068 }
1069
1070 pub fn contents(&self) -> (&MlsMessageOut, Option<&Welcome>, Option<&GroupInfo>) {
1073 (
1074 &self.commit,
1075 self.welcome.as_ref(),
1076 self.group_info.as_ref(),
1077 )
1078 }
1079
1080 pub fn into_commit(self) -> MlsMessageOut {
1084 self.commit
1085 }
1086
1087 pub fn into_welcome(self) -> Option<Welcome> {
1091 self.welcome
1092 }
1093
1094 pub fn into_welcome_msg(self) -> Option<MlsMessageOut> {
1097 self.welcome
1098 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version))
1099 }
1100
1101 pub fn into_group_info(self) -> Option<GroupInfo> {
1106 self.group_info
1107 }
1108
1109 pub fn into_group_info_msg(self) -> Option<MlsMessageOut> {
1111 self.group_info.map(|group_info| group_info.into())
1112 }
1113
1114 pub fn into_contents(self) -> (MlsMessageOut, Option<Welcome>, Option<GroupInfo>) {
1117 (self.commit, self.welcome, self.group_info)
1118 }
1119
1120 pub fn into_messages(self) -> (MlsMessageOut, Option<MlsMessageOut>, Option<MlsMessageOut>) {
1123 (
1124 self.commit,
1125 self.welcome
1126 .map(|welcome| MlsMessageOut::from_welcome(welcome, self.version)),
1127 self.group_info.map(|group_info| group_info.into()),
1128 )
1129 }
1130}
1131
1132impl IntoIterator for CommitMessageBundle {
1133 type Item = MlsMessageOut;
1134
1135 type IntoIter = core::iter::Chain<
1136 core::iter::Chain<
1137 core::option::IntoIter<MlsMessageOut>,
1138 core::option::IntoIter<MlsMessageOut>,
1139 >,
1140 core::option::IntoIter<MlsMessageOut>,
1141 >;
1142
1143 fn into_iter(self) -> Self::IntoIter {
1144 let welcome = self.to_welcome_msg();
1145 let group_info = self.group_info.map(|group_info| group_info.into());
1146
1147 Some(self.commit)
1148 .into_iter()
1149 .chain(welcome)
1150 .chain(group_info)
1151 }
1152}