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 remove_proposal = self
237            .create_remove_proposal(self.framing_parameters(), removed, signer)
238            .map_err(|_| LibraryError::custom("Creating a self removal should not fail"))?;
239
240        let ciphersuite = self.ciphersuite();
241        let queued_remove_proposal = QueuedProposal::from_authenticated_content_by_ref(
242            ciphersuite,
243            provider.crypto(),
244            remove_proposal.clone(),
245        )?;
246
247        provider
248            .storage()
249            .queue_proposal(
250                self.group_id(),
251                &queued_remove_proposal.proposal_reference(),
252                &queued_remove_proposal,
253            )
254            .map_err(LeaveGroupError::StorageError)?;
255
256        self.proposal_store_mut().add(queued_remove_proposal);
257
258        self.reset_aad();
259        Ok(self.content_to_mls_message(remove_proposal, provider)?)
260    }
261
262    /// Leave the group via a SelfRemove proposal.
263    ///
264    /// Creates a SelfRemove Proposal that needs to be covered by a Commit from
265    /// a different member. The SelfRemove Proposal is returned as a
266    /// [`MlsMessageOut`].
267    ///
268    /// Since SelfRemove proposals are always sent as [`PublicMessage`]s, this
269    /// function can only be used if the group's [`WireFormatPolicy`] allows for
270    /// it.
271    ///
272    /// Returns an error if there is a pending commit.
273    pub fn leave_group_via_self_remove<Provider: OpenMlsProvider>(
274        &mut self,
275        provider: &Provider,
276        signer: &impl Signer,
277    ) -> Result<MlsMessageOut, LeaveGroupError<Provider::StorageError>> {
278        self.is_operational()?;
279
280        if matches!(
281            self.configuration().wire_format_policy().outgoing(),
282            OutgoingWireFormatPolicy::AlwaysCiphertext
283        ) {
284            return Err(LeaveGroupError::CannotSelfRemoveWithPureCiphertext);
285        }
286        let self_remove_proposal =
287            self.create_self_remove_proposal(self.framing_parameters().aad(), signer)?;
288
289        let ciphersuite = self.ciphersuite();
290        let queued_self_remove_proposal = QueuedProposal::from_authenticated_content_by_ref(
291            ciphersuite,
292            provider.crypto(),
293            self_remove_proposal.clone(),
294        )?;
295
296        provider
297            .storage()
298            .queue_proposal(
299                self.group_id(),
300                &queued_self_remove_proposal.proposal_reference(),
301                &queued_self_remove_proposal,
302            )
303            .map_err(LeaveGroupError::StorageError)?;
304
305        self.proposal_store_mut().add(queued_self_remove_proposal);
306
307        self.reset_aad();
308        Ok(self.content_to_mls_message(self_remove_proposal, provider)?)
309    }
310
311    /// Returns a list of [`Member`]s in the group.
312    pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
313        self.public_group().members()
314    }
315
316    /// Returns the [`LeafNodeIndex`] of a member corresponding to the given
317    /// credential. Returns `None` if the member can not be found in this group.
318    pub fn member_leaf_index(&self, credential: &Credential) -> Option<LeafNodeIndex> {
319        self.members()
320            .find(|m| &m.credential == credential)
321            .map(|m| m.index)
322    }
323
324    /// Returns the [`Credential`] of a member corresponding to the given
325    /// leaf index. Returns `None` if the member can not be found in this group.
326    pub fn member(&self, leaf_index: LeafNodeIndex) -> Option<&Credential> {
327        self.public_group()
328            // This will return an error if the member can't be found.
329            .leaf(leaf_index)
330            .map(|leaf| leaf.credential())
331    }
332
333    /// Returns the [`Member`] corresponding to the given
334    /// leaf index. Returns `None` if the member can not be found in this group.
335    pub fn member_at(&self, leaf_index: LeafNodeIndex) -> Option<Member> {
336        self.public_group()
337            // This will return None if the member can't be found.
338            .leaf(leaf_index)
339            .map(|leaf_node| {
340                Member::new(
341                    leaf_index,
342                    leaf_node.encryption_key().as_slice().to_vec(),
343                    leaf_node.signature_key().as_slice().to_vec(),
344                    leaf_node.credential().clone(),
345                )
346            })
347    }
348}
349
350/// Helper `enum` that classifies the kind of remove operation. This can be used to
351/// better interpret the semantic value of a remove proposal that is covered in a
352/// Commit message.
353#[derive(Debug)]
354pub enum RemoveOperation {
355    /// We issued a remove proposal for ourselves in the previous epoch and
356    /// the proposal has now been committed.
357    WeLeft,
358    /// Someone else (indicated by the [`Sender`]) removed us from the group.
359    WeWereRemovedBy(Sender),
360    /// Another member (indicated by the leaf index) requested to leave
361    /// the group by issuing a remove proposal in the previous epoch and the
362    /// proposal has now been committed.
363    TheyLeft(LeafNodeIndex),
364    /// Another member (indicated by the leaf index) was removed by the [`Sender`].
365    TheyWereRemovedBy((LeafNodeIndex, Sender)),
366    /// We removed another member (indicated by the leaf index).
367    WeRemovedThem(LeafNodeIndex),
368}
369
370impl RemoveOperation {
371    /// Constructs a new [`RemoveOperation`] from a [`QueuedRemoveProposal`] and the
372    /// corresponding [`MlsGroup`].
373    pub fn new(
374        queued_remove_proposal: QueuedRemoveProposal,
375        group: &MlsGroup,
376    ) -> Result<Self, LibraryError> {
377        let own_index = group.own_leaf_index();
378        let sender = queued_remove_proposal.sender();
379        let removed = queued_remove_proposal.remove_proposal().removed();
380
381        // We start with the cases where the sender is a group member
382        if let Sender::Member(leaf_index) = sender {
383            // We authored the remove proposal
384            if *leaf_index == own_index {
385                if removed == own_index {
386                    // We left
387                    return Ok(Self::WeLeft);
388                } else {
389                    // We removed another member
390                    return Ok(Self::WeRemovedThem(removed));
391                }
392            }
393
394            // Another member left
395            if removed == *leaf_index {
396                return Ok(Self::TheyLeft(removed));
397            }
398        }
399
400        // The sender is not necessarily a group member. This covers all sender
401        // types (members, pre-configured senders and new members).
402
403        if removed == own_index {
404            // We were removed
405            Ok(Self::WeWereRemovedBy(sender.clone()))
406        } else {
407            // Another member was removed
408            Ok(Self::TheyWereRemovedBy((removed, sender.clone())))
409        }
410    }
411}