core_crypto/mls/conversation/mutable/
commit.rs1use 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 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 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 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 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 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 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 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 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 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 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}