Skip to main content

core_crypto/mls/conversation/mutable/
commit.rs

1//! The methods in this module all produce or handle commits.
2
3use std::{borrow::Borrow, collections::HashMap};
4
5use openmls::prelude::KeyPackageIn;
6
7use super::history_sharing::HistoryClientUpdateOutcome;
8use crate::{
9    ClientId, ClientIdRef, CredentialRef, GroupInfoBundle, LeafError, OpenMlsError, RecursiveError,
10    mls::{
11        conversation::{ConversationMut, Error, Result, commit::CommitBundle},
12        credential::Credential,
13    },
14};
15
16impl ConversationMut {
17    pub(super) async fn send_and_merge_commit(&mut self, commit: CommitBundle) -> Result<()> {
18        let history_client_update_result = self.update_history_client().await?;
19        if history_client_update_result == HistoryClientUpdateOutcome::CommitSentAndMerged {
20            return Ok(());
21        }
22
23        match self.send_commit(commit).await {
24            Ok(()) => self.merge_commit().await,
25            e @ Err(_) => {
26                self.clear_pending_commit().await?;
27                e
28            }
29        }
30    }
31
32    pub(super) async fn merge_commit(&mut self) -> Result<()> {
33        self.commit_accepted().await?;
34        let conversation_id = self.id().to_owned();
35        let epoch = self.epoch().await;
36
37        self.tx_context
38            .queue_epoch_changed(conversation_id, epoch)
39            .await
40            .map_err(RecursiveError::transaction("queueing epoch changed notification"))?;
41
42        Ok(())
43    }
44
45    /// Send the commit via [crate::MlsTransport] and handle the response.
46    pub(super) async fn send_commit(&mut self, commit: CommitBundle) -> Result<()> {
47        let transport = self.transport().await?;
48
49        transport
50            .send_commit_bundle(commit)
51            .await
52            .map_err(RecursiveError::root("sending commit bundle"))
53            .map_err(Into::into)
54    }
55
56    /// Adds new members to the group/conversation
57    pub async fn add_members(&mut self, key_packages: Vec<KeyPackageIn>) -> Result<()> {
58        let commit = self.add_members_inner(key_packages).await?;
59
60        self.send_and_merge_commit(commit).await?;
61
62        Ok(())
63    }
64
65    pub(super) async fn add_members_inner(&mut self, key_packages: Vec<KeyPackageIn>) -> Result<CommitBundle> {
66        self.ensure_no_pending_commit().await?;
67        let backend = self.crypto_provider().await?;
68        let credential = self.credential().await?;
69
70        self.mutate_group(async |_, group, _| {
71            let signer = credential.signature_key();
72            let (commit, welcome, group_info) = group
73                .add_members(&backend, signer, key_packages.clone())
74                .await
75                .map_err(|err| {
76                    if Self::err_is_duplicate_signature_key(&err) {
77                        let affected_clients = Self::clients_with_duplicate_signature_keys(key_packages.as_ref());
78                        Error::DuplicateSignature { affected_clients }
79                    } else {
80                        OpenMlsError::wrap("group add members")(err).into()
81                    }
82                })?;
83
84            Ok(CommitBundle {
85                commit,
86                welcome: Some(welcome),
87                group_info: Self::group_info(group_info)?,
88                encrypted_message: None,
89            })
90        })
91        .await
92    }
93
94    fn err_is_duplicate_signature_key(
95        err: &openmls::prelude::AddMembersError<core_crypto_keystore::CryptoKeystoreError>,
96    ) -> bool {
97        matches!(
98            err,
99            openmls::prelude::AddMembersError::CreateCommitError(
100                openmls::prelude::CreateCommitError::ProposalValidationError(
101                    openmls::prelude::ProposalValidationError::DuplicateSignatureKey
102                )
103            )
104        )
105    }
106
107    fn clients_with_duplicate_signature_keys(key_packages: &[KeyPackageIn]) -> Vec<(ClientId, ClientId)> {
108        let mut seen_signature_keys = HashMap::new();
109        let mut duplicate_pairs = Vec::new();
110
111        for key_package in key_packages {
112            let signature_key = key_package.unverified_credential().signature_key.as_slice().to_vec();
113
114            let client_id: ClientId = key_package.credential().identity().to_vec().into();
115
116            if let Some(previous_client_id) = seen_signature_keys.insert(signature_key, client_id.clone()) {
117                duplicate_pairs.push((previous_client_id, client_id));
118            }
119        }
120
121        duplicate_pairs
122    }
123
124    /// Removes clients from the group/conversation.
125    ///
126    /// # Arguments
127    /// * `id` - group/conversation id
128    /// * `clients` - list of client ids to be removed from the group
129    pub async fn remove_members(&mut self, clients: &[impl Borrow<ClientIdRef>]) -> Result<()> {
130        self.ensure_no_pending_commit().await?;
131        let backend = self.crypto_provider().await?;
132        let credential = self.credential().await?;
133        let signer = credential.signature_key();
134
135        let (commit, welcome, group_info) = self
136            .mutate_group(async |_, group, _| {
137                let members = group
138                    .members()
139                    .filter_map(|kp| {
140                        clients
141                            .iter()
142                            .any(move |client_id| client_id.borrow() == kp.credential.identity())
143                            .then_some(kp.index)
144                    })
145                    .collect::<Vec<_>>();
146
147                group
148                    .remove_members(&backend, signer, &members)
149                    .await
150                    .map_err(OpenMlsError::wrap("group remove members"))
151                    .map_err(Into::into)
152            })
153            .await?;
154
155        let group_info = Self::group_info(group_info)?;
156
157        self.send_and_merge_commit(CommitBundle {
158            commit,
159            welcome,
160            group_info,
161            encrypted_message: None,
162        })
163        .await
164    }
165
166    /// Self updates the own leaf node and automatically commits. Pending proposals will be committed.
167    pub async fn update_key_material(&mut self) -> Result<()> {
168        let credential = self.credential().await?;
169        let commit = self.set_credential_inner(&credential).await?;
170        self.send_and_merge_commit(commit).await
171    }
172
173    /// Set the referenced credential for this conversation.
174    pub async fn set_credential_by_ref(&mut self, credential_ref: &CredentialRef) -> Result<()> {
175        let database = self.database().await?;
176        let credential = credential_ref
177            .load(&database)
178            .await
179            .map_err(RecursiveError::mls_credential_ref("loading credential from ref"))?;
180        let commit = self.set_credential_inner(&credential).await?;
181
182        self.send_and_merge_commit(commit).await
183    }
184
185    /// Self updates the own leaf node with the given credential and automatically commits. Pending proposals will be
186    /// committed.
187    pub(crate) async fn set_credential_inner(&mut self, credential: &Credential) -> Result<CommitBundle> {
188        self.ensure_no_pending_commit().await?;
189        let backend = self.crypto_provider().await?;
190        let credential = credential.clone();
191
192        self.mutate_group(async |_, group, _| {
193            // If the credential remains the same and we still want to update, we explicitly need to pass `None` to
194            // openmls, if we just passed an unchanged leaf node, no update commit would be created.
195            // Also, we can avoid cloning in the case we don't need to create a new leaf node.
196            let updated_leaf_node = {
197                let leaf_node = group.own_leaf().ok_or(LeafError::InternalMlsError)?;
198                if leaf_node.credential() == &credential.mls_credential {
199                    None
200                } else {
201                    let mut leaf_node = leaf_node.clone();
202                    leaf_node.set_credential_with_key(credential.to_mls_credential_with_key());
203                    Some(leaf_node)
204                }
205            };
206
207            let (commit, welcome, group_info) = group
208                .explicit_self_update(&backend, &credential.signature_key_pair, updated_leaf_node)
209                .await
210                .map_err(OpenMlsError::wrap("group self update"))?;
211
212            // We should always have ratchet tree extension turned on hence GroupInfo should always be present
213            let group_info = group_info.ok_or(LeafError::MissingGroupInfo)?;
214            let group_info = GroupInfoBundle::try_new_full_plaintext(group_info)?;
215
216            Ok(CommitBundle {
217                welcome,
218                commit,
219                group_info,
220                encrypted_message: None,
221            })
222        })
223        .await
224    }
225
226    /// Commits all pending proposals of the group
227    pub async fn commit_pending_proposals(&mut self) -> Result<()> {
228        self.ensure_no_pending_commit().await?;
229        let commit = self.commit_pending_proposals_inner().await?;
230        let Some(commit) = commit else {
231            return Ok(());
232        };
233        self.send_and_merge_commit(commit).await
234    }
235
236    pub(crate) async fn commit_pending_proposals_inner(&mut self) -> Result<Option<CommitBundle>> {
237        if self.group().await.pending_proposals().next().is_none() {
238            return Ok(None);
239        }
240
241        let crypto_provider = self.crypto_provider().await?;
242        let credential = self.credential().await?;
243
244        let (commit, welcome, openmls_group_info) = self
245            .mutate_group(async |_, group, _| {
246                let signer = &credential.signature_key_pair;
247                group
248                    .commit_to_pending_proposals(&crypto_provider, signer)
249                    .await
250                    .map_err(OpenMlsError::wrap("group commit to pending proposals"))
251                    .map_err(Into::into)
252            })
253            .await?;
254        let group_info = GroupInfoBundle::try_new_full_plaintext(
255            openmls_group_info.expect("creating a commit always produces a group info"),
256        )?;
257
258        Ok(Some(CommitBundle {
259            welcome,
260            commit,
261            group_info,
262            encrypted_message: None,
263        }))
264    }
265
266    pub(crate) async fn commit_inline_proposals(
267        &mut self,
268        proposals: Vec<openmls::prelude::Proposal>,
269    ) -> Result<Option<CommitBundle>> {
270        if proposals.is_empty() {
271            return Ok(None);
272        }
273
274        let provider = &self.crypto_provider().await?;
275        let credential = self.credential().await?;
276
277        let (commit, welcome, openmls_group_info) = self
278            .mutate_group(async |_, group, _| {
279                let signer = &credential.signature_key_pair;
280                group
281                    .commit_to_inline_proposals(provider, signer, proposals)
282                    .await
283                    .map_err(OpenMlsError::wrap("group commit to pending proposals"))
284                    .map_err(Into::into)
285            })
286            .await?;
287        let group_info = GroupInfoBundle::try_new_full_plaintext(
288            openmls_group_info.expect("creating a commit always produces a group info"),
289        )?;
290
291        Ok(Some(CommitBundle {
292            welcome,
293            commit,
294            group_info,
295            encrypted_message: None,
296        }))
297    }
298}