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::Session};
21
22/// Abstraction over a MLS group capable of merging a commit
23impl MlsConversation {
24    /// see [TransactionContext::commit_accepted]
25    #[cfg_attr(test, crate::durable)]
26    pub(crate) async fn commit_accepted(&mut self, client: &Session, 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: TestContext) {
64            let [alice, bob] = case.sessions().await;
65            Box::pin(async move {
66                let conversation = case.create_conversation([&alice, &bob]).await;
67                let commit_guard = conversation.update_unmerged().await.notify_member(&bob).await;
68                let conversation = commit_guard.conversation();
69
70                assert!(conversation.has_pending_commit().await);
71
72                conversation
73                    .guard()
74                    .await
75                    .conversation_mut()
76                    .await
77                    .commit_accepted(
78                        &alice.transaction.session().await.unwrap(),
79                        &alice.session.crypto_provider,
80                    )
81                    .await
82                    .unwrap();
83
84                assert_eq!(conversation.member_count().await, 2);
85                assert!(conversation.is_functional_and_contains([&alice, &bob]).await);
86            })
87            .await
88        }
89
90        #[apply(all_cred_cipher)]
91        #[wasm_bindgen_test]
92        async fn should_clear_pending_commit_and_proposals(case: TestContext) {
93            let [alice] = case.sessions().await;
94            Box::pin(async move {
95                let commit = case
96                    .create_conversation([&alice])
97                    .await
98                    .update_proposal_notify()
99                    .await
100                    .update_unmerged()
101                    .await;
102
103                let conversation = commit.conversation();
104
105                assert!(conversation.has_pending_proposals().await);
106                assert!(conversation.has_pending_commit().await);
107
108                conversation
109                    .guard()
110                    .await
111                    .conversation_mut()
112                    .await
113                    .commit_accepted(
114                        &alice.transaction.session().await.unwrap(),
115                        &alice.session.crypto_provider,
116                    )
117                    .await
118                    .unwrap();
119                assert!(!conversation.has_pending_proposals().await);
120                assert!(!conversation.has_pending_commit().await);
121            })
122            .await
123        }
124
125        #[apply(all_cred_cipher)]
126        #[wasm_bindgen_test]
127        async fn should_clean_associated_key_material(case: TestContext) {
128            let [alice] = case.sessions().await;
129            Box::pin(async move {
130                let conversation = case.create_conversation([&alice]).await;
131                let initial_count = alice.transaction.count_entities().await;
132
133                let conversation = conversation.update_proposal_notify().await;
134                let post_proposal_count = alice.transaction.count_entities().await;
135                assert_eq!(
136                    post_proposal_count.encryption_keypair,
137                    initial_count.encryption_keypair + 1
138                );
139
140                conversation.commit_pending_proposals_notify().await;
141
142                let final_count = alice.transaction.count_entities().await;
143                assert_eq!(initial_count, final_count);
144            })
145            .await
146        }
147    }
148}