Skip to main content

openmls/group/mls_group/
mod.rs

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