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::{InitKey, 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::{
47    crypto::OpenMlsCrypto, signatures::Signer, storage::StorageProvider as _, types::Ciphersuite,
48};
49
50#[cfg(feature = "extensions-draft")]
51use crate::schedule::{application_export_tree::ApplicationExportTree, ApplicationExportSecret};
52
53// Private
54mod application;
55mod exporting;
56mod updates;
57
58#[cfg(feature = "migration-import")]
59pub(crate) mod migration_import;
60
61#[cfg(feature = "virtual-clients-draft")]
62pub use application::UnconfirmedMessage;
63pub use proposal::Propose;
64
65use config::*;
66
67// Crate
68pub(crate) mod builder;
69pub(crate) mod commit_builder;
70pub(crate) mod config;
71pub(crate) mod creation;
72pub(crate) mod errors;
73pub(crate) mod membership;
74pub(crate) mod past_secrets;
75pub(crate) mod processing;
76pub(crate) mod proposal;
77pub(crate) mod proposal_store;
78pub(crate) mod staged_commit;
79
80#[cfg(feature = "extensions-draft")]
81pub(crate) mod app_ephemeral;
82
83#[cfg(feature = "targeted-messages-draft")]
84mod targeted_messages;
85
86// Tests
87#[cfg(test)]
88pub(crate) mod tests_and_kats;
89
90#[derive(Debug)]
91pub(crate) struct CreateCommitResult {
92    pub(crate) commit: AuthenticatedContent,
93    pub(crate) welcome_option: Option<Welcome>,
94    pub(crate) staged_commit: StagedCommit,
95    pub(crate) group_info: Option<GroupInfo>,
96}
97
98/// A member in the group is identified by this [`Member`] struct.
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
100pub struct Member {
101    /// The member's leaf index in the ratchet tree.
102    pub index: LeafNodeIndex,
103    /// The member's credential.
104    pub credential: Credential,
105    /// The member's public HPHKE encryption key.
106    pub encryption_key: Vec<u8>,
107    /// The member's public signature key.
108    pub signature_key: Vec<u8>,
109}
110
111impl Member {
112    /// Create new member.
113    pub fn new(
114        index: LeafNodeIndex,
115        encryption_key: Vec<u8>,
116        signature_key: Vec<u8>,
117        credential: Credential,
118    ) -> Self {
119        Self {
120            index,
121            encryption_key,
122            signature_key,
123            credential,
124        }
125    }
126}
127
128/// Pending Commit state. Differentiates between Commits issued by group members
129/// and External Commits.
130#[derive(Debug, Serialize, Deserialize)]
131#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
132pub enum PendingCommitState {
133    /// Commit from a group member
134    Member(StagedCommit),
135    /// Commit from an external joiner
136    External(StagedCommit),
137}
138
139impl PendingCommitState {
140    /// Returns a reference to the [`StagedCommit`] contained in the
141    /// [`PendingCommitState`] enum.
142    pub(crate) fn staged_commit(&self) -> &StagedCommit {
143        match self {
144            PendingCommitState::Member(pc) => pc,
145            PendingCommitState::External(pc) => pc,
146        }
147    }
148}
149
150impl From<PendingCommitState> for StagedCommit {
151    fn from(pcs: PendingCommitState) -> Self {
152        match pcs {
153            PendingCommitState::Member(pc) => pc,
154            PendingCommitState::External(pc) => pc,
155        }
156    }
157}
158
159/// [`MlsGroupState`] determines the state of an [`MlsGroup`]. The different
160/// states and their transitions are as follows:
161///
162/// * [`MlsGroupState::Operational`]: This is the main state of the group, which
163///   allows access to all of its functionality, (except merging pending commits,
164///   see the [`MlsGroupState::PendingCommit`] for more information) and it's the
165///   state the group starts in (except when created via
166///   [`MlsGroup::external_commit_builder()`], see the functions documentation for
167///   more information). From this `Operational`, the group state can either
168///   transition to [`MlsGroupState::Inactive`], when it processes a commit that
169///   removes this client from the group, or to [`MlsGroupState::PendingCommit`],
170///   when this client creates a commit.
171///
172/// * [`MlsGroupState::Inactive`]: A group can enter this state from any other
173///   state when it processes a commit that removes this client from the group.
174///   This is a terminal state that the group can not exit from. If the clients
175///   wants to re-join the group, it can either be added by a group member or it
176///   can join via external commit.
177///
178/// * [`MlsGroupState::PendingCommit`]: This state is split into two possible
179///   sub-states, one for each Commit type:
180///   [`PendingCommitState::Member`] and [`PendingCommitState::External`]:
181///
182///   * If the client creates a commit for this group, the `PendingCommit` state
183///     is entered with [`PendingCommitState::Member`] and with the [`StagedCommit`] as
184///     additional state variable. In this state, it can perform the same
185///     operations as in the [`MlsGroupState::Operational`], except that it cannot
186///     create proposals or commits. However, it can merge or clear the stored
187///     [`StagedCommit`], where both actions result in a transition to the
188///     [`MlsGroupState::Operational`]. Additionally, if a commit from another
189///     group member is processed, the own pending commit is also cleared and
190///     either the `Inactive` state is entered (if this client was removed from
191///     the group as part of the processed commit), or the `Operational` state is
192///     entered.
193///
194///   * A group can enter the [`PendingCommitState::External`] sub-state only as
195///     the initial state when the group is created via
196///     [`MlsGroup::external_commit_builder()`]. In contrast to the
197///     [`PendingCommitState::Member`] `PendingCommit` state, the only possible
198///     functionality that can be used is the [`MlsGroup::merge_pending_commit()`]
199///     function, which merges the pending external commit and transitions the
200///     state to [`MlsGroupState::PendingCommit`]. For more information on the
201///     external commit process, see [`MlsGroup::external_commit_builder()`] or
202///     Section 11.2.1 of the MLS specification.
203#[derive(Debug, Serialize, Deserialize)]
204#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
205pub enum MlsGroupState {
206    /// There is currently a pending Commit that hasn't been merged yet.
207    PendingCommit(Box<PendingCommitState>),
208    /// The group state is in an opertaional state, where new messages and Commits can be created.
209    Operational,
210    /// The group is inactive because the member has been removed.
211    Inactive,
212}
213
214/// A `MlsGroup` represents an MLS group with a high-level API. The API exposes
215/// high level functions to manage a group by adding/removing members, get the
216/// current member list, etc.
217///
218/// The API is modeled such that it can serve as a direct interface to the
219/// Delivery Service. Functions that modify the public state of the group will
220/// return a `Vec<MLSMessageOut>` that can be sent to the Delivery Service
221/// directly. Conversely, incoming messages from the Delivery Service can be fed
222/// into [process_message()](`MlsGroup::process_message()`).
223///
224/// An `MlsGroup` has an internal queue of pending proposals that builds up as
225/// new messages are processed. When creating proposals, those messages are not
226/// automatically appended to this queue, instead they have to be processed
227/// again through [process_message()](`MlsGroup::process_message()`). This
228/// allows the Delivery Service to reject them (e.g. if they reference the wrong
229/// epoch).
230///
231/// If incoming messages or applied operations are semantically or syntactically
232/// incorrect, an error event will be returned with a corresponding error
233/// message and the state of the group will remain unchanged.
234///
235/// An `MlsGroup` has an internal state variable determining if it is active or
236/// inactive, as well as if it has a pending commit. See [`MlsGroupState`] for
237/// more information.
238#[derive(Debug)]
239#[cfg_attr(feature = "migration-import", derive(serde::Deserialize))]
240#[cfg_attr(
241    all(feature = "migration-import", feature = "test-utils"),
242    derive(serde::Serialize)
243)]
244#[cfg_attr(feature = "test-utils", derive(Clone, PartialEq))]
245pub struct MlsGroup {
246    /// The group configuration. See [`MlsGroupJoinConfig`] for more information.
247    mls_group_config: MlsGroupJoinConfig,
248    /// The public state of the group.
249    public_group: PublicGroup,
250    /// Epoch-specific secrets of the group.
251    group_epoch_secrets: GroupEpochSecrets,
252    /// The own leaf index in the ratchet tree.
253    own_leaf_index: LeafNodeIndex,
254    /// A [`MessageSecretsStore`] that stores message secrets.
255    /// By default this store has the length of 1, i.e. only the [`MessageSecrets`]
256    /// of the current epoch is kept.
257    /// If more secrets from past epochs should be kept in order to be
258    /// able to decrypt application messages from previous epochs, the size of
259    /// the store must be increased through [`max_past_epochs()`].
260    message_secrets_store: MessageSecretsStore,
261    // Resumption psk store. This is where the resumption psks are kept in a rollover list.
262    resumption_psk_store: ResumptionPskStore,
263    // Own [`LeafNode`]s that were created for update proposals and that
264    // are needed in case an update proposal is committed by another group
265    // member. The vector is emptied after every epoch change.
266    own_leaf_nodes: Vec<LeafNode>,
267    // Additional authenticated data (AAD) for the next outgoing message. This
268    // is ephemeral and will be reset by every API call that successfully
269    // returns an [`MlsMessageOut`].
270    aad: Vec<u8>,
271    // Safe AAD items to attach to the next outgoing message. Ephemeral, reset
272    // alongside `aad`. Only consulted when the group's GroupContext requires
273    // Safe AAD framing.
274    #[cfg(feature = "extensions-draft")]
275    // Migration bridge:
276    // absent from older serializations, so default it to empty on import — it is
277    // ephemeral, so a freshly migrated group has nothing staged anyway.
278    #[cfg_attr(
279        feature = "migration-import",
280        serde(default = "crate::framing::SafeAad::empty")
281    )]
282    safe_aad: SafeAad,
283    // A variable that indicates the state of the group. See [`MlsGroupState`]
284    // for more information.
285    group_state: MlsGroupState,
286    /// The state of the Application Exporter. See the MLS Extensions Draft 08
287    /// for more information. This is `None` if an old OpenMLS group state was
288    /// loaded and has not yet merged a commit.
289    #[cfg(feature = "extensions-draft")]
290    // Migration bridge (see the note on the struct): absent when migrating in a
291    // group from a version that did not have `extensions-draft`, so default it to
292    // `None` on import — it initializes on the next merged commit.
293    #[cfg_attr(feature = "migration-import", serde(default))]
294    application_export_tree: Option<ApplicationExportTree>,
295    /// Whether this group is an emulation group of a virtual client. Not
296    /// persisted on its own: [`MlsGroup::load`] recovers it from the presence of
297    /// the group's derivation-epoch registration record.
298    #[cfg(feature = "virtual-clients-draft")]
299    // Migration bridge (see the note on the struct): a group migrated in from a
300    // version without `virtual-clients-draft` is never an emulation group.
301    #[cfg_attr(feature = "migration-import", serde(default))]
302    emulation_group: bool,
303}
304
305impl MlsGroup {
306    // === Configuration ===
307
308    /// Returns the configuration.
309    pub fn configuration(&self) -> &MlsGroupJoinConfig {
310        &self.mls_group_config
311    }
312
313    /// Sets the configuration.
314    pub fn set_configuration<Storage: StorageProvider>(
315        &mut self,
316        storage: &Storage,
317        mls_group_config: &MlsGroupJoinConfig,
318    ) -> Result<(), Storage::Error> {
319        let policy_changed = self.mls_group_config.past_epoch_deletion_policy()
320            != mls_group_config.past_epoch_deletion_policy();
321
322        self.mls_group_config = mls_group_config.clone();
323        storage.write_mls_join_config(self.group_id(), mls_group_config)?;
324
325        if policy_changed {
326            // Resize the store to adhere to the new policy.
327            self.resize_message_secrets_store(mls_group_config.past_epoch_deletion_policy());
328            storage.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
329        }
330
331        Ok(())
332    }
333
334    /// Sets the additional authenticated data (AAD) for the next outgoing
335    /// message. This is ephemeral and will be reset by every API call that
336    /// successfully returns an [`MlsMessageOut`].
337    pub fn set_aad(&mut self, aad: Vec<u8>) {
338        self.aad = aad;
339    }
340
341    /// Returns the additional authenticated data (AAD) for the next outgoing
342    /// message.
343    pub fn aad(&self) -> &[u8] {
344        &self.aad
345    }
346
347    /// Stage Safe AAD items for the next outgoing message. Items must be
348    /// sorted by [`ComponentId`] in strictly-increasing order and contain no
349    /// duplicates; otherwise the call fails and the previously staged items
350    /// are left untouched.
351    ///
352    /// Ephemeral, like [`Self::set_aad`]: cleared whenever an outgoing message
353    /// is produced.
354    ///
355    /// [`ComponentId`]: crate::component::ComponentId
356    #[cfg(feature = "extensions-draft")]
357    pub fn set_safe_aad(&mut self, items: Vec<SafeAadItem>) -> Result<(), SafeAadError> {
358        self.safe_aad = SafeAad::from_items(items)?;
359        Ok(())
360    }
361
362    /// Returns the currently staged Safe AAD items for the next outgoing
363    /// message.
364    #[cfg(feature = "extensions-draft")]
365    pub fn safe_aad_items(&self) -> &[SafeAadItem] {
366        self.safe_aad.items()
367    }
368
369    // === Advanced functions ===
370
371    /// Returns the group's ciphersuite.
372    pub fn ciphersuite(&self) -> Ciphersuite {
373        self.public_group.ciphersuite()
374    }
375
376    /// Get confirmation tag.
377    pub fn confirmation_tag(&self) -> &ConfirmationTag {
378        self.public_group.confirmation_tag()
379    }
380
381    /// Returns whether the own client is still a member of the group or if it
382    /// was already evicted
383    pub fn is_active(&self) -> bool {
384        !matches!(self.group_state, MlsGroupState::Inactive)
385    }
386
387    /// Returns own credential. If the group is inactive, it returns a
388    /// `UseAfterEviction` error.
389    pub fn credential(&self) -> Result<&Credential, MlsGroupStateError> {
390        if !self.is_active() {
391            return Err(MlsGroupStateError::UseAfterEviction);
392        }
393        self.public_group
394            .leaf(self.own_leaf_index())
395            .map(|node| node.credential())
396            .ok_or_else(|| LibraryError::custom("Own leaf node missing").into())
397    }
398
399    /// Returns the leaf index of the client in the tree owning this group.
400    pub fn own_leaf_index(&self) -> LeafNodeIndex {
401        self.own_leaf_index
402    }
403
404    /// Returns the leaf node of the client in the tree owning this group.
405    pub fn own_leaf_node(&self) -> Option<&LeafNode> {
406        self.public_group().leaf(self.own_leaf_index())
407    }
408
409    /// Returns the group ID.
410    pub fn group_id(&self) -> &GroupId {
411        self.public_group.group_id()
412    }
413
414    /// Returns the epoch.
415    pub fn epoch(&self) -> GroupEpoch {
416        self.public_group.group_context().epoch()
417    }
418
419    /// Returns an `Iterator` over pending proposals.
420    pub fn pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
421        self.proposal_store().proposals()
422    }
423
424    /// Returns the current tree state of the group, in the form of a [`TreeSync`].
425    pub fn treesync(&self) -> &TreeSync {
426        self.public_group.treesync()
427    }
428
429    /// Returns a reference to the [`StagedCommit`] of the most recently created
430    /// commit. If there was no commit created in this epoch, either because
431    /// this commit or another commit was merged, it returns `None`.
432    pub fn pending_commit(&self) -> Option<&StagedCommit> {
433        match self.group_state {
434            MlsGroupState::PendingCommit(ref pending_commit_state) => {
435                Some(pending_commit_state.staged_commit())
436            }
437            MlsGroupState::Operational => None,
438            MlsGroupState::Inactive => None,
439        }
440    }
441
442    /// Sets the `group_state` to [`MlsGroupState::Operational`], thus clearing
443    /// any potentially pending commits.
444    ///
445    /// Note that this has no effect if the group was created through an external commit and
446    /// the resulting external commit has not been merged yet. For more
447    /// information, see [`MlsGroup::external_commit_builder()`].
448    ///
449    /// Use with caution! This function should only be used if it is clear that
450    /// the pending commit will not be used in the group. In particular, if a
451    /// pending commit is later accepted by the group, this client will lack the
452    /// key material to encrypt or decrypt group messages.
453    pub fn clear_pending_commit<Storage: StorageProvider>(
454        &mut self,
455        storage: &Storage,
456    ) -> Result<(), Storage::Error> {
457        match self.group_state {
458            MlsGroupState::PendingCommit(ref pending_commit_state) => {
459                if let PendingCommitState::Member(_) = **pending_commit_state {
460                    self.group_state = MlsGroupState::Operational;
461                    storage.write_group_state(self.group_id(), &self.group_state)
462                } else {
463                    Ok(())
464                }
465            }
466            MlsGroupState::Operational | MlsGroupState::Inactive => Ok(()),
467        }
468    }
469
470    /// Clear the pending proposals, if the proposal store is not empty.
471    ///
472    /// Warning: Once the pending proposals are cleared it will be impossible to process
473    /// a Commit message that references those proposals. Only use this
474    /// function as a last resort, e.g. when a call to
475    /// `MlsGroup::commit_to_pending_proposals` fails.
476    pub fn clear_pending_proposals<Storage: StorageProvider>(
477        &mut self,
478        storage: &Storage,
479    ) -> Result<(), Storage::Error> {
480        // If the proposal store is not empty...
481        if !self.proposal_store().is_empty() {
482            // Empty the proposal store
483            self.proposal_store_mut().empty();
484
485            // Clear proposals in storage
486            storage.clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())?;
487        }
488
489        Ok(())
490    }
491
492    /// Get a reference to the group context [`Extensions`] of this [`MlsGroup`].
493    pub fn extensions(&self) -> &Extensions<GroupContext> {
494        self.public_group().group_context().extensions()
495    }
496
497    /// Returns the index of the sender of a staged, external commit.
498    pub fn ext_commit_sender_index(
499        &self,
500        commit: &StagedCommit,
501    ) -> Result<LeafNodeIndex, LibraryError> {
502        self.public_group().ext_commit_sender_index(commit)
503    }
504
505    // === Storage Methods ===
506
507    /// Loads the state of the group with given id from persisted state.
508    pub fn load<Storage: crate::storage::StorageProvider>(
509        storage: &Storage,
510        group_id: &GroupId,
511    ) -> Result<Option<MlsGroup>, Storage::Error> {
512        let public_group = PublicGroup::load(storage, group_id)?;
513        let group_epoch_secrets = storage.group_epoch_secrets(group_id)?;
514        let own_leaf_index = storage.own_leaf_index(group_id)?;
515        let message_secrets_store = storage.message_secrets(group_id)?;
516        let resumption_psk_store = storage.resumption_psk_store(group_id)?;
517        let mls_group_config = storage.mls_group_join_config(group_id)?;
518        let own_leaf_nodes = storage.own_leaf_nodes(group_id)?;
519        let group_state = storage.group_state(group_id)?;
520        #[cfg(feature = "extensions-draft")]
521        let application_export_tree = storage.application_export_tree(group_id)?;
522        // A group has a derivation-epoch registration record for exactly as long
523        // as it is an emulation group. The record is written by the initial
524        // registration at creation or Welcome join and removed by `delete`.
525        #[cfg(feature = "virtual-clients-draft")]
526        let emulation_group =
527            crate::components::vc_derivation_info::newest_vc_derivation_epoch(storage, group_id)?
528                .is_some();
529
530        let build = || -> Option<Self> {
531            Some(Self {
532                public_group: public_group?,
533                group_epoch_secrets: group_epoch_secrets?,
534                own_leaf_index: own_leaf_index?,
535                message_secrets_store: message_secrets_store?,
536                resumption_psk_store: resumption_psk_store?,
537                mls_group_config: mls_group_config?,
538                own_leaf_nodes,
539                aad: vec![],
540                #[cfg(feature = "extensions-draft")]
541                safe_aad: SafeAad::empty(),
542                group_state: group_state?,
543                #[cfg(feature = "extensions-draft")]
544                application_export_tree,
545                #[cfg(feature = "virtual-clients-draft")]
546                emulation_group,
547            })
548        };
549
550        Ok(build())
551    }
552
553    /// Remove the persisted state of this group from storage. Note that
554    /// signature key material is not managed by OpenMLS and has to be removed
555    /// from the storage provider separately (if desired).
556    pub fn delete<Storage: crate::storage::StorageProvider>(
557        &mut self,
558        storage: &Storage,
559    ) -> Result<(), Storage::Error> {
560        PublicGroup::delete(storage, self.group_id())?;
561        storage.delete_own_leaf_index(self.group_id())?;
562        storage.delete_group_epoch_secrets(self.group_id())?;
563        storage.delete_message_secrets(self.group_id())?;
564        storage.delete_all_resumption_psk_secrets(self.group_id())?;
565        storage.delete_group_config(self.group_id())?;
566        storage.delete_own_leaf_nodes(self.group_id())?;
567        storage.delete_group_state(self.group_id())?;
568        storage.clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())?;
569
570        #[cfg(feature = "extensions-draft")]
571        storage.delete_application_export_tree::<_, ApplicationExportTree>(self.group_id())?;
572
573        // Drop this group's derivation-epoch bindings and its registration
574        // record. `VcDerivationEpochState` and the operation secret tree are
575        // keyed on the derivation epoch and may still be referenced by other
576        // higher-level groups, so they're not deleted here.
577        #[cfg(feature = "virtual-clients-draft")]
578        {
579            storage.delete_vc_emulation_bindings(self.group_id())?;
580            storage.delete_registered_vc_derivation_epoch(self.group_id())?;
581        }
582
583        self.proposal_store_mut().empty();
584        storage.delete_encryption_epoch_key_pairs(
585            self.group_id(),
586            &self.epoch(),
587            self.own_leaf_index().u32(),
588        )?;
589
590        Ok(())
591    }
592
593    // === Extensions ===
594
595    /// Exports the Ratchet Tree.
596    pub fn export_ratchet_tree(&self) -> RatchetTree {
597        self.public_group().export_ratchet_tree()
598    }
599}
600
601/// Error resolving the [`VcDerivationEpochState`] bound to a group at a given
602/// epoch via [`MlsGroup::vc_derivation_state_at_epoch`]. Callers map it to
603/// their own error type.
604///
605/// [`VcDerivationEpochState`]: crate::components::vc_derivation_info::VcDerivationEpochState
606#[cfg(feature = "virtual-clients-draft")]
607#[derive(thiserror::Error, Debug, PartialEq, Clone)]
608pub(crate) enum VcDerivationStateError<StorageError> {
609    /// Reading the binding or the derivation-epoch state from storage failed.
610    #[error("Error reading the binding or derivation-epoch state from storage: {0}")]
611    Storage(StorageError),
612    /// The group is bound to a derivation epoch, but its state is missing.
613    #[error("The group is bound to a derivation epoch, but its state is missing.")]
614    MissingDerivationEpochState,
615}
616
617// Crate-public functions
618impl MlsGroup {
619    /// Get the required capabilities extension of this group.
620    pub(crate) fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
621        self.public_group.required_capabilities()
622    }
623
624    /// Get a reference to the group epoch secrets from the group
625    pub(crate) fn group_epoch_secrets(&self) -> &GroupEpochSecrets {
626        &self.group_epoch_secrets
627    }
628
629    /// Get a reference to the message secrets from a group
630    pub(crate) fn message_secrets(&self) -> &MessageSecrets {
631        self.message_secrets_store.message_secrets()
632    }
633
634    /// Sets the size of the [`MessageSecretsStore`], i.e. the number of past
635    /// epochs to keep.
636    /// This allows application messages from previous epochs to be decrypted.
637    pub(crate) fn resize_message_secrets_store(&mut self, policy: &PastEpochDeletionPolicy) {
638        self.message_secrets_store.resize(policy);
639    }
640
641    /// Get the past epoch secret deletion policy for the group.
642    pub fn past_epoch_deletion_policy(&self) -> &PastEpochDeletionPolicy {
643        self.mls_group_config.past_epoch_deletion_policy()
644    }
645
646    /// Set the past epoch secret deletion policy for the group.
647    pub fn set_past_epoch_deletion_policy<Provider: OpenMlsProvider>(
648        &mut self,
649        provider: &Provider,
650        policy: PastEpochDeletionPolicy,
651    ) -> Result<(), SetPastEpochDeletionPolicyError<Provider::StorageError>> {
652        // resize the store
653        self.resize_message_secrets_store(&policy);
654
655        // set the policy on the join config
656        self.mls_group_config.past_epoch_deletion_policy = policy;
657
658        // persist the join config
659        provider
660            .storage()
661            .write_mls_join_config(self.group_id(), &self.mls_group_config)?;
662
663        // update the message secrets store in storage
664        provider
665            .storage()
666            .write_message_secrets(self.group_id(), &self.message_secrets_store)?;
667
668        Ok(())
669    }
670
671    /// Get the message secrets. Either from the secrets store or from the group.
672    pub(crate) fn message_secrets_for_epoch_mut(
673        &mut self,
674        epoch: GroupEpoch,
675    ) -> Result<&mut MessageSecrets, SecretTreeError> {
676        if epoch < self.context().epoch() {
677            self.message_secrets_store
678                .secrets_for_epoch_mut(epoch)
679                .ok_or(SecretTreeError::TooDistantInThePast)
680        } else {
681            Ok(self.message_secrets_store.message_secrets_mut())
682        }
683    }
684
685    /// Get the message secrets. Either from the secrets store or from the group.
686    pub(crate) fn message_secrets_for_epoch(
687        &self,
688        epoch: GroupEpoch,
689    ) -> Result<&MessageSecrets, SecretTreeError> {
690        if epoch < self.context().epoch() {
691            self.message_secrets_store
692                .secrets_for_epoch(epoch)
693                .ok_or(SecretTreeError::TooDistantInThePast)
694        } else {
695            Ok(self.message_secrets_store.message_secrets())
696        }
697    }
698
699    /// Get the message secrets and leaves for the given epoch. Either from the
700    /// secrets store or from the group.
701    ///
702    /// Note that the leaves vector is empty for message secrets of the current
703    /// epoch. The caller can use treesync in this case.
704    pub(crate) fn message_secrets_and_leaves(
705        &self,
706        epoch: GroupEpoch,
707    ) -> Result<(&MessageSecrets, &[Member]), SecretTreeError> {
708        if epoch < self.context().epoch() {
709            self.message_secrets_store
710                .secrets_and_leaves_for_epoch(epoch)
711                .ok_or(SecretTreeError::TooDistantInThePast)
712        } else {
713            // No need for leaves here. The tree of the current epoch is
714            // available to the caller.
715            Ok((self.message_secrets_store.message_secrets(), &[]))
716        }
717    }
718
719    /// Create a new group context extension proposal
720    pub(crate) fn create_group_context_ext_proposal<Provider: OpenMlsProvider>(
721        &self,
722        framing_parameters: FramingParameters,
723        extensions: Extensions<GroupContext>,
724        signer: &impl Signer,
725    ) -> Result<AuthenticatedContent, CreateGroupContextExtProposalError<Provider::StorageError>>
726    {
727        // Ensure that the group supports all the extensions that are wanted.
728        let required_extension = extensions
729            .iter()
730            .find(|extension| extension.extension_type() == ExtensionType::RequiredCapabilities);
731        if let Some(required_extension) = required_extension {
732            let required_capabilities = required_extension.as_required_capabilities_extension()?;
733            // Ensure we support all the capabilities.
734            self.own_leaf_node()
735                .ok_or_else(|| LibraryError::custom("Tree has no own leaf."))?
736                .capabilities()
737                .supports_required_capabilities(required_capabilities)?;
738
739            // Ensure that all other leaf nodes support all the required
740            // extensions as well.
741            self.public_group()
742                .check_extension_support(required_capabilities.extension_types())?;
743        }
744        let proposal = GroupContextExtensionProposal::new(extensions);
745        let proposal = Proposal::GroupContextExtensions(Box::new(proposal));
746        AuthenticatedContent::member_proposal(
747            framing_parameters,
748            self.own_leaf_index(),
749            proposal,
750            self.context(),
751            signer,
752        )
753        .map_err(|e| e.into())
754    }
755
756    /// Load the [`VcDerivationEpochState`] this group is bound to at `epoch`,
757    /// if any. Returns `None` when the group has no virtual-clients binding for
758    /// that epoch. The binding is resolved at the epoch a message was sent in,
759    /// so a delayed message from a past epoch deprotects with the state that
760    /// was bound then, not the latest one.
761    ///
762    /// [`VcDerivationEpochState`]: crate::components::vc_derivation_info::VcDerivationEpochState
763    #[cfg(feature = "virtual-clients-draft")]
764    pub(crate) fn vc_derivation_state_at_epoch<Storage: StorageProvider>(
765        &self,
766        storage: &Storage,
767        epoch: GroupEpoch,
768    ) -> Result<
769        Option<crate::components::vc_derivation_info::VcDerivationEpochState>,
770        VcDerivationStateError<Storage::Error>,
771    > {
772        let bindings: Option<crate::components::vc_derivation_info::VcEmulationBindings> = storage
773            .vc_emulation_bindings(self.group_id())
774            .map_err(VcDerivationStateError::Storage)?;
775        let Some(epoch_id) = bindings.and_then(|bindings| bindings.get(epoch).cloned()) else {
776            return Ok(None);
777        };
778        let state = storage
779            .vc_derivation_epoch_state(&epoch_id)
780            .map_err(VcDerivationStateError::Storage)?
781            .ok_or_else(|| {
782                log::error!("vc: group is bound to derivation epoch, but state is missing");
783                VcDerivationStateError::MissingDerivationEpochState
784            })?;
785        Ok(Some(state))
786    }
787
788    /// Returns whether this group is an emulation group of a virtual client.
789    ///
790    /// The flag is set when the application creates the group as an emulation
791    /// group or joins one, and it is restored from storage when the group is
792    /// loaded. See [`MlsGroupCreateConfigBuilder::emulation_group`].
793    ///
794    /// [`MlsGroupCreateConfigBuilder::emulation_group`]: crate::group::MlsGroupCreateConfigBuilder::emulation_group
795    #[cfg(feature = "virtual-clients-draft")]
796    pub fn is_emulation_group(&self) -> bool {
797        self.emulation_group
798    }
799
800    /// Returns the [`EpochId`] of the newest derivation epoch of this emulation
801    /// group, or `None` if none was registered yet.
802    ///
803    /// All virtual-client operations resolve to this derivation epoch. It is
804    /// sourced from the newest group epoch that was a derivation epoch, which
805    /// may be older than the group's current epoch: only commits that change
806    /// membership or that carry a `new_derivation_epoch` action create one.
807    ///
808    /// The sender-side operation entry points take the emulation group and
809    /// resolve the epoch themselves, so this getter is for inspection only.
810    ///
811    /// Returns `None` for groups that are not emulation groups.
812    ///
813    /// [`EpochId`]: crate::components::vc_derivation_info::EpochId
814    #[cfg(feature = "virtual-clients-draft")]
815    pub fn newest_vc_derivation_epoch<Storage: StorageProvider>(
816        &self,
817        storage: &Storage,
818    ) -> Result<Option<crate::components::vc_derivation_info::EpochId>, Storage::Error> {
819        crate::components::vc_derivation_info::newest_vc_derivation_epoch(storage, self.group_id())
820    }
821
822    // Encrypt an AuthenticatedContent into an PrivateMessage
823    pub(crate) fn encrypt<Provider: OpenMlsProvider>(
824        &mut self,
825        public_message: AuthenticatedContent,
826        provider: &Provider,
827    ) -> Result<EncryptionOutput, MessageEncryptionError<Provider::StorageError>> {
828        let padding_size = self.configuration().padding_size();
829
830        // If this group is bound to a derivation epoch at its current epoch,
831        // load the state so the framing layer can derive a deterministic
832        // reuse guard.
833        #[cfg(feature = "virtual-clients-draft")]
834        let derivation_state = self
835            .vc_derivation_state_at_epoch(provider.storage(), self.epoch())
836            .map_err(|e| match e {
837                VcDerivationStateError::Storage(e) => MessageEncryptionError::StorageError(e),
838                VcDerivationStateError::MissingDerivationEpochState => {
839                    MessageEncryptionError::VirtualClientsError(
840                        crate::components::vc_derivation_info::VirtualClientsError::MissingDerivationEpochState,
841                    )
842                }
843            })?;
844        #[cfg(feature = "virtual-clients-draft")]
845        let emulator_ctx: Option<crate::framing::EmulatorReuseGuardCtx<'_>> = derivation_state
846            .as_ref()
847            .map(|state| state.reuse_guard_inputs());
848
849        let msg = PrivateMessage::try_from_authenticated_content(
850            provider.crypto(),
851            provider.rand(),
852            &public_message,
853            self.ciphersuite(),
854            self.message_secrets_store.message_secrets_mut(),
855            padding_size,
856            #[cfg(feature = "virtual-clients-draft")]
857            emulator_ctx.as_ref(),
858        )?;
859
860        // When the group is bound to a derivation epoch, derive the generation
861        // ID the application hands to the DS to detect generation collisions
862        // between siblings. Application content draws it from the application
863        // ratchet, proposals and commits from the handshake ratchet.
864        #[cfg(feature = "virtual-clients-draft")]
865        let msg = {
866            use crate::components::vc_derivation_info::RatchetType;
867            let mut msg = msg;
868            if let Some(state) = &derivation_state {
869                let ratchet_type = match public_message.content().content_type() {
870                    ContentType::Application => RatchetType::Application,
871                    ContentType::Proposal | ContentType::Commit => RatchetType::Handshake,
872                };
873                let generation_id = state
874                    .derive_generation_id(
875                        provider.crypto(),
876                        self.group_id(),
877                        self.epoch(),
878                        msg.generation,
879                        ratchet_type,
880                    )
881                    .map_err(MessageEncryptionError::VirtualClientsError)?;
882                msg.generation_id = Some(generation_id);
883            }
884            msg
885        };
886
887        provider
888            .storage()
889            .write_message_secrets(self.group_id(), &self.message_secrets_store)
890            .map_err(MessageEncryptionError::StorageError)?;
891
892        Ok(msg)
893    }
894
895    /// Outgoing wire format derived from the group's configured policy.
896    pub(crate) fn outgoing_wire_format(&self) -> WireFormat {
897        self.mls_group_config.wire_format_policy().outgoing().into()
898    }
899
900    /// Owned `authenticated_data` bytes for the next outgoing message, taking
901    /// the GroupContext's Safe AAD requirement into account.
902    ///
903    /// Callers borrow the returned buffer into a [`FramingParameters`] for the
904    /// duration of message construction.
905    pub(crate) fn outgoing_authenticated_data(&self) -> Result<Vec<u8>, LibraryError> {
906        #[cfg(feature = "extensions-draft")]
907        {
908            self.assembled_authenticated_data()
909        }
910        #[cfg(not(feature = "extensions-draft"))]
911        {
912            Ok(self.aad.clone())
913        }
914    }
915
916    /// Build the bytes that go into `authenticated_data` for the next outgoing
917    /// message. When the GroupContext requires Safe AAD framing, the result is
918    /// the TLS serialization of the staged [`SafeAad`] followed by the bytes of
919    /// `self.aad`. Otherwise, the result is `self.aad` unchanged.
920    #[cfg(feature = "extensions-draft")]
921    pub(crate) fn assembled_authenticated_data(&self) -> Result<Vec<u8>, LibraryError> {
922        if !self.context().safe_aad_required() {
923            return Ok(self.aad.clone());
924        }
925        crate::framing::safe_aad::assemble_authenticated_data(&self.safe_aad, &self.aad)
926            .map_err(|_| LibraryError::custom("SafeAad serialization failed"))
927    }
928
929    /// Delete all past epoch secrets.
930    ///
931    /// For more information on the arguments to this method, see [`PastEpochDeletion`].
932    pub fn delete_past_epoch_secrets<Provider: OpenMlsProvider>(
933        &mut self,
934        provider: &Provider,
935        policy: PastEpochDeletion,
936    ) -> Result<(), DeletePastEpochSecretsError<Provider::StorageError>> {
937        // delete past epoch secrets in memory
938        self.message_secrets_store.delete_past_epoch_secrets(policy);
939        // update the message secrets store in storage
940        provider
941            .storage()
942            .write_message_secrets(self.group_id(), &self.message_secrets_store)?;
943
944        Ok(())
945    }
946
947    /// Returns a reference to the proposal store.
948    pub fn proposal_store(&self) -> &ProposalStore {
949        self.public_group.proposal_store()
950    }
951
952    /// Returns a mutable reference to the proposal store.
953    pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
954        self.public_group.proposal_store_mut()
955    }
956
957    /// Get the group context
958    pub(crate) fn context(&self) -> &GroupContext {
959        self.public_group.group_context()
960    }
961
962    /// Get the MLS version used in this group.
963    pub(crate) fn version(&self) -> ProtocolVersion {
964        self.public_group.version()
965    }
966
967    /// Resets the AAD, including any staged Safe AAD items.
968    #[inline]
969    pub(crate) fn reset_aad(&mut self) {
970        self.aad.clear();
971        #[cfg(feature = "extensions-draft")]
972        {
973            self.safe_aad = SafeAad::empty();
974        }
975    }
976
977    /// Returns a reference to the public group.
978    pub fn public_group(&self) -> &PublicGroup {
979        &self.public_group
980    }
981}
982
983/// Bookkeeping a virtual client needs to confirm a handshake message
984/// (proposal or commit) that was framed as a PrivateMessage.
985///
986/// Pass `epoch` and `generation` to [`MlsGroup::confirm_handshake_message`]
987/// once the DS has accepted the message, to delete the retained handshake
988/// secret. `generation_id` is present when the group is bound to a derivation
989/// epoch and is attached to the fanned-out message so a strongly-consistent DS
990/// can detect generation collisions between siblings; it is `None` otherwise.
991///
992/// [`MlsGroup::confirm_handshake_message`]: crate::group::MlsGroup::confirm_handshake_message
993#[cfg(feature = "virtual-clients-draft")]
994#[derive(Debug, Clone)]
995pub struct HandshakeConfirmationData {
996    /// The epoch the message was encrypted in, which is the epoch before a
997    /// commit is merged.
998    pub epoch: GroupEpoch,
999    /// The handshake-ratchet generation used for encryption.
1000    pub generation: u32,
1001    /// The [`GenerationId`] to attach to the fanned-out message, present when
1002    /// the group is bound to a derivation epoch and `None` otherwise.
1003    ///
1004    /// [`GenerationId`]: crate::components::vc_derivation_info::GenerationId
1005    pub generation_id: Option<crate::components::vc_derivation_info::GenerationId>,
1006}
1007
1008/// Result of framing an [`AuthenticatedContent`] handshake message into an
1009/// [`MlsMessageOut`]. Mirrors the cfg-gated field pattern of
1010/// [`EncryptionOutput`]: with the `virtual-clients-draft` feature it also
1011/// carries the [`HandshakeConfirmationData`] for a ciphertext-framed message
1012/// (`None` when the message was framed as a plaintext PublicMessage).
1013///
1014/// [`EncryptionOutput`]: crate::framing::EncryptionOutput
1015pub(crate) struct HandshakeFramingOutput {
1016    pub(crate) message: MlsMessageOut,
1017    #[cfg(feature = "virtual-clients-draft")]
1018    pub(crate) confirmation: Option<HandshakeConfirmationData>,
1019}
1020
1021// Private methods of MlsGroup
1022impl MlsGroup {
1023    /// Store the given [`EncryptionKeyPair`]s in the `provider`'s key store
1024    /// indexed by this group's [`GroupId`] and [`GroupEpoch`].
1025    ///
1026    /// Returns an error if access to the key store fails.
1027    pub(super) fn store_epoch_keypairs<Storage: StorageProvider>(
1028        &self,
1029        store: &Storage,
1030        keypair_references: &[EncryptionKeyPair],
1031    ) -> Result<(), Storage::Error> {
1032        store.write_encryption_epoch_key_pairs(
1033            self.group_id(),
1034            &self.context().epoch(),
1035            self.own_leaf_index().u32(),
1036            keypair_references,
1037        )
1038    }
1039
1040    /// Read the [`EncryptionKeyPair`]s of this group and its current
1041    /// [`GroupEpoch`] from the `provider`'s storage.
1042    ///
1043    /// Returns an error if the lookup in the [`StorageProvider`] fails.
1044    pub(super) fn read_epoch_keypairs<Storage: StorageProvider>(
1045        &self,
1046        store: &Storage,
1047    ) -> Result<Vec<EncryptionKeyPair>, Storage::Error> {
1048        store.encryption_epoch_key_pairs(
1049            self.group_id(),
1050            &self.context().epoch(),
1051            self.own_leaf_index().u32(),
1052        )
1053    }
1054
1055    /// Delete the [`EncryptionKeyPair`]s from the previous [`GroupEpoch`] from
1056    /// the `provider`'s key store.
1057    ///
1058    /// Returns an error if access to the key store fails.
1059    #[cfg(not(feature = "virtual-clients-draft"))]
1060    pub(super) fn delete_previous_epoch_keypairs<Storage: StorageProvider>(
1061        &self,
1062        store: &Storage,
1063    ) -> Result<(), Storage::Error> {
1064        store.delete_encryption_epoch_key_pairs(
1065            self.group_id(),
1066            &GroupEpoch::from(self.context().epoch().as_u64() - 1),
1067            self.own_leaf_index().u32(),
1068        )
1069    }
1070
1071    #[cfg(feature = "virtual-clients-draft")]
1072    pub(super) fn delete_previous_epoch_keypairs<Storage: StorageProvider>(
1073        &self,
1074        store: &Storage,
1075        previous_own_leaf_index: LeafNodeIndex,
1076    ) -> Result<(), Storage::Error> {
1077        // In the sibling-resync flow, `merge_commit` installs the joiner's
1078        // leaf as our own leaf before it filters and stores the new epoch
1079        // keypairs. Previous-epoch keypairs are still stored under the leaf
1080        // index from that previous epoch, so the caller must pass that index
1081        // explicitly instead of having this helper read `self.own_leaf_index()`.
1082        store.delete_encryption_epoch_key_pairs(
1083            self.group_id(),
1084            &GroupEpoch::from(self.context().epoch().as_u64() - 1),
1085            previous_own_leaf_index.u32(),
1086        )
1087    }
1088
1089    /// Stores the state of this group. Only to be called from constructors to
1090    /// store the initial state of the group.
1091    pub(super) fn store<Storage: crate::storage::StorageProvider>(
1092        &self,
1093        storage: &Storage,
1094    ) -> Result<(), Storage::Error> {
1095        self.public_group.store(storage)?;
1096        storage.write_group_epoch_secrets(self.group_id(), &self.group_epoch_secrets)?;
1097        storage.write_own_leaf_index(self.group_id(), &self.own_leaf_index)?;
1098        storage.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
1099        storage.write_resumption_psk_store(self.group_id(), &self.resumption_psk_store)?;
1100        storage.write_mls_join_config(self.group_id(), &self.mls_group_config)?;
1101        storage.write_group_state(self.group_id(), &self.group_state)?;
1102        #[cfg(feature = "extensions-draft")]
1103        if let Some(application_export_tree) = &self.application_export_tree {
1104            storage.write_application_export_tree(self.group_id(), application_export_tree)?;
1105        }
1106
1107        Ok(())
1108    }
1109
1110    /// Converts PublicMessage to MlsMessage. Depending on whether handshake
1111    /// message should be encrypted, PublicMessage messages are encrypted to
1112    /// PrivateMessage first.
1113    fn content_to_mls_message(
1114        &mut self,
1115        mls_auth_content: AuthenticatedContent,
1116        provider: &impl OpenMlsProvider,
1117    ) -> Result<HandshakeFramingOutput, LibraryError> {
1118        let output = match self.configuration().wire_format_policy().outgoing() {
1119            OutgoingWireFormatPolicy::AlwaysPlaintext => {
1120                let mut plaintext: PublicMessage = mls_auth_content.into();
1121                // Set the membership tag only if the sender type is `Member`.
1122                if plaintext.sender().is_member() {
1123                    plaintext.set_membership_tag(
1124                        provider.crypto(),
1125                        self.ciphersuite(),
1126                        self.message_secrets().membership_key(),
1127                        self.message_secrets().serialized_context(),
1128                    )?;
1129                }
1130                HandshakeFramingOutput {
1131                    message: plaintext.into(),
1132                    #[cfg(feature = "virtual-clients-draft")]
1133                    confirmation: None,
1134                }
1135            }
1136            OutgoingWireFormatPolicy::AlwaysCiphertext => {
1137                // A ciphertext-framed handshake message ties its confirmation
1138                // to the epoch it was encrypted in, which is the current epoch
1139                // at framing time, before a commit is merged.
1140                #[cfg(feature = "virtual-clients-draft")]
1141                let epoch = self.epoch();
1142                let encryption_output = self
1143                    .encrypt(mls_auth_content, provider)
1144                    // We can be sure the encryption will work because the plaintext was created by us
1145                    .map_err(|_| LibraryError::custom("Malformed plaintext"))?;
1146                let message = MlsMessageOut::from_private_message(
1147                    encryption_output.private_message,
1148                    self.version(),
1149                );
1150                HandshakeFramingOutput {
1151                    message,
1152                    #[cfg(feature = "virtual-clients-draft")]
1153                    confirmation: Some(HandshakeConfirmationData {
1154                        epoch,
1155                        generation: encryption_output.generation,
1156                        generation_id: encryption_output.generation_id,
1157                    }),
1158                }
1159            }
1160        };
1161        Ok(output)
1162    }
1163
1164    /// Check if the group is operational. Throws an error if the group is
1165    /// inactive or if there is a pending commit.
1166    fn is_operational(&self) -> Result<(), MlsGroupStateError> {
1167        match self.group_state {
1168            MlsGroupState::PendingCommit(_) => Err(MlsGroupStateError::PendingCommit),
1169            MlsGroupState::Inactive => Err(MlsGroupStateError::UseAfterEviction),
1170            MlsGroupState::Operational => Ok(()),
1171        }
1172    }
1173}
1174
1175// Methods used in tests
1176impl MlsGroup {
1177    #[cfg(any(feature = "test-utils", test))]
1178    pub fn export_group_context(&self) -> &GroupContext {
1179        self.context()
1180    }
1181
1182    #[cfg(any(feature = "test-utils", test))]
1183    pub fn tree_hash(&self) -> &[u8] {
1184        self.public_group().group_context().tree_hash()
1185    }
1186
1187    #[cfg(any(feature = "test-utils", test))]
1188    pub(crate) fn message_secrets_test_mut(&mut self) -> &mut MessageSecrets {
1189        self.message_secrets_store.message_secrets_mut()
1190    }
1191
1192    #[cfg(any(feature = "test-utils", test))]
1193    pub fn print_ratchet_tree(&self, message: &str) {
1194        println!("{}: {}", message, self.public_group().export_ratchet_tree());
1195    }
1196
1197    #[cfg(any(feature = "test-utils", test))]
1198    pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
1199        self.public_group.context_mut()
1200    }
1201
1202    #[cfg(test)]
1203    pub(crate) fn set_own_leaf_index(&mut self, own_leaf_index: LeafNodeIndex) {
1204        self.own_leaf_index = own_leaf_index;
1205    }
1206
1207    #[cfg(test)]
1208    pub(crate) fn own_tree_position(&self) -> TreePosition {
1209        TreePosition::new(self.group_id().clone(), self.own_leaf_index())
1210    }
1211
1212    #[cfg(test)]
1213    pub(crate) fn message_secrets_store(&self) -> &MessageSecretsStore {
1214        &self.message_secrets_store
1215    }
1216
1217    #[cfg(test)]
1218    pub(crate) fn resumption_psk_store(&self) -> &ResumptionPskStore {
1219        &self.resumption_psk_store
1220    }
1221
1222    #[cfg(test)]
1223    pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
1224        self.public_group.set_group_context(group_context)
1225    }
1226
1227    #[cfg(any(test, feature = "test-utils"))]
1228    pub fn ensure_persistence(&self, storage: &impl StorageProvider) -> Result<(), LibraryError> {
1229        let loaded = MlsGroup::load(storage, self.group_id())
1230            .map_err(|_| LibraryError::custom("Failed to load group from storage"))?;
1231        let other = loaded.ok_or_else(|| LibraryError::custom("Group not found in storage"))?;
1232
1233        if self != &other {
1234            let mut diagnostics = Vec::new();
1235
1236            if self.mls_group_config != other.mls_group_config {
1237                diagnostics.push(format!(
1238                    "mls_group_config:\n  Current: {:?}\n  Loaded:  {:?}",
1239                    self.mls_group_config, other.mls_group_config
1240                ));
1241            }
1242            if self.public_group != other.public_group {
1243                diagnostics.push(format!(
1244                    "public_group:\n  Current: {:?}\n  Loaded:  {:?}",
1245                    self.public_group, other.public_group
1246                ));
1247            }
1248            if self.group_epoch_secrets != other.group_epoch_secrets {
1249                diagnostics.push(format!(
1250                    "group_epoch_secrets:\n  Current: {:?}\n  Loaded:  {:?}",
1251                    self.group_epoch_secrets, other.group_epoch_secrets
1252                ));
1253            }
1254            if self.own_leaf_index != other.own_leaf_index {
1255                diagnostics.push(format!(
1256                    "own_leaf_index:\n  Current: {:?}\n  Loaded:  {:?}",
1257                    self.own_leaf_index, other.own_leaf_index
1258                ));
1259            }
1260            if self.message_secrets_store != other.message_secrets_store {
1261                diagnostics.push(format!(
1262                    "message_secrets_store:\n  Current: {:?}\n  Loaded:  {:?}",
1263                    self.message_secrets_store, other.message_secrets_store
1264                ));
1265            }
1266            if self.resumption_psk_store != other.resumption_psk_store {
1267                diagnostics.push(format!(
1268                    "resumption_psk_store:\n  Current: {:?}\n  Loaded:  {:?}",
1269                    self.resumption_psk_store, other.resumption_psk_store
1270                ));
1271            }
1272            if self.own_leaf_nodes != other.own_leaf_nodes {
1273                diagnostics.push(format!(
1274                    "own_leaf_nodes:\n  Current: {:?}\n  Loaded:  {:?}",
1275                    self.own_leaf_nodes, other.own_leaf_nodes
1276                ));
1277            }
1278            if self.aad != other.aad {
1279                diagnostics.push(format!(
1280                    "aad:\n  Current: {:?}\n  Loaded:  {:?}",
1281                    self.aad, other.aad
1282                ));
1283            }
1284            if self.group_state != other.group_state {
1285                diagnostics.push(format!(
1286                    "group_state:\n  Current: {:?}\n  Loaded:  {:?}",
1287                    self.group_state, other.group_state
1288                ));
1289            }
1290            #[cfg(feature = "extensions-draft")]
1291            if self.application_export_tree != other.application_export_tree {
1292                diagnostics.push(format!(
1293                    "application_export_tree:\n  Current: {:?}\n  Loaded:  {:?}",
1294                    self.application_export_tree, other.application_export_tree
1295                ));
1296            }
1297            #[cfg(feature = "virtual-clients-draft")]
1298            if self.emulation_group != other.emulation_group {
1299                diagnostics.push(format!(
1300                    "emulation_group:\n  Current: {:?}\n  Loaded:  {:?}",
1301                    self.emulation_group, other.emulation_group
1302                ));
1303            }
1304
1305            log::error!(
1306                "Loaded group does not match current group! Differing fields ({}):\n\n{}",
1307                diagnostics.len(),
1308                diagnostics.join("\n\n")
1309            );
1310
1311            return Err(LibraryError::custom(
1312                "Loaded group does not match current group",
1313            ));
1314        }
1315
1316        Ok(())
1317    }
1318}
1319
1320/// A [`StagedWelcome`] can be inspected and then turned into a [`MlsGroup`].
1321/// This allows checking who authored the Welcome message.
1322#[derive(Debug)]
1323pub struct StagedWelcome {
1324    // The group configuration. See [`MlsGroupJoinConfig`] for more information.
1325    mls_group_config: MlsGroupJoinConfig,
1326    public_group: PublicGroup,
1327    group_epoch_secrets: GroupEpochSecrets,
1328    own_leaf_index: LeafNodeIndex,
1329
1330    /// A [`MessageSecretsStore`] that stores message secrets.
1331    /// By default this store has the length of 1, i.e. only the [`MessageSecrets`]
1332    /// of the current epoch is kept.
1333    /// If more secrets from past epochs should be kept in order to be
1334    /// able to decrypt application messages from previous epochs, the size of
1335    /// the store must be increased through [`max_past_epochs()`].
1336    message_secrets_store: MessageSecretsStore,
1337
1338    /// A secret that is not stored as part of the [`MlsGroup`] after the group is created.
1339    /// It can be used by the application to derive forward secure secrets.
1340    #[cfg(feature = "extensions-draft")]
1341    application_export_secret: ApplicationExportSecret,
1342
1343    /// Resumption psk store. This is where the resumption psks are kept in a rollover list.
1344    resumption_psk_store: ResumptionPskStore,
1345
1346    /// The [`VerifiableGroupInfo`] from the [`Welcome`] message.
1347    verifiable_group_info: VerifiableGroupInfo,
1348
1349    /// The key material used to join via this welcome.
1350    key_material: WelcomeKeyMaterial,
1351
1352    /// If we got a path secret, these are the derived path keys.
1353    path_keypairs: Option<Vec<EncryptionKeyPair>>,
1354
1355    /// Whether to join the group as an emulation group of a virtual client. Set
1356    /// by [`Self::emulation_group`].
1357    #[cfg(feature = "virtual-clients-draft")]
1358    emulation_group: bool,
1359}
1360
1361/// A `Welcome` message that has been processed but not staged yet.
1362///
1363/// This may be used in order to retrieve information from the `Welcome` about
1364/// the ratchet tree and PSKs.
1365///
1366/// Use `into_staged_welcome` to stage it into a [`StagedWelcome`].
1367pub struct ProcessedWelcome {
1368    // The group configuration. See [`MlsGroupJoinConfig`] for more information.
1369    mls_group_config: MlsGroupJoinConfig,
1370
1371    // The following is the state after parsing the Welcome message, before actually
1372    // building the group.
1373    ciphersuite: Ciphersuite,
1374    group_secrets: GroupSecrets,
1375    epoch_secrets: crate::schedule::EpochSecretsResult,
1376    verifiable_group_info: crate::messages::group_info::VerifiableGroupInfo,
1377    resumption_psk_store: crate::schedule::psk::store::ResumptionPskStore,
1378    key_material: WelcomeKeyMaterial,
1379}
1380
1381/// The key material a client uses to process a [`Welcome`] message.
1382#[derive(Debug)]
1383pub struct WelcomeKeyMaterial {
1384    inner: WelcomeKeyMaterialInner,
1385}
1386
1387/// The inner data of a [`WelcomeKeyMaterial`].
1388///
1389/// A regular member holds a local [`KeyPackageBundle`]. A sibling emulator
1390/// joining a higher-level group as a virtual client has no local bundle: it
1391/// derives the init and leaf-encryption keys from the operation secret tree of
1392/// the derivation epoch the KeyPackage belongs to.
1393///
1394/// [`Welcome`]: crate::messages::Welcome
1395#[derive(Debug)]
1396pub(crate) enum WelcomeKeyMaterialInner {
1397    /// A locally stored [`KeyPackageBundle`]. Boxed to keep the enum small,
1398    /// since the virtual-client variant is much smaller.
1399    KeyPackage(Box<KeyPackageBundle>),
1400    /// Virtual-client material derived from a derivation epoch's operation
1401    /// secret tree.
1402    #[cfg(feature = "virtual-clients-draft")]
1403    VirtualClient(crate::components::vc_derivation_info::VcWelcomeMaterial),
1404}
1405
1406impl WelcomeKeyMaterial {
1407    /// Create a new [`WelcomeKeyMaterial`] from a [`KeyPackageBundle`].
1408    pub(crate) fn with_key_package_bundle(key_package: KeyPackageBundle) -> Self {
1409        Self {
1410            inner: WelcomeKeyMaterialInner::KeyPackage(Box::new(key_package)),
1411        }
1412    }
1413
1414    /// Create a new [`WelcomeKeyMaterial`] from a [`VcWelcomeMaterial`].
1415    ///
1416    /// [`VcWelcomeMaterial`]: crate::components::vc_derivation_info::VcWelcomeMaterial
1417    #[cfg(feature = "virtual-clients-draft")]
1418    pub(crate) fn with_vc_welcome_material(
1419        material: crate::components::vc_derivation_info::VcWelcomeMaterial,
1420    ) -> Self {
1421        Self {
1422            inner: WelcomeKeyMaterialInner::VirtualClient(material),
1423        }
1424    }
1425
1426    pub(crate) fn inner(&self) -> &WelcomeKeyMaterialInner {
1427        &self.inner
1428    }
1429
1430    /// The [`KeyPackageRef`] addressed by the welcome's encrypted group
1431    /// secrets. The bundle computes it from its KeyPackage, the virtual-client
1432    /// material carries the ref it was matched on.
1433    ///
1434    /// [`KeyPackageRef`]: crate::ciphersuite::hash_ref::KeyPackageRef
1435    pub fn key_package_ref(
1436        &self,
1437        crypto: &impl OpenMlsCrypto,
1438    ) -> Result<crate::ciphersuite::hash_ref::KeyPackageRef, LibraryError> {
1439        match &self.inner {
1440            WelcomeKeyMaterialInner::KeyPackage(bundle) => bundle.key_package().hash_ref(crypto),
1441            #[cfg(feature = "virtual-clients-draft")]
1442            WelcomeKeyMaterialInner::VirtualClient(material) => {
1443                Ok(material.key_package_ref.clone())
1444            }
1445        }
1446    }
1447
1448    /// The init private key used to decrypt the encrypted group secrets.
1449    pub fn init_private_key(&self) -> &crate::ciphersuite::HpkePrivateKey {
1450        match &self.inner {
1451            WelcomeKeyMaterialInner::KeyPackage(bundle) => bundle.init_private_key(),
1452            #[cfg(feature = "virtual-clients-draft")]
1453            WelcomeKeyMaterialInner::VirtualClient(material) => &material.init_private_key,
1454        }
1455    }
1456
1457    /// The public init key the encrypted group secrets are encrypted to.
1458    pub fn hpke_init_key(&self) -> &InitKey {
1459        match &self.inner {
1460            WelcomeKeyMaterialInner::KeyPackage(bundle) => bundle.key_package().hpke_init_key(),
1461            #[cfg(feature = "virtual-clients-draft")]
1462            WelcomeKeyMaterialInner::VirtualClient(material) => &material.init_key,
1463        }
1464    }
1465
1466    /// The local [`KeyPackageBundle`] on the regular path, or `None` on the
1467    /// virtual-client path. Checks that only apply when there is a local
1468    /// KeyPackage to compare against branch on this.
1469    pub fn key_package_bundle(&self) -> Option<&KeyPackageBundle> {
1470        match &self.inner {
1471            WelcomeKeyMaterialInner::KeyPackage(bundle) => Some(bundle),
1472            #[cfg(feature = "virtual-clients-draft")]
1473            WelcomeKeyMaterialInner::VirtualClient(_) => None,
1474        }
1475    }
1476
1477    /// The joiner's leaf encryption keypair.
1478    fn encryption_key_pair(&self) -> EncryptionKeyPair {
1479        match &self.inner {
1480            WelcomeKeyMaterialInner::KeyPackage(bundle) => bundle.encryption_key_pair(),
1481            #[cfg(feature = "virtual-clients-draft")]
1482            WelcomeKeyMaterialInner::VirtualClient(material) => material.encryption_keypair.clone(),
1483        }
1484    }
1485}