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