core_crypto/mls/conversation/
decrypt.rs

1//! MLS defines 3 kind of messages: Proposal, Commits and Application messages. Since they can (should)
2//! be all encrypted we need to first decrypt them before deciding what to do with them.
3//!
4//! This table summarizes when a MLS group can decrypt any message:
5//!
6//! | can decrypt ?     | 0 pend. Commit | 1 pend. Commit |
7//! |-------------------|----------------|----------------|
8//! | 0 pend. Proposal  | ✅              | ✅              |
9//! | 1+ pend. Proposal | ✅              | ✅              |
10
11use 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/// Represents the potential items a consumer might require after passing us an encrypted message we
51/// have decrypted for him
52#[derive(Debug)]
53pub struct MlsConversationDecryptMessage {
54    /// Decrypted text message
55    pub app_msg: Option<Vec<u8>>,
56    /// Only when decrypted message is a commit, CoreCrypto will renew local proposal which could not make it in the commit.
57    /// This will contain either:
58    /// * local pending proposal not in the accepted commit
59    /// * If there is a pending commit, its proposals which are not in the accepted commit
60    pub proposals: Vec<MlsProposalBundle>,
61    /// Is the conversation still active after receiving this commit aka has the user been removed from the group
62    pub is_active: bool,
63    /// Delay time in seconds to feed caller timer for committing
64    pub delay: Option<u64>,
65    /// [ClientId] of the sender of the message being decrypted. Only present for application messages.
66    pub sender_client_id: Option<ClientId>,
67    /// Is the epoch changed after decrypting this message
68    pub has_epoch_changed: bool,
69    /// Identity claims present in the sender credential
70    /// Present for all messages
71    pub identity: WireIdentity,
72    /// Only set when the decrypted message is a commit.
73    /// Contains buffered messages for next epoch which were received before the commit creating the epoch
74    /// because the DS did not fan them out in order.
75    pub buffered_messages: Option<Vec<MlsBufferedConversationDecryptMessage>>,
76    /// New CRL distribution points that appeared by the introduction of a new credential
77    pub crl_new_distribution_points: NewCrlDistributionPoint,
78}
79
80/// Type safe recursion of [MlsConversationDecryptMessage]
81#[derive(Debug)]
82pub struct MlsBufferedConversationDecryptMessage {
83    /// see [MlsConversationDecryptMessage]
84    pub app_msg: Option<Vec<u8>>,
85    /// see [MlsConversationDecryptMessage]
86    pub proposals: Vec<MlsProposalBundle>,
87    /// see [MlsConversationDecryptMessage]
88    pub is_active: bool,
89    /// see [MlsConversationDecryptMessage]
90    pub delay: Option<u64>,
91    /// see [MlsConversationDecryptMessage]
92    pub sender_client_id: Option<ClientId>,
93    /// see [MlsConversationDecryptMessage]
94    pub has_epoch_changed: bool,
95    /// see [MlsConversationDecryptMessage]
96    pub identity: WireIdentity,
97    /// see [MlsConversationDecryptMessage]
98    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
122/// Abstraction over a MLS group capable of decrypting a MLS message
123impl MlsConversation {
124    /// see [CentralContext::decrypt_message]
125    #[allow(clippy::too_many_arguments)]
126    #[cfg_attr(test, crate::durable)]
127    // FIXME: this might be causing stack overflow. Retry when this is solved: https://github.com/tokio-rs/tracing/issues/1147. Tracking issue: WPB-9654
128    // #[cfg_attr(not(test), tracing::instrument(err, skip_all))]
129    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        // Handles the case where we receive our own commits.
143        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            // can't use `.then` because async
150            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        // In this error case, we have a missing proposal, so we need to buffer the commit.
164        // We can't do that here--we don't have the appropriate data in scope--but we can at least
165        // produce the proper error and return that, so our caller can handle it.
166        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 we got back a buffered commit error, then we still don't have enough proposals.
228                    // In that case, we want to just proceed as normal for this proposal.
229                    //
230                    // In any other case, the result from the commit overrides the result from the proposal.
231                    if !matches!(process_result, Err(CryptoError::BufferedCommit)) {
232                        // either the commit applied successfully, in which case its return value
233                        // should override the return value from the proposal, or it raised some kind
234                        // of error, in which case the caller needs to know about that.
235                        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                // - This requires a change in OpenMLS to get access to it
275                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                // getting the pending has to be done before `merge_staged_commit` otherwise it's wiped out
281                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                // can't use `.then` because async
299                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    /// Cache the bytes of a pending commit in the backend.
359    ///
360    /// By storing the raw commit bytes and doing deserialization/decryption from scratch, we preserve all
361    /// security guarantees. When we do restore, it's as though the commit had simply been received later.
362    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    /// Retrieve the bytes of a pending commit.
372    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    /// Try to apply a buffered commit.
384    ///
385    /// This is largely a convenience function which handles deserializing the message, and
386    /// gives a convenient point around which we can add context to errors. However, it's also
387    /// a place where we can introduce a pin, given that we're otherwise doing a recursive
388    /// async call, which would result in an infinitely-sized future.
389    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    /// Remove the buffered commit for this conversation; it has been applied.
405    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                        // limit to next epoch otherwise if we were buffering a commit for epoch + 2
483                        // we would fail when trying to decrypt it in [MlsCentral::commit_accepted]
484                        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                // FIXME: Uncomment when PKI env can be seeded - the computation is still done to assess performance and impact of the validations. Tracking issue: WPB-9665
530                // return Err(CryptoError::InvalidCertificateChain);
531            }
532        }
533        Ok(())
534    }
535}
536
537impl CentralContext {
538    /// Deserializes a TLS-serialized message, then deciphers it
539    ///
540    /// # Arguments
541    /// * `conversation` - the group/conversation id
542    /// * `message` - the encrypted message as a byte array
543    ///
544    /// # Return type
545    /// This method will return a tuple containing an optional message and an optional delay time
546    /// for the callers to wait for committing. A message will be `None` in case the provided payload in
547    /// case of a system message, such as Proposals and Commits. Otherwise it will return the message as a
548    /// byte array. The delay will be `Some` when the message has a proposal
549    ///
550    /// # Errors
551    /// If the conversation can't be found, an error will be returned. Other errors are originating
552    /// from OpenMls and the KeyStore
553    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        // In the inner `decrypt_message` above, we raise the `BufferedCommit` error, but we only handle it here.
580        // That's because in that scope we don't have access to the raw message bytes; here, we do.
581        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                        // Alice creates a commit which will be superseded by Bob's one
727                        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                        // Now Debbie should be in members and not Charlie
748                        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                        // Previous commit to add Charlie has been discarded but its proposals will be renewed
759                        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                        // Alice will create a commit to add Charlie
784                        // Bob will create a commit which will be accepted first by DS so Alice will decrypt it
785                        // Then Alice will renew the proposal in her pending commit
786                        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 propose to add Charlie
792                        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                        // But first she receives Bob commit
800                        let MlsConversationDecryptMessage { proposals, delay, .. } = alice_central
801                            .context
802                            .decrypt_message(&id, bob_commit.to_bytes().unwrap())
803                            .await
804                            .unwrap();
805                        // So Charlie has not been added to the group
806                        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                        // Make sure we are suggesting a commit delay
815                        assert!(delay.is_some());
816
817                        // But its proposal to add Charlie has been renewed and is also in store
818                        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                        // Let's commit this proposal to see if it works
823                        for p in proposals {
824                            // But first, proposals have to be fan out to Bob
825                            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                        // Charlie is now in the group
840                        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                        // Bob also has Charlie in the group
853                        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                        // Charlie can join with the Welcome from renewed Add proposal
864                        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                        // Bob will create a proposal to add Charlie
894                        // Alice will decrypt this proposal
895                        // Then Bob will create a commit to update
896                        // Alice will decrypt the commit but musn't renew the proposal to add Charlie
897                        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        // orphan proposal = not backed by the pending commit
929        #[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                        // Alice will create a proposal to add Charlie
946                        // Bob will create a commit which Alice will decrypt
947                        // Then Alice will renew her proposal
948                        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                        // Alice propose to add Charlie
953                        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                        // But first she receives Bob commit
958                        let MlsConversationDecryptMessage { proposals, delay, .. } = alice_central
959                            .context
960                            .decrypt_message(&id, bob_commit.to_bytes().unwrap())
961                            .await
962                            .unwrap();
963                        // So Charlie has not been added to the group
964                        assert!(alice_central
965                            .get_conversation_unchecked(&id)
966                            .await
967                            .members()
968                            .get(&b"charlie".to_vec())
969                            .is_none());
970                        // Make sure we are suggesting a commit delay
971                        assert!(delay.is_some());
972
973                        // But its proposal to add Charlie has been renewed and is also in store
974                        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                        // Let's use this proposal to see if it works
983                        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                        // Charlie is now in the group
1001                        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 also has Charlie in the group
1009                        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                        // DS will create an external proposal to add Charlie
1040                        // But meanwhile Bob, before receiving the external proposal,
1041                        // will create a commit and send it to Alice.
1042                        // Alice will not renew the external proposal
1043                        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        // Ensures decrypting an proposal is durable
1260        #[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                        // if 'decrypt_message' is not durable the commit won't contain the add proposal
1292                        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                    // encrypt a message in epoch 1
1389                    let msg = b"Hello bob";
1390                    let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
1391
1392                    // Now Bob will rejoin the group and try to decrypt Alice's message
1393                    // in epoch 2 which should fail
1394                    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                    // fails because of Forward Secrecy
1407                    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                    // only Alice will change epoch without notifying Bob
1428                    let commit = alice_central.context.update_keying_material(&id).await.unwrap().commit;
1429                    alice_central.context.commit_accepted(&id).await.unwrap();
1430
1431                    // Now in epoch 2 Alice will encrypt a message
1432                    let msg = b"Hello bob";
1433                    let encrypted = alice_central.context.encrypt_message(&id, msg).await.unwrap();
1434
1435                    // which Bob cannot decrypt because of Post CompromiseSecurity
1436                    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            // otherwise the test would fail because we decrypt messages in reverse order which is
1459            // kinda dropping them
1460            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                    // stack up encrypted messages..
1476                    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                    // ..then unstack them to see out_of_order_tolerance come into play
1483                    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                    // Alice encrypts a message to Bob
1547                    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                    // Move group's epoch forward by self updating
1555                    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                    // Decrypt should work
1565                    let decrypt = bob_central.context.decrypt_message(&id, &bob_message1).await.unwrap();
1566                    assert_eq!(decrypt.app_msg.unwrap(), b"Hello Bob");
1567
1568                    // Moving the epochs once more should cause an error
1569                    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                    // Alice generates a bunch of soon to be outdated messages
1599                    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                    // Now let's jump to next epoch
1623                    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                    // trying to consume outdated messages should fail with a dedicated error
1632                    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}