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                        Self::clients_with_duplicate_signature_keys(key_packages.as_ref())
78                            .map(|affected_clients| Error::DuplicateSignature { affected_clients })
79                            .unwrap_or_else(|e| e)
80                    } else {
81                        OpenMlsError::wrap("group add members")(err).into()
82                    }
83                })?;
84
85            Ok(CommitBundle {
86                commit,
87                welcome: Some(welcome),
88                group_info: Self::group_info(group_info)?,
89                encrypted_message: None,
90            })
91        })
92        .await
93    }
94
95    fn err_is_duplicate_signature_key(
96        err: &openmls::prelude::AddMembersError<core_crypto_keystore::CryptoKeystoreError>,
97    ) -> bool {
98        matches!(
99            err,
100            openmls::prelude::AddMembersError::CreateCommitError(
101                openmls::prelude::CreateCommitError::ProposalValidationError(
102                    openmls::prelude::ProposalValidationError::DuplicateSignatureKey
103                )
104            )
105        )
106    }
107
108    fn clients_with_duplicate_signature_keys(key_packages: &[KeyPackageIn]) -> Result<Vec<(ClientId, ClientId)>> {
109        let mut seen_signature_keys = HashMap::new();
110        let mut duplicate_pairs = Vec::new();
111
112        for key_package in key_packages {
113            let signature_key = key_package.unverified_credential().signature_key.as_slice().to_vec();
114
115            let client_id: ClientId = key_package
116                .credential()
117                .identity()
118                .try_into()
119                .map_err(RecursiveError::mls_client("client id from bytes"))?;
120
121            if let Some(previous_client_id) = seen_signature_keys.insert(signature_key, client_id.clone()) {
122                duplicate_pairs.push((previous_client_id, client_id));
123            }
124        }
125
126        Ok(duplicate_pairs)
127    }
128
129    /// Removes clients from the group/conversation.
130    ///
131    /// # Arguments
132    /// * `id` - group/conversation id
133    /// * `clients` - list of client ids to be removed from the group
134    pub async fn remove_members(&mut self, clients: &[impl Borrow<ClientIdRef>]) -> Result<()> {
135        self.remove_members_or_history_clients(clients.iter().map(|e| e.borrow().as_ref()))
136            .await
137    }
138
139    pub(crate) async fn remove_members_or_history_clients(
140        &mut self,
141        clients: impl Iterator<Item = &[u8]> + Clone,
142    ) -> Result<()> {
143        self.ensure_no_pending_commit().await?;
144        let backend = self.crypto_provider().await?;
145        let credential = self.credential().await?;
146        let signer = credential.signature_key();
147        let (commit, welcome, group_info) = self
148            .mutate_group(async |_, group, _| {
149                let members = group
150                    .members()
151                    .filter_map(|member| {
152                        clients
153                            .clone()
154                            .any(move |client_id| client_id == member.credential.identity())
155                            .then_some(member.index)
156                    })
157                    .collect::<Vec<_>>();
158                group
159                    .remove_members(&backend, signer, &members)
160                    .await
161                    .map_err(OpenMlsError::wrap("group remove members"))
162                    .map_err(Into::into)
163            })
164            .await?;
165
166        let group_info = Self::group_info(group_info)?;
167
168        self.send_and_merge_commit(CommitBundle {
169            commit,
170            welcome,
171            group_info,
172            encrypted_message: None,
173        })
174        .await
175    }
176
177    /// Self updates the own leaf node and automatically commits. Pending proposals will be committed.
178    pub async fn update_key_material(&mut self) -> Result<()> {
179        let credential = self.credential().await?;
180        let commit = self.set_credential_inner(&credential).await?;
181        self.send_and_merge_commit(commit).await
182    }
183
184    /// Set the referenced credential for this conversation.
185    pub async fn set_credential_by_ref(&mut self, credential_ref: &CredentialRef) -> Result<()> {
186        let database = self.database().await?;
187        let credential = credential_ref
188            .load(&*database)
189            .await
190            .map_err(RecursiveError::mls_credential_ref("loading credential from ref"))?;
191        let commit = self.set_credential_inner(&credential).await?;
192
193        self.send_and_merge_commit(commit).await
194    }
195
196    /// Self updates the own leaf node with the given credential and automatically commits. Pending proposals will be
197    /// committed.
198    pub(crate) async fn set_credential_inner(&mut self, credential: &Credential) -> Result<CommitBundle> {
199        self.ensure_no_pending_commit().await?;
200        let backend = self.crypto_provider().await?;
201        let credential = credential.clone();
202
203        self.mutate_group(async |_, group, _| {
204            // If the credential remains the same and we still want to update, we explicitly need to pass `None` to
205            // openmls, if we just passed an unchanged leaf node, no update commit would be created.
206            // Also, we can avoid cloning in the case we don't need to create a new leaf node.
207            let updated_leaf_node = {
208                let leaf_node = group.own_leaf().ok_or(LeafError::InternalMlsError)?;
209                if leaf_node.credential() == &credential.mls_credential {
210                    None
211                } else {
212                    let mut leaf_node = leaf_node.clone();
213                    leaf_node.set_credential_with_key(credential.to_mls_credential_with_key());
214                    Some(leaf_node)
215                }
216            };
217
218            let (commit, welcome, group_info) = group
219                .explicit_self_update(&backend, &credential.signature_key_pair, updated_leaf_node)
220                .await
221                .map_err(OpenMlsError::wrap("group self update"))?;
222
223            // We should always have ratchet tree extension turned on hence GroupInfo should always be present
224            let group_info = group_info.ok_or(LeafError::MissingGroupInfo)?;
225            let group_info = GroupInfoBundle::try_new_full_plaintext(group_info)?;
226
227            Ok(CommitBundle {
228                welcome,
229                commit,
230                group_info,
231                encrypted_message: None,
232            })
233        })
234        .await
235    }
236
237    /// Commits all pending proposals of the group
238    pub async fn commit_pending_proposals(&mut self) -> Result<()> {
239        self.ensure_no_pending_commit().await?;
240        let commit = self.commit_pending_proposals_inner().await?;
241        let Some(commit) = commit else {
242            return Ok(());
243        };
244        self.send_and_merge_commit(commit).await
245    }
246
247    pub(crate) async fn commit_pending_proposals_inner(&mut self) -> Result<Option<CommitBundle>> {
248        if self.group().await.pending_proposals().next().is_none() {
249            return Ok(None);
250        }
251
252        let crypto_provider = self.crypto_provider().await?;
253        let credential = self.credential().await?;
254
255        let (commit, welcome, openmls_group_info) = self
256            .mutate_group(async |_, group, _| {
257                let signer = &credential.signature_key_pair;
258                group
259                    .commit_to_pending_proposals(&crypto_provider, signer)
260                    .await
261                    .map_err(OpenMlsError::wrap("group commit to pending proposals"))
262                    .map_err(Into::into)
263            })
264            .await?;
265        let group_info = GroupInfoBundle::try_new_full_plaintext(
266            openmls_group_info.expect("creating a commit always produces a group info"),
267        )?;
268
269        Ok(Some(CommitBundle {
270            welcome,
271            commit,
272            group_info,
273            encrypted_message: None,
274        }))
275    }
276
277    pub(crate) async fn commit_inline_proposals(
278        &mut self,
279        proposals: Vec<openmls::prelude::Proposal>,
280    ) -> Result<Option<CommitBundle>> {
281        if proposals.is_empty() {
282            return Ok(None);
283        }
284
285        let provider = &self.crypto_provider().await?;
286        let credential = self.credential().await?;
287
288        let (commit, welcome, openmls_group_info) = self
289            .mutate_group(async |_, group, _| {
290                let signer = &credential.signature_key_pair;
291                group
292                    .commit_to_inline_proposals(provider, signer, proposals)
293                    .await
294                    .map_err(OpenMlsError::wrap("group commit to pending proposals"))
295                    .map_err(Into::into)
296            })
297            .await?;
298        let group_info = GroupInfoBundle::try_new_full_plaintext(
299            openmls_group_info.expect("creating a commit always produces a group info"),
300        )?;
301
302        Ok(Some(CommitBundle {
303            welcome,
304            commit,
305            group_info,
306            encrypted_message: None,
307        }))
308    }
309}