core_crypto/mls/conversation/
merge.rs

1//! A MLS group can be merged (aka committed) when it has a pending commit. The latter is a commit
2//! we created which is still waiting to be "committed". By doing so, we will apply all the
3//! modifications present in the commit to the ratchet tree and also persist the new group in the
4//! keystore. Like this, even if the application crashes we will be able to restore.
5//!
6//! This table summarizes when a MLS group can be merged:
7//!
8//! | can be merged ?   | 0 pend. Commit | 1 pend. Commit |
9//! |-------------------|----------------|----------------|
10//! | 0 pend. Proposal  | ❌              | ✅              |
11//! | 1+ pend. Proposal | ❌              | ✅              |
12//!
13
14use core_crypto_keystore::entities::MlsEncryptionKeyPair;
15use openmls_traits::OpenMlsCryptoProvider;
16
17use mls_crypto_provider::MlsCryptoProvider;
18
19use super::Result;
20use crate::{MlsError, mls::MlsConversation, prelude::Client};
21
22/// Abstraction over a MLS group capable of merging a commit
23impl MlsConversation {
24    /// see [CentralContext::commit_accepted]
25    #[cfg_attr(test, crate::durable)]
26    pub(crate) async fn commit_accepted(&mut self, client: &Client, backend: &MlsCryptoProvider) -> Result<()> {
27        // openmls stores here all the encryption keypairs used for update proposals..
28        let previous_own_leaf_nodes = self.group.own_leaf_nodes.clone();
29
30        self.group
31            .merge_pending_commit(backend)
32            .await
33            .map_err(MlsError::wrap("merging pending commit"))?;
34        self.persist_group_when_changed(&backend.keystore(), false).await?;
35
36        // ..so if there's any, we clear them after the commit is merged
37        for oln in &previous_own_leaf_nodes {
38            let ek = oln.encryption_key().as_slice();
39            let _ = backend.key_store().remove::<MlsEncryptionKeyPair, _>(ek).await;
40        }
41
42        client
43            .notify_epoch_changed(self.id.clone(), self.group.epoch().as_u64())
44            .await;
45
46        Ok(())
47    }
48}
49
50#[cfg(test)]
51mod tests {
52    use wasm_bindgen_test::*;
53
54    use crate::test_utils::*;
55
56    wasm_bindgen_test_configure!(run_in_browser);
57
58    mod commit_accepted {
59        use super::*;
60
61        #[apply(all_cred_cipher)]
62        #[wasm_bindgen_test]
63        async fn should_apply_pending_commit(case: TestCase) {
64            run_test_with_client_ids(case.clone(), ["alice", "bob"], move |[alice_central, bob_central]| {
65                Box::pin(async move {
66                    let id = conversation_id();
67                    alice_central
68                        .context
69                        .new_conversation(&id, case.credential_type, case.cfg.clone())
70                        .await
71                        .unwrap();
72                    alice_central.invite_all(&case, &id, [&bob_central]).await.unwrap();
73                    assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 2);
74                    alice_central
75                        .context
76                        .conversation(&id)
77                        .await
78                        .unwrap()
79                        .remove_members(&[bob_central.get_client_id().await])
80                        .await
81                        .unwrap();
82                    assert_eq!(alice_central.get_conversation_unchecked(&id).await.members().len(), 1);
83                })
84            })
85            .await
86        }
87
88        #[apply(all_cred_cipher)]
89        #[wasm_bindgen_test]
90        async fn should_clear_pending_commit_and_proposals(case: TestCase) {
91            use crate::mls::HasClientAndProvider as _;
92
93            run_test_with_client_ids(case.clone(), ["alice"], move |[mut alice_central]| {
94                Box::pin(async move {
95                    let id = conversation_id();
96                    alice_central
97                        .context
98                        .new_conversation(&id, case.credential_type, case.cfg.clone())
99                        .await
100                        .unwrap();
101                    alice_central.context.new_update_proposal(&id).await.unwrap();
102                    alice_central.create_unmerged_commit(&id).await;
103                    assert!(!alice_central.pending_proposals(&id).await.is_empty());
104                    assert!(alice_central.pending_commit(&id).await.is_some());
105                    alice_central
106                        .get_conversation_unchecked(&id)
107                        .await
108                        .commit_accepted(
109                            &alice_central.context.client().await.unwrap(),
110                            &alice_central.central.mls_backend,
111                        )
112                        .await
113                        .unwrap();
114                    assert!(alice_central.pending_commit(&id).await.is_none());
115                    assert!(alice_central.pending_proposals(&id).await.is_empty());
116                })
117            })
118            .await
119        }
120
121        #[apply(all_cred_cipher)]
122        #[wasm_bindgen_test]
123        async fn should_clean_associated_key_material(case: TestCase) {
124            run_test_with_client_ids(case.clone(), ["alice"], move |[alice_central]| {
125                Box::pin(async move {
126                    let id = conversation_id();
127                    alice_central
128                        .context
129                        .new_conversation(&id, case.credential_type, case.cfg.clone())
130                        .await
131                        .unwrap();
132
133                    let initial_count = alice_central.context.count_entities().await;
134
135                    alice_central.context.new_update_proposal(&id).await.unwrap();
136                    let post_proposal_count = alice_central.context.count_entities().await;
137                    assert_eq!(
138                        post_proposal_count.encryption_keypair,
139                        initial_count.encryption_keypair + 1
140                    );
141
142                    alice_central
143                        .context
144                        .conversation(&id)
145                        .await
146                        .unwrap()
147                        .commit_pending_proposals()
148                        .await
149                        .unwrap();
150
151                    let final_count = alice_central.context.count_entities().await;
152                    assert_eq!(initial_count, final_count);
153                })
154            })
155            .await
156        }
157    }
158}