Skip to main content

openmls/group/mls_group/
mod.rs

1//! MLS Group
2//!
3//! This module contains [`MlsGroup`] and its submodules.
4//!
5
6use past_secrets::MessageSecretsStore;
7use proposal_store::ProposalQueue;
8use serde::{Deserialize, Serialize};
9use tls_codec::Serialize as _;
10
11#[cfg(test)]
12use crate::treesync::node::leaf_node::TreePosition;
13
14use super::proposal_store::{ProposalStore, QueuedProposal};
15use crate::{
16    binary_tree::array_representation::LeafNodeIndex,
17    ciphersuite::{hash_ref::ProposalRef, signable::Signable},
18    credentials::Credential,
19    error::LibraryError,
20    extensions::Extensions,
21    framing::{mls_auth_content::AuthenticatedContent, *},
22    group::{
23        CreateGroupContextExtProposalError, DeletePastEpochSecretsError, Extension, ExtensionType,
24        ExternalPubExtension, GroupContext, GroupEpoch, GroupId, MlsGroupJoinConfig,
25        MlsGroupStateError, OutgoingWireFormatPolicy, PublicGroup, RatchetTreeExtension,
26        RequiredCapabilitiesExtension, SetPastEpochDeletionPolicyError, StagedCommit,
27    },
28    key_packages::KeyPackageBundle,
29    messages::{
30        group_info::{GroupInfo, GroupInfoTBS, VerifiableGroupInfo},
31        proposals::*,
32        ConfirmationTag, GroupSecrets, Welcome,
33    },
34    schedule::{
35        message_secrets::MessageSecrets,
36        psk::{load_psks, store::ResumptionPskStore, PskSecret},
37        GroupEpochSecrets, JoinerSecret, KeySchedule,
38    },
39    storage::{OpenMlsProvider, StorageProvider},
40    treesync::{
41        node::{encryption_keys::EncryptionKeyPair, leaf_node::LeafNode},
42        RatchetTree, TreeSync,
43    },
44    versions::ProtocolVersion,
45};
46use openmls_traits::{signatures::Signer, storage::StorageProvider as _, types::Ciphersuite};
47
48#[cfg(feature = "extensions-draft-08")]
49use crate::schedule::{application_export_tree::ApplicationExportTree, ApplicationExportSecret};
50
51// Private
52mod application;
53mod exporting;
54mod updates;
55
56use config::*;
57
58// Crate
59pub(crate) mod builder;
60pub(crate) mod commit_builder;
61pub(crate) mod config;
62pub(crate) mod creation;
63pub(crate) mod errors;
64pub(crate) mod membership;
65pub(crate) mod past_secrets;
66pub(crate) mod processing;
67pub(crate) mod proposal;
68pub(crate) mod proposal_store;
69pub(crate) mod staged_commit;
70
71#[cfg(feature = "extensions-draft-08")]
72pub(crate) mod app_ephemeral;
73
74// Tests
75#[cfg(test)]
76pub(crate) mod tests_and_kats;
77
78#[derive(Debug)]
79pub(crate) struct CreateCommitResult {
80    pub(crate) commit: AuthenticatedContent,
81    pub(crate) welcome_option: Option<Welcome>,
82    pub(crate) staged_commit: StagedCommit,
83    pub(crate) group_info: Option<GroupInfo>,
84}
85
86/// A member in the group is identified by this [`Member`] struct.
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
88pub struct Member {
89    /// The member's leaf index in the ratchet tree.
90    pub index: LeafNodeIndex,
91    /// The member's credential.
92    pub credential: Credential,
93    /// The member's public HPHKE encryption key.
94    pub encryption_key: Vec<u8>,
95    /// The member's public signature key.
96    pub signature_key: Vec<u8>,
97}
98
99impl Member {
100    /// Create new member.
101    pub fn new(
102        index: LeafNodeIndex,
103        encryption_key: Vec<u8>,
104        signature_key: Vec<u8>,
105        credential: Credential,
106    ) -> Self {
107        Self {
108            index,
109            encryption_key,
110            signature_key,
111            credential,
112        }
113    }
114}
115
116/// Pending Commit state. Differentiates between Commits issued by group members
117/// and External Commits.
118#[derive(Debug, Serialize, Deserialize)]
119#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
120pub enum PendingCommitState {
121    /// Commit from a group member
122    Member(StagedCommit),
123    /// Commit from an external joiner
124    External(StagedCommit),
125}
126
127impl PendingCommitState {
128    /// Returns a reference to the [`StagedCommit`] contained in the
129    /// [`PendingCommitState`] enum.
130    pub(crate) fn staged_commit(&self) -> &StagedCommit {
131        match self {
132            PendingCommitState::Member(pc) => pc,
133            PendingCommitState::External(pc) => pc,
134        }
135    }
136}
137
138impl From<PendingCommitState> for StagedCommit {
139    fn from(pcs: PendingCommitState) -> Self {
140        match pcs {
141            PendingCommitState::Member(pc) => pc,
142            PendingCommitState::External(pc) => pc,
143        }
144    }
145}
146
147/// [`MlsGroupState`] determines the state of an [`MlsGroup`]. The different
148/// states and their transitions are as follows:
149///
150/// * [`MlsGroupState::Operational`]: This is the main state of the group, which
151///   allows access to all of its functionality, (except merging pending commits,
152///   see the [`MlsGroupState::PendingCommit`] for more information) and it's the
153///   state the group starts in (except when created via
154///   [`MlsGroup::external_commit_builder()`], see the functions documentation for
155///   more information). From this `Operational`, the group state can either
156///   transition to [`MlsGroupState::Inactive`], when it processes a commit that
157///   removes this client from the group, or to [`MlsGroupState::PendingCommit`],
158///   when this client creates a commit.
159///
160/// * [`MlsGroupState::Inactive`]: A group can enter this state from any other
161///   state when it processes a commit that removes this client from the group.
162///   This is a terminal state that the group can not exit from. If the clients
163///   wants to re-join the group, it can either be added by a group member or it
164///   can join via external commit.
165///
166/// * [`MlsGroupState::PendingCommit`]: This state is split into two possible
167///   sub-states, one for each Commit type:
168///   [`PendingCommitState::Member`] and [`PendingCommitState::External`]:
169///
170///   * If the client creates a commit for this group, the `PendingCommit` state
171///     is entered with [`PendingCommitState::Member`] and with the [`StagedCommit`] as
172///     additional state variable. In this state, it can perform the same
173///     operations as in the [`MlsGroupState::Operational`], except that it cannot
174///     create proposals or commits. However, it can merge or clear the stored
175///     [`StagedCommit`], where both actions result in a transition to the
176///     [`MlsGroupState::Operational`]. Additionally, if a commit from another
177///     group member is processed, the own pending commit is also cleared and
178///     either the `Inactive` state is entered (if this client was removed from
179///     the group as part of the processed commit), or the `Operational` state is
180///     entered.
181///
182///   * A group can enter the [`PendingCommitState::External`] sub-state only as
183///     the initial state when the group is created via
184///     [`MlsGroup::external_commit_builder()`]. In contrast to the
185///     [`PendingCommitState::Member`] `PendingCommit` state, the only possible
186///     functionality that can be used is the [`MlsGroup::merge_pending_commit()`]
187///     function, which merges the pending external commit and transitions the
188///     state to [`MlsGroupState::PendingCommit`]. For more information on the
189///     external commit process, see [`MlsGroup::external_commit_builder()`] or
190///     Section 11.2.1 of the MLS specification.
191#[derive(Debug, Serialize, Deserialize)]
192#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
193pub enum MlsGroupState {
194    /// There is currently a pending Commit that hasn't been merged yet.
195    PendingCommit(Box<PendingCommitState>),
196    /// The group state is in an opertaional state, where new messages and Commits can be created.
197    Operational,
198    /// The group is inactive because the member has been removed.
199    Inactive,
200}
201
202/// A `MlsGroup` represents an MLS group with a high-level API. The API exposes
203/// high level functions to manage a group by adding/removing members, get the
204/// current member list, etc.
205///
206/// The API is modeled such that it can serve as a direct interface to the
207/// Delivery Service. Functions that modify the public state of the group will
208/// return a `Vec<MLSMessageOut>` that can be sent to the Delivery Service
209/// directly. Conversely, incoming messages from the Delivery Service can be fed
210/// into [process_message()](`MlsGroup::process_message()`).
211///
212/// An `MlsGroup` has an internal queue of pending proposals that builds up as
213/// new messages are processed. When creating proposals, those messages are not
214/// automatically appended to this queue, instead they have to be processed
215/// again through [process_message()](`MlsGroup::process_message()`). This
216/// allows the Delivery Service to reject them (e.g. if they reference the wrong
217/// epoch).
218///
219/// If incoming messages or applied operations are semantically or syntactically
220/// incorrect, an error event will be returned with a corresponding error
221/// message and the state of the group will remain unchanged.
222///
223/// An `MlsGroup` has an internal state variable determining if it is active or
224/// inactive, as well as if it has a pending commit. See [`MlsGroupState`] for
225/// more information.
226#[derive(Debug)]
227#[cfg_attr(feature = "test-utils", derive(Clone, PartialEq))]
228pub struct MlsGroup {
229    /// The group configuration. See [`MlsGroupJoinConfig`] for more information.
230    mls_group_config: MlsGroupJoinConfig,
231    /// The public state of the group.
232    public_group: PublicGroup,
233    /// Epoch-specific secrets of the group.
234    group_epoch_secrets: GroupEpochSecrets,
235    /// The own leaf index in the ratchet tree.
236    own_leaf_index: LeafNodeIndex,
237    /// A [`MessageSecretsStore`] that stores message secrets.
238    /// By default this store has the length of 1, i.e. only the [`MessageSecrets`]
239    /// of the current epoch is kept.
240    /// If more secrets from past epochs should be kept in order to be
241    /// able to decrypt application messages from previous epochs, the size of
242    /// the store must be increased through [`max_past_epochs()`].
243    message_secrets_store: MessageSecretsStore,
244    // Resumption psk store. This is where the resumption psks are kept in a rollover list.
245    resumption_psk_store: ResumptionPskStore,
246    // Own [`LeafNode`]s that were created for update proposals and that
247    // are needed in case an update proposal is committed by another group
248    // member. The vector is emptied after every epoch change.
249    own_leaf_nodes: Vec<LeafNode>,
250    // Additional authenticated data (AAD) for the next outgoing message. This
251    // is ephemeral and will be reset by every API call that successfully
252    // returns an [`MlsMessageOut`].
253    aad: Vec<u8>,
254    // Safe AAD items to attach to the next outgoing message. Ephemeral, reset
255    // alongside `aad`. Only consulted when the group's GroupContext requires
256    // Safe AAD framing.
257    #[cfg(feature = "extensions-draft-08")]
258    safe_aad: SafeAad,
259    // A variable that indicates the state of the group. See [`MlsGroupState`]
260    // for more information.
261    group_state: MlsGroupState,
262    /// The state of the Application Exporter. See the MLS Extensions Draft 08
263    /// for more information. This is `None` if an old OpenMLS group state was
264    /// loaded and has not yet merged a commit.
265    #[cfg(feature = "extensions-draft-08")]
266    application_export_tree: Option<ApplicationExportTree>,
267}
268
269impl MlsGroup {
270    // === Configuration ===
271
272    /// Returns the configuration.
273    pub fn configuration(&self) -> &MlsGroupJoinConfig {
274        &self.mls_group_config
275    }
276
277    /// Sets the configuration.
278    pub fn set_configuration<Storage: StorageProvider>(
279        &mut self,
280        storage: &Storage,
281        mls_group_config: &MlsGroupJoinConfig,
282    ) -> Result<(), Storage::Error> {
283        self.mls_group_config = mls_group_config.clone();
284        storage.write_mls_join_config(self.group_id(), mls_group_config)
285    }
286
287    /// Sets the additional authenticated data (AAD) for the next outgoing
288    /// message. This is ephemeral and will be reset by every API call that
289    /// successfully returns an [`MlsMessageOut`].
290    pub fn set_aad(&mut self, aad: Vec<u8>) {
291        self.aad = aad;
292    }
293
294    /// Returns the additional authenticated data (AAD) for the next outgoing
295    /// message.
296    pub fn aad(&self) -> &[u8] {
297        &self.aad
298    }
299
300    /// Stage Safe AAD items for the next outgoing message. Items must be
301    /// sorted by [`ComponentId`] in strictly-increasing order and contain no
302    /// duplicates; otherwise the call fails and the previously staged items
303    /// are left untouched.
304    ///
305    /// Ephemeral, like [`Self::set_aad`]: cleared whenever an outgoing message
306    /// is produced.
307    ///
308    /// [`ComponentId`]: crate::component::ComponentId
309    #[cfg(feature = "extensions-draft-08")]
310    pub fn set_safe_aad(&mut self, items: Vec<SafeAadItem>) -> Result<(), SafeAadError> {
311        self.safe_aad = SafeAad::from_items(items)?;
312        Ok(())
313    }
314
315    /// Returns the currently staged Safe AAD items for the next outgoing
316    /// message.
317    #[cfg(feature = "extensions-draft-08")]
318    pub fn safe_aad_items(&self) -> &[SafeAadItem] {
319        self.safe_aad.items()
320    }
321
322    // === Advanced functions ===
323
324    /// Returns the group's ciphersuite.
325    pub fn ciphersuite(&self) -> Ciphersuite {
326        self.public_group.ciphersuite()
327    }
328
329    /// Get confirmation tag.
330    pub fn confirmation_tag(&self) -> &ConfirmationTag {
331        self.public_group.confirmation_tag()
332    }
333
334    /// Returns whether the own client is still a member of the group or if it
335    /// was already evicted
336    pub fn is_active(&self) -> bool {
337        !matches!(self.group_state, MlsGroupState::Inactive)
338    }
339
340    /// Returns own credential. If the group is inactive, it returns a
341    /// `UseAfterEviction` error.
342    pub fn credential(&self) -> Result<&Credential, MlsGroupStateError> {
343        if !self.is_active() {
344            return Err(MlsGroupStateError::UseAfterEviction);
345        }
346        self.public_group
347            .leaf(self.own_leaf_index())
348            .map(|node| node.credential())
349            .ok_or_else(|| LibraryError::custom("Own leaf node missing").into())
350    }
351
352    /// Returns the leaf index of the client in the tree owning this group.
353    pub fn own_leaf_index(&self) -> LeafNodeIndex {
354        self.own_leaf_index
355    }
356
357    /// Returns the leaf node of the client in the tree owning this group.
358    pub fn own_leaf_node(&self) -> Option<&LeafNode> {
359        self.public_group().leaf(self.own_leaf_index())
360    }
361
362    /// Returns the group ID.
363    pub fn group_id(&self) -> &GroupId {
364        self.public_group.group_id()
365    }
366
367    /// Returns the epoch.
368    pub fn epoch(&self) -> GroupEpoch {
369        self.public_group.group_context().epoch()
370    }
371
372    /// Returns an `Iterator` over pending proposals.
373    pub fn pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
374        self.proposal_store().proposals()
375    }
376
377    /// Returns the current tree state of the group, in the form of a [`TreeSync`].
378    pub fn treesync(&self) -> &TreeSync {
379        self.public_group.treesync()
380    }
381
382    /// Returns a reference to the [`StagedCommit`] of the most recently created
383    /// commit. If there was no commit created in this epoch, either because
384    /// this commit or another commit was merged, it returns `None`.
385    pub fn pending_commit(&self) -> Option<&StagedCommit> {
386        match self.group_state {
387            MlsGroupState::PendingCommit(ref pending_commit_state) => {
388                Some(pending_commit_state.staged_commit())
389            }
390            MlsGroupState::Operational => None,
391            MlsGroupState::Inactive => None,
392        }
393    }
394
395    /// Sets the `group_state` to [`MlsGroupState::Operational`], thus clearing
396    /// any potentially pending commits.
397    ///
398    /// Note that this has no effect if the group was created through an external commit and
399    /// the resulting external commit has not been merged yet. For more
400    /// information, see [`MlsGroup::external_commit_builder()`].
401    ///
402    /// Use with caution! This function should only be used if it is clear that
403    /// the pending commit will not be used in the group. In particular, if a
404    /// pending commit is later accepted by the group, this client will lack the
405    /// key material to encrypt or decrypt group messages.
406    pub fn clear_pending_commit<Storage: StorageProvider>(
407        &mut self,
408        storage: &Storage,
409    ) -> Result<(), Storage::Error> {
410        match self.group_state {
411            MlsGroupState::PendingCommit(ref pending_commit_state) => {
412                if let PendingCommitState::Member(_) = **pending_commit_state {
413                    self.group_state = MlsGroupState::Operational;
414                    storage.write_group_state(self.group_id(), &self.group_state)
415                } else {
416                    Ok(())
417                }
418            }
419            MlsGroupState::Operational | MlsGroupState::Inactive => Ok(()),
420        }
421    }
422
423    /// Clear the pending proposals, if the proposal store is not empty.
424    ///
425    /// Warning: Once the pending proposals are cleared it will be impossible to process
426    /// a Commit message that references those proposals. Only use this
427    /// function as a last resort, e.g. when a call to
428    /// `MlsGroup::commit_to_pending_proposals` fails.
429    pub fn clear_pending_proposals<Storage: StorageProvider>(
430        &mut self,
431        storage: &Storage,
432    ) -> Result<(), Storage::Error> {
433        // If the proposal store is not empty...
434        if !self.proposal_store().is_empty() {
435            // Empty the proposal store
436            self.proposal_store_mut().empty();
437
438            // Clear proposals in storage
439            storage.clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())?;
440        }
441
442        Ok(())
443    }
444
445    /// Get a reference to the group context [`Extensions`] of this [`MlsGroup`].
446    pub fn extensions(&self) -> &Extensions<GroupContext> {
447        self.public_group().group_context().extensions()
448    }
449
450    /// Returns the index of the sender of a staged, external commit.
451    pub fn ext_commit_sender_index(
452        &self,
453        commit: &StagedCommit,
454    ) -> Result<LeafNodeIndex, LibraryError> {
455        self.public_group().ext_commit_sender_index(commit)
456    }
457
458    // === Storage Methods ===
459
460    /// Loads the state of the group with given id from persisted state.
461    pub fn load<Storage: crate::storage::StorageProvider>(
462        storage: &Storage,
463        group_id: &GroupId,
464    ) -> Result<Option<MlsGroup>, Storage::Error> {
465        let public_group = PublicGroup::load(storage, group_id)?;
466        let group_epoch_secrets = storage.group_epoch_secrets(group_id)?;
467        let own_leaf_index = storage.own_leaf_index(group_id)?;
468        let message_secrets_store = storage.message_secrets(group_id)?;
469        let resumption_psk_store = storage.resumption_psk_store(group_id)?;
470        let mls_group_config = storage.mls_group_join_config(group_id)?;
471        let own_leaf_nodes = storage.own_leaf_nodes(group_id)?;
472        let group_state = storage.group_state(group_id)?;
473        #[cfg(feature = "extensions-draft-08")]
474        let application_export_tree = storage.application_export_tree(group_id)?;
475
476        let build = || -> Option<Self> {
477            Some(Self {
478                public_group: public_group?,
479                group_epoch_secrets: group_epoch_secrets?,
480                own_leaf_index: own_leaf_index?,
481                message_secrets_store: message_secrets_store?,
482                resumption_psk_store: resumption_psk_store?,
483                mls_group_config: mls_group_config?,
484                own_leaf_nodes,
485                aad: vec![],
486                #[cfg(feature = "extensions-draft-08")]
487                safe_aad: SafeAad::empty(),
488                group_state: group_state?,
489                #[cfg(feature = "extensions-draft-08")]
490                application_export_tree,
491            })
492        };
493
494        Ok(build())
495    }
496
497    /// Remove the persisted state of this group from storage. Note that
498    /// signature key material is not managed by OpenMLS and has to be removed
499    /// from the storage provider separately (if desired).
500    pub fn delete<Storage: crate::storage::StorageProvider>(
501        &mut self,
502        storage: &Storage,
503    ) -> Result<(), Storage::Error> {
504        PublicGroup::delete(storage, self.group_id())?;
505        storage.delete_own_leaf_index(self.group_id())?;
506        storage.delete_group_epoch_secrets(self.group_id())?;
507        storage.delete_message_secrets(self.group_id())?;
508        storage.delete_all_resumption_psk_secrets(self.group_id())?;
509        storage.delete_group_config(self.group_id())?;
510        storage.delete_own_leaf_nodes(self.group_id())?;
511        storage.delete_group_state(self.group_id())?;
512        storage.clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())?;
513
514        #[cfg(feature = "extensions-draft-08")]
515        storage.delete_application_export_tree::<_, ApplicationExportTree>(self.group_id())?;
516
517        self.proposal_store_mut().empty();
518        storage.delete_encryption_epoch_key_pairs(
519            self.group_id(),
520            &self.epoch(),
521            self.own_leaf_index().u32(),
522        )?;
523
524        Ok(())
525    }
526
527    // === Extensions ===
528
529    /// Exports the Ratchet Tree.
530    pub fn export_ratchet_tree(&self) -> RatchetTree {
531        self.public_group().export_ratchet_tree()
532    }
533}
534
535// Crate-public functions
536impl MlsGroup {
537    /// Get the required capabilities extension of this group.
538    pub(crate) fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
539        self.public_group.required_capabilities()
540    }
541
542    /// Get a reference to the group epoch secrets from the group
543    pub(crate) fn group_epoch_secrets(&self) -> &GroupEpochSecrets {
544        &self.group_epoch_secrets
545    }
546
547    /// Get a reference to the message secrets from a group
548    pub(crate) fn message_secrets(&self) -> &MessageSecrets {
549        self.message_secrets_store.message_secrets()
550    }
551
552    /// Sets the size of the [`MessageSecretsStore`], i.e. the number of past
553    /// epochs to keep.
554    /// This allows application messages from previous epochs to be decrypted.
555    pub(crate) fn resize_message_secrets_store(&mut self, policy: &PastEpochDeletionPolicy) {
556        self.message_secrets_store.resize(policy);
557    }
558
559    /// Get the past epoch secret deletion policy for the group.
560    pub fn past_epoch_deletion_policy(&self) -> &PastEpochDeletionPolicy {
561        self.mls_group_config.past_epoch_deletion_policy()
562    }
563
564    /// Set the past epoch secret deletion policy for the group.
565    pub fn set_past_epoch_deletion_policy<Provider: OpenMlsProvider>(
566        &mut self,
567        provider: &Provider,
568        policy: PastEpochDeletionPolicy,
569    ) -> Result<(), SetPastEpochDeletionPolicyError<Provider::StorageError>> {
570        // resize the store
571        self.resize_message_secrets_store(&policy);
572
573        // set the policy on the join config
574        self.mls_group_config.past_epoch_deletion_policy = policy;
575
576        // persist the join config
577        provider
578            .storage()
579            .write_mls_join_config(self.group_id(), &self.mls_group_config)?;
580
581        // update the message secrets store in storage
582        provider
583            .storage()
584            .write_message_secrets(self.group_id(), &self.message_secrets_store)?;
585
586        Ok(())
587    }
588
589    /// Get the message secrets. Either from the secrets store or from the group.
590    pub(crate) fn message_secrets_for_epoch_mut(
591        &mut self,
592        epoch: GroupEpoch,
593    ) -> Result<&mut MessageSecrets, SecretTreeError> {
594        if epoch < self.context().epoch() {
595            self.message_secrets_store
596                .secrets_for_epoch_mut(epoch)
597                .ok_or(SecretTreeError::TooDistantInThePast)
598        } else {
599            Ok(self.message_secrets_store.message_secrets_mut())
600        }
601    }
602
603    /// Get the message secrets. Either from the secrets store or from the group.
604    pub(crate) fn message_secrets_for_epoch(
605        &self,
606        epoch: GroupEpoch,
607    ) -> Result<&MessageSecrets, SecretTreeError> {
608        if epoch < self.context().epoch() {
609            self.message_secrets_store
610                .secrets_for_epoch(epoch)
611                .ok_or(SecretTreeError::TooDistantInThePast)
612        } else {
613            Ok(self.message_secrets_store.message_secrets())
614        }
615    }
616
617    /// Get the message secrets and leaves for the given epoch. Either from the
618    /// secrets store or from the group.
619    ///
620    /// Note that the leaves vector is empty for message secrets of the current
621    /// epoch. The caller can use treesync in this case.
622    pub(crate) fn message_secrets_and_leaves(
623        &self,
624        epoch: GroupEpoch,
625    ) -> Result<(&MessageSecrets, &[Member]), SecretTreeError> {
626        if epoch < self.context().epoch() {
627            self.message_secrets_store
628                .secrets_and_leaves_for_epoch(epoch)
629                .ok_or(SecretTreeError::TooDistantInThePast)
630        } else {
631            // No need for leaves here. The tree of the current epoch is
632            // available to the caller.
633            Ok((self.message_secrets_store.message_secrets(), &[]))
634        }
635    }
636
637    /// Create a new group context extension proposal
638    pub(crate) fn create_group_context_ext_proposal<Provider: OpenMlsProvider>(
639        &self,
640        framing_parameters: FramingParameters,
641        extensions: Extensions<GroupContext>,
642        signer: &impl Signer,
643    ) -> Result<AuthenticatedContent, CreateGroupContextExtProposalError<Provider::StorageError>>
644    {
645        // Ensure that the group supports all the extensions that are wanted.
646        let required_extension = extensions
647            .iter()
648            .find(|extension| extension.extension_type() == ExtensionType::RequiredCapabilities);
649        if let Some(required_extension) = required_extension {
650            let required_capabilities = required_extension.as_required_capabilities_extension()?;
651            // Ensure we support all the capabilities.
652            self.own_leaf_node()
653                .ok_or_else(|| LibraryError::custom("Tree has no own leaf."))?
654                .capabilities()
655                .supports_required_capabilities(required_capabilities)?;
656
657            // Ensure that all other leaf nodes support all the required
658            // extensions as well.
659            self.public_group()
660                .check_extension_support(required_capabilities.extension_types())?;
661        }
662        let proposal = GroupContextExtensionProposal::new(extensions);
663        let proposal = Proposal::GroupContextExtensions(Box::new(proposal));
664        AuthenticatedContent::member_proposal(
665            framing_parameters,
666            self.own_leaf_index(),
667            proposal,
668            self.context(),
669            signer,
670        )
671        .map_err(|e| e.into())
672    }
673
674    // Encrypt an AuthenticatedContent into an PrivateMessage
675    pub(crate) fn encrypt<Provider: OpenMlsProvider>(
676        &mut self,
677        public_message: AuthenticatedContent,
678        provider: &Provider,
679    ) -> Result<EncryptionOutput, MessageEncryptionError<Provider::StorageError>> {
680        let padding_size = self.configuration().padding_size();
681        let msg = PrivateMessage::try_from_authenticated_content(
682            provider.crypto(),
683            provider.rand(),
684            &public_message,
685            self.ciphersuite(),
686            self.message_secrets_store.message_secrets_mut(),
687            padding_size,
688        )?;
689
690        provider
691            .storage()
692            .write_message_secrets(self.group_id(), &self.message_secrets_store)
693            .map_err(MessageEncryptionError::StorageError)?;
694
695        Ok(msg)
696    }
697
698    /// Outgoing wire format derived from the group's configured policy.
699    pub(crate) fn outgoing_wire_format(&self) -> WireFormat {
700        self.mls_group_config.wire_format_policy().outgoing().into()
701    }
702
703    /// Owned `authenticated_data` bytes for the next outgoing message, taking
704    /// the GroupContext's Safe AAD requirement into account.
705    ///
706    /// Callers borrow the returned buffer into a [`FramingParameters`] for the
707    /// duration of message construction.
708    pub(crate) fn outgoing_authenticated_data(&self) -> Result<Vec<u8>, LibraryError> {
709        #[cfg(feature = "extensions-draft-08")]
710        {
711            self.assembled_authenticated_data()
712        }
713        #[cfg(not(feature = "extensions-draft-08"))]
714        {
715            Ok(self.aad.clone())
716        }
717    }
718
719    /// Build the bytes that go into `authenticated_data` for the next outgoing
720    /// message. When the GroupContext requires Safe AAD framing, the result is
721    /// the TLS serialization of the staged [`SafeAad`] followed by the bytes of
722    /// `self.aad`. Otherwise, the result is `self.aad` unchanged.
723    #[cfg(feature = "extensions-draft-08")]
724    pub(crate) fn assembled_authenticated_data(&self) -> Result<Vec<u8>, LibraryError> {
725        if !self.context().safe_aad_required() {
726            return Ok(self.aad.clone());
727        }
728        crate::framing::safe_aad::assemble_authenticated_data(&self.safe_aad, &self.aad)
729            .map_err(|_| LibraryError::custom("SafeAad serialization failed"))
730    }
731
732    /// Delete all past epoch secrets.
733    ///
734    /// For more information on the arguments to this method, see [`PastEpochDeletion`].
735    pub fn delete_past_epoch_secrets<Provider: OpenMlsProvider>(
736        &mut self,
737        provider: &Provider,
738        policy: PastEpochDeletion,
739    ) -> Result<(), DeletePastEpochSecretsError<Provider::StorageError>> {
740        // delete past epoch secrets in memory
741        self.message_secrets_store.delete_past_epoch_secrets(policy);
742        // update the message secrets store in storage
743        provider
744            .storage()
745            .write_message_secrets(self.group_id(), &self.message_secrets_store)?;
746
747        Ok(())
748    }
749
750    /// Returns a reference to the proposal store.
751    pub fn proposal_store(&self) -> &ProposalStore {
752        self.public_group.proposal_store()
753    }
754
755    /// Returns a mutable reference to the proposal store.
756    pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
757        self.public_group.proposal_store_mut()
758    }
759
760    /// Get the group context
761    pub(crate) fn context(&self) -> &GroupContext {
762        self.public_group.group_context()
763    }
764
765    /// Get the MLS version used in this group.
766    pub(crate) fn version(&self) -> ProtocolVersion {
767        self.public_group.version()
768    }
769
770    /// Resets the AAD, including any staged Safe AAD items.
771    #[inline]
772    pub(crate) fn reset_aad(&mut self) {
773        self.aad.clear();
774        #[cfg(feature = "extensions-draft-08")]
775        {
776            self.safe_aad = SafeAad::empty();
777        }
778    }
779
780    /// Returns a reference to the public group.
781    pub fn public_group(&self) -> &PublicGroup {
782        &self.public_group
783    }
784}
785
786// Private methods of MlsGroup
787impl MlsGroup {
788    /// Store the given [`EncryptionKeyPair`]s in the `provider`'s key store
789    /// indexed by this group's [`GroupId`] and [`GroupEpoch`].
790    ///
791    /// Returns an error if access to the key store fails.
792    pub(super) fn store_epoch_keypairs<Storage: StorageProvider>(
793        &self,
794        store: &Storage,
795        keypair_references: &[EncryptionKeyPair],
796    ) -> Result<(), Storage::Error> {
797        store.write_encryption_epoch_key_pairs(
798            self.group_id(),
799            &self.context().epoch(),
800            self.own_leaf_index().u32(),
801            keypair_references,
802        )
803    }
804
805    /// Read the [`EncryptionKeyPair`]s of this group and its current
806    /// [`GroupEpoch`] from the `provider`'s storage.
807    ///
808    /// Returns an error if the lookup in the [`StorageProvider`] fails.
809    pub(super) fn read_epoch_keypairs<Storage: StorageProvider>(
810        &self,
811        store: &Storage,
812    ) -> Result<Vec<EncryptionKeyPair>, Storage::Error> {
813        store.encryption_epoch_key_pairs(
814            self.group_id(),
815            &self.context().epoch(),
816            self.own_leaf_index().u32(),
817        )
818    }
819
820    /// Delete the [`EncryptionKeyPair`]s from the previous [`GroupEpoch`] from
821    /// the `provider`'s key store.
822    ///
823    /// Returns an error if access to the key store fails.
824    pub(super) fn delete_previous_epoch_keypairs<Storage: StorageProvider>(
825        &self,
826        store: &Storage,
827    ) -> Result<(), Storage::Error> {
828        store.delete_encryption_epoch_key_pairs(
829            self.group_id(),
830            &GroupEpoch::from(self.context().epoch().as_u64() - 1),
831            self.own_leaf_index().u32(),
832        )
833    }
834
835    /// Stores the state of this group. Only to be called from constructors to
836    /// store the initial state of the group.
837    pub(super) fn store<Storage: crate::storage::StorageProvider>(
838        &self,
839        storage: &Storage,
840    ) -> Result<(), Storage::Error> {
841        self.public_group.store(storage)?;
842        storage.write_group_epoch_secrets(self.group_id(), &self.group_epoch_secrets)?;
843        storage.write_own_leaf_index(self.group_id(), &self.own_leaf_index)?;
844        storage.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
845        storage.write_resumption_psk_store(self.group_id(), &self.resumption_psk_store)?;
846        storage.write_mls_join_config(self.group_id(), &self.mls_group_config)?;
847        storage.write_group_state(self.group_id(), &self.group_state)?;
848        #[cfg(feature = "extensions-draft-08")]
849        if let Some(application_export_tree) = &self.application_export_tree {
850            storage.write_application_export_tree(self.group_id(), application_export_tree)?;
851        }
852
853        Ok(())
854    }
855
856    /// Converts PublicMessage to MlsMessage. Depending on whether handshake
857    /// message should be encrypted, PublicMessage messages are encrypted to
858    /// PrivateMessage first.
859    fn content_to_mls_message(
860        &mut self,
861        mls_auth_content: AuthenticatedContent,
862        provider: &impl OpenMlsProvider,
863    ) -> Result<MlsMessageOut, LibraryError> {
864        let msg = match self.configuration().wire_format_policy().outgoing() {
865            OutgoingWireFormatPolicy::AlwaysPlaintext => {
866                let mut plaintext: PublicMessage = mls_auth_content.into();
867                // Set the membership tag only if the sender type is `Member`.
868                if plaintext.sender().is_member() {
869                    plaintext.set_membership_tag(
870                        provider.crypto(),
871                        self.ciphersuite(),
872                        self.message_secrets().membership_key(),
873                        self.message_secrets().serialized_context(),
874                    )?;
875                }
876                plaintext.into()
877            }
878            OutgoingWireFormatPolicy::AlwaysCiphertext => {
879                // Decrypting own handshake messages is not supported yet with
880                // the `virtual-clients` feature, so the generation is unused.
881                let EncryptionOutput {
882                    private_message, ..
883                } = self
884                    .encrypt(mls_auth_content, provider)
885                    // We can be sure the encryption will work because the plaintext was created by us
886                    .map_err(|_| LibraryError::custom("Malformed plaintext"))?;
887                MlsMessageOut::from_private_message(private_message, self.version())
888            }
889        };
890        Ok(msg)
891    }
892
893    /// Check if the group is operational. Throws an error if the group is
894    /// inactive or if there is a pending commit.
895    fn is_operational(&self) -> Result<(), MlsGroupStateError> {
896        match self.group_state {
897            MlsGroupState::PendingCommit(_) => Err(MlsGroupStateError::PendingCommit),
898            MlsGroupState::Inactive => Err(MlsGroupStateError::UseAfterEviction),
899            MlsGroupState::Operational => Ok(()),
900        }
901    }
902}
903
904// Methods used in tests
905impl MlsGroup {
906    #[cfg(any(feature = "test-utils", test))]
907    pub fn export_group_context(&self) -> &GroupContext {
908        self.context()
909    }
910
911    #[cfg(any(feature = "test-utils", test))]
912    pub fn tree_hash(&self) -> &[u8] {
913        self.public_group().group_context().tree_hash()
914    }
915
916    #[cfg(any(feature = "test-utils", test))]
917    pub(crate) fn message_secrets_test_mut(&mut self) -> &mut MessageSecrets {
918        self.message_secrets_store.message_secrets_mut()
919    }
920
921    #[cfg(any(feature = "test-utils", test))]
922    pub fn print_ratchet_tree(&self, message: &str) {
923        println!("{}: {}", message, self.public_group().export_ratchet_tree());
924    }
925
926    #[cfg(any(feature = "test-utils", test))]
927    pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
928        self.public_group.context_mut()
929    }
930
931    #[cfg(test)]
932    pub(crate) fn set_own_leaf_index(&mut self, own_leaf_index: LeafNodeIndex) {
933        self.own_leaf_index = own_leaf_index;
934    }
935
936    #[cfg(test)]
937    pub(crate) fn own_tree_position(&self) -> TreePosition {
938        TreePosition::new(self.group_id().clone(), self.own_leaf_index())
939    }
940
941    #[cfg(test)]
942    pub(crate) fn message_secrets_store(&self) -> &MessageSecretsStore {
943        &self.message_secrets_store
944    }
945
946    #[cfg(test)]
947    pub(crate) fn resumption_psk_store(&self) -> &ResumptionPskStore {
948        &self.resumption_psk_store
949    }
950
951    #[cfg(test)]
952    pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
953        self.public_group.set_group_context(group_context)
954    }
955
956    #[cfg(any(test, feature = "test-utils"))]
957    pub fn ensure_persistence(&self, storage: &impl StorageProvider) -> Result<(), LibraryError> {
958        let loaded = MlsGroup::load(storage, self.group_id())
959            .map_err(|_| LibraryError::custom("Failed to load group from storage"))?;
960        let other = loaded.ok_or_else(|| LibraryError::custom("Group not found in storage"))?;
961
962        if self != &other {
963            let mut diagnostics = Vec::new();
964
965            if self.mls_group_config != other.mls_group_config {
966                diagnostics.push(format!(
967                    "mls_group_config:\n  Current: {:?}\n  Loaded:  {:?}",
968                    self.mls_group_config, other.mls_group_config
969                ));
970            }
971            if self.public_group != other.public_group {
972                diagnostics.push(format!(
973                    "public_group:\n  Current: {:?}\n  Loaded:  {:?}",
974                    self.public_group, other.public_group
975                ));
976            }
977            if self.group_epoch_secrets != other.group_epoch_secrets {
978                diagnostics.push(format!(
979                    "group_epoch_secrets:\n  Current: {:?}\n  Loaded:  {:?}",
980                    self.group_epoch_secrets, other.group_epoch_secrets
981                ));
982            }
983            if self.own_leaf_index != other.own_leaf_index {
984                diagnostics.push(format!(
985                    "own_leaf_index:\n  Current: {:?}\n  Loaded:  {:?}",
986                    self.own_leaf_index, other.own_leaf_index
987                ));
988            }
989            if self.message_secrets_store != other.message_secrets_store {
990                diagnostics.push(format!(
991                    "message_secrets_store:\n  Current: {:?}\n  Loaded:  {:?}",
992                    self.message_secrets_store, other.message_secrets_store
993                ));
994            }
995            if self.resumption_psk_store != other.resumption_psk_store {
996                diagnostics.push(format!(
997                    "resumption_psk_store:\n  Current: {:?}\n  Loaded:  {:?}",
998                    self.resumption_psk_store, other.resumption_psk_store
999                ));
1000            }
1001            if self.own_leaf_nodes != other.own_leaf_nodes {
1002                diagnostics.push(format!(
1003                    "own_leaf_nodes:\n  Current: {:?}\n  Loaded:  {:?}",
1004                    self.own_leaf_nodes, other.own_leaf_nodes
1005                ));
1006            }
1007            if self.aad != other.aad {
1008                diagnostics.push(format!(
1009                    "aad:\n  Current: {:?}\n  Loaded:  {:?}",
1010                    self.aad, other.aad
1011                ));
1012            }
1013            if self.group_state != other.group_state {
1014                diagnostics.push(format!(
1015                    "group_state:\n  Current: {:?}\n  Loaded:  {:?}",
1016                    self.group_state, other.group_state
1017                ));
1018            }
1019            #[cfg(feature = "extensions-draft-08")]
1020            if self.application_export_tree != other.application_export_tree {
1021                diagnostics.push(format!(
1022                    "application_export_tree:\n  Current: {:?}\n  Loaded:  {:?}",
1023                    self.application_export_tree, other.application_export_tree
1024                ));
1025            }
1026
1027            log::error!(
1028                "Loaded group does not match current group! Differing fields ({}):\n\n{}",
1029                diagnostics.len(),
1030                diagnostics.join("\n\n")
1031            );
1032
1033            return Err(LibraryError::custom(
1034                "Loaded group does not match current group",
1035            ));
1036        }
1037
1038        Ok(())
1039    }
1040}
1041
1042/// A [`StagedWelcome`] can be inspected and then turned into a [`MlsGroup`].
1043/// This allows checking who authored the Welcome message.
1044#[derive(Debug)]
1045pub struct StagedWelcome {
1046    // The group configuration. See [`MlsGroupJoinConfig`] for more information.
1047    mls_group_config: MlsGroupJoinConfig,
1048    public_group: PublicGroup,
1049    group_epoch_secrets: GroupEpochSecrets,
1050    own_leaf_index: LeafNodeIndex,
1051
1052    /// A [`MessageSecretsStore`] that stores message secrets.
1053    /// By default this store has the length of 1, i.e. only the [`MessageSecrets`]
1054    /// of the current epoch is kept.
1055    /// If more secrets from past epochs should be kept in order to be
1056    /// able to decrypt application messages from previous epochs, the size of
1057    /// the store must be increased through [`max_past_epochs()`].
1058    message_secrets_store: MessageSecretsStore,
1059
1060    /// A secret that is not stored as part of the [`MlsGroup`] after the group is created.
1061    /// It can be used by the application to derive forward secure secrets.
1062    #[cfg(feature = "extensions-draft-08")]
1063    application_export_secret: ApplicationExportSecret,
1064
1065    /// Resumption psk store. This is where the resumption psks are kept in a rollover list.
1066    resumption_psk_store: ResumptionPskStore,
1067
1068    /// The [`VerifiableGroupInfo`] from the [`Welcome`] message.
1069    verifiable_group_info: VerifiableGroupInfo,
1070
1071    /// The key package bundle used for this welcome.
1072    key_package_bundle: KeyPackageBundle,
1073
1074    /// If we got a path secret, these are the derived path keys.
1075    path_keypairs: Option<Vec<EncryptionKeyPair>>,
1076}
1077
1078/// A `Welcome` message that has been processed but not staged yet.
1079///
1080/// This may be used in order to retrieve information from the `Welcome` about
1081/// the ratchet tree and PSKs.
1082///
1083/// Use `into_staged_welcome` to stage it into a [`StagedWelcome`].
1084pub struct ProcessedWelcome {
1085    // The group configuration. See [`MlsGroupJoinConfig`] for more information.
1086    mls_group_config: MlsGroupJoinConfig,
1087
1088    // The following is the state after parsing the Welcome message, before actually
1089    // building the group.
1090    ciphersuite: Ciphersuite,
1091    group_secrets: GroupSecrets,
1092    key_schedule: crate::schedule::KeySchedule,
1093    verifiable_group_info: crate::messages::group_info::VerifiableGroupInfo,
1094    resumption_psk_store: crate::schedule::psk::store::ResumptionPskStore,
1095    key_package_bundle: KeyPackageBundle,
1096}