1use openmls_traits::{signatures::Signer, storage::StorageProvider as _, types::Ciphersuite};
2
3use super::{
4 errors::{ProposalError, ProposeAddMemberError, ProposeRemoveMemberError, RemoveProposalError},
5 AddProposal, CreateGroupContextExtProposalError, CustomProposal, FramingParameters, MlsGroup,
6 PreSharedKeyProposal, Proposal, QueuedProposal, RemoveProposal, UpdateProposal, WireFormat,
7};
8use crate::{
9 binary_tree::LeafNodeIndex,
10 ciphersuite::hash_ref::ProposalRef,
11 credentials::Credential,
12 error::LibraryError,
13 extensions::Extensions,
14 framing::{mls_auth_content::AuthenticatedContent, MlsMessageOut},
15 group::{errors::CreateAddProposalError, GroupContext, GroupId, ValidationError},
16 key_packages::KeyPackage,
17 messages::{group_info::GroupInfo, proposals::ProposalOrRefType},
18 schedule::PreSharedKeyId,
19 storage::{OpenMlsProvider, StorageProvider},
20 treesync::{LeafNode, LeafNodeParameters},
21 versions::ProtocolVersion,
22};
23
24#[cfg(feature = "extensions-draft-08")]
25use crate::{
26 component::ComponentId,
27 messages::proposals::{AppDataUpdateOperation, AppDataUpdateProposal},
28};
29
30#[derive(Debug, PartialEq, Clone)]
32pub enum Propose {
33 Add(KeyPackage),
35
36 Update(LeafNodeParameters),
38
39 Remove(u32),
41
42 RemoveCredential(Credential),
44
45 PreSharedKey(PreSharedKeyId),
47
48 ReInit {
50 group_id: GroupId,
51 version: ProtocolVersion,
52 ciphersuite: Ciphersuite,
53 extensions: Extensions<GroupContext>,
54 },
55
56 ExternalInit(Vec<u8>),
58
59 GroupContextExtensions(Extensions<GroupContext>),
61
62 #[cfg(feature = "extensions-draft-08")]
63 UpdateAppDataComponent {
65 component_id: ComponentId,
67 update: Vec<u8>,
69 },
70 #[cfg(feature = "extensions-draft-08")]
71 RemoveAppDataComponent {
73 component_id: ComponentId,
75 },
76
77 Custom(CustomProposal),
79}
80
81macro_rules! impl_propose_fun {
82 ($name:ident, $value_ty:ty, $group_fun:ident, $ref_or_value:expr) => {
83 pub fn $name<Provider: OpenMlsProvider>(
88 &mut self,
89 provider: &Provider,
90 signer: &impl Signer,
91 value: $value_ty,
92 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
93 self.is_operational()?;
94
95 let proposal = self.$group_fun(self.framing_parameters(), value, signer)?;
96
97 let queued_proposal = QueuedProposal::from_authenticated_content(
98 self.ciphersuite(),
99 provider.crypto(),
100 proposal.clone(),
101 $ref_or_value,
102 )?;
103 let proposal_ref = queued_proposal.proposal_reference();
104
105 log::trace!("Storing proposal in queue {:?}", queued_proposal);
106 provider
107 .storage()
108 .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
109 .map_err(ProposalError::StorageError)?;
110 self.proposal_store_mut().add(queued_proposal);
111
112 let mls_message = self.content_to_mls_message(proposal, provider)?;
113
114 self.reset_aad();
115 Ok((mls_message, proposal_ref))
116 }
117 };
118}
119
120impl MlsGroup {
121 impl_propose_fun!(
122 propose_add_member_by_value,
123 KeyPackage,
124 create_add_proposal,
125 ProposalOrRefType::Proposal
126 );
127
128 impl_propose_fun!(
129 propose_remove_member_by_value,
130 LeafNodeIndex,
131 create_remove_proposal,
132 ProposalOrRefType::Proposal
133 );
134
135 impl_propose_fun!(
136 propose_external_psk,
137 PreSharedKeyId,
138 create_presharedkey_proposal,
139 ProposalOrRefType::Reference
140 );
141
142 impl_propose_fun!(
143 propose_external_psk_by_value,
144 PreSharedKeyId,
145 create_presharedkey_proposal,
146 ProposalOrRefType::Proposal
147 );
148
149 impl_propose_fun!(
150 propose_custom_proposal_by_value,
151 CustomProposal,
152 create_custom_proposal,
153 ProposalOrRefType::Proposal
154 );
155
156 impl_propose_fun!(
157 propose_custom_proposal_by_reference,
158 CustomProposal,
159 create_custom_proposal,
160 ProposalOrRefType::Reference
161 );
162
163 pub fn propose<Provider: OpenMlsProvider>(
165 &mut self,
166 provider: &Provider,
167 signer: &impl Signer,
168 propose: Propose,
169 ref_or_value: ProposalOrRefType,
170 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
171 match propose {
172 Propose::Add(key_package) => match ref_or_value {
173 ProposalOrRefType::Proposal => {
174 self.propose_add_member_by_value(provider, signer, key_package)
175 }
176 ProposalOrRefType::Reference => self
177 .propose_add_member(provider, signer, &key_package)
178 .map_err(|e| e.into()),
179 },
180
181 Propose::Update(leaf_node_parameters) => match ref_or_value {
182 ProposalOrRefType::Proposal => self
183 .propose_self_update(provider, signer, leaf_node_parameters)
184 .map_err(|e| e.into()),
185 ProposalOrRefType::Reference => self
186 .propose_self_update(provider, signer, leaf_node_parameters)
187 .map_err(|e| e.into()),
188 },
189
190 Propose::Remove(leaf_index) => match ref_or_value {
191 ProposalOrRefType::Proposal => self.propose_remove_member_by_value(
192 provider,
193 signer,
194 LeafNodeIndex::new(leaf_index),
195 ),
196 ProposalOrRefType::Reference => self
197 .propose_remove_member(provider, signer, LeafNodeIndex::new(leaf_index))
198 .map_err(|e| e.into()),
199 },
200
201 Propose::RemoveCredential(credential) => match ref_or_value {
202 ProposalOrRefType::Proposal => {
203 self.propose_remove_member_by_credential_by_value(provider, signer, &credential)
204 }
205 ProposalOrRefType::Reference => self
206 .propose_remove_member_by_credential(provider, signer, &credential)
207 .map_err(|e| e.into()),
208 },
209 Propose::PreSharedKey(psk_id) => match psk_id.psk() {
210 crate::schedule::Psk::External(_) => match ref_or_value {
211 ProposalOrRefType::Proposal => {
212 self.propose_external_psk_by_value(provider, signer, psk_id)
213 }
214 ProposalOrRefType::Reference => {
215 self.propose_external_psk(provider, signer, psk_id)
216 }
217 },
218 crate::schedule::Psk::Resumption(_) => Err(ProposalError::LibraryError(
219 LibraryError::custom("Invalid PSk argument"),
220 )),
221 },
222 Propose::ReInit {
223 group_id: _,
224 version: _,
225 ciphersuite: _,
226 extensions: _,
227 } => Err(ProposalError::LibraryError(LibraryError::custom(
228 "Unsupported proposal type ReInit",
229 ))),
230 Propose::ExternalInit(_) => Err(ProposalError::LibraryError(LibraryError::custom(
231 "Unsupported proposal type ExternalInit",
232 ))),
233 Propose::GroupContextExtensions(_) => Err(ProposalError::LibraryError(
234 LibraryError::custom("Unsupported proposal type GroupContextExtensions"),
235 )),
236 #[cfg(feature = "extensions-draft-08")]
238 Propose::UpdateAppDataComponent {
239 component_id,
240 update,
241 } => self.propose_app_data_update(
242 provider,
243 signer,
244 component_id,
245 AppDataUpdateOperation::Update(update.into()),
246 ),
247 #[cfg(feature = "extensions-draft-08")]
248 Propose::RemoveAppDataComponent { component_id } => self.propose_app_data_update(
249 provider,
250 signer,
251 component_id,
252 AppDataUpdateOperation::Remove,
253 ),
254
255 Propose::Custom(custom_proposal) => match ref_or_value {
257 ProposalOrRefType::Proposal => {
258 self.propose_custom_proposal_by_value(provider, signer, custom_proposal)
259 }
260 ProposalOrRefType::Reference => {
261 self.propose_custom_proposal_by_reference(provider, signer, custom_proposal)
262 }
263 },
264 }
265 }
266
267 pub fn propose_add_member<Provider: OpenMlsProvider>(
271 &mut self,
272 provider: &Provider,
273 signer: &impl Signer,
274 key_package: &KeyPackage,
275 ) -> Result<(MlsMessageOut, ProposalRef), ProposeAddMemberError<Provider::StorageError>> {
276 self.is_operational()?;
277
278 let add_proposal = self
279 .create_add_proposal(self.framing_parameters(), key_package.clone(), signer)
280 .map_err(|e| match e {
281 CreateAddProposalError::LibraryError(e) => e.into(),
282 CreateAddProposalError::LeafNodeValidation(error) => {
283 ProposeAddMemberError::LeafNodeValidation(error)
284 }
285 })?;
286
287 let proposal = QueuedProposal::from_authenticated_content_by_ref(
288 self.ciphersuite(),
289 provider.crypto(),
290 add_proposal.clone(),
291 )?;
292 let proposal_ref = proposal.proposal_reference();
293 provider
294 .storage()
295 .queue_proposal(self.group_id(), &proposal_ref, &proposal)
296 .map_err(ProposeAddMemberError::StorageError)?;
297 self.proposal_store_mut().add(proposal);
298
299 let mls_message = self.content_to_mls_message(add_proposal, provider)?;
300
301 self.reset_aad();
302 Ok((mls_message, proposal_ref))
303 }
304
305 pub fn propose_remove_member<Provider: OpenMlsProvider>(
310 &mut self,
311 provider: &Provider,
312 signer: &impl Signer,
313 member: LeafNodeIndex,
314 ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
315 {
316 self.is_operational()?;
317
318 let remove_proposal = self
319 .create_remove_proposal(self.framing_parameters(), member, signer)
320 .map_err(|_| ProposeRemoveMemberError::UnknownMember)?;
321
322 let proposal = QueuedProposal::from_authenticated_content_by_ref(
323 self.ciphersuite(),
324 provider.crypto(),
325 remove_proposal.clone(),
326 )?;
327 let proposal_ref = proposal.proposal_reference();
328 provider
329 .storage()
330 .queue_proposal(self.group_id(), &proposal_ref, &proposal)
331 .map_err(ProposeRemoveMemberError::StorageError)?;
332 self.proposal_store_mut().add(proposal);
333
334 let mls_message = self.content_to_mls_message(remove_proposal, provider)?;
335
336 self.reset_aad();
337 Ok((mls_message, proposal_ref))
338 }
339
340 pub fn propose_remove_member_by_credential<Provider: OpenMlsProvider>(
345 &mut self,
346 provider: &Provider,
347 signer: &impl Signer,
348 member: &Credential,
349 ) -> Result<(MlsMessageOut, ProposalRef), ProposeRemoveMemberError<Provider::StorageError>>
350 {
351 let member_index = self
353 .public_group()
354 .members()
355 .find(|m| &m.credential == member)
356 .map(|m| m.index);
357
358 if let Some(member_index) = member_index {
359 self.propose_remove_member(provider, signer, member_index)
360 } else {
361 Err(ProposeRemoveMemberError::UnknownMember)
362 }
363 }
364
365 pub fn propose_remove_member_by_credential_by_value<Provider: OpenMlsProvider>(
370 &mut self,
371 provider: &Provider,
372 signer: &impl Signer,
373 member: &Credential,
374 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
375 let member_index = self
377 .public_group()
378 .members()
379 .find(|m| &m.credential == member)
380 .map(|m| m.index);
381
382 if let Some(member_index) = member_index {
383 self.propose_remove_member_by_value(provider, signer, member_index)
384 } else {
385 Err(ProposalError::ProposeRemoveMemberError(
386 ProposeRemoveMemberError::UnknownMember,
387 ))
388 }
389 }
390
391 pub fn propose_group_context_extensions<Provider: OpenMlsProvider>(
396 &mut self,
397 provider: &Provider,
398 extensions: Extensions<GroupContext>,
399 signer: &impl Signer,
400 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
401 self.is_operational()?;
402
403 let proposal = self.create_group_context_ext_proposal::<Provider>(
404 self.framing_parameters(),
405 extensions,
406 signer,
407 )?;
408
409 let queued_proposal = QueuedProposal::from_authenticated_content_by_ref(
410 self.ciphersuite(),
411 provider.crypto(),
412 proposal.clone(),
413 )?;
414
415 let proposal_ref = queued_proposal.proposal_reference();
416 provider
417 .storage()
418 .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
419 .map_err(ProposalError::StorageError)?;
420 self.proposal_store_mut().add(queued_proposal);
421
422 let mls_message = self.content_to_mls_message(proposal, provider)?;
423
424 self.reset_aad();
425 Ok((mls_message, proposal_ref))
426 }
427
428 #[allow(clippy::type_complexity)]
436 pub fn update_group_context_extensions<Provider: OpenMlsProvider>(
437 &mut self,
438 provider: &Provider,
439 extensions: Extensions<GroupContext>,
440 signer: &impl Signer,
441 ) -> Result<
442 (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
443 CreateGroupContextExtProposalError<Provider::StorageError>,
444 > {
445 self.is_operational()?;
446
447 let bundle = self
449 .commit_builder()
450 .propose_group_context_extensions(extensions)?
451 .load_psks(provider.storage())?
452 .build(provider.rand(), provider.crypto(), signer, |_| true)?
453 .stage_commit(provider)?;
454
455 let (commit, welcome, group_info) = bundle.into_contents();
457 let welcome = welcome.map(|welcome| MlsMessageOut::from_welcome(welcome, self.version()));
458
459 Ok((commit, welcome, group_info))
460 }
461
462 #[cfg(feature = "extensions-draft-08")]
464 pub fn propose_app_data_update<Provider: OpenMlsProvider>(
465 &mut self,
466 provider: &Provider,
467 signer: &impl Signer,
468 component_id: ComponentId,
469 operation: AppDataUpdateOperation,
470 ) -> Result<(MlsMessageOut, ProposalRef), ProposalError<Provider::StorageError>> {
471 self.is_operational()?;
472
473 let proposal = self.create_app_data_update_proposal(
474 self.framing_parameters(),
475 component_id,
476 operation,
477 signer,
478 )?;
479
480 let queued_proposal = QueuedProposal::from_authenticated_content(
481 self.ciphersuite(),
482 provider.crypto(),
483 proposal.clone(),
484 ProposalOrRefType::Proposal,
485 )?;
486 let proposal_ref = queued_proposal.proposal_reference();
487
488 log::trace!("Storing proposal in queue {:?}", queued_proposal);
489 provider
490 .storage()
491 .queue_proposal(self.group_id(), &proposal_ref, &queued_proposal)
492 .map_err(ProposalError::StorageError)?;
493 self.proposal_store_mut().add(queued_proposal);
494
495 let mls_message = self.content_to_mls_message(proposal, provider)?;
496
497 self.reset_aad();
498 Ok((mls_message, proposal_ref))
499 }
500
501 pub fn remove_pending_proposal<Storage: StorageProvider>(
503 &mut self,
504 storage: &Storage,
505 proposal_ref: &ProposalRef,
506 ) -> Result<(), RemoveProposalError<Storage::Error>> {
507 storage
508 .remove_proposal(self.group_id(), proposal_ref)
509 .map_err(RemoveProposalError::Storage)?;
510 self.proposal_store_mut()
511 .remove(proposal_ref)
512 .ok_or(RemoveProposalError::ProposalNotFound)
513 }
514
515 pub(crate) fn create_add_proposal(
522 &self,
523 framing_parameters: FramingParameters,
524 joiner_key_package: KeyPackage,
525 signer: &impl Signer,
526 ) -> Result<AuthenticatedContent, CreateAddProposalError> {
527 if let Some(required_capabilities) = self.required_capabilities() {
528 joiner_key_package
529 .leaf_node()
530 .capabilities()
531 .supports_required_capabilities(required_capabilities)?;
532 }
533 let add_proposal = AddProposal {
534 key_package: joiner_key_package,
535 };
536 let proposal = Proposal::add(add_proposal);
537 AuthenticatedContent::member_proposal(
538 framing_parameters,
539 self.own_leaf_index(),
540 proposal,
541 self.context(),
542 signer,
543 )
544 .map_err(|e| e.into())
545 }
546
547 pub(crate) fn create_update_proposal(
552 &self,
553 framing_parameters: FramingParameters,
554 leaf_node: LeafNode,
557 signer: &impl Signer,
558 ) -> Result<AuthenticatedContent, LibraryError> {
559 let update_proposal = UpdateProposal { leaf_node };
560 let proposal = Proposal::update(update_proposal);
561 AuthenticatedContent::member_proposal(
562 framing_parameters,
563 self.own_leaf_index(),
564 proposal,
565 self.context(),
566 signer,
567 )
568 }
569
570 pub(crate) fn create_remove_proposal(
575 &self,
576 framing_parameters: FramingParameters,
577 removed: LeafNodeIndex,
578 signer: &impl Signer,
579 ) -> Result<AuthenticatedContent, ValidationError> {
580 if self.public_group().leaf(removed).is_none() {
581 return Err(ValidationError::UnknownMember);
582 }
583 let remove_proposal = RemoveProposal { removed };
584 let proposal = Proposal::remove(remove_proposal);
585 AuthenticatedContent::member_proposal(
586 framing_parameters,
587 self.own_leaf_index(),
588 proposal,
589 self.context(),
590 signer,
591 )
592 .map_err(ValidationError::LibraryError)
593 }
594
595 pub(crate) fn create_self_remove_proposal(
598 &self,
599 aad: &[u8],
600 signer: &impl Signer,
601 ) -> Result<AuthenticatedContent, LibraryError> {
602 let proposal = Proposal::SelfRemove;
603 let framing_parameters = FramingParameters::new(aad, WireFormat::PublicMessage);
604 AuthenticatedContent::member_proposal(
605 framing_parameters,
606 self.own_leaf_index(),
607 proposal,
608 self.context(),
609 signer,
610 )
611 }
612
613 pub(crate) fn create_presharedkey_proposal(
619 &self,
620 framing_parameters: FramingParameters,
621 psk: PreSharedKeyId,
622 signer: &impl Signer,
623 ) -> Result<AuthenticatedContent, LibraryError> {
624 let presharedkey_proposal = PreSharedKeyProposal::new(psk);
625 let proposal = Proposal::psk(presharedkey_proposal);
626 AuthenticatedContent::member_proposal(
627 framing_parameters,
628 self.own_leaf_index(),
629 proposal,
630 self.context(),
631 signer,
632 )
633 }
634
635 #[cfg(feature = "extensions-draft-08")]
636 pub(crate) fn create_app_data_update_proposal(
637 &self,
638 framing_parameters: FramingParameters,
639 component_id: ComponentId,
640 operation: AppDataUpdateOperation,
641 signer: &impl Signer,
642 ) -> Result<AuthenticatedContent, LibraryError> {
643 let proposal = Proposal::AppDataUpdate(Box::new(AppDataUpdateProposal::new(
644 component_id,
645 operation,
646 )));
647 AuthenticatedContent::member_proposal(
648 framing_parameters,
649 self.own_leaf_index(),
650 proposal,
651 self.context(),
652 signer,
653 )
654 }
655
656 pub(crate) fn create_custom_proposal(
657 &self,
658 framing_parameters: FramingParameters,
659 custom_proposal: CustomProposal,
660 signer: &impl Signer,
661 ) -> Result<AuthenticatedContent, LibraryError> {
662 let proposal = Proposal::custom(custom_proposal);
663 AuthenticatedContent::member_proposal(
664 framing_parameters,
665 self.own_leaf_index(),
666 proposal,
667 self.context(),
668 signer,
669 )
670 }
671}