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