Skip to main content

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}