1use log::{debug, info};
12use openmls::prelude::StageCommitError;
13use openmls::{
14 framing::errors::{MessageDecryptionError, SecretTreeError},
15 group::StagedCommit,
16 prelude::{
17 ContentType, CredentialType, MlsMessageIn, MlsMessageInBody, ProcessMessageError, ProcessedMessage,
18 ProcessedMessageContent, Proposal, ProtocolMessage, ValidationError,
19 },
20};
21use openmls_traits::OpenMlsCryptoProvider;
22use tls_codec::Deserialize;
23
24use core_crypto_keystore::{
25 connection::FetchFromDatabase,
26 entities::{MlsBufferedCommit, MlsPendingMessage},
27};
28use mls_crypto_provider::MlsCryptoProvider;
29
30use crate::context::CentralContext;
31use crate::obfuscate::Obfuscated;
32use crate::{
33 e2e_identity::{conversation_state::compute_state, init_certificates::NewCrlDistributionPoint},
34 group_store::GroupStoreValue,
35 mls::{
36 client::Client,
37 conversation::renew::Renew,
38 credential::{
39 crl::{
40 extract_crl_uris_from_proposals, extract_crl_uris_from_update_path, get_new_crl_distribution_points,
41 },
42 ext::CredentialExt,
43 },
44 ClientId, ConversationId, MlsConversation,
45 },
46 prelude::{E2eiConversationState, MlsProposalBundle, WireIdentity},
47 CoreCryptoCallbacks, CryptoError, CryptoResult, MlsError,
48};
49
50#[derive(Debug)]
53pub struct MlsConversationDecryptMessage {
54 pub app_msg: Option<Vec<u8>>,
56 pub proposals: Vec<MlsProposalBundle>,
61 pub is_active: bool,
63 pub delay: Option<u64>,
65 pub sender_client_id: Option<ClientId>,
67 pub has_epoch_changed: bool,
69 pub identity: WireIdentity,
72 pub buffered_messages: Option<Vec<MlsBufferedConversationDecryptMessage>>,
76 pub crl_new_distribution_points: NewCrlDistributionPoint,
78}
79
80#[derive(Debug)]
82pub struct MlsBufferedConversationDecryptMessage {
83 pub app_msg: Option<Vec<u8>>,
85 pub proposals: Vec<MlsProposalBundle>,
87 pub is_active: bool,
89 pub delay: Option<u64>,
91 pub sender_client_id: Option<ClientId>,
93 pub has_epoch_changed: bool,
95 pub identity: WireIdentity,
97 pub crl_new_distribution_points: NewCrlDistributionPoint,
99}
100
101impl From<MlsConversationDecryptMessage> for MlsBufferedConversationDecryptMessage {
102 fn from(from: MlsConversationDecryptMessage) -> Self {
103 Self {
104 app_msg: from.app_msg,
105 proposals: from.proposals,
106 is_active: from.is_active,
107 delay: from.delay,
108 sender_client_id: from.sender_client_id,
109 has_epoch_changed: from.has_epoch_changed,
110 identity: from.identity,
111 crl_new_distribution_points: from.crl_new_distribution_points,
112 }
113 }
114}
115
116struct ParsedMessage {
117 is_duplicate: bool,
118 protocol_message: ProtocolMessage,
119 content_type: ContentType,
120}
121
122impl MlsConversation {
124 #[allow(clippy::too_many_arguments)]
126 #[cfg_attr(test, crate::durable)]
127 pub async fn decrypt_message(
130 &mut self,
131 message: MlsMessageIn,
132 parent_conv: Option<&GroupStoreValue<MlsConversation>>,
133 client: &Client,
134 backend: &MlsCryptoProvider,
135 callbacks: Option<&dyn CoreCryptoCallbacks>,
136 restore_pending: bool,
137 ) -> CryptoResult<MlsConversationDecryptMessage> {
138 let parsed_message = self.parse_message(backend, message.clone())?;
139
140 let message_result = self.process_message(backend, parsed_message).await;
141
142 if let Err(CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::InvalidCommit(
144 StageCommitError::OwnCommit,
145 )))) = message_result
146 {
147 let ct = self.extract_confirmation_tag_from_own_commit(&message)?;
148 let mut decrypted_message = self.handle_own_commit(backend, ct).await?;
149 debug_assert!(
151 decrypted_message.buffered_messages.is_none(),
152 "decrypted message should be constructed with empty buffer"
153 );
154 if restore_pending {
155 decrypted_message.buffered_messages = self
156 .decrypt_and_clear_pending_messages(client, backend, parent_conv, callbacks)
157 .await?;
158 }
159
160 return Ok(decrypted_message);
161 }
162
163 if let Err(CryptoError::MlsError(MlsError::MlsMessageError(ProcessMessageError::InvalidCommit(
167 StageCommitError::MissingProposal,
168 )))) = message_result
169 {
170 return Err(CryptoError::BufferedCommit);
171 }
172
173 let message = message_result?;
174
175 let credential = message.credential();
176 let epoch = message.epoch();
177
178 let identity = credential.extract_identity(
179 self.ciphersuite(),
180 backend.authentication_service().borrow().await.as_ref(),
181 )?;
182
183 let sender_client_id: ClientId = credential.credential.identity().into();
184
185 let decrypted = match message.into_content() {
186 ProcessedMessageContent::ApplicationMessage(app_msg) => {
187 debug!(
188 group_id = Obfuscated::from(&self.id),
189 epoch = epoch.as_u64(),
190 sender_client_id = Obfuscated::from(&sender_client_id);
191 "Application message"
192 );
193
194 MlsConversationDecryptMessage {
195 app_msg: Some(app_msg.into_bytes()),
196 proposals: vec![],
197 is_active: true,
198 delay: None,
199 sender_client_id: Some(sender_client_id),
200 has_epoch_changed: false,
201 identity,
202 buffered_messages: None,
203 crl_new_distribution_points: None.into(),
204 }
205 }
206 ProcessedMessageContent::ProposalMessage(proposal) => {
207 let crl_dps = extract_crl_uris_from_proposals(&[proposal.proposal().clone()])?;
208 let crl_new_distribution_points = get_new_crl_distribution_points(backend, crl_dps).await?;
209
210 info!(
211 group_id = Obfuscated::from(&self.id),
212 sender = Obfuscated::from(proposal.sender()),
213 proposals = Obfuscated::from(&proposal.proposal);
214 "Received proposal"
215 );
216
217 self.group.store_pending_proposal(*proposal);
218
219 if let Some(commit) = self.retrieve_buffered_commit(backend).await? {
220 let process_result = self
221 .try_process_buffered_commit(commit, parent_conv, client, backend, restore_pending)
222 .await;
223
224 if process_result.is_ok() {
225 self.clear_buffered_commit(backend).await?;
226 }
227 if !matches!(process_result, Err(CryptoError::BufferedCommit)) {
232 return process_result;
236 }
237 }
238
239 MlsConversationDecryptMessage {
240 app_msg: None,
241 proposals: vec![],
242 is_active: true,
243 delay: self.compute_next_commit_delay(),
244 sender_client_id: None,
245 has_epoch_changed: false,
246 identity,
247 buffered_messages: None,
248 crl_new_distribution_points,
249 }
250 }
251 ProcessedMessageContent::StagedCommitMessage(staged_commit) => {
252 self.validate_external_commit(&staged_commit, sender_client_id, parent_conv, backend, callbacks)
253 .await?;
254
255 self.validate_commit(&staged_commit, backend).await?;
256
257 let pending_proposals = self.self_pending_proposals().cloned().collect::<Vec<_>>();
258
259 let proposal_refs: Vec<Proposal> = pending_proposals
260 .iter()
261 .map(|p| p.proposal().clone())
262 .chain(
263 staged_commit
264 .add_proposals()
265 .map(|p| Proposal::Add(p.add_proposal().clone())),
266 )
267 .chain(
268 staged_commit
269 .update_proposals()
270 .map(|p| Proposal::Update(p.update_proposal().clone())),
271 )
272 .collect();
273
274 let mut crl_dps = extract_crl_uris_from_proposals(&proposal_refs)?;
276 crl_dps.extend(extract_crl_uris_from_update_path(&staged_commit)?);
277
278 let crl_new_distribution_points = get_new_crl_distribution_points(backend, crl_dps).await?;
279
280 let pending_commit = self.group.pending_commit().cloned();
282
283 self.group
284 .merge_staged_commit(backend, *staged_commit.clone())
285 .await
286 .map_err(MlsError::from)?;
287
288 let (proposals_to_renew, needs_update) = Renew::renew(
289 &self.group.own_leaf_index(),
290 pending_proposals.into_iter(),
291 pending_commit.as_ref(),
292 staged_commit.as_ref(),
293 );
294 let proposals = self
295 .renew_proposals_for_current_epoch(client, backend, proposals_to_renew.into_iter(), needs_update)
296 .await?;
297
298 let mut buffered_messages = None;
300 if restore_pending {
301 buffered_messages = self
302 .decrypt_and_clear_pending_messages(client, backend, parent_conv, callbacks)
303 .await?;
304 }
305
306 info!(
307 group_id = Obfuscated::from(&self.id),
308 epoch = staged_commit.staged_context().epoch().as_u64(),
309 proposals:? = staged_commit.queued_proposals().map(Obfuscated::from).collect::<Vec<_>>();
310 "Epoch advanced"
311 );
312
313 MlsConversationDecryptMessage {
314 app_msg: None,
315 proposals,
316 is_active: self.group.is_active(),
317 delay: self.compute_next_commit_delay(),
318 sender_client_id: None,
319 has_epoch_changed: true,
320 identity,
321 buffered_messages,
322 crl_new_distribution_points,
323 }
324 }
325 ProcessedMessageContent::ExternalJoinProposalMessage(proposal) => {
326 self.validate_external_proposal(&proposal, parent_conv, callbacks)
327 .await?;
328
329 info!(
330 group_id = Obfuscated::from(&self.id),
331 sender = Obfuscated::from(proposal.sender());
332 "Received external join proposal"
333 );
334
335 let crl_dps = extract_crl_uris_from_proposals(&[proposal.proposal().clone()])?;
336 let crl_new_distribution_points = get_new_crl_distribution_points(backend, crl_dps).await?;
337 self.group.store_pending_proposal(*proposal);
338
339 MlsConversationDecryptMessage {
340 app_msg: None,
341 proposals: vec![],
342 is_active: true,
343 delay: self.compute_next_commit_delay(),
344 sender_client_id: None,
345 has_epoch_changed: false,
346 identity,
347 buffered_messages: None,
348 crl_new_distribution_points,
349 }
350 }
351 };
352
353 self.persist_group_when_changed(&backend.keystore(), false).await?;
354
355 Ok(decrypted)
356 }
357
358 async fn buffer_pending_commit(&self, backend: &MlsCryptoProvider, commit: impl AsRef<[u8]>) -> CryptoResult<()> {
363 info!(group_id = Obfuscated::from(&self.id); "buffering pending commit");
364
365 let pending_commit = MlsBufferedCommit::new(self.id.clone(), commit.as_ref().to_owned());
366
367 backend.key_store().save(pending_commit).await?;
368 Ok(())
369 }
370
371 async fn retrieve_buffered_commit(&self, backend: &MlsCryptoProvider) -> CryptoResult<Option<Vec<u8>>> {
373 info!(group_id = Obfuscated::from(&self.id); "attempting to retrieve pending commit");
374
375 backend
376 .keystore()
377 .find::<MlsBufferedCommit>(&self.id)
378 .await
379 .map(|option| option.map(MlsBufferedCommit::into_commit_data))
380 .map_err(Into::into)
381 }
382
383 async fn try_process_buffered_commit(
390 &mut self,
391 commit: impl AsRef<[u8]>,
392 parent_conv: Option<&GroupStoreValue<MlsConversation>>,
393 client: &Client,
394 backend: &MlsCryptoProvider,
395 restore_pending: bool,
396 ) -> CryptoResult<MlsConversationDecryptMessage> {
397 info!(group_id = Obfuscated::from(&self.id); "attempting to process pending commit");
398
399 let message = MlsMessageIn::tls_deserialize(&mut commit.as_ref()).map_err(MlsError::from)?;
400
401 Box::pin(self.decrypt_message(message, parent_conv, client, backend, None, restore_pending)).await
402 }
403
404 async fn clear_buffered_commit(&self, backend: &MlsCryptoProvider) -> CryptoResult<()> {
406 info!(group_id = Obfuscated::from(&self.id); "attempting to delete pending commit");
407
408 backend
409 .keystore()
410 .remove::<MlsBufferedCommit, _>(&self.id)
411 .await
412 .map_err(Into::into)
413 }
414
415 async fn decrypt_and_clear_pending_messages(
416 &mut self,
417 client: &Client,
418 backend: &MlsCryptoProvider,
419 parent_conv: Option<&GroupStoreValue<MlsConversation>>,
420 callbacks: Option<&dyn CoreCryptoCallbacks>,
421 ) -> CryptoResult<Option<Vec<MlsBufferedConversationDecryptMessage>>> {
422 let pending_messages = self
423 .restore_pending_messages(client, backend, callbacks, parent_conv, false)
424 .await?;
425
426 if pending_messages.is_some() {
427 info!(group_id = Obfuscated::from(&self.id); "Clearing all buffered messages for conversation");
428 backend.key_store().remove::<MlsPendingMessage, _>(self.id()).await?;
429 }
430
431 Ok(pending_messages)
432 }
433
434 fn parse_message(&self, backend: &MlsCryptoProvider, msg_in: MlsMessageIn) -> CryptoResult<ParsedMessage> {
435 let mut is_duplicate = false;
436 let (protocol_message, content_type) = match msg_in.extract() {
437 MlsMessageInBody::PublicMessage(m) => {
438 is_duplicate = self.is_duplicate_message(backend, &m)?;
439 let ct = m.content_type();
440 (ProtocolMessage::PublicMessage(m), ct)
441 }
442 MlsMessageInBody::PrivateMessage(m) => {
443 let ct = m.content_type();
444 (ProtocolMessage::PrivateMessage(m), ct)
445 }
446 _ => {
447 return Err(CryptoError::MlsError(
448 ProcessMessageError::IncompatibleWireFormat.into(),
449 ))
450 }
451 };
452 Ok(ParsedMessage {
453 is_duplicate,
454 protocol_message,
455 content_type,
456 })
457 }
458
459 async fn process_message(
460 &mut self,
461 backend: &MlsCryptoProvider,
462 ParsedMessage {
463 is_duplicate,
464 protocol_message,
465 content_type,
466 }: ParsedMessage,
467 ) -> CryptoResult<ProcessedMessage> {
468 let msg_epoch = protocol_message.epoch().as_u64();
469 let group_epoch = self.group.epoch().as_u64();
470 let processed_msg = self
471 .group
472 .process_message(backend, protocol_message)
473 .await
474 .map_err(|e| match e {
475 ProcessMessageError::ValidationError(ValidationError::UnableToDecrypt(
476 MessageDecryptionError::GenerationOutOfBound,
477 )) => CryptoError::DuplicateMessage,
478 ProcessMessageError::ValidationError(ValidationError::WrongEpoch) => {
479 if is_duplicate {
480 CryptoError::DuplicateMessage
481 } else if msg_epoch == group_epoch + 1 {
482 CryptoError::BufferedFutureMessage {
485 message_epoch: msg_epoch,
486 }
487 } else if msg_epoch < group_epoch {
488 match content_type {
489 ContentType::Application => CryptoError::StaleMessage,
490 ContentType::Commit => CryptoError::StaleCommit,
491 ContentType::Proposal => CryptoError::StaleProposal,
492 }
493 } else {
494 CryptoError::WrongEpoch
495 }
496 }
497 ProcessMessageError::ValidationError(ValidationError::UnableToDecrypt(
498 MessageDecryptionError::AeadError,
499 )) => CryptoError::DecryptionError,
500 ProcessMessageError::ValidationError(ValidationError::UnableToDecrypt(
501 MessageDecryptionError::SecretTreeError(SecretTreeError::TooDistantInThePast),
502 )) => CryptoError::MessageEpochTooOld,
503 _ => CryptoError::from(MlsError::from(e)),
504 })?;
505 if is_duplicate {
506 return Err(CryptoError::DuplicateMessage);
507 }
508 Ok(processed_msg)
509 }
510
511 async fn validate_commit(&self, commit: &StagedCommit, backend: &MlsCryptoProvider) -> CryptoResult<()> {
512 if backend.authentication_service().is_env_setup().await {
513 let credentials: Vec<_> = commit
514 .add_proposals()
515 .filter_map(|add_proposal| {
516 let credential = add_proposal.add_proposal().key_package().leaf_node().credential();
517
518 matches!(credential.credential_type(), CredentialType::X509).then(|| credential.clone())
519 })
520 .collect();
521 let state = compute_state(
522 self.ciphersuite(),
523 credentials.iter(),
524 crate::prelude::MlsCredentialType::X509,
525 backend.authentication_service().borrow().await.as_ref(),
526 )
527 .await;
528 if state != E2eiConversationState::Verified {
529 }
532 }
533 Ok(())
534 }
535}
536
537impl CentralContext {
538 pub async fn decrypt_message(
554 &self,
555 id: &ConversationId,
556 message: impl AsRef<[u8]>,
557 ) -> CryptoResult<MlsConversationDecryptMessage> {
558 let msg = MlsMessageIn::tls_deserialize(&mut message.as_ref()).map_err(MlsError::from)?;
559 let Ok(conversation) = self.get_conversation(id).await else {
560 return self.handle_when_group_is_pending(id, message).await;
561 };
562 let parent_conversation = self.get_parent_conversation(&conversation).await?;
563 let guard = self.callbacks().await?;
564 let callbacks = guard.as_ref().map(AsRef::as_ref);
565 let client = &self.mls_client().await?;
566 let backend = &self.mls_provider().await?;
567
568 let decrypt_message_result = conversation
569 .write()
570 .await
571 .decrypt_message(msg, parent_conversation.as_ref(), client, backend, callbacks, true)
572 .await;
573
574 if let Err(CryptoError::BufferedFutureMessage { message_epoch }) = decrypt_message_result {
575 self.handle_future_message(id, message.as_ref()).await?;
576 info!(group_id = Obfuscated::from(id); "Buffered future message from epoch {message_epoch}");
577 }
578
579 if let Err(CryptoError::BufferedCommit) = decrypt_message_result {
582 conversation
583 .read()
584 .await
585 .buffer_pending_commit(backend, message)
586 .await?;
587 }
588
589 let decrypt_message = decrypt_message_result?;
590
591 if !decrypt_message.is_active {
592 self.wipe_conversation(id).await?;
593 }
594 Ok(decrypt_message)
595 }
596}
597
598#[cfg(test)]
599mod tests {
600 use wasm_bindgen_test::*;
601
602 use crate::{
603 mls::conversation::config::MAX_PAST_EPOCHS,
604 prelude::MlsCommitBundle,
605 test_utils::{ValidationCallbacks, *},
606 CryptoError,
607 };
608
609 use super::*;
610
611 wasm_bindgen_test_configure!(run_in_browser);
612
613 mod is_active {
614 use super::*;
615
616 #[apply(all_cred_cipher)]
617 #[wasm_bindgen_test]
618 pub async fn decrypting_a_regular_commit_should_leave_conversation_active(case: TestCase) {
619 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
620 Box::pin(async move {
621 let id = conversation_id();
622 alice_central
623 .context
624 .new_conversation(&id, case.credential_type, case.cfg.clone())
625 .await
626 .unwrap();
627 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
628
629 let MlsCommitBundle { commit, .. } = bob_central.context.update_keying_material(&id).await.unwrap();
630 let MlsConversationDecryptMessage { is_active, .. } = alice_central
631 .context
632 .decrypt_message(&id, commit.to_bytes().unwrap())
633 .await
634 .unwrap();
635 assert!(is_active)
636 })
637 })
638 .await
639 }
640
641 #[apply(all_cred_cipher)]
642 #[wasm_bindgen_test]
643 pub async fn decrypting_a_commit_removing_self_should_set_conversation_inactive(case: TestCase) {
644 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
645 Box::pin(async move {
646 let id = conversation_id();
647 alice_central
648 .context
649 .new_conversation(&id, case.credential_type, case.cfg.clone())
650 .await
651 .unwrap();
652 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
653
654 let MlsCommitBundle { commit, .. } = bob_central
655 .context
656 .remove_members_from_conversation(&id, &[alice_central.get_client_id().await])
657 .await
658 .unwrap();
659 let MlsConversationDecryptMessage { is_active, .. } = alice_central
660 .context
661 .decrypt_message(&id, commit.to_bytes().unwrap())
662 .await
663 .unwrap();
664 assert!(!is_active)
665 })
666 })
667 .await
668 }
669 }
670
671 mod commit {
672 use super::*;
673
674 #[apply(all_cred_cipher)]
675 #[wasm_bindgen_test]
676 pub async fn decrypting_a_commit_should_succeed(case: TestCase) {
677 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
678 Box::pin(async move {
679 let id = conversation_id();
680 alice_central
681 .context
682 .new_conversation(&id, case.credential_type, case.cfg.clone())
683 .await
684 .unwrap();
685 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
686
687 let epoch_before = alice_central.context.conversation_epoch(&id).await.unwrap();
688
689 let MlsCommitBundle { commit, .. } =
690 alice_central.context.update_keying_material(&id).await.unwrap();
691 alice_central.context.commit_accepted(&id).await.unwrap();
692
693 let decrypted = bob_central
694 .context
695 .decrypt_message(&id, commit.to_bytes().unwrap())
696 .await
697 .unwrap();
698 let epoch_after = bob_central.context.conversation_epoch(&id).await.unwrap();
699 assert_eq!(epoch_after, epoch_before + 1);
700 assert!(decrypted.has_epoch_changed);
701 assert!(decrypted.delay.is_none());
702 assert!(decrypted.app_msg.is_none());
703
704 alice_central.verify_sender_identity(&case, &decrypted).await;
705 })
706 })
707 .await
708 }
709
710 #[apply(all_cred_cipher)]
711 #[wasm_bindgen_test]
712 pub async fn decrypting_a_commit_should_clear_pending_commit(case: TestCase) {
713 run_test_with_client_ids(
714 case.clone(),
715 ["alice", "bob", "charlie", "debbie"],
716 move |[alice_central, bob_central, charlie_central, debbie_central]| {
717 Box::pin(async move {
718 let id = conversation_id();
719 alice_central
720 .context
721 .new_conversation(&id, case.credential_type, case.cfg.clone())
722 .await
723 .unwrap();
724 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
725
726 let charlie = charlie_central.rand_key_package(&case).await;
728 let debbie = debbie_central.rand_key_package(&case).await;
729 alice_central
730 .context
731 .add_members_to_conversation(&id, vec![charlie.clone()])
732 .await
733 .unwrap();
734 assert!(alice_central.pending_commit(&id).await.is_some());
735
736 let add_debbie_commit = bob_central
737 .context
738 .add_members_to_conversation(&id, vec![debbie.clone()])
739 .await
740 .unwrap()
741 .commit;
742 let decrypted = alice_central
743 .context
744 .decrypt_message(&id, add_debbie_commit.to_bytes().unwrap())
745 .await
746 .unwrap();
747 let members = alice_central.get_conversation_unchecked(&id).await.members();
749
750 let dc = debbie.unverified_credential();
751 let debbie_id = dc.credential.identity();
752 assert!(members.get(debbie_id).is_some());
753
754 let cc = charlie.unverified_credential();
755 let charlie_id = cc.credential.identity();
756 assert!(members.get(charlie_id).is_none());
757
758 assert!(alice_central.pending_commit(&id).await.is_none());
760 assert!(decrypted.has_epoch_changed)
761 })
762 },
763 )
764 .await
765 }
766
767 #[apply(all_cred_cipher)]
768 #[wasm_bindgen_test]
769 pub async fn decrypting_a_commit_should_renew_proposals_in_pending_commit(case: TestCase) {
770 run_test_with_client_ids(
771 case.clone(),
772 ["alice", "bob", "charlie"],
773 move |[mut alice_central, bob_central, charlie_central]| {
774 Box::pin(async move {
775 let id = conversation_id();
776 alice_central
777 .context
778 .new_conversation(&id, case.credential_type, case.cfg.clone())
779 .await
780 .unwrap();
781 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
782
783 let charlie = charlie_central.rand_key_package(&case).await;
787
788 let bob_commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
789 bob_central.context.commit_accepted(&id).await.unwrap();
790
791 alice_central
793 .context
794 .add_members_to_conversation(&id, vec![charlie.clone()])
795 .await
796 .unwrap();
797 assert!(alice_central.pending_commit(&id).await.is_some());
798
799 let MlsConversationDecryptMessage { proposals, delay, .. } = alice_central
801 .context
802 .decrypt_message(&id, bob_commit.to_bytes().unwrap())
803 .await
804 .unwrap();
805 let cc = charlie.unverified_credential();
807 let charlie_id = cc.credential.identity();
808 assert!(alice_central
809 .get_conversation_unchecked(&id)
810 .await
811 .members()
812 .get(charlie_id)
813 .is_none());
814 assert!(delay.is_some());
816
817 assert!(!proposals.is_empty());
819 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
820 assert!(alice_central.pending_commit(&id).await.is_none());
821
822 for p in proposals {
824 bob_central
826 .context
827 .decrypt_message(&id, p.proposal.to_bytes().unwrap())
828 .await
829 .unwrap();
830 }
831
832 let MlsCommitBundle { commit, welcome, .. } = alice_central
833 .context
834 .commit_pending_proposals(&id)
835 .await
836 .unwrap()
837 .unwrap();
838 alice_central.context.commit_accepted(&id).await.unwrap();
839 assert!(alice_central
841 .get_conversation_unchecked(&id)
842 .await
843 .members()
844 .get(charlie_id)
845 .is_some());
846
847 let decrypted = bob_central
848 .context
849 .decrypt_message(&id, commit.to_bytes().unwrap())
850 .await
851 .unwrap();
852 let cc = charlie.unverified_credential();
854 let charlie_id = cc.credential.identity();
855 assert!(bob_central
856 .get_conversation_unchecked(&id)
857 .await
858 .members()
859 .get(charlie_id)
860 .is_some());
861 assert!(decrypted.has_epoch_changed);
862
863 let id = charlie_central
865 .context
866 .process_welcome_message(welcome.unwrap().into(), case.custom_cfg())
867 .await
868 .unwrap()
869 .id;
870 assert!(charlie_central.try_talk_to(&id, &alice_central).await.is_ok());
871 })
872 },
873 )
874 .await
875 }
876
877 #[apply(all_cred_cipher)]
878 #[wasm_bindgen_test]
879 pub async fn decrypting_a_commit_should_not_renew_proposals_in_valid_commit(case: TestCase) {
880 run_test_with_client_ids(
881 case.clone(),
882 ["alice", "bob", "charlie"],
883 move |[mut alice_central, bob_central, charlie_central]| {
884 Box::pin(async move {
885 let id = conversation_id();
886 alice_central
887 .context
888 .new_conversation(&id, case.credential_type, case.cfg.clone())
889 .await
890 .unwrap();
891 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
892
893 let charlie_kp = charlie_central.get_one_key_package(&case).await;
898
899 let add_charlie_proposal = bob_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
900 alice_central
901 .context
902 .decrypt_message(&id, add_charlie_proposal.proposal.to_bytes().unwrap())
903 .await
904 .unwrap();
905
906 let MlsCommitBundle { commit, .. } =
907 bob_central.context.update_keying_material(&id).await.unwrap();
908 let MlsConversationDecryptMessage {
909 proposals,
910 delay,
911 has_epoch_changed,
912 ..
913 } = alice_central
914 .context
915 .decrypt_message(&id, commit.to_bytes().unwrap())
916 .await
917 .unwrap();
918 assert!(proposals.is_empty());
919 assert!(delay.is_none());
920 assert!(alice_central.pending_proposals(&id).await.is_empty());
921 assert!(has_epoch_changed)
922 })
923 },
924 )
925 .await
926 }
927
928 #[apply(all_cred_cipher)]
930 #[wasm_bindgen_test]
931 pub async fn decrypting_a_commit_should_renew_orphan_pending_proposals(case: TestCase) {
932 run_test_with_client_ids(
933 case.clone(),
934 ["alice", "bob", "charlie"],
935 move |[mut alice_central, mut bob_central, charlie_central]| {
936 Box::pin(async move {
937 let id = conversation_id();
938 alice_central
939 .context
940 .new_conversation(&id, case.credential_type, case.cfg.clone())
941 .await
942 .unwrap();
943 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
944
945 let bob_commit = bob_central.context.update_keying_material(&id).await.unwrap().commit;
949 bob_central.context.commit_accepted(&id).await.unwrap();
950 let commit_epoch = bob_commit.epoch().unwrap();
951
952 let charlie_kp = charlie_central.get_one_key_package(&case).await;
954 alice_central.context.new_add_proposal(&id, charlie_kp).await.unwrap();
955 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
956
957 let MlsConversationDecryptMessage { proposals, delay, .. } = alice_central
959 .context
960 .decrypt_message(&id, bob_commit.to_bytes().unwrap())
961 .await
962 .unwrap();
963 assert!(alice_central
965 .get_conversation_unchecked(&id)
966 .await
967 .members()
968 .get(&b"charlie".to_vec())
969 .is_none());
970 assert!(delay.is_some());
972
973 assert!(!proposals.is_empty());
975 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
976 let renewed_proposal = proposals.first().unwrap();
977 assert_eq!(
978 commit_epoch.as_u64() + 1,
979 renewed_proposal.proposal.epoch().unwrap().as_u64()
980 );
981
982 bob_central
984 .context
985 .decrypt_message(&id, renewed_proposal.proposal.to_bytes().unwrap())
986 .await
987 .unwrap();
988 assert_eq!(bob_central.pending_proposals(&id).await.len(), 1);
989 let MlsCommitBundle { commit, .. } = bob_central
990 .context
991 .commit_pending_proposals(&id)
992 .await
993 .unwrap()
994 .unwrap();
995 let decrypted = alice_central
996 .context
997 .decrypt_message(&id, commit.to_bytes().unwrap())
998 .await
999 .unwrap();
1000 assert!(alice_central
1002 .get_conversation_unchecked(&id)
1003 .await
1004 .members()
1005 .get::<Vec<u8>>(&charlie_central.get_client_id().await.to_vec())
1006 .is_some());
1007
1008 bob_central.context.commit_accepted(&id).await.unwrap();
1010 assert!(bob_central
1011 .get_conversation_unchecked(&id)
1012 .await
1013 .members()
1014 .get::<Vec<u8>>(&charlie_central.get_client_id().await.to_vec())
1015 .is_some());
1016 assert!(decrypted.has_epoch_changed);
1017 })
1018 },
1019 )
1020 .await
1021 }
1022
1023 #[apply(all_cred_cipher)]
1024 #[wasm_bindgen_test]
1025 pub async fn decrypting_a_commit_should_discard_pending_external_proposals(case: TestCase) {
1026 run_test_with_client_ids(
1027 case.clone(),
1028 ["alice", "bob", "charlie"],
1029 move |[mut alice_central, bob_central, charlie_central]| {
1030 Box::pin(async move {
1031 let id = conversation_id();
1032 alice_central
1033 .context
1034 .new_conversation(&id, case.credential_type, case.cfg.clone())
1035 .await
1036 .unwrap();
1037 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1038
1039 let ext_proposal = charlie_central
1044 .context
1045 .new_external_add_proposal(
1046 id.clone(),
1047 alice_central.get_conversation_unchecked(&id).await.group.epoch(),
1048 case.ciphersuite(),
1049 case.credential_type,
1050 )
1051 .await
1052 .unwrap();
1053 assert!(alice_central.pending_proposals(&id).await.is_empty());
1054 alice_central
1055 .context
1056 .decrypt_message(&id, ext_proposal.to_bytes().unwrap())
1057 .await
1058 .unwrap();
1059 assert_eq!(alice_central.pending_proposals(&id).await.len(), 1);
1060
1061 let MlsCommitBundle { commit, .. } =
1062 bob_central.context.update_keying_material(&id).await.unwrap();
1063 let alice_renewed_proposals = alice_central
1064 .context
1065 .decrypt_message(&id, commit.to_bytes().unwrap())
1066 .await
1067 .unwrap()
1068 .proposals;
1069 assert!(alice_renewed_proposals.is_empty());
1070 assert!(alice_central.pending_proposals(&id).await.is_empty());
1071 })
1072 },
1073 )
1074 .await
1075 }
1076
1077 #[apply(all_cred_cipher)]
1078 #[wasm_bindgen_test]
1079 async fn should_not_return_sender_client_id(case: TestCase) {
1080 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1081 Box::pin(async move {
1082 let id = conversation_id();
1083 alice_central
1084 .context
1085 .new_conversation(&id, case.credential_type, case.cfg.clone())
1086 .await
1087 .unwrap();
1088 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1089
1090 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1091
1092 let sender_client_id = bob_central
1093 .context
1094 .decrypt_message(&id, commit.to_bytes().unwrap())
1095 .await
1096 .unwrap()
1097 .sender_client_id;
1098 assert!(sender_client_id.is_none());
1099 })
1100 })
1101 .await
1102 }
1103 }
1104
1105 mod external_proposal {
1106 use super::*;
1107 use std::sync::Arc;
1108
1109 #[apply(all_cred_cipher)]
1110 #[wasm_bindgen_test]
1111 async fn can_decrypt_external_proposal(case: TestCase) {
1112 run_test_with_client_ids(
1113 case.clone(),
1114 ["alice", "bob", "alice2"],
1115 move |[alice_central, bob_central, alice2_central]| {
1116 Box::pin(async move {
1117 let id = conversation_id();
1118 alice_central
1119 .context
1120 .new_conversation(&id, case.credential_type, case.cfg.clone())
1121 .await
1122 .unwrap();
1123 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1124
1125 let epoch = alice_central.get_conversation_unchecked(&id).await.group.epoch();
1126 let ext_proposal = alice2_central
1127 .context
1128 .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
1129 .await
1130 .unwrap();
1131
1132 let decrypted = alice_central
1133 .context
1134 .decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
1135 .await
1136 .unwrap();
1137 assert!(decrypted.app_msg.is_none());
1138 assert!(decrypted.delay.is_some());
1139
1140 let decrypted = bob_central
1141 .context
1142 .decrypt_message(&id, &ext_proposal.to_bytes().unwrap())
1143 .await
1144 .unwrap();
1145 assert!(decrypted.app_msg.is_none());
1146 assert!(decrypted.delay.is_some());
1147 assert!(!decrypted.has_epoch_changed)
1148 })
1149 },
1150 )
1151 .await
1152 }
1153
1154 #[apply(all_cred_cipher)]
1155 #[wasm_bindgen_test]
1156 async fn cannot_decrypt_proposal_no_callback(case: TestCase) {
1157 run_test_with_client_ids(
1158 case.clone(),
1159 ["alice", "bob", "alice2"],
1160 move |[alice_central, bob_central, alice2_central]| {
1161 Box::pin(async move {
1162 let id = conversation_id();
1163
1164 alice_central
1165 .context
1166 .new_conversation(&id, case.credential_type, case.cfg.clone())
1167 .await
1168 .unwrap();
1169 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1170
1171 let epoch = alice_central.get_conversation_unchecked(&id).await.group.epoch();
1172 let message = alice2_central
1173 .context
1174 .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
1175 .await
1176 .unwrap();
1177
1178 alice_central.context.set_callbacks(None).await.unwrap();
1179 let error = alice_central
1180 .context
1181 .decrypt_message(&id, &message.to_bytes().unwrap())
1182 .await
1183 .unwrap_err();
1184
1185 assert!(matches!(error, CryptoError::CallbacksNotSet));
1186
1187 bob_central.context.set_callbacks(None).await.unwrap();
1188 let error = bob_central
1189 .context
1190 .decrypt_message(&id, &message.to_bytes().unwrap())
1191 .await
1192 .unwrap_err();
1193
1194 assert!(matches!(error, CryptoError::CallbacksNotSet));
1195 })
1196 },
1197 )
1198 .await
1199 }
1200
1201 #[apply(all_cred_cipher)]
1202 #[wasm_bindgen_test]
1203 async fn cannot_decrypt_proposal_validation(case: TestCase) {
1204 run_test_with_client_ids(
1205 case.clone(),
1206 ["alice", "bob", "alice2"],
1207 move |[alice_central, bob_central, alice2_central]| {
1208 Box::pin(async move {
1209 let id = conversation_id();
1210 alice_central
1211 .context
1212 .set_callbacks(Some(Arc::new(ValidationCallbacks {
1213 client_is_existing_group_user: false,
1214 ..Default::default()
1215 })))
1216 .await
1217 .unwrap();
1218
1219 alice_central
1220 .context
1221 .new_conversation(&id, case.credential_type, case.cfg.clone())
1222 .await
1223 .unwrap();
1224 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1225
1226 let epoch = alice_central.get_conversation_unchecked(&id).await.group.epoch();
1227 let external_proposal = alice2_central
1228 .context
1229 .new_external_add_proposal(id.clone(), epoch, case.ciphersuite(), case.credential_type)
1230 .await
1231 .unwrap();
1232
1233 let error = alice_central
1234 .context
1235 .decrypt_message(&id, &external_proposal.to_bytes().unwrap())
1236 .await
1237 .unwrap_err();
1238
1239 assert!(matches!(error, CryptoError::UnauthorizedExternalAddProposal));
1240
1241 bob_central.context.set_callbacks(None).await.unwrap();
1242 let error = bob_central
1243 .context
1244 .decrypt_message(&id, &external_proposal.to_bytes().unwrap())
1245 .await
1246 .unwrap_err();
1247
1248 assert!(matches!(error, CryptoError::CallbacksNotSet));
1249 })
1250 },
1251 )
1252 .await
1253 }
1254 }
1255
1256 mod proposal {
1257 use super::*;
1258
1259 #[apply(all_cred_cipher)]
1261 #[wasm_bindgen_test]
1262 async fn can_decrypt_proposal(case: TestCase) {
1263 run_test_with_client_ids(
1264 case.clone(),
1265 ["alice", "bob", "charlie"],
1266 move |[alice_central, bob_central, charlie_central]| {
1267 Box::pin(async move {
1268 let id = conversation_id();
1269 alice_central
1270 .context
1271 .new_conversation(&id, case.credential_type, case.cfg.clone())
1272 .await
1273 .unwrap();
1274 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1275
1276 let charlie_kp = charlie_central.get_one_key_package(&case).await;
1277 let proposal = alice_central
1278 .context
1279 .new_add_proposal(&id, charlie_kp)
1280 .await
1281 .unwrap()
1282 .proposal;
1283
1284 let decrypted = bob_central
1285 .context
1286 .decrypt_message(&id, proposal.to_bytes().unwrap())
1287 .await
1288 .unwrap();
1289
1290 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 2);
1291 bob_central
1293 .context
1294 .commit_pending_proposals(&id)
1295 .await
1296 .unwrap()
1297 .unwrap();
1298 bob_central.context.commit_accepted(&id).await.unwrap();
1299 assert_eq!(bob_central.get_conversation_unchecked(&id).await.members().len(), 3);
1300 assert!(!decrypted.has_epoch_changed);
1301
1302 alice_central.verify_sender_identity(&case, &decrypted).await;
1303 })
1304 },
1305 )
1306 .await
1307 }
1308
1309 #[apply(all_cred_cipher)]
1310 #[wasm_bindgen_test]
1311 async fn should_not_return_sender_client_id(case: TestCase) {
1312 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1313 Box::pin(async move {
1314 let id = conversation_id();
1315 alice_central
1316 .context
1317 .new_conversation(&id, case.credential_type, case.cfg.clone())
1318 .await
1319 .unwrap();
1320 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1321
1322 let proposal = alice_central.context.new_update_proposal(&id).await.unwrap().proposal;
1323
1324 let sender_client_id = bob_central
1325 .context
1326 .decrypt_message(&id, proposal.to_bytes().unwrap())
1327 .await
1328 .unwrap()
1329 .sender_client_id;
1330 assert!(sender_client_id.is_none());
1331 })
1332 })
1333 .await
1334 }
1335 }
1336
1337 mod app_message {
1338 use super::*;
1339
1340 #[apply(all_cred_cipher)]
1341 #[wasm_bindgen_test]
1342 async fn can_decrypt_app_message(case: TestCase) {
1343 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1344 Box::pin(async move {
1345 let id = conversation_id();
1346 alice_central
1347 .context
1348 .new_conversation(&id, case.credential_type, case.cfg.clone())
1349 .await
1350 .unwrap();
1351 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1352
1353 let msg = b"Hello bob";
1354 let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
1355 assert_ne!(&msg[..], &encrypted[..]);
1356 let decrypted = bob_central.context.decrypt_message(&id, encrypted).await.unwrap();
1357 let dec_msg = decrypted.app_msg.as_ref().unwrap().as_slice();
1358 assert_eq!(dec_msg, &msg[..]);
1359 assert!(!decrypted.has_epoch_changed);
1360 alice_central.verify_sender_identity(&case, &decrypted).await;
1361
1362 let msg = b"Hello alice";
1363 let encrypted = bob_central.context.encrypt_message(&id, msg).await.unwrap();
1364 assert_ne!(&msg[..], &encrypted[..]);
1365 let decrypted = alice_central.context.decrypt_message(&id, encrypted).await.unwrap();
1366 let dec_msg = decrypted.app_msg.as_ref().unwrap().as_slice();
1367 assert_eq!(dec_msg, &msg[..]);
1368 assert!(!decrypted.has_epoch_changed);
1369 bob_central.verify_sender_identity(&case, &decrypted).await;
1370 })
1371 })
1372 .await
1373 }
1374
1375 #[apply(all_cred_cipher)]
1376 #[wasm_bindgen_test]
1377 async fn cannot_decrypt_app_message_after_rejoining(case: TestCase) {
1378 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1379 Box::pin(async move {
1380 let id = conversation_id();
1381 alice_central
1382 .context
1383 .new_conversation(&id, case.credential_type, case.cfg.clone())
1384 .await
1385 .unwrap();
1386 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1387
1388 let msg = b"Hello bob";
1390 let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
1391
1392 let gi = alice_central.get_group_info(&id).await;
1395 bob_central
1396 .context
1397 .join_by_external_commit(gi, case.custom_cfg(), case.credential_type)
1398 .await
1399 .unwrap();
1400 bob_central
1401 .context
1402 .merge_pending_group_from_external_commit(&id)
1403 .await
1404 .unwrap();
1405
1406 let decrypt = bob_central.context.decrypt_message(&id, &encrypted).await;
1408 assert!(matches!(decrypt.unwrap_err(), CryptoError::DecryptionError));
1409 })
1410 })
1411 .await
1412 }
1413
1414 #[apply(all_cred_cipher)]
1415 #[wasm_bindgen_test]
1416 async fn cannot_decrypt_app_message_from_future_epoch(case: TestCase) {
1417 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1418 Box::pin(async move {
1419 let id = conversation_id();
1420 alice_central
1421 .context
1422 .new_conversation(&id, case.credential_type, case.cfg.clone())
1423 .await
1424 .unwrap();
1425 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1426
1427 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1429 alice_central.context.commit_accepted(&id).await.unwrap();
1430
1431 let msg = b"Hello bob";
1433 let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
1434
1435 let decrypt = bob_central.context.decrypt_message(&id, &encrypted).await;
1437 assert!(matches!(
1438 decrypt.unwrap_err(),
1439 CryptoError::BufferedFutureMessage { .. }
1440 ));
1441
1442 let decrypted_commit = bob_central
1443 .context
1444 .decrypt_message(&id, commit.to_bytes().unwrap())
1445 .await
1446 .unwrap();
1447 let buffered_msg = decrypted_commit.buffered_messages.unwrap();
1448 let decrypted_msg = buffered_msg.first().unwrap().app_msg.clone().unwrap();
1449 assert_eq!(&decrypted_msg, msg);
1450 })
1451 })
1452 .await
1453 }
1454
1455 #[apply(all_cred_cipher)]
1456 #[wasm_bindgen_test]
1457 async fn can_decrypt_app_message_in_any_order(mut case: TestCase) {
1458 case.cfg.custom.maximum_forward_distance = 0;
1461 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1462 Box::pin(async move {
1463 let id = conversation_id();
1464 alice_central
1465 .context
1466 .new_conversation(&id, case.credential_type, case.cfg.clone())
1467 .await
1468 .unwrap();
1469 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1470
1471 let out_of_order_tolerance = case.custom_cfg().out_of_order_tolerance;
1472 let nb_messages = out_of_order_tolerance * 2;
1473 let mut messages = vec![];
1474
1475 for i in 0..nb_messages {
1477 let msg = format!("Hello {i}");
1478 let encrypted = alice_central.context.encrypt_message(&id, &msg).await.unwrap();
1479 messages.push((msg, encrypted));
1480 }
1481
1482 messages.reverse();
1484 for (i, (original, encrypted)) in messages.iter().enumerate() {
1485 let decrypt = bob_central.context.decrypt_message(&id, encrypted).await;
1486 if i > out_of_order_tolerance as usize {
1487 let decrypted = decrypt.unwrap().app_msg.unwrap();
1488 assert_eq!(decrypted, original.as_bytes());
1489 } else {
1490 assert!(matches!(decrypt.unwrap_err(), CryptoError::DuplicateMessage))
1491 }
1492 }
1493 })
1494 })
1495 .await
1496 }
1497
1498 #[apply(all_cred_cipher)]
1499 #[wasm_bindgen_test]
1500 async fn returns_sender_client_id(case: TestCase) {
1501 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1502 Box::pin(async move {
1503 let id = conversation_id();
1504 alice_central
1505 .context
1506 .new_conversation(&id, case.credential_type, case.cfg.clone())
1507 .await
1508 .unwrap();
1509 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1510
1511 let msg = b"Hello bob";
1512 let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
1513 assert_ne!(&msg[..], &encrypted[..]);
1514
1515 let sender_client_id = bob_central
1516 .context
1517 .decrypt_message(&id, encrypted)
1518 .await
1519 .unwrap()
1520 .sender_client_id
1521 .unwrap();
1522 assert_eq!(sender_client_id, alice_central.get_client_id().await);
1523 })
1524 })
1525 .await
1526 }
1527 }
1528
1529 mod epoch_sync {
1530 use super::*;
1531
1532 #[apply(all_cred_cipher)]
1533 #[wasm_bindgen_test]
1534 async fn should_throw_specialized_error_when_epoch_too_old(mut case: TestCase) {
1535 case.cfg.custom.out_of_order_tolerance = 0;
1536 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1537 Box::pin(async move {
1538 let id = conversation_id();
1539 alice_central
1540 .context
1541 .new_conversation(&id, case.credential_type, case.cfg.clone())
1542 .await
1543 .unwrap();
1544 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1545
1546 let bob_message1 = alice_central.context.encrypt_message(&id, b"Hello Bob").await.unwrap();
1548 let bob_message2 = alice_central
1549 .context
1550 .encrypt_message(&id, b"Hello again Bob")
1551 .await
1552 .unwrap();
1553
1554 for _ in 0..MAX_PAST_EPOCHS {
1556 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1557 alice_central.context.commit_accepted(&id).await.unwrap();
1558 bob_central
1559 .context
1560 .decrypt_message(&id, commit.to_bytes().unwrap())
1561 .await
1562 .unwrap();
1563 }
1564 let decrypt = bob_central.context.decrypt_message(&id, &bob_message1).await.unwrap();
1566 assert_eq!(decrypt.app_msg.unwrap(), b"Hello Bob");
1567
1568 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1570 alice_central.context.commit_accepted(&id).await.unwrap();
1571 bob_central
1572 .context
1573 .decrypt_message(&id, commit.to_bytes().unwrap())
1574 .await
1575 .unwrap();
1576
1577 let decrypt = bob_central.context.decrypt_message(&id, &bob_message2).await;
1578 assert!(matches!(decrypt.unwrap_err(), CryptoError::MessageEpochTooOld));
1579 })
1580 })
1581 .await
1582 }
1583
1584 #[apply(all_cred_cipher)]
1585 #[wasm_bindgen_test]
1586 async fn should_throw_specialized_error_when_epoch_desynchronized(mut case: TestCase) {
1587 case.cfg.custom.out_of_order_tolerance = 0;
1588 run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
1589 Box::pin(async move {
1590 let id = conversation_id();
1591 alice_central
1592 .context
1593 .new_conversation(&id, case.credential_type, case.cfg.clone())
1594 .await
1595 .unwrap();
1596 alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
1597
1598 let old_proposal = alice_central
1600 .context
1601 .new_update_proposal(&id)
1602 .await
1603 .unwrap()
1604 .proposal
1605 .to_bytes()
1606 .unwrap();
1607 alice_central
1608 .get_conversation_unchecked(&id)
1609 .await
1610 .group
1611 .clear_pending_proposals();
1612 let old_commit = alice_central
1613 .context
1614 .update_keying_material(&id)
1615 .await
1616 .unwrap()
1617 .commit
1618 .to_bytes()
1619 .unwrap();
1620 alice_central.context.clear_pending_commit(&id).await.unwrap();
1621
1622 let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1624 alice_central.context.commit_accepted(&id).await.unwrap();
1625 bob_central
1626 .context
1627 .decrypt_message(&id, commit.to_bytes().unwrap())
1628 .await
1629 .unwrap();
1630
1631 let decrypt_err = bob_central
1633 .context
1634 .decrypt_message(&id, &old_proposal)
1635 .await
1636 .unwrap_err();
1637
1638 assert!(matches!(decrypt_err, CryptoError::StaleProposal));
1639
1640 let decrypt_err = bob_central.context.decrypt_message(&id, &old_commit).await.unwrap_err();
1641
1642 assert!(matches!(decrypt_err, CryptoError::StaleCommit));
1643 })
1644 })
1645 .await
1646 }
1647 }
1648}