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, 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 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 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 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 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 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 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 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 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 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}