Skip to main content

openmls/group/mls_group/
mod.rs

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