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