1use openmls::prelude::{LeafNode, MlsMessageOut};
9
10use mls_crypto_provider::MlsCryptoProvider;
11
12use super::{Error, Result};
13use crate::{
14 LeafError, RecursiveError,
15 e2e_identity::init_certificates::NewCrlDistributionPoint,
16 mls::{MlsConversation, credential::CredentialBundle},
17 prelude::{Client, MlsError, MlsGroupInfoBundle},
18};
19
20impl MlsConversation {
21 #[cfg_attr(test, crate::durable)]
23 pub(crate) async fn update_keying_material(
24 &mut self,
25 client: &Client,
26 backend: &MlsCryptoProvider,
27 cb: Option<&CredentialBundle>,
28 leaf_node: Option<LeafNode>,
29 ) -> Result<MlsCommitBundle> {
30 let cb = match cb {
31 None => &self
32 .find_most_recent_credential_bundle(client)
33 .await
34 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?,
35 Some(cb) => cb,
36 };
37 let (commit, welcome, group_info) = self
38 .group
39 .explicit_self_update(backend, &cb.signature_key, leaf_node)
40 .await
41 .map_err(MlsError::wrap("group self update"))?;
42
43 let group_info = group_info.ok_or(LeafError::MissingGroupInfo)?;
45 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(group_info)?;
46
47 self.persist_group_when_changed(&backend.keystore(), false).await?;
48
49 Ok(MlsCommitBundle {
50 welcome,
51 commit,
52 group_info,
53 })
54 }
55
56 #[cfg_attr(test, crate::durable)]
58 pub(crate) async fn commit_pending_proposals(
59 &mut self,
60 client: &Client,
61 backend: &MlsCryptoProvider,
62 ) -> Result<Option<MlsCommitBundle>> {
63 if self.group.pending_proposals().count() == 0 {
64 return Ok(None);
65 }
66 let signer = &self
67 .find_most_recent_credential_bundle(client)
68 .await
69 .map_err(RecursiveError::mls_client("finding most recent credential bundle"))?
70 .signature_key;
71
72 let (commit, welcome, gi) = self
73 .group
74 .commit_to_pending_proposals(backend, signer)
75 .await
76 .map_err(MlsError::wrap("group commit to pending proposals"))?;
77 let group_info = MlsGroupInfoBundle::try_new_full_plaintext(gi.unwrap())?;
78
79 self.persist_group_when_changed(&backend.keystore(), false).await?;
80
81 Ok(Some(MlsCommitBundle {
82 welcome,
83 commit,
84 group_info,
85 }))
86 }
87}
88
89#[derive(Debug)]
92pub struct MlsConversationCreationMessage {
93 pub welcome: MlsMessageOut,
95 pub commit: MlsMessageOut,
97 pub group_info: MlsGroupInfoBundle,
99 pub crl_new_distribution_points: NewCrlDistributionPoint,
101}
102
103impl MlsConversationCreationMessage {
104 #[allow(clippy::type_complexity)]
109 pub fn to_bytes(self) -> Result<(Vec<u8>, Vec<u8>, MlsGroupInfoBundle, NewCrlDistributionPoint)> {
110 use openmls::prelude::TlsSerializeTrait as _;
111 let welcome = self
112 .welcome
113 .tls_serialize_detached()
114 .map_err(Error::tls_serialize("serialize welcome"))?;
115 let msg = self
116 .commit
117 .tls_serialize_detached()
118 .map_err(Error::tls_serialize("serialize commit"))?;
119 Ok((welcome, msg, self.group_info, self.crl_new_distribution_points))
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct MlsCommitBundle {
126 pub welcome: Option<MlsMessageOut>,
128 pub commit: MlsMessageOut,
130 pub group_info: MlsGroupInfoBundle,
132}
133
134impl MlsCommitBundle {
135 #[allow(clippy::type_complexity)]
140 pub fn to_bytes_triple(self) -> Result<(Option<Vec<u8>>, Vec<u8>, MlsGroupInfoBundle)> {
141 use openmls::prelude::TlsSerializeTrait as _;
142 let welcome = self
143 .welcome
144 .as_ref()
145 .map(|w| {
146 w.tls_serialize_detached()
147 .map_err(Error::tls_serialize("serialize welcome"))
148 })
149 .transpose()?;
150 let commit = self
151 .commit
152 .tls_serialize_detached()
153 .map_err(Error::tls_serialize("serialize commit"))?;
154 Ok((welcome, commit, self.group_info))
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use itertools::Itertools;
161 use openmls::prelude::SignaturePublicKey;
162 use wasm_bindgen_test::*;
163
164 use crate::test_utils::*;
165
166 use super::{Error, *};
167
168 wasm_bindgen_test_configure!(run_in_browser);
169
170 mod transport {
171 use super::*;
172 use std::sync::Arc;
173
174 #[apply(all_cred_cipher)]
175 #[wasm_bindgen_test]
176 async fn retry_should_work(case: TestCase) {
177 run_test_with_client_ids(
178 case.clone(),
179 ["alice", "bob", "charlie"],
180 move |[alice_central, bob_central, charlie_central]| {
181 Box::pin(async move {
182 let id = conversation_id();
184 alice_central
185 .context
186 .new_conversation(&id, case.credential_type, case.cfg.clone())
187 .await
188 .unwrap();
189 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
191
192 bob_central
194 .context
195 .conversation(&id)
196 .await
197 .unwrap()
198 .update_key_material()
199 .await
200 .unwrap();
201 let bob_epoch = bob_central.get_conversation_unchecked(&id).await.group.epoch().as_u64();
202 assert_eq!(2, bob_epoch);
203 let alice_epoch = alice_central
204 .get_conversation_unchecked(&id)
205 .await
206 .group
207 .epoch()
208 .as_u64();
209 assert_eq!(1, alice_epoch);
210 let intermediate_commit = bob_central.mls_transport.latest_commit().await;
211
212 let retry_provider = Arc::new(
214 CoreCryptoTransportRetrySuccessProvider::default().with_intermediate_commits(
215 alice_central.clone(),
216 &[intermediate_commit],
217 &id,
218 ),
219 );
220
221 alice_central
222 .context
223 .set_transport_callbacks(Some(retry_provider.clone()))
224 .await
225 .unwrap();
226
227 alice_central
229 .context
230 .conversation(&id)
231 .await
232 .unwrap()
233 .update_key_material()
234 .await
235 .unwrap();
236 let commit = retry_provider.latest_commit().await;
237 bob_central
238 .context
239 .conversation(&id)
240 .await
241 .unwrap()
242 .decrypt_message(&commit.to_bytes().unwrap())
243 .await
244 .unwrap();
245
246 alice_central
249 .context
250 .conversation(&id)
251 .await
252 .unwrap()
253 .add_members(vec![charlie_central.rand_key_package(&case).await])
254 .await
255 .unwrap();
256 let commit = retry_provider.latest_commit().await;
257 bob_central
258 .context
259 .conversation(&id)
260 .await
261 .unwrap()
262 .decrypt_message(&commit.to_bytes().unwrap())
263 .await
264 .unwrap();
265
266 assert_eq!(retry_provider.retry_count().await, 2);
268 assert_eq!(retry_provider.success_count().await, 2);
270
271 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
273 })
274 },
275 )
276 .await;
277 }
278 }
279
280 mod add_members {
281 use super::*;
282 use std::sync::Arc;
283
284 #[apply(all_cred_cipher)]
285 #[wasm_bindgen_test]
286 async fn can_add_members_to_conversation(case: TestCase) {
287 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
288 Box::pin(async move {
289 let id = conversation_id();
290
291 alice_central
292 .context
293 .new_conversation(&id, case.credential_type, case.cfg.clone())
294 .await
295 .unwrap();
296 let bob = bob_central.rand_key_package(&case).await;
297 alice_central
299 .context
300 .set_transport_callbacks(Some(Arc::<CoreCryptoTransportAbortProvider>::default()))
301 .await
302 .unwrap();
303 alice_central
304 .context
305 .conversation(&id)
306 .await
307 .unwrap()
308 .add_members(vec![bob.clone()])
309 .await
310 .unwrap_err();
311
312 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
314
315 let success_provider = Arc::<CoreCryptoTransportSuccessProvider>::default();
316 alice_central
317 .context
318 .set_transport_callbacks(Some(success_provider.clone()))
319 .await
320 .unwrap();
321 alice_central
322 .context
323 .conversation(&id)
324 .await
325 .unwrap()
326 .add_members(vec![bob])
327 .await
328 .unwrap();
329
330 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
331 assert_eq!(
332 alice_central
333 .get_conversation_unchecked(&id)
334 .await
335 .group
336 .group_id()
337 .as_slice(),
338 id
339 );
340 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
341 let commit = success_provider.latest_commit_bundle().await;
342 bob_central
343 .context
344 .process_welcome_message(commit.welcome.unwrap().into(), case.custom_cfg())
345 .await
346 .unwrap();
347 assert_eq!(
348 alice_central.get_conversation_unchecked(&id).await.id(),
349 bob_central.get_conversation_unchecked(&id).await.id()
350 );
351 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
352 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
353 })
354 })
355 .await
356 }
357
358 #[apply(all_cred_cipher)]
359 #[wasm_bindgen_test]
360 async fn should_return_valid_welcome(case: TestCase) {
361 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
362 Box::pin(async move {
363 let id = conversation_id();
364 alice_central
365 .context
366 .new_conversation(&id, case.credential_type, case.cfg.clone())
367 .await
368 .unwrap();
369
370 let bob = bob_central.rand_key_package(&case).await;
371 alice_central
372 .context
373 .conversation(&id)
374 .await
375 .unwrap()
376 .add_members(vec![bob])
377 .await
378 .unwrap();
379
380 let welcome = alice_central
381 .mls_transport
382 .latest_commit_bundle()
383 .await
384 .welcome
385 .unwrap();
386
387 bob_central
388 .context
389 .process_welcome_message(welcome.into(), case.custom_cfg())
390 .await
391 .unwrap();
392 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
393 })
394 })
395 .await
396 }
397
398 #[apply(all_cred_cipher)]
399 #[wasm_bindgen_test]
400 async fn should_return_valid_group_info(case: TestCase) {
401 run_test_with_client_ids(
402 case.clone(),
403 ["alice", "bob", "guest"],
404 move |[alice_central, bob_central, mut guest_central]| {
405 Box::pin(async move {
406 let id = conversation_id();
407 alice_central
408 .context
409 .new_conversation(&id, case.credential_type, case.cfg.clone())
410 .await
411 .unwrap();
412
413 let bob = bob_central.rand_key_package(&case).await;
414 alice_central
415 .context
416 .conversation(&id)
417 .await
418 .unwrap()
419 .add_members(vec![bob])
420 .await
421 .unwrap();
422 let commit_bundle = alice_central.mls_transport.latest_commit_bundle().await;
423 let group_info = commit_bundle.group_info.get_group_info();
424
425 assert!(
426 guest_central
427 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
428 .await
429 .is_ok()
430 );
431 })
432 },
433 )
434 .await
435 }
436 }
437
438 mod remove_members {
439 use super::*;
440
441 #[apply(all_cred_cipher)]
442 #[wasm_bindgen_test]
443 async fn alice_can_remove_bob_from_conversation(case: TestCase) {
444 use crate::mls;
445
446 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
447 Box::pin(async move {
448 let id = conversation_id();
449
450 alice_central
451 .context
452 .new_conversation(&id, case.credential_type, case.cfg.clone())
453 .await
454 .unwrap();
455 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
456
457 alice_central
458 .context
459 .conversation(&id)
460 .await
461 .unwrap()
462 .remove_members(&[bob_central.get_client_id().await])
463 .await
464 .unwrap();
465 let MlsCommitBundle { commit, welcome, .. } =
466 alice_central.mls_transport.latest_commit_bundle().await;
467 assert!(welcome.is_none());
468
469 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
470
471 bob_central
472 .context
473 .conversation(&id)
474 .await
475 .unwrap()
476 .decrypt_message(commit.to_bytes().unwrap())
477 .await
478 .unwrap();
479
480 assert!(matches!(
482 bob_central.context.conversation(&id).await.unwrap_err(),
483 mls::conversation::error::Error::Leaf(LeafError::ConversationNotFound(conv_id)) if conv_id == id
484 ));
485 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_err());
486 })
487 })
488 .await;
489 }
490
491 #[apply(all_cred_cipher)]
492 #[wasm_bindgen_test]
493 async fn should_return_valid_welcome(case: TestCase) {
494 run_test_with_client_ids(
495 case.clone(),
496 ["alice", "bob", "guest"],
497 move |[alice_central, bob_central, mut guest_central]| {
498 Box::pin(async move {
499 let id = conversation_id();
500
501 alice_central
502 .context
503 .new_conversation(&id, case.credential_type, case.cfg.clone())
504 .await
505 .unwrap();
506 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
507
508 let proposal = alice_central
509 .context
510 .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
511 .await
512 .unwrap();
513 bob_central
514 .context
515 .conversation(&id)
516 .await
517 .unwrap()
518 .decrypt_message(proposal.proposal.to_bytes().unwrap())
519 .await
520 .unwrap();
521
522 alice_central
523 .context
524 .conversation(&id)
525 .await
526 .unwrap()
527 .remove_members(&[bob_central.get_client_id().await])
528 .await
529 .unwrap();
530
531 let welcome = alice_central.mls_transport.latest_welcome_message().await;
532
533 assert!(
534 guest_central
535 .try_join_from_welcome(&id, welcome.into(), case.custom_cfg(), vec![&alice_central])
536 .await
537 .is_ok()
538 );
539 assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
541 })
542 },
543 )
544 .await;
545 }
546
547 #[apply(all_cred_cipher)]
548 #[wasm_bindgen_test]
549 async fn should_return_valid_group_info(case: TestCase) {
550 run_test_with_client_ids(
551 case.clone(),
552 ["alice", "bob", "guest"],
553 move |[alice_central, bob_central, mut guest_central]| {
554 Box::pin(async move {
555 let id = conversation_id();
556
557 alice_central
558 .context
559 .new_conversation(&id, case.credential_type, case.cfg.clone())
560 .await
561 .unwrap();
562 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
563
564 alice_central
565 .context
566 .conversation(&id)
567 .await
568 .unwrap()
569 .remove_members(&[bob_central.get_client_id().await])
570 .await
571 .unwrap();
572
573 let commit_bundle = alice_central.mls_transport.latest_commit_bundle().await;
574
575 let group_info = commit_bundle.group_info.get_group_info();
576
577 assert!(
578 guest_central
579 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
580 .await
581 .is_ok()
582 );
583 assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
585 })
586 },
587 )
588 .await;
589 }
590 }
591
592 mod update_keying_material {
593 use super::*;
594
595 #[apply(all_cred_cipher)]
596 #[wasm_bindgen_test]
597 async fn should_succeed(case: TestCase) {
598 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
599 Box::pin(async move {
600 let id = conversation_id();
601 alice_central
602 .context
603 .new_conversation(&id, case.credential_type, case.cfg.clone())
604 .await
605 .unwrap();
606 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
607
608 let init_count = alice_central.context.count_entities().await;
609
610 let bob_keys = bob_central
611 .get_conversation_unchecked(&id)
612 .await
613 .encryption_keys()
614 .collect::<Vec<Vec<u8>>>();
615 let alice_keys = alice_central
616 .get_conversation_unchecked(&id)
617 .await
618 .encryption_keys()
619 .collect::<Vec<Vec<u8>>>();
620 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
621
622 let alice_key = alice_central
623 .encryption_key_of(&id, alice_central.get_client_id().await)
624 .await;
625
626 alice_central
628 .context
629 .conversation(&id)
630 .await
631 .unwrap()
632 .update_key_material()
633 .await
634 .unwrap();
635 let MlsCommitBundle { commit, welcome, .. } =
636 alice_central.mls_transport.latest_commit_bundle().await;
637 assert!(welcome.is_none());
638
639 assert!(
640 !alice_central
641 .get_conversation_unchecked(&id)
642 .await
643 .encryption_keys()
644 .contains(&alice_key)
645 );
646
647 let alice_new_keys = alice_central
648 .get_conversation_unchecked(&id)
649 .await
650 .encryption_keys()
651 .collect::<Vec<_>>();
652 assert!(!alice_new_keys.contains(&alice_key));
653
654 bob_central
656 .context
657 .conversation(&id)
658 .await
659 .unwrap()
660 .decrypt_message(&commit.to_bytes().unwrap())
661 .await
662 .unwrap();
663
664 let bob_new_keys = bob_central
665 .get_conversation_unchecked(&id)
666 .await
667 .encryption_keys()
668 .collect::<Vec<_>>();
669 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
670
671 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
673
674 let final_count = alice_central.context.count_entities().await;
677 assert_eq!(init_count, final_count);
678 })
679 })
680 .await;
681 }
682
683 #[apply(all_cred_cipher)]
684 #[wasm_bindgen_test]
685 async fn should_create_welcome_for_pending_add_proposals(case: TestCase) {
686 run_test_with_client_ids(
687 case.clone(),
688 ["alice", "bob", "charlie"],
689 move |[alice_central, bob_central, charlie_central]| {
690 Box::pin(async move {
691 let id = conversation_id();
692 alice_central
693 .context
694 .new_conversation(&id, case.credential_type, case.cfg.clone())
695 .await
696 .unwrap();
697 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
698
699 let bob_keys = bob_central
700 .get_conversation_unchecked(&id)
701 .await
702 .signature_keys()
703 .collect::<Vec<SignaturePublicKey>>();
704 let alice_keys = alice_central
705 .get_conversation_unchecked(&id)
706 .await
707 .signature_keys()
708 .collect::<Vec<SignaturePublicKey>>();
709
710 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
712
713 let alice_key = alice_central
714 .encryption_key_of(&id, alice_central.get_client_id().await)
715 .await;
716
717 let charlie_kp = charlie_central.get_one_key_package(&case).await;
719 let add_charlie_proposal =
720 alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
721
722 bob_central
724 .context
725 .conversation(&id)
726 .await
727 .unwrap()
728 .decrypt_message(add_charlie_proposal.proposal.to_bytes().unwrap())
729 .await
730 .unwrap();
731
732 assert!(
733 alice_central
734 .get_conversation_unchecked(&id)
735 .await
736 .encryption_keys()
737 .contains(&alice_key)
738 );
739
740 alice_central
742 .context
743 .conversation(&id)
744 .await
745 .unwrap()
746 .update_key_material()
747 .await
748 .unwrap();
749 let MlsCommitBundle { commit, welcome, .. } =
750 alice_central.mls_transport.latest_commit_bundle().await;
751 assert!(welcome.is_some());
752 assert!(
753 !alice_central
754 .get_conversation_unchecked(&id)
755 .await
756 .encryption_keys()
757 .contains(&alice_key)
758 );
759
760 charlie_central
762 .context
763 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
764 .await
765 .unwrap();
766
767 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
768 assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3);
769 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
771
772 let alice_new_keys = alice_central
773 .get_conversation_unchecked(&id)
774 .await
775 .encryption_keys()
776 .collect::<Vec<Vec<u8>>>();
777 assert!(!alice_new_keys.contains(&alice_key));
778
779 bob_central
781 .context
782 .conversation(&id)
783 .await
784 .unwrap()
785 .decrypt_message(&commit.to_bytes().unwrap())
786 .await
787 .unwrap();
788 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
789
790 let bob_new_keys = bob_central
791 .get_conversation_unchecked(&id)
792 .await
793 .encryption_keys()
794 .collect::<Vec<Vec<u8>>>();
795 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
796
797 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
799 assert!(bob_central.try_talk_to(&id, &charlie_central).await.is_ok());
800 assert!(charlie_central.try_talk_to(&id, &alice_central).await.is_ok());
801 })
802 },
803 )
804 .await;
805 }
806
807 #[apply(all_cred_cipher)]
808 #[wasm_bindgen_test]
809 async fn should_return_valid_welcome(case: TestCase) {
810 run_test_with_client_ids(
811 case.clone(),
812 ["alice", "bob", "guest"],
813 move |[alice_central, bob_central, mut guest_central]| {
814 Box::pin(async move {
815 let id = conversation_id();
816 alice_central
817 .context
818 .new_conversation(&id, case.credential_type, case.cfg.clone())
819 .await
820 .unwrap();
821 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
822
823 let proposal = alice_central
824 .context
825 .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
826 .await
827 .unwrap()
828 .proposal;
829 bob_central
830 .context
831 .conversation(&id)
832 .await
833 .unwrap()
834 .decrypt_message(proposal.to_bytes().unwrap())
835 .await
836 .unwrap();
837
838 alice_central
839 .context
840 .conversation(&id)
841 .await
842 .unwrap()
843 .update_key_material()
844 .await
845 .unwrap();
846 let MlsCommitBundle { commit, welcome, .. } =
847 alice_central.mls_transport.latest_commit_bundle().await;
848
849 bob_central
850 .context
851 .conversation(&id)
852 .await
853 .unwrap()
854 .decrypt_message(commit.to_bytes().unwrap())
855 .await
856 .unwrap();
857
858 assert!(
859 guest_central
860 .try_join_from_welcome(
861 &id,
862 welcome.unwrap().into(),
863 case.custom_cfg(),
864 vec![&alice_central, &bob_central]
865 )
866 .await
867 .is_ok()
868 );
869 })
870 },
871 )
872 .await;
873 }
874
875 #[apply(all_cred_cipher)]
876 #[wasm_bindgen_test]
877 async fn should_return_valid_group_info(case: TestCase) {
878 run_test_with_client_ids(
879 case.clone(),
880 ["alice", "bob", "guest"],
881 move |[alice_central, bob_central, mut guest_central]| {
882 Box::pin(async move {
883 let id = conversation_id();
884 alice_central
885 .context
886 .new_conversation(&id, case.credential_type, case.cfg.clone())
887 .await
888 .unwrap();
889 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
890
891 alice_central
892 .context
893 .conversation(&id)
894 .await
895 .unwrap()
896 .update_key_material()
897 .await
898 .unwrap();
899 let group_info = alice_central.mls_transport.latest_group_info().await;
900 let group_info = group_info.get_group_info();
901
902 assert!(
903 guest_central
904 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
905 .await
906 .is_ok()
907 );
908 })
909 },
910 )
911 .await;
912 }
913 }
914
915 mod commit_pending_proposals {
916 use super::*;
917
918 #[apply(all_cred_cipher)]
919 #[wasm_bindgen_test]
920 async fn should_create_a_commit_out_of_self_pending_proposals(case: TestCase) {
921 run_test_with_client_ids(
922 case.clone(),
923 ["alice", "bob"],
924 move |[mut alice_central, bob_central]| {
925 Box::pin(async move {
926 let id = conversation_id();
927 alice_central
928 .context
929 .new_conversation(&id, case.credential_type, case.cfg.clone())
930 .await
931 .unwrap();
932 alice_central
933 .context
934 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
935 .await
936 .unwrap();
937 assert!(!alice_central.pending_proposals(&id).await.is_empty());
938 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
939 alice_central
940 .context
941 .conversation(&id)
942 .await
943 .unwrap()
944 .commit_pending_proposals()
945 .await
946 .unwrap();
947 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
948
949 let welcome = alice_central.mls_transport.latest_commit_bundle().await.welcome;
950 bob_central
951 .context
952 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
953 .await
954 .unwrap();
955 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
956 })
957 },
958 )
959 .await;
960 }
961
962 #[apply(all_cred_cipher)]
963 #[wasm_bindgen_test]
964 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestCase) {
965 run_test_with_client_ids(
966 case.clone(),
967 ["alice", "bob", "charlie"],
968 move |[alice_central, mut bob_central, charlie_central]| {
969 Box::pin(async move {
970 let id = conversation_id();
971 alice_central
972 .context
973 .new_conversation(&id, case.credential_type, case.cfg.clone())
974 .await
975 .unwrap();
976 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
977 let proposal = bob_central
978 .context
979 .new_add_proposal(&id, charlie_central.get_one_key_package(&case).await)
980 .await
981 .unwrap();
982 assert!(!bob_central.pending_proposals(&id).await.is_empty());
983 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
984 alice_central
985 .context
986 .conversation(&id)
987 .await
988 .unwrap()
989 .decrypt_message(proposal.proposal.to_bytes().unwrap())
990 .await
991 .unwrap();
992
993 alice_central
994 .context
995 .conversation(&id)
996 .await
997 .unwrap()
998 .commit_pending_proposals()
999 .await
1000 .unwrap();
1001 let commit = alice_central.mls_transport.latest_commit_bundle().await.commit;
1002 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
1003
1004 bob_central
1005 .context
1006 .conversation(&id)
1007 .await
1008 .unwrap()
1009 .decrypt_message(commit.to_bytes().unwrap())
1010 .await
1011 .unwrap();
1012 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
1013 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
1014 })
1015 },
1016 )
1017 .await;
1018 }
1019
1020 #[apply(all_cred_cipher)]
1021 #[wasm_bindgen_test]
1022 async fn should_return_valid_welcome(case: TestCase) {
1023 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1024 Box::pin(async move {
1025 let id = conversation_id();
1026 alice_central
1027 .context
1028 .new_conversation(&id, case.credential_type, case.cfg.clone())
1029 .await
1030 .unwrap();
1031 alice_central
1032 .context
1033 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
1034 .await
1035 .unwrap();
1036 alice_central
1037 .context
1038 .conversation(&id)
1039 .await
1040 .unwrap()
1041 .commit_pending_proposals()
1042 .await
1043 .unwrap();
1044
1045 let welcome = alice_central.mls_transport.latest_commit_bundle().await.welcome;
1046
1047 bob_central
1048 .context
1049 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
1050 .await
1051 .unwrap();
1052 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
1053 })
1054 })
1055 .await;
1056 }
1057
1058 #[apply(all_cred_cipher)]
1059 #[wasm_bindgen_test]
1060 async fn should_return_valid_group_info(case: TestCase) {
1061 run_test_with_client_ids(
1062 case.clone(),
1063 ["alice", "bob", "guest"],
1064 move |[alice_central, bob_central, mut guest_central]| {
1065 Box::pin(async move {
1066 let id = conversation_id();
1067 alice_central
1068 .context
1069 .new_conversation(&id, case.credential_type, case.cfg.clone())
1070 .await
1071 .unwrap();
1072 alice_central
1073 .context
1074 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
1075 .await
1076 .unwrap();
1077 alice_central
1078 .context
1079 .conversation(&id)
1080 .await
1081 .unwrap()
1082 .commit_pending_proposals()
1083 .await
1084 .unwrap();
1085 let commit_bundle = alice_central.mls_transport.latest_commit_bundle().await;
1086 let group_info = commit_bundle.group_info.get_group_info();
1087
1088 assert!(
1089 guest_central
1090 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
1091 .await
1092 .is_ok()
1093 );
1094 })
1095 },
1096 )
1097 .await;
1098 }
1099 }
1100
1101 mod delivery_semantics {
1102 use super::*;
1103
1104 #[apply(all_cred_cipher)]
1105 #[wasm_bindgen_test]
1106 async fn should_prevent_out_of_order_commits(case: TestCase) {
1107 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1108 Box::pin(async move {
1109 let id = conversation_id();
1110 alice_central
1111 .context
1112 .new_conversation(&id, case.credential_type, case.cfg.clone())
1113 .await
1114 .unwrap();
1115 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1116
1117 alice_central
1118 .context
1119 .conversation(&id)
1120 .await
1121 .unwrap()
1122 .update_key_material()
1123 .await
1124 .unwrap();
1125 let commit1 = alice_central.mls_transport.latest_commit().await;
1126 let commit1 = commit1.to_bytes().unwrap();
1127 alice_central
1128 .context
1129 .conversation(&id)
1130 .await
1131 .unwrap()
1132 .update_key_material()
1133 .await
1134 .unwrap();
1135 let commit2 = alice_central.mls_transport.latest_commit().await;
1136 let commit2 = commit2.to_bytes().unwrap();
1137
1138 let out_of_order = bob_central
1140 .context
1141 .conversation(&id)
1142 .await
1143 .unwrap()
1144 .decrypt_message(&commit2)
1145 .await;
1146 assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
1147
1148 bob_central
1151 .context
1152 .conversation(&id)
1153 .await
1154 .unwrap()
1155 .decrypt_message(&commit1)
1156 .await
1157 .unwrap();
1158
1159 let past_commit = bob_central
1161 .context
1162 .conversation(&id)
1163 .await
1164 .unwrap()
1165 .decrypt_message(&commit1)
1166 .await;
1167 assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
1168 })
1169 })
1170 .await;
1171 }
1172
1173 #[apply(all_cred_cipher)]
1174 #[wasm_bindgen_test]
1175 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestCase) {
1176 if !case.is_pure_ciphertext() {
1177 return;
1178 }
1179 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1180 Box::pin(async move {
1181 let id = conversation_id();
1182 alice_central
1183 .context
1184 .new_conversation(&id, case.credential_type, case.cfg.clone())
1185 .await
1186 .unwrap();
1187 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1188
1189 let proposal1 = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
1190 let proposal2 = proposal1.clone();
1191 alice_central
1192 .get_conversation_unchecked(&id)
1193 .await
1194 .group
1195 .clear_pending_proposals();
1196
1197 alice_central
1198 .context
1199 .conversation(&id)
1200 .await
1201 .unwrap()
1202 .update_key_material()
1203 .await
1204 .unwrap();
1205 let commit1 = alice_central.mls_transport.latest_commit().await;
1206 let commit2 = commit1.clone();
1207
1208 bob_central
1210 .context
1211 .conversation(&id)
1212 .await
1213 .unwrap()
1214 .decrypt_message(proposal1.to_bytes().unwrap())
1215 .await
1216 .unwrap();
1217 assert!(matches!(
1218 bob_central
1219 .context
1220 .conversation(&id)
1221 .await
1222 .unwrap()
1223 .decrypt_message(proposal2.to_bytes().unwrap())
1224 .await
1225 .unwrap_err(),
1226 Error::DuplicateMessage
1227 ));
1228 bob_central
1229 .get_conversation_unchecked(&id)
1230 .await
1231 .group
1232 .clear_pending_proposals();
1233
1234 bob_central
1236 .context
1237 .conversation(&id)
1238 .await
1239 .unwrap()
1240 .decrypt_message(commit1.to_bytes().unwrap())
1241 .await
1242 .unwrap();
1243 assert!(matches!(
1244 bob_central
1245 .context
1246 .conversation(&id)
1247 .await
1248 .unwrap()
1249 .decrypt_message(commit2.to_bytes().unwrap())
1250 .await
1251 .unwrap_err(),
1252 Error::StaleCommit
1253 ));
1254 })
1255 })
1256 .await;
1257 }
1258 }
1259}