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