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