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 let [alice_central, bob_central, charlie_central] = case.sessions().await;
104 Box::pin(async move {
105 let id = conversation_id();
107 alice_central
108 .transaction
109 .new_conversation(&id, case.credential_type, case.cfg.clone())
110 .await
111 .unwrap();
112 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
114
115 bob_central
117 .transaction
118 .conversation(&id)
119 .await
120 .unwrap()
121 .update_key_material()
122 .await
123 .unwrap();
124 let bob_epoch = bob_central.get_conversation_unchecked(&id).await.group.epoch().as_u64();
125 assert_eq!(2, bob_epoch);
126 let alice_epoch = alice_central
127 .get_conversation_unchecked(&id)
128 .await
129 .group
130 .epoch()
131 .as_u64();
132 assert_eq!(1, alice_epoch);
133 let intermediate_commit = bob_central.mls_transport.latest_commit().await;
134
135 let retry_provider = Arc::new(
137 CoreCryptoTransportRetrySuccessProvider::default().with_intermediate_commits(
138 alice_central.clone(),
139 &[intermediate_commit],
140 &id,
141 ),
142 );
143
144 alice_central
145 .transaction
146 .set_transport_callbacks(Some(retry_provider.clone()))
147 .await
148 .unwrap();
149
150 alice_central
152 .transaction
153 .conversation(&id)
154 .await
155 .unwrap()
156 .update_key_material()
157 .await
158 .unwrap();
159 let commit = retry_provider.latest_commit().await;
160 bob_central
161 .transaction
162 .conversation(&id)
163 .await
164 .unwrap()
165 .decrypt_message(&commit.to_bytes().unwrap())
166 .await
167 .unwrap();
168
169 alice_central
172 .transaction
173 .conversation(&id)
174 .await
175 .unwrap()
176 .add_members(vec![charlie_central.rand_key_package(&case).await])
177 .await
178 .unwrap();
179 let commit = retry_provider.latest_commit().await;
180 bob_central
181 .transaction
182 .conversation(&id)
183 .await
184 .unwrap()
185 .decrypt_message(&commit.to_bytes().unwrap())
186 .await
187 .unwrap();
188
189 assert_eq!(retry_provider.retry_count().await, 2);
191 assert_eq!(retry_provider.success_count().await, 2);
193
194 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
196 })
197 .await;
198 }
199 }
200
201 mod add_members {
202 use super::*;
203 use std::sync::Arc;
204
205 #[apply(all_cred_cipher)]
206 #[wasm_bindgen_test]
207 async fn can_add_members_to_conversation(case: TestContext) {
208 let [alice_central, bob_central] = case.sessions().await;
209 Box::pin(async move {
210 let id = conversation_id();
211
212 alice_central
213 .transaction
214 .new_conversation(&id, case.credential_type, case.cfg.clone())
215 .await
216 .unwrap();
217 let bob = bob_central.rand_key_package(&case).await;
218 alice_central
220 .transaction
221 .set_transport_callbacks(Some(Arc::<CoreCryptoTransportAbortProvider>::default()))
222 .await
223 .unwrap();
224 alice_central
225 .transaction
226 .conversation(&id)
227 .await
228 .unwrap()
229 .add_members(vec![bob.clone()])
230 .await
231 .unwrap_err();
232
233 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
235
236 let success_provider = Arc::<CoreCryptoTransportSuccessProvider>::default();
237 alice_central
238 .transaction
239 .set_transport_callbacks(Some(success_provider.clone()))
240 .await
241 .unwrap();
242 alice_central
243 .transaction
244 .conversation(&id)
245 .await
246 .unwrap()
247 .add_members(vec![bob])
248 .await
249 .unwrap();
250
251 assert_eq!(alice_central.get_conversation_unchecked(&id).await.id, id);
252 assert_eq!(
253 alice_central
254 .get_conversation_unchecked(&id)
255 .await
256 .group
257 .group_id()
258 .as_slice(),
259 id
260 );
261 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
262 let commit = success_provider.latest_commit_bundle().await;
263 bob_central
264 .transaction
265 .process_welcome_message(commit.welcome.unwrap().into(), case.custom_cfg())
266 .await
267 .unwrap();
268 assert_eq!(
269 alice_central.get_conversation_unchecked(&id).await.id(),
270 bob_central.get_conversation_unchecked(&id).await.id()
271 );
272 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
273 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
274 })
275 .await
276 }
277
278 #[apply(all_cred_cipher)]
279 #[wasm_bindgen_test]
280 async fn should_return_valid_welcome(case: TestContext) {
281 let [alice_central, bob_central] = case.sessions().await;
282 Box::pin(async move {
283 let id = conversation_id();
284 alice_central
285 .transaction
286 .new_conversation(&id, case.credential_type, case.cfg.clone())
287 .await
288 .unwrap();
289
290 let bob = bob_central.rand_key_package(&case).await;
291 alice_central
292 .transaction
293 .conversation(&id)
294 .await
295 .unwrap()
296 .add_members(vec![bob])
297 .await
298 .unwrap();
299
300 let welcome = alice_central
301 .mls_transport
302 .latest_commit_bundle()
303 .await
304 .welcome
305 .unwrap();
306
307 bob_central
308 .transaction
309 .process_welcome_message(welcome.into(), case.custom_cfg())
310 .await
311 .unwrap();
312 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
313 })
314 .await
315 }
316
317 #[apply(all_cred_cipher)]
318 #[wasm_bindgen_test]
319 async fn should_return_valid_group_info(case: TestContext) {
320 let [alice_central, bob_central, mut guest_central] = case.sessions().await;
321 Box::pin(async move {
322 let id = conversation_id();
323 alice_central
324 .transaction
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 .transaction
332 .conversation(&id)
333 .await
334 .unwrap()
335 .add_members(vec![bob])
336 .await
337 .unwrap();
338 let commit_bundle = alice_central.mls_transport.latest_commit_bundle().await;
339 let group_info = commit_bundle.group_info.get_group_info();
340
341 assert!(
342 guest_central
343 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
344 .await
345 .is_ok()
346 );
347 })
348 .await
349 }
350 }
351
352 mod remove_members {
353 use super::*;
354
355 #[apply(all_cred_cipher)]
356 #[wasm_bindgen_test]
357 async fn alice_can_remove_bob_from_conversation(case: TestContext) {
358 let [alice_central, bob_central] = case.sessions().await;
359 Box::pin(async move {
360 let id = conversation_id();
361
362 alice_central
363 .transaction
364 .new_conversation(&id, case.credential_type, case.cfg.clone())
365 .await
366 .unwrap();
367 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
368
369 alice_central
370 .transaction
371 .conversation(&id)
372 .await
373 .unwrap()
374 .remove_members(&[bob_central.get_client_id().await])
375 .await
376 .unwrap();
377 let MlsCommitBundle { commit, welcome, .. } = alice_central.mls_transport.latest_commit_bundle().await;
378 assert!(welcome.is_none());
379
380 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
381
382 bob_central
383 .transaction
384 .conversation(&id)
385 .await
386 .unwrap()
387 .decrypt_message(commit.to_bytes().unwrap())
388 .await
389 .unwrap();
390
391 assert!(matches!(
393 bob_central.transaction.conversation(&id).await.unwrap_err(),
394 TransactionError::Leaf(crate::LeafError::ConversationNotFound(ref i))
395 if i == &id
396 ));
397 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_err());
398 })
399 .await;
400 }
401
402 #[apply(all_cred_cipher)]
403 #[wasm_bindgen_test]
404 async fn should_return_valid_welcome(case: TestContext) {
405 let [alice_central, bob_central, mut guest_central] = case.sessions().await;
406 Box::pin(async move {
407 let id = conversation_id();
408
409 alice_central
410 .transaction
411 .new_conversation(&id, case.credential_type, case.cfg.clone())
412 .await
413 .unwrap();
414 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
415
416 let proposal = alice_central
417 .transaction
418 .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
419 .await
420 .unwrap();
421 bob_central
422 .transaction
423 .conversation(&id)
424 .await
425 .unwrap()
426 .decrypt_message(proposal.proposal.to_bytes().unwrap())
427 .await
428 .unwrap();
429
430 alice_central
431 .transaction
432 .conversation(&id)
433 .await
434 .unwrap()
435 .remove_members(&[bob_central.get_client_id().await])
436 .await
437 .unwrap();
438
439 let welcome = alice_central.mls_transport.latest_welcome_message().await;
440
441 assert!(
442 guest_central
443 .try_join_from_welcome(&id, welcome.into(), case.custom_cfg(), vec![&alice_central])
444 .await
445 .is_ok()
446 );
447 assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
449 })
450 .await;
451 }
452
453 #[apply(all_cred_cipher)]
454 #[wasm_bindgen_test]
455 async fn should_return_valid_group_info(case: TestContext) {
456 let [alice_central, bob_central, mut guest_central] = case.sessions().await;
457 Box::pin(async move {
458 let id = conversation_id();
459
460 alice_central
461 .transaction
462 .new_conversation(&id, case.credential_type, case.cfg.clone())
463 .await
464 .unwrap();
465 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
466
467 alice_central
468 .transaction
469 .conversation(&id)
470 .await
471 .unwrap()
472 .remove_members(&[bob_central.get_client_id().await])
473 .await
474 .unwrap();
475
476 let commit_bundle = alice_central.mls_transport.latest_commit_bundle().await;
477
478 let group_info = commit_bundle.group_info.get_group_info();
479
480 assert!(
481 guest_central
482 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
483 .await
484 .is_ok()
485 );
486 assert!(guest_central.try_talk_to(&id, &bob_central).await.is_err());
488 })
489 .await;
490 }
491 }
492
493 mod update_keying_material {
494 use super::*;
495
496 #[apply(all_cred_cipher)]
497 #[wasm_bindgen_test]
498 async fn should_succeed(case: TestContext) {
499 let [alice_central, bob_central] = case.sessions().await;
500 Box::pin(async move {
501 let id = conversation_id();
502 alice_central
503 .transaction
504 .new_conversation(&id, case.credential_type, case.cfg.clone())
505 .await
506 .unwrap();
507 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
508
509 let init_count = alice_central.transaction.count_entities().await;
510
511 let bob_keys = bob_central
512 .get_conversation_unchecked(&id)
513 .await
514 .encryption_keys()
515 .collect::<Vec<Vec<u8>>>();
516 let alice_keys = alice_central
517 .get_conversation_unchecked(&id)
518 .await
519 .encryption_keys()
520 .collect::<Vec<Vec<u8>>>();
521 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
522
523 let alice_key = alice_central
524 .encryption_key_of(&id, alice_central.get_client_id().await)
525 .await;
526
527 alice_central
529 .transaction
530 .conversation(&id)
531 .await
532 .unwrap()
533 .update_key_material()
534 .await
535 .unwrap();
536 let MlsCommitBundle { commit, welcome, .. } = alice_central.mls_transport.latest_commit_bundle().await;
537 assert!(welcome.is_none());
538
539 assert!(
540 !alice_central
541 .get_conversation_unchecked(&id)
542 .await
543 .encryption_keys()
544 .contains(&alice_key)
545 );
546
547 let alice_new_keys = alice_central
548 .get_conversation_unchecked(&id)
549 .await
550 .encryption_keys()
551 .collect::<Vec<_>>();
552 assert!(!alice_new_keys.contains(&alice_key));
553
554 bob_central
556 .transaction
557 .conversation(&id)
558 .await
559 .unwrap()
560 .decrypt_message(&commit.to_bytes().unwrap())
561 .await
562 .unwrap();
563
564 let bob_new_keys = bob_central
565 .get_conversation_unchecked(&id)
566 .await
567 .encryption_keys()
568 .collect::<Vec<_>>();
569 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
570
571 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
573
574 let final_count = alice_central.transaction.count_entities().await;
577 assert_eq!(init_count, final_count);
578 })
579 .await;
580 }
581
582 #[apply(all_cred_cipher)]
583 #[wasm_bindgen_test]
584 async fn should_create_welcome_for_pending_add_proposals(case: TestContext) {
585 let [alice_central, bob_central, charlie_central] = case.sessions().await;
586 Box::pin(async move {
587 let id = conversation_id();
588 alice_central
589 .transaction
590 .new_conversation(&id, case.credential_type, case.cfg.clone())
591 .await
592 .unwrap();
593 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
594
595 let bob_keys = bob_central
596 .get_conversation_unchecked(&id)
597 .await
598 .signature_keys()
599 .collect::<Vec<SignaturePublicKey>>();
600 let alice_keys = alice_central
601 .get_conversation_unchecked(&id)
602 .await
603 .signature_keys()
604 .collect::<Vec<SignaturePublicKey>>();
605
606 assert!(alice_keys.iter().all(|a_key| bob_keys.contains(a_key)));
608
609 let alice_key = alice_central
610 .encryption_key_of(&id, alice_central.get_client_id().await)
611 .await;
612
613 let charlie_kp = charlie_central.get_one_key_package(&case).await;
615 let add_charlie_proposal = alice_central
616 .transaction
617 .new_add_proposal(&id, charlie_kp)
618 .await
619 .unwrap();
620
621 bob_central
623 .transaction
624 .conversation(&id)
625 .await
626 .unwrap()
627 .decrypt_message(add_charlie_proposal.proposal.to_bytes().unwrap())
628 .await
629 .unwrap();
630
631 assert!(
632 alice_central
633 .get_conversation_unchecked(&id)
634 .await
635 .encryption_keys()
636 .contains(&alice_key)
637 );
638
639 alice_central
641 .transaction
642 .conversation(&id)
643 .await
644 .unwrap()
645 .update_key_material()
646 .await
647 .unwrap();
648 let MlsCommitBundle { commit, welcome, .. } = alice_central.mls_transport.latest_commit_bundle().await;
649 assert!(welcome.is_some());
650 assert!(
651 !alice_central
652 .get_conversation_unchecked(&id)
653 .await
654 .encryption_keys()
655 .contains(&alice_key)
656 );
657
658 charlie_central
660 .transaction
661 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
662 .await
663 .unwrap();
664
665 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
666 assert_eq!(charlie_central.get_conversation_unchecked(&id).await.members().len(), 3);
667 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
669
670 let alice_new_keys = alice_central
671 .get_conversation_unchecked(&id)
672 .await
673 .encryption_keys()
674 .collect::<Vec<Vec<u8>>>();
675 assert!(!alice_new_keys.contains(&alice_key));
676
677 bob_central
679 .transaction
680 .conversation(&id)
681 .await
682 .unwrap()
683 .decrypt_message(&commit.to_bytes().unwrap())
684 .await
685 .unwrap();
686 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
687
688 let bob_new_keys = bob_central
689 .get_conversation_unchecked(&id)
690 .await
691 .encryption_keys()
692 .collect::<Vec<Vec<u8>>>();
693 assert!(alice_new_keys.iter().all(|a_key| bob_new_keys.contains(a_key)));
694
695 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
697 assert!(bob_central.try_talk_to(&id, &charlie_central).await.is_ok());
698 assert!(charlie_central.try_talk_to(&id, &alice_central).await.is_ok());
699 })
700 .await;
701 }
702
703 #[apply(all_cred_cipher)]
704 #[wasm_bindgen_test]
705 async fn should_return_valid_welcome(case: TestContext) {
706 let [alice_central, bob_central, mut guest_central] = case.sessions().await;
707 Box::pin(async move {
708 let id = conversation_id();
709 alice_central
710 .transaction
711 .new_conversation(&id, case.credential_type, case.cfg.clone())
712 .await
713 .unwrap();
714 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
715
716 let proposal = alice_central
717 .transaction
718 .new_add_proposal(&id, guest_central.get_one_key_package(&case).await)
719 .await
720 .unwrap()
721 .proposal;
722 bob_central
723 .transaction
724 .conversation(&id)
725 .await
726 .unwrap()
727 .decrypt_message(proposal.to_bytes().unwrap())
728 .await
729 .unwrap();
730
731 alice_central
732 .transaction
733 .conversation(&id)
734 .await
735 .unwrap()
736 .update_key_material()
737 .await
738 .unwrap();
739 let MlsCommitBundle { commit, welcome, .. } = alice_central.mls_transport.latest_commit_bundle().await;
740
741 bob_central
742 .transaction
743 .conversation(&id)
744 .await
745 .unwrap()
746 .decrypt_message(commit.to_bytes().unwrap())
747 .await
748 .unwrap();
749
750 assert!(
751 guest_central
752 .try_join_from_welcome(
753 &id,
754 welcome.unwrap().into(),
755 case.custom_cfg(),
756 vec![&alice_central, &bob_central]
757 )
758 .await
759 .is_ok()
760 );
761 })
762 .await;
763 }
764
765 #[apply(all_cred_cipher)]
766 #[wasm_bindgen_test]
767 async fn should_return_valid_group_info(case: TestContext) {
768 let [alice_central, bob_central, mut guest_central] = case.sessions().await;
769 Box::pin(async move {
770 let id = conversation_id();
771 alice_central
772 .transaction
773 .new_conversation(&id, case.credential_type, case.cfg.clone())
774 .await
775 .unwrap();
776 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
777
778 alice_central
779 .transaction
780 .conversation(&id)
781 .await
782 .unwrap()
783 .update_key_material()
784 .await
785 .unwrap();
786 let group_info = alice_central.mls_transport.latest_group_info().await;
787 let group_info = group_info.get_group_info();
788
789 assert!(
790 guest_central
791 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
792 .await
793 .is_ok()
794 );
795 })
796 .await;
797 }
798 }
799
800 mod commit_pending_proposals {
801 use super::*;
802
803 #[apply(all_cred_cipher)]
804 #[wasm_bindgen_test]
805 async fn should_create_a_commit_out_of_self_pending_proposals(case: TestContext) {
806 let [mut alice_central, bob_central] = case.sessions().await;
807 Box::pin(async move {
808 let id = conversation_id();
809 alice_central
810 .transaction
811 .new_conversation(&id, case.credential_type, case.cfg.clone())
812 .await
813 .unwrap();
814 alice_central
815 .transaction
816 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
817 .await
818 .unwrap();
819 assert!(!alice_central.pending_proposals(&id).await.is_empty());
820 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
821 alice_central
822 .transaction
823 .conversation(&id)
824 .await
825 .unwrap()
826 .commit_pending_proposals()
827 .await
828 .unwrap();
829 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
830
831 let welcome = alice_central.mls_transport.latest_commit_bundle().await.welcome;
832 bob_central
833 .transaction
834 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
835 .await
836 .unwrap();
837 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
838 })
839 .await;
840 }
841
842 #[apply(all_cred_cipher)]
843 #[wasm_bindgen_test]
844 async fn should_create_a_commit_out_of_pending_proposals_by_ref(case: TestContext) {
845 let [alice_central, mut bob_central, charlie_central] = case.sessions().await;
846 Box::pin(async move {
847 let id = conversation_id();
848 alice_central
849 .transaction
850 .new_conversation(&id, case.credential_type, case.cfg.clone())
851 .await
852 .unwrap();
853 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
854 let proposal = bob_central
855 .transaction
856 .new_add_proposal(&id, charlie_central.get_one_key_package(&case).await)
857 .await
858 .unwrap();
859 assert!(!bob_central.pending_proposals(&id).await.is_empty());
860 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
861 alice_central
862 .transaction
863 .conversation(&id)
864 .await
865 .unwrap()
866 .decrypt_message(proposal.proposal.to_bytes().unwrap())
867 .await
868 .unwrap();
869
870 alice_central
871 .transaction
872 .conversation(&id)
873 .await
874 .unwrap()
875 .commit_pending_proposals()
876 .await
877 .unwrap();
878 let commit = alice_central.mls_transport.latest_commit_bundle().await.commit;
879 assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 3);
880
881 bob_central
882 .transaction
883 .conversation(&id)
884 .await
885 .unwrap()
886 .decrypt_message(commit.to_bytes().unwrap())
887 .await
888 .unwrap();
889 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
890 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
891 })
892 .await;
893 }
894
895 #[apply(all_cred_cipher)]
896 #[wasm_bindgen_test]
897 async fn should_return_valid_welcome(case: TestContext) {
898 let [alice_central, bob_central] = case.sessions().await;
899 Box::pin(async move {
900 let id = conversation_id();
901 alice_central
902 .transaction
903 .new_conversation(&id, case.credential_type, case.cfg.clone())
904 .await
905 .unwrap();
906 alice_central
907 .transaction
908 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
909 .await
910 .unwrap();
911 alice_central
912 .transaction
913 .conversation(&id)
914 .await
915 .unwrap()
916 .commit_pending_proposals()
917 .await
918 .unwrap();
919
920 let welcome = alice_central.mls_transport.latest_commit_bundle().await.welcome;
921
922 bob_central
923 .transaction
924 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
925 .await
926 .unwrap();
927 assert!(alice_central.try_talk_to(&id, &bob_central).await.is_ok());
928 })
929 .await;
930 }
931
932 #[apply(all_cred_cipher)]
933 #[wasm_bindgen_test]
934 async fn should_return_valid_group_info(case: TestContext) {
935 let [alice_central, bob_central, mut guest_central] = case.sessions().await;
936 Box::pin(async move {
937 let id = conversation_id();
938 alice_central
939 .transaction
940 .new_conversation(&id, case.credential_type, case.cfg.clone())
941 .await
942 .unwrap();
943 alice_central
944 .transaction
945 .new_add_proposal(&id, bob_central.get_one_key_package(&case).await)
946 .await
947 .unwrap();
948 alice_central
949 .transaction
950 .conversation(&id)
951 .await
952 .unwrap()
953 .commit_pending_proposals()
954 .await
955 .unwrap();
956 let commit_bundle = alice_central.mls_transport.latest_commit_bundle().await;
957 let group_info = commit_bundle.group_info.get_group_info();
958
959 assert!(
960 guest_central
961 .try_join_from_group_info(&case, &id, group_info, vec![&alice_central])
962 .await
963 .is_ok()
964 );
965 })
966 .await;
967 }
968 }
969
970 mod delivery_semantics {
971 use super::*;
972
973 #[apply(all_cred_cipher)]
974 #[wasm_bindgen_test]
975 async fn should_prevent_out_of_order_commits(case: TestContext) {
976 let [alice_central, bob_central] = case.sessions().await;
977 Box::pin(async move {
978 let id = conversation_id();
979 alice_central
980 .transaction
981 .new_conversation(&id, case.credential_type, case.cfg.clone())
982 .await
983 .unwrap();
984 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
985
986 alice_central
987 .transaction
988 .conversation(&id)
989 .await
990 .unwrap()
991 .update_key_material()
992 .await
993 .unwrap();
994 let commit1 = alice_central.mls_transport.latest_commit().await;
995 let commit1 = commit1.to_bytes().unwrap();
996 alice_central
997 .transaction
998 .conversation(&id)
999 .await
1000 .unwrap()
1001 .update_key_material()
1002 .await
1003 .unwrap();
1004 let commit2 = alice_central.mls_transport.latest_commit().await;
1005 let commit2 = commit2.to_bytes().unwrap();
1006
1007 let out_of_order = bob_central
1009 .transaction
1010 .conversation(&id)
1011 .await
1012 .unwrap()
1013 .decrypt_message(&commit2)
1014 .await;
1015 assert!(matches!(out_of_order.unwrap_err(), Error::BufferedFutureMessage { .. }));
1016
1017 bob_central
1020 .transaction
1021 .conversation(&id)
1022 .await
1023 .unwrap()
1024 .decrypt_message(&commit1)
1025 .await
1026 .unwrap();
1027
1028 let past_commit = bob_central
1030 .transaction
1031 .conversation(&id)
1032 .await
1033 .unwrap()
1034 .decrypt_message(&commit1)
1035 .await;
1036 assert!(matches!(past_commit.unwrap_err(), Error::StaleCommit));
1037 })
1038 .await;
1039 }
1040
1041 #[apply(all_cred_cipher)]
1042 #[wasm_bindgen_test]
1043 async fn should_prevent_replayed_encrypted_handshake_messages(case: TestContext) {
1044 if !case.is_pure_ciphertext() {
1045 return;
1046 }
1047
1048 let [alice_central, bob_central] = case.sessions().await;
1049 Box::pin(async move {
1050 let id = conversation_id();
1051 alice_central
1052 .transaction
1053 .new_conversation(&id, case.credential_type, case.cfg.clone())
1054 .await
1055 .unwrap();
1056 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1057
1058 let proposal1 = alice_central
1059 .transaction
1060 .new_update_proposal(&id)
1061 .await
1062 .unwrap()
1063 .proposal;
1064 let proposal2 = proposal1.clone();
1065 alice_central
1066 .get_conversation_unchecked(&id)
1067 .await
1068 .group
1069 .clear_pending_proposals();
1070
1071 alice_central
1072 .transaction
1073 .conversation(&id)
1074 .await
1075 .unwrap()
1076 .update_key_material()
1077 .await
1078 .unwrap();
1079 let commit1 = alice_central.mls_transport.latest_commit().await;
1080 let commit2 = commit1.clone();
1081
1082 bob_central
1084 .transaction
1085 .conversation(&id)
1086 .await
1087 .unwrap()
1088 .decrypt_message(proposal1.to_bytes().unwrap())
1089 .await
1090 .unwrap();
1091 assert!(matches!(
1092 bob_central
1093 .transaction
1094 .conversation(&id)
1095 .await
1096 .unwrap()
1097 .decrypt_message(proposal2.to_bytes().unwrap())
1098 .await
1099 .unwrap_err(),
1100 Error::DuplicateMessage
1101 ));
1102 bob_central
1103 .get_conversation_unchecked(&id)
1104 .await
1105 .group
1106 .clear_pending_proposals();
1107
1108 bob_central
1110 .transaction
1111 .conversation(&id)
1112 .await
1113 .unwrap()
1114 .decrypt_message(commit1.to_bytes().unwrap())
1115 .await
1116 .unwrap();
1117 assert!(matches!(
1118 bob_central
1119 .transaction
1120 .conversation(&id)
1121 .await
1122 .unwrap()
1123 .decrypt_message(commit2.to_bytes().unwrap())
1124 .await
1125 .unwrap_err(),
1126 Error::StaleCommit
1127 ));
1128 })
1129 .await;
1130 }
1131 }
1132}