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