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