Skip to main content

openmls/group/mls_group/
mod.rs

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