openmls/group/mls_group/membership.rs
1//! MLS group membership
2//!
3//! This module contains membership-related operations and exposes [`RemoveOperation`].
4
5#[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
6use errors::EmptyInputError;
7use openmls_traits::signatures::Signer;
8use proposal_store::QueuedRemoveProposal;
9
10#[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
11use super::errors::{AddMembersError, RemoveMembersError};
12use super::{errors::LeaveGroupError, *};
13use crate::{
14 binary_tree::array_representation::LeafNodeIndex, storage::OpenMlsProvider, treesync::LeafNode,
15};
16#[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
17use crate::{
18 group::{SwapMembersError, WelcomeCommitMessages},
19 key_packages::KeyPackage,
20 messages::group_info::GroupInfo,
21};
22
23impl MlsGroup {
24 /// Adds members to the group.
25 ///
26 /// New members are added by providing a `KeyPackage` for each member.
27 ///
28 /// This operation results in a Commit with a `path`, i.e. it includes an
29 /// update of the committer's leaf [KeyPackage]. To add members without
30 /// forcing an update of the committer's leaf [KeyPackage], use
31 /// [`Self::add_members_without_update()`].
32 ///
33 /// If successful, it returns a triple of [`MlsMessageOut`]s, where the first
34 /// contains the commit, the second one the [`Welcome`] and the third an optional [GroupInfo] that
35 /// will be [Some] if the group has the `use_ratchet_tree_extension` flag set.
36 ///
37 /// Returns an error if there is a pending commit.
38 ///
39 /// Under the `virtual-clients-draft` feature this function is unavailable.
40 /// Use [`MlsGroup::commit_builder`], whose
41 /// [`CommitMessageBundle::confirmation`] surfaces the handshake confirmation
42 /// data.
43 ///
44 /// [`Welcome`]: crate::messages::Welcome
45 /// [`CommitMessageBundle::confirmation`]: crate::group::CommitMessageBundle::confirmation
46 // FIXME: #1217
47 #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
48 #[allow(clippy::type_complexity)]
49 #[openmls_traits::maybe_async]
50 pub async fn add_members<Provider: OpenMlsProvider>(
51 &mut self,
52 provider: &Provider,
53 signer: &impl Signer,
54 key_packages: &[KeyPackage],
55 ) -> Result<
56 (MlsMessageOut, MlsMessageOut, Option<GroupInfo>),
57 AddMembersError<Provider::StorageError>,
58 > {
59 self.add_members_internal(provider, signer, key_packages, true)
60 .await
61 }
62
63 /// Swap members.
64 ///
65 /// This function replaces a set of `members` of the group with new members.
66 /// The members-to-be-replaced are identified by their index, and the new
67 /// members are identified by the provided `key_packages`.
68 ///
69 /// This function can be used in scenarios where members are no
70 /// longer in sync with the rest of the group and need to be re-added.
71 /// Note however that this function _does not_ enforce that the
72 /// removed `members` and new members in the `key_packages` correspond.
73 ///
74 /// Under the `virtual-clients-draft` feature this function is unavailable.
75 /// Use [`MlsGroup::commit_builder`], whose
76 /// [`CommitMessageBundle::confirmation`](crate::group::CommitMessageBundle::confirmation)
77 /// surfaces the handshake confirmation data.
78 #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
79 #[openmls_traits::maybe_async]
80 pub async fn swap_members<Provider: OpenMlsProvider>(
81 &mut self,
82 provider: &Provider,
83 signer: &impl Signer,
84 members: &[LeafNodeIndex],
85 key_packages: &[KeyPackage],
86 ) -> Result<WelcomeCommitMessages, SwapMembersError<Provider::StorageError>> {
87 self.is_operational()?;
88
89 if members.is_empty() {
90 return Err(EmptyInputError::RemoveMembers.into());
91 }
92
93 if key_packages.is_empty() {
94 return Err(EmptyInputError::AddMembers.into());
95 }
96
97 if members.len() != key_packages.len() {
98 return Err(SwapMembersError::InvalidInput);
99 }
100
101 let bundle = self
102 .commit_builder()
103 .propose_removals(members.iter().cloned())
104 .propose_adds(key_packages.iter().cloned())
105 .load_psks(provider.storage())
106 .await?
107 .build(provider.rand(), provider.crypto(), signer, |_| true)?
108 .stage_commit(provider)
109 .await?;
110
111 self.reset_aad();
112
113 Ok(bundle.try_into()?)
114 }
115
116 /// Adds members to the group.
117 ///
118 /// New members are added by providing a `KeyPackage` for each member.
119 ///
120 /// This operation results in a Commit that does not necessarily include a
121 /// `path`, i.e. an update of the committer's leaf [KeyPackage]. In
122 /// particular, it will only include a path if the group's proposal store
123 /// includes one or more proposals that require a path (see [Section 17.4 of
124 /// RFC 9420](https://www.rfc-editor.org/rfc/rfc9420.html#section-17.4) for
125 /// a list of proposals and whether they require a path).
126 ///
127 /// If successful, it returns a triple of [`MlsMessageOut`]s, where the
128 /// first contains the commit, the second one the [`Welcome`] and the third
129 /// an optional [GroupInfo] that will be [Some] if the group has the
130 /// `use_ratchet_tree_extension` flag set.
131 ///
132 /// Returns an error if there is a pending commit.
133 ///
134 /// Under the `virtual-clients-draft` feature this function is unavailable.
135 /// Use [`MlsGroup::commit_builder`], whose
136 /// [`CommitMessageBundle::confirmation`] surfaces the handshake confirmation
137 /// data.
138 ///
139 /// [`Welcome`]: crate::messages::Welcome
140 /// [`CommitMessageBundle::confirmation`]: crate::group::CommitMessageBundle::confirmation
141 // FIXME: #1217
142 #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
143 #[allow(clippy::type_complexity)]
144 #[openmls_traits::maybe_async]
145 pub async fn add_members_without_update<Provider: OpenMlsProvider>(
146 &mut self,
147 provider: &Provider,
148 signer: &impl Signer,
149 key_packages: &[KeyPackage],
150 ) -> Result<
151 (MlsMessageOut, MlsMessageOut, Option<GroupInfo>),
152 AddMembersError<Provider::StorageError>,
153 > {
154 self.add_members_internal(provider, signer, key_packages, false)
155 .await
156 }
157
158 #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
159 #[allow(clippy::type_complexity)]
160 #[openmls_traits::maybe_async]
161 async fn add_members_internal<Provider: OpenMlsProvider>(
162 &mut self,
163 provider: &Provider,
164 signer: &impl Signer,
165 key_packages: &[KeyPackage],
166 force_self_update: bool,
167 ) -> Result<
168 (MlsMessageOut, MlsMessageOut, Option<GroupInfo>),
169 AddMembersError<Provider::StorageError>,
170 > {
171 self.is_operational()?;
172
173 if key_packages.is_empty() {
174 return Err(AddMembersError::EmptyInput(EmptyInputError::AddMembers));
175 }
176
177 let bundle = self
178 .commit_builder()
179 .propose_adds(key_packages.iter().cloned())
180 .force_self_update(force_self_update)
181 .load_psks(provider.storage())
182 .await?
183 .build(provider.rand(), provider.crypto(), signer, |_| true)?
184 .stage_commit(provider)
185 .await?;
186
187 let welcome: MlsMessageOut = bundle
188 .to_welcome_msg()
189 .ok_or_else(|| LibraryError::custom("No secrets to generate commit message."))?;
190 let (commit, _, group_info) = bundle.into_contents();
191
192 self.reset_aad();
193
194 Ok((commit, welcome, group_info))
195 }
196
197 /// Returns a reference to the own [`LeafNode`].
198 pub fn own_leaf(&self) -> Option<&LeafNode> {
199 self.public_group().leaf(self.own_leaf_index())
200 }
201
202 /// Removes members from the group.
203 ///
204 /// Members are removed by providing the member's leaf index.
205 ///
206 /// If successful, it returns a tuple of [`MlsMessageOut`] (containing the
207 /// commit), an optional [`MlsMessageOut`] (containing the [`Welcome`]) and the current
208 /// [GroupInfo].
209 /// The [`Welcome`] is [Some] when the queue of pending proposals contained
210 /// add proposals
211 /// The [GroupInfo] is [Some] if the group has the `use_ratchet_tree_extension` flag set.
212 ///
213 /// Returns an error if there is a pending commit.
214 ///
215 /// Under the `virtual-clients-draft` feature this function is unavailable.
216 /// Use [`MlsGroup::commit_builder`], whose
217 /// [`CommitMessageBundle::confirmation`] surfaces the handshake confirmation
218 /// data.
219 ///
220 /// [`Welcome`]: crate::messages::Welcome
221 /// [`CommitMessageBundle::confirmation`]: crate::group::CommitMessageBundle::confirmation
222 // FIXME: #1217
223 #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
224 #[allow(clippy::type_complexity)]
225 #[openmls_traits::maybe_async]
226 pub async fn remove_members<Provider: OpenMlsProvider>(
227 &mut self,
228 provider: &Provider,
229 signer: &impl Signer,
230 members: &[LeafNodeIndex],
231 ) -> Result<
232 (MlsMessageOut, Option<MlsMessageOut>, Option<GroupInfo>),
233 RemoveMembersError<Provider::StorageError>,
234 > {
235 self.is_operational()?;
236
237 if members.is_empty() {
238 return Err(RemoveMembersError::EmptyInput(
239 EmptyInputError::RemoveMembers,
240 ));
241 }
242
243 let bundle = self
244 .commit_builder()
245 .propose_removals(members.iter().cloned())
246 .load_psks(provider.storage())
247 .await?
248 .build(provider.rand(), provider.crypto(), signer, |_| true)?
249 .stage_commit(provider)
250 .await?;
251
252 let welcome = bundle.to_welcome_msg();
253 let (commit, _, group_info) = bundle.into_contents();
254
255 provider
256 .storage()
257 .write_group_state(self.group_id(), &self.group_state)
258 .await
259 .map_err(RemoveMembersError::StorageError)?;
260
261 self.reset_aad();
262 Ok((commit, welcome, group_info))
263 }
264
265 /// Leave the group.
266 ///
267 /// Creates a Remove Proposal that needs to be covered by a Commit from a different member.
268 /// The Remove Proposal is returned as a [`MlsMessageOut`].
269 ///
270 /// Returns an error if there is a pending commit.
271 ///
272 /// Under the `virtual-clients-draft` feature this function is unavailable.
273 /// Use [`Self::propose_unconfirmed`] with
274 /// [`Propose::Remove`](crate::group::Propose::Remove) of the own leaf index,
275 /// which retains the handshake secret and returns the confirmation data.
276 #[cfg(any(not(feature = "virtual-clients-draft"), feature = "test-utils", test))]
277 #[openmls_traits::maybe_async]
278 pub async fn leave_group<Provider: OpenMlsProvider>(
279 &mut self,
280 provider: &Provider,
281 signer: &impl Signer,
282 ) -> Result<MlsMessageOut, LeaveGroupError<Provider::StorageError>> {
283 self.is_operational()?;
284
285 let removed = self.own_leaf_index();
286 let aad = self.outgoing_authenticated_data()?;
287 let framing_parameters = FramingParameters::new(&aad, self.outgoing_wire_format());
288 let remove_proposal = self
289 .create_remove_proposal(framing_parameters, removed, signer)
290 .map_err(|_| LibraryError::custom("Creating a self removal should not fail"))?;
291
292 let ciphersuite = self.ciphersuite();
293 let queued_remove_proposal = QueuedProposal::from_authenticated_content_by_ref(
294 ciphersuite,
295 provider.crypto(),
296 remove_proposal.clone(),
297 )?;
298
299 provider
300 .storage()
301 .queue_proposal(
302 self.group_id(),
303 &queued_remove_proposal.proposal_reference(),
304 &queued_remove_proposal,
305 )
306 .await
307 .map_err(LeaveGroupError::StorageError)?;
308
309 self.proposal_store_mut().add(queued_remove_proposal);
310
311 let framing = self
312 .content_to_mls_message(remove_proposal, provider)
313 .await?;
314
315 self.reset_aad();
316 Ok(framing.message)
317 }
318
319 /// Leave the group via a SelfRemove proposal.
320 ///
321 /// Creates a SelfRemove Proposal that needs to be covered by a Commit from
322 /// a different member. The SelfRemove Proposal is returned as a
323 /// [`MlsMessageOut`].
324 ///
325 /// Since SelfRemove proposals are always sent as [`PublicMessage`]s, this
326 /// function can only be used if the group's [`WireFormatPolicy`] allows for
327 /// it.
328 ///
329 /// Returns an error if there is a pending commit.
330 #[openmls_traits::maybe_async]
331 pub async fn leave_group_via_self_remove<Provider: OpenMlsProvider>(
332 &mut self,
333 provider: &Provider,
334 signer: &impl Signer,
335 ) -> Result<MlsMessageOut, LeaveGroupError<Provider::StorageError>> {
336 self.is_operational()?;
337
338 if matches!(
339 self.configuration().wire_format_policy().outgoing(),
340 OutgoingWireFormatPolicy::AlwaysCiphertext
341 ) {
342 return Err(LeaveGroupError::CannotSelfRemoveWithPureCiphertext);
343 }
344 let aad = self.outgoing_authenticated_data()?;
345 let self_remove_proposal = self.create_self_remove_proposal(&aad, signer)?;
346
347 let ciphersuite = self.ciphersuite();
348 let queued_self_remove_proposal = QueuedProposal::from_authenticated_content_by_ref(
349 ciphersuite,
350 provider.crypto(),
351 self_remove_proposal.clone(),
352 )?;
353
354 provider
355 .storage()
356 .queue_proposal(
357 self.group_id(),
358 &queued_self_remove_proposal.proposal_reference(),
359 &queued_self_remove_proposal,
360 )
361 .await
362 .map_err(LeaveGroupError::StorageError)?;
363
364 self.proposal_store_mut().add(queued_self_remove_proposal);
365
366 self.reset_aad();
367 Ok(self
368 .content_to_mls_message(self_remove_proposal, provider)
369 .await?
370 .message)
371 }
372
373 /// Returns a list of [`Member`]s in the group.
374 pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
375 self.public_group().members()
376 }
377
378 /// Returns the [`LeafNodeIndex`] of a member corresponding to the given
379 /// credential. Returns `None` if the member can not be found in this group.
380 pub fn member_leaf_index(&self, credential: &Credential) -> Option<LeafNodeIndex> {
381 self.members()
382 .find(|m| &m.credential == credential)
383 .map(|m| m.index)
384 }
385
386 /// Returns the [`Credential`] of a member corresponding to the given
387 /// leaf index. Returns `None` if the member can not be found in this group.
388 pub fn member(&self, leaf_index: LeafNodeIndex) -> Option<&Credential> {
389 self.public_group()
390 // This will return an error if the member can't be found.
391 .leaf(leaf_index)
392 .map(|leaf| leaf.credential())
393 }
394
395 /// Returns the [`Member`] corresponding to the given
396 /// leaf index. Returns `None` if the member can not be found in this group.
397 pub fn member_at(&self, leaf_index: LeafNodeIndex) -> Option<Member> {
398 self.public_group()
399 // This will return None if the member can't be found.
400 .leaf(leaf_index)
401 .map(|leaf_node| {
402 Member::new(
403 leaf_index,
404 leaf_node.encryption_key().as_slice().to_vec(),
405 leaf_node.signature_key().as_slice().to_vec(),
406 leaf_node.credential().clone(),
407 )
408 })
409 }
410}
411
412/// Helper `enum` that classifies the kind of remove operation. This can be used to
413/// better interpret the semantic value of a remove proposal that is covered in a
414/// Commit message.
415#[derive(Debug)]
416pub enum RemoveOperation {
417 /// We issued a remove proposal for ourselves in the previous epoch and
418 /// the proposal has now been committed.
419 WeLeft,
420 /// Someone else (indicated by the [`Sender`]) removed us from the group.
421 WeWereRemovedBy(Sender),
422 /// Another member (indicated by the leaf index) requested to leave
423 /// the group by issuing a remove proposal in the previous epoch and the
424 /// proposal has now been committed.
425 TheyLeft(LeafNodeIndex),
426 /// Another member (indicated by the leaf index) was removed by the [`Sender`].
427 TheyWereRemovedBy((LeafNodeIndex, Sender)),
428 /// We removed another member (indicated by the leaf index).
429 WeRemovedThem(LeafNodeIndex),
430}
431
432impl RemoveOperation {
433 /// Constructs a new [`RemoveOperation`] from a [`QueuedRemoveProposal`] and the
434 /// corresponding [`MlsGroup`].
435 pub fn new(
436 queued_remove_proposal: QueuedRemoveProposal,
437 group: &MlsGroup,
438 ) -> Result<Self, LibraryError> {
439 let own_index = group.own_leaf_index();
440 let sender = queued_remove_proposal.sender();
441 let removed = queued_remove_proposal.remove_proposal().removed();
442
443 // We start with the cases where the sender is a group member
444 if let Sender::Member(leaf_index) = sender {
445 // We authored the remove proposal
446 if *leaf_index == own_index {
447 if removed == own_index {
448 // We left
449 return Ok(Self::WeLeft);
450 } else {
451 // We removed another member
452 return Ok(Self::WeRemovedThem(removed));
453 }
454 }
455
456 // Another member left
457 if removed == *leaf_index {
458 return Ok(Self::TheyLeft(removed));
459 }
460 }
461
462 // The sender is not necessarily a group member. This covers all sender
463 // types (members, pre-configured senders and new members).
464
465 if removed == own_index {
466 // We were removed
467 Ok(Self::WeWereRemovedBy(sender.clone()))
468 } else {
469 // Another member was removed
470 Ok(Self::TheyWereRemovedBy((removed, sender.clone())))
471 }
472 }
473}