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