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