openmls/group/mls_group/
staged_commit.rs

1use core::fmt::Debug;
2use std::mem;
3
4#[cfg(feature = "extensions-draft-08")]
5use openmls_traits::crypto::OpenMlsCrypto;
6use openmls_traits::storage::StorageProvider as _;
7use serde::{Deserialize, Serialize};
8use tls_codec::Serialize as _;
9
10use super::proposal_store::{
11    QueuedAddProposal, QueuedPskProposal, QueuedRemoveProposal, QueuedUpdateProposal,
12};
13
14#[cfg(feature = "extensions-draft-08")]
15use super::proposal_store::QueuedAppEphemeralProposal;
16
17use super::{
18    super::errors::*, load_psks, Credential, Extension, GroupContext, GroupEpochSecrets, GroupId,
19    JoinerSecret, KeySchedule, LeafNode, LibraryError, MessageSecrets, MlsGroup, OpenMlsProvider,
20    Proposal, ProposalQueue, PskSecret, QueuedProposal, Sender,
21};
22#[cfg(feature = "extensions-draft-08")]
23use crate::schedule::application_export_tree::ApplicationExportTree;
24
25use crate::{
26    ciphersuite::{hash_ref::ProposalRef, Secret},
27    framing::mls_auth_content::AuthenticatedContent,
28    group::public_group::{
29        diff::{apply_proposals::ApplyProposalsValues, StagedPublicGroupDiff},
30        staged_commit::PublicStagedCommitState,
31    },
32    schedule::{CommitSecret, EpochAuthenticator, EpochSecretsResult, InitSecret, PreSharedKeyId},
33    treesync::node::encryption_keys::EncryptionKeyPair,
34};
35
36impl MlsGroup {
37    fn derive_epoch_secrets(
38        &self,
39        provider: &impl OpenMlsProvider,
40        apply_proposals_values: ApplyProposalsValues,
41        epoch_secrets: &GroupEpochSecrets,
42        commit_secret: CommitSecret,
43        serialized_provisional_group_context: &[u8],
44    ) -> Result<EpochSecretsResult, StageCommitError> {
45        // Check if we need to include the init secret from an external commit
46        // we applied earlier or if we use the one from the previous epoch.
47        let joiner_secret = if let Some(ref external_init_proposal) =
48            apply_proposals_values.external_init_proposal_option
49        {
50            // Decrypt the content and derive the external init secret.
51            let external_priv = epoch_secrets
52                .external_secret()
53                .derive_external_keypair(provider.crypto(), self.ciphersuite())
54                .map_err(LibraryError::unexpected_crypto_error)?
55                .private;
56            let init_secret = InitSecret::from_kem_output(
57                provider.crypto(),
58                self.ciphersuite(),
59                self.version(),
60                &external_priv,
61                external_init_proposal.kem_output(),
62            )?;
63            JoinerSecret::new(
64                provider.crypto(),
65                self.ciphersuite(),
66                commit_secret,
67                &init_secret,
68                serialized_provisional_group_context,
69            )
70            .map_err(LibraryError::unexpected_crypto_error)?
71        } else {
72            JoinerSecret::new(
73                provider.crypto(),
74                self.ciphersuite(),
75                commit_secret,
76                epoch_secrets.init_secret(),
77                serialized_provisional_group_context,
78            )
79            .map_err(LibraryError::unexpected_crypto_error)?
80        };
81
82        // Prepare the PskSecret
83        // Fails if PSKs are missing ([valn1205](https://validation.openmls.tech/#valn1205))
84        let psk_secret = {
85            let psks: Vec<(&PreSharedKeyId, Secret)> = load_psks(
86                provider.storage(),
87                &self.resumption_psk_store,
88                &apply_proposals_values.presharedkeys,
89            )?;
90
91            PskSecret::new(provider.crypto(), self.ciphersuite(), psks)?
92        };
93
94        // Create key schedule
95        let mut key_schedule = KeySchedule::init(
96            self.ciphersuite(),
97            provider.crypto(),
98            &joiner_secret,
99            psk_secret,
100        )?;
101
102        key_schedule
103            .add_context(provider.crypto(), serialized_provisional_group_context)
104            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?;
105        Ok(key_schedule
106            .epoch_secrets(provider.crypto(), self.ciphersuite())
107            .map_err(|_| LibraryError::custom("Using the key schedule in the wrong state"))?)
108    }
109
110    /// Stages a commit message that was sent by another group member. This
111    /// function does the following:
112    ///  - Applies the proposals covered by the commit to the tree
113    ///  - Applies the (optional) update path to the tree
114    ///  - Decrypts and calculates the path secrets
115    ///  - Initializes the key schedule for epoch rollover
116    ///  - Verifies the confirmation tag
117    ///
118    /// Returns a [StagedCommit] that can be inspected and later merged into the
119    /// group state with [MlsGroup::merge_commit()]. If the member was not
120    /// removed from the group, the function also returns an
121    /// [ApplicationExportSecret].
122    ///
123    /// This function does the following checks:
124    ///  - ValSem101
125    ///  - ValSem102
126    ///  - ValSem104
127    ///  - ValSem105
128    ///  - ValSem106
129    ///  - ValSem107
130    ///  - ValSem108
131    ///  - ValSem110
132    ///  - ValSem111
133    ///  - ValSem112
134    ///  - ValSem113: All Proposals: The proposal type must be supported by all
135    ///    members of the group
136    ///  - ValSem200
137    ///  - ValSem201
138    ///  - ValSem202: Path must be the right length
139    ///  - ValSem203: Path secrets must decrypt correctly
140    ///  - ValSem204: Public keys from Path must be verified and match the
141    ///    private keys from the direct path
142    ///  - ValSem205
143    ///  - ValSem240
144    ///  - ValSem241
145    ///  - ValSem242
146    ///  - ValSem244 Returns an error if the given commit was sent by the owner
147    ///    of this group.
148    pub(crate) fn stage_commit(
149        &self,
150        mls_content: &AuthenticatedContent,
151        old_epoch_keypairs: Vec<EncryptionKeyPair>,
152        leaf_node_keypairs: Vec<EncryptionKeyPair>,
153        provider: &impl OpenMlsProvider,
154    ) -> Result<StagedCommit, StageCommitError> {
155        // Check that the sender is another member of the group
156        if let Sender::Member(member) = mls_content.sender() {
157            if member == &self.own_leaf_index() {
158                return Err(StageCommitError::OwnCommit);
159            }
160        }
161
162        let ciphersuite = self.ciphersuite();
163
164        let (commit, proposal_queue, sender_index) = self
165            .public_group
166            .validate_commit(mls_content, provider.crypto())?;
167
168        // Create the provisional public group state (including the tree and
169        // group context) and apply proposals.
170        let mut diff = self.public_group.empty_diff();
171
172        let apply_proposals_values =
173            diff.apply_proposals(&proposal_queue, self.own_leaf_index())?;
174
175        // Determine if Commit has a path
176        let (commit_secret, new_keypairs, new_leaf_keypair_option, update_path_leaf_node) =
177            if let Some(path) = commit.path.clone() {
178                // Update the public group
179                // ValSem202: Path must be the right length
180                diff.apply_received_update_path(
181                    provider.crypto(),
182                    ciphersuite,
183                    sender_index,
184                    &path,
185                )?;
186
187                // Update group context
188                diff.update_group_context(
189                    provider.crypto(),
190                    apply_proposals_values.extensions.clone(),
191                )?;
192
193                // Check if we were removed from the group
194                if apply_proposals_values.self_removed {
195                    // If so, we return here, because we can't decrypt the path
196                    let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
197                    let staged_state = PublicStagedCommitState::new(
198                        staged_diff,
199                        commit.path.as_ref().map(|path| path.leaf_node().clone()),
200                    );
201                    let staged_commit = StagedCommit::new(
202                        proposal_queue,
203                        StagedCommitState::PublicState(Box::new(staged_state)),
204                    );
205                    return Ok(staged_commit);
206                }
207
208                let decryption_keypairs: Vec<&EncryptionKeyPair> = old_epoch_keypairs
209                    .iter()
210                    .chain(leaf_node_keypairs.iter())
211                    .collect();
212
213                // ValSem203: Path secrets must decrypt correctly
214                // ValSem204: Public keys from Path must be verified and match the private keys from the direct path
215                let (new_keypairs, commit_secret) = diff.decrypt_path(
216                    provider.crypto(),
217                    &decryption_keypairs,
218                    self.own_leaf_index(),
219                    sender_index,
220                    path.nodes(),
221                    &apply_proposals_values.exclusion_list(),
222                )?;
223
224                // Check if one of our update proposals was applied. If so, we
225                // need to store that keypair separately, because after merging
226                // it needs to be removed from the key store separately and in
227                // addition to the removal of the keypairs of the previous
228                // epoch.
229                let new_leaf_keypair_option = if let Some(leaf) = diff.leaf(self.own_leaf_index()) {
230                    leaf_node_keypairs.into_iter().find_map(|keypair| {
231                        if leaf.encryption_key() == keypair.public_key() {
232                            Some(keypair)
233                        } else {
234                            None
235                        }
236                    })
237                } else {
238                    // We should have an own leaf at this point.
239                    debug_assert!(false);
240                    None
241                };
242
243                // Return the leaf node in the update path so the credential can be validated.
244                // Since the diff has already been updated, this should be the same as the leaf
245                // at the sender index.
246                let update_path_leaf_node = Some(path.leaf_node().clone());
247                debug_assert_eq!(diff.leaf(sender_index), path.leaf_node().into());
248
249                (
250                    commit_secret,
251                    new_keypairs,
252                    new_leaf_keypair_option,
253                    update_path_leaf_node,
254                )
255            } else {
256                if apply_proposals_values.path_required {
257                    // ValSem201
258                    return Err(StageCommitError::RequiredPathNotFound);
259                }
260
261                // Even if there is no path, we have to update the group context.
262                diff.update_group_context(
263                    provider.crypto(),
264                    apply_proposals_values.extensions.clone(),
265                )?;
266
267                (CommitSecret::zero_secret(ciphersuite), vec![], None, None)
268            };
269
270        // Update the confirmed transcript hash before we compute the confirmation tag.
271        diff.update_confirmed_transcript_hash(provider.crypto(), mls_content)?;
272
273        let received_confirmation_tag = mls_content
274            .confirmation_tag()
275            .ok_or(StageCommitError::ConfirmationTagMissing)?;
276
277        let serialized_provisional_group_context = diff
278            .group_context()
279            .tls_serialize_detached()
280            .map_err(LibraryError::missing_bound_check)?;
281
282        let EpochSecretsResult {
283            epoch_secrets,
284            #[cfg(feature = "extensions-draft-08")]
285            application_exporter,
286        } = self.derive_epoch_secrets(
287            provider,
288            apply_proposals_values,
289            self.group_epoch_secrets(),
290            commit_secret,
291            &serialized_provisional_group_context,
292        )?;
293        let (provisional_group_secrets, provisional_message_secrets) = epoch_secrets.split_secrets(
294            serialized_provisional_group_context,
295            diff.tree_size(),
296            self.own_leaf_index(),
297        );
298
299        // Verify confirmation tag
300        // ValSem205
301        let own_confirmation_tag = provisional_message_secrets
302            .confirmation_key()
303            .tag(
304                provider.crypto(),
305                self.ciphersuite(),
306                diff.group_context().confirmed_transcript_hash(),
307            )
308            .map_err(LibraryError::unexpected_crypto_error)?;
309        if &own_confirmation_tag != received_confirmation_tag {
310            log::error!("Confirmation tag mismatch");
311            log_crypto!(trace, "  Got:      {:x?}", received_confirmation_tag);
312            log_crypto!(trace, "  Expected: {:x?}", own_confirmation_tag);
313            // TODO: We have tests expecting this error.
314            //       They need to be rewritten.
315            // debug_assert!(false, "Confirmation tag mismatch");
316
317            // in some tests we need to be able to proceed despite the tag being wrong,
318            // e.g. to test whether a later validation check is performed correctly.
319            if !crate::skip_validation::is_disabled::confirmation_tag() {
320                return Err(StageCommitError::ConfirmationTagMismatch);
321            }
322        }
323
324        diff.update_interim_transcript_hash(ciphersuite, provider.crypto(), own_confirmation_tag)?;
325
326        let staged_diff = diff.into_staged_diff(provider.crypto(), ciphersuite)?;
327        #[cfg(feature = "extensions-draft-08")]
328        let application_export_tree = ApplicationExportTree::new(application_exporter);
329        let staged_commit_state =
330            StagedCommitState::GroupMember(Box::new(MemberStagedCommitState::new(
331                provisional_group_secrets,
332                provisional_message_secrets,
333                staged_diff,
334                new_keypairs,
335                new_leaf_keypair_option,
336                update_path_leaf_node,
337                #[cfg(feature = "extensions-draft-08")]
338                application_export_tree,
339            )));
340        let staged_commit = StagedCommit::new(proposal_queue, staged_commit_state);
341
342        Ok(staged_commit)
343    }
344
345    /// Merges a [StagedCommit] into the group state and optionally return a [`SecretTree`]
346    /// from the previous epoch. The secret tree is returned if the Commit does not contain a self removal.
347    ///
348    /// This function should not fail and only returns a [`Result`], because it
349    /// might throw a `LibraryError`.
350    pub(crate) fn merge_commit<Provider: OpenMlsProvider>(
351        &mut self,
352        provider: &Provider,
353        staged_commit: StagedCommit,
354    ) -> Result<(), MergeCommitError<Provider::StorageError>> {
355        // Get all keypairs from the old epoch, so we can later store the ones
356        // that are still relevant in the new epoch.
357        let old_epoch_keypairs = self
358            .read_epoch_keypairs(provider.storage())
359            .map_err(MergeCommitError::StorageError)?;
360        match staged_commit.state {
361            StagedCommitState::PublicState(staged_state) => {
362                self.public_group
363                    .merge_diff(staged_state.into_staged_diff());
364                self.store(provider.storage())
365                    .map_err(MergeCommitError::StorageError)?;
366                Ok(())
367            }
368            StagedCommitState::GroupMember(state) => {
369                // Save the past epoch
370                let past_epoch = self.context().epoch();
371                // Get all the full leaves
372                let leaves = self.public_group().members().collect();
373                // Merge the staged commit into the group state and store the secret tree from the
374                // previous epoch in the message secrets store.
375                self.group_epoch_secrets = state.group_epoch_secrets;
376
377                // Replace the previous message secrets with the new ones and return the previous message secrets
378                let mut message_secrets = state.message_secrets;
379                mem::swap(
380                    &mut message_secrets,
381                    self.message_secrets_store.message_secrets_mut(),
382                );
383                self.message_secrets_store
384                    .add(past_epoch, message_secrets, leaves);
385
386                // Replace the previous exporter tree with the new one.
387                #[cfg(feature = "extensions-draft-08")]
388                {
389                    // The application exporter is only None if the group was
390                    // stored using an older version of OpenMLS that did not
391                    // support the application exporter.
392                    if let Some(application_export_tree) = state.application_export_tree {
393                        // Overwrite the existing exporter tree in the storage.
394
395                        use openmls_traits::storage::StorageProvider as _;
396                        provider
397                            .storage()
398                            .write_application_export_tree(
399                                self.group_id(),
400                                &application_export_tree,
401                            )
402                            .map_err(MergeCommitError::StorageError)?;
403
404                        self.application_export_tree = Some(application_export_tree);
405                    }
406                }
407
408                self.public_group.merge_diff(state.staged_diff);
409
410                let leaf_keypair = if let Some(keypair) = &state.new_leaf_keypair_option {
411                    vec![keypair.clone()]
412                } else {
413                    vec![]
414                };
415
416                // Figure out which keys we need in the new epoch.
417                let new_owned_encryption_keys = self
418                    .public_group()
419                    .owned_encryption_keys(self.own_leaf_index());
420                // From the old and new keys, keep the ones that are still relevant in the new epoch.
421                let epoch_keypairs: Vec<EncryptionKeyPair> = old_epoch_keypairs
422                    .into_iter()
423                    .chain(state.new_keypairs)
424                    .chain(leaf_keypair)
425                    .filter(|keypair| new_owned_encryption_keys.contains(keypair.public_key()))
426                    .collect();
427
428                // We should have private keys for all owned encryption keys.
429                debug_assert_eq!(new_owned_encryption_keys.len(), epoch_keypairs.len());
430                if new_owned_encryption_keys.len() != epoch_keypairs.len() {
431                    return Err(LibraryError::custom(
432                        "We should have all the private key material we need.",
433                    )
434                    .into());
435                }
436
437                // Store the updated group state
438                let storage = provider.storage();
439                let group_id = self.group_id();
440
441                self.public_group
442                    .store(storage)
443                    .map_err(MergeCommitError::StorageError)?;
444                storage
445                    .write_group_epoch_secrets(group_id, &self.group_epoch_secrets)
446                    .map_err(MergeCommitError::StorageError)?;
447                storage
448                    .write_message_secrets(group_id, &self.message_secrets_store)
449                    .map_err(MergeCommitError::StorageError)?;
450
451                // Store the relevant keys under the new epoch
452                self.store_epoch_keypairs(storage, epoch_keypairs.as_slice())
453                    .map_err(MergeCommitError::StorageError)?;
454
455                // Delete the old keys.
456                self.delete_previous_epoch_keypairs(storage)
457                    .map_err(MergeCommitError::StorageError)?;
458                if let Some(keypair) = state.new_leaf_keypair_option {
459                    keypair
460                        .delete(storage)
461                        .map_err(MergeCommitError::StorageError)?;
462                }
463
464                // Empty the proposal store
465                storage
466                    .clear_proposal_queue::<GroupId, ProposalRef>(group_id)
467                    .map_err(MergeCommitError::StorageError)?;
468                self.proposal_store_mut().empty();
469
470                Ok(())
471            }
472        }
473    }
474}
475
476/// The staged commit state.
477#[derive(Debug, Serialize, Deserialize)]
478#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
479pub(crate) enum StagedCommitState {
480    /// The public group variant of the staged commit state.
481    PublicState(Box<PublicStagedCommitState>),
482    /// The group member variant of the staged commit state.
483    GroupMember(Box<MemberStagedCommitState>),
484}
485
486/// Contains the changes from a commit to the group state.
487#[derive(Debug, Serialize, Deserialize)]
488#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
489pub struct StagedCommit {
490    /// A queue containing the proposals associated with the commit.
491    pub staged_proposal_queue: ProposalQueue,
492    /// The staged commit state.
493    state: StagedCommitState,
494}
495
496impl StagedCommit {
497    /// Create a new [`StagedCommit`] from the provisional group state created
498    /// during the commit process.
499    pub(crate) fn new(staged_proposal_queue: ProposalQueue, state: StagedCommitState) -> Self {
500        StagedCommit {
501            staged_proposal_queue,
502            state,
503        }
504    }
505
506    /// Returns the Add proposals that are covered by the Commit message as in iterator over [QueuedAddProposal].
507    pub fn add_proposals(&self) -> impl Iterator<Item = QueuedAddProposal<'_>> {
508        self.staged_proposal_queue.add_proposals()
509    }
510
511    /// Returns the Remove proposals that are covered by the Commit message as in iterator over [QueuedRemoveProposal].
512    pub fn remove_proposals(&self) -> impl Iterator<Item = QueuedRemoveProposal<'_>> {
513        self.staged_proposal_queue.remove_proposals()
514    }
515
516    /// Returns the Update proposals that are covered by the Commit message as in iterator over [QueuedUpdateProposal].
517    pub fn update_proposals(&self) -> impl Iterator<Item = QueuedUpdateProposal<'_>> {
518        self.staged_proposal_queue.update_proposals()
519    }
520
521    /// Returns the PresharedKey proposals that are covered by the Commit message as in iterator over [QueuedPskProposal].
522    pub fn psk_proposals(&self) -> impl Iterator<Item = QueuedPskProposal<'_>> {
523        self.staged_proposal_queue.psk_proposals()
524    }
525
526    #[cfg(feature = "extensions-draft-08")]
527    /// Returns the AppEphemeral proposals that are covered by the Commit message as an iterator
528    /// over [`QueuedAppEphemeralProposal`].
529    pub fn queued_app_ephemeral_proposals(
530        &self,
531    ) -> impl Iterator<Item = QueuedAppEphemeralProposal<'_>> {
532        self.staged_proposal_queue.app_ephemeral_proposals()
533    }
534
535    /// Returns an iterator over all [`QueuedProposal`]s.
536    pub fn queued_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
537        self.staged_proposal_queue.queued_proposals()
538    }
539
540    /// Returns the leaf node of the (optional) update path.
541    pub fn update_path_leaf_node(&self) -> Option<&LeafNode> {
542        match self.state {
543            StagedCommitState::PublicState(ref public_state) => {
544                public_state.update_path_leaf_node()
545            }
546            StagedCommitState::GroupMember(ref group_member_state) => {
547                group_member_state.update_path_leaf_node.as_ref()
548            }
549        }
550    }
551
552    /// Returns the credentials that the caller needs to verify are valid.
553    pub fn credentials_to_verify(&self) -> impl Iterator<Item = &Credential> {
554        let update_path_leaf_node_cred = if let Some(node) = self.update_path_leaf_node() {
555            vec![node.credential()]
556        } else {
557            vec![]
558        };
559
560        update_path_leaf_node_cred
561            .into_iter()
562            .chain(
563                self.queued_proposals()
564                    .flat_map(|proposal: &QueuedProposal| match proposal.proposal() {
565                        Proposal::Update(update_proposal) => {
566                            vec![update_proposal.leaf_node().credential()].into_iter()
567                        }
568                        Proposal::Add(add_proposal) => {
569                            vec![add_proposal.key_package().leaf_node().credential()].into_iter()
570                        }
571                        Proposal::GroupContextExtensions(gce_proposal) => gce_proposal
572                            .extensions()
573                            .iter()
574                            .flat_map(|extension| {
575                                match extension {
576                                    Extension::ExternalSenders(external_senders) => {
577                                        external_senders
578                                            .iter()
579                                            .map(|external_sender| external_sender.credential())
580                                            .collect()
581                                    }
582                                    _ => vec![],
583                                }
584                                .into_iter()
585                            })
586                            // TODO: ideally we wouldn't collect in between here, but the match arms
587                            //       have to all return the same type. We solve this by having them all
588                            //       be vec::IntoIter, but it would be nice if we just didn't have to
589                            //       do this.
590                            //       It might be possible to solve this by letting all match arms
591                            //       evaluate to a dyn Iterator.
592                            .collect::<Vec<_>>()
593                            .into_iter(),
594                        _ => vec![].into_iter(),
595                    }),
596            )
597    }
598
599    /// Returns `true` if the member was removed through a proposal covered by this Commit message
600    /// and `false` otherwise.
601    pub fn self_removed(&self) -> bool {
602        matches!(self.state, StagedCommitState::PublicState(_))
603    }
604
605    /// Returns the [`GroupContext`] of the staged commit state.
606    pub fn group_context(&self) -> &GroupContext {
607        match self.state {
608            StagedCommitState::PublicState(ref ps) => ps.staged_diff().group_context(),
609            StagedCommitState::GroupMember(ref gm) => gm.group_context(),
610        }
611    }
612    /// Consume this [`StagedCommit`] and return the internal [`StagedCommitState`].
613    pub(crate) fn into_state(self) -> StagedCommitState {
614        self.state
615    }
616
617    /// Returns the [`EpochAuthenticator`] of the staged commit state if the
618    /// owner of the originating group state is a member of the group. Returns
619    /// `None` otherwise.
620    pub fn epoch_authenticator(&self) -> Option<&EpochAuthenticator> {
621        if let StagedCommitState::GroupMember(ref gm) = self.state {
622            Some(gm.group_epoch_secrets.epoch_authenticator())
623        } else {
624            None
625        }
626    }
627
628    #[cfg(feature = "extensions-draft-08")]
629    pub(crate) fn safe_export_secret(
630        &mut self,
631        crypto: &impl OpenMlsCrypto,
632        component_id: u16,
633    ) -> Result<Vec<u8>, StagedSafeExportSecretError> {
634        let ciphersuite = self.group_context().ciphersuite();
635        let StagedCommitState::GroupMember(ref mut staged_commit) = self.state else {
636            return Err(StagedSafeExportSecretError::NotGroupMember);
637        };
638        let Some(application_export_tree) = staged_commit.application_export_tree.as_mut() else {
639            return Err(StagedSafeExportSecretError::Unsupported);
640        };
641        let secret =
642            application_export_tree.safe_export_secret(crypto, ciphersuite, component_id)?;
643        Ok(secret.as_slice().to_vec())
644    }
645}
646
647/// This struct is used internally by [`StagedCommit`] to encapsulate all the modified group state.
648#[derive(Debug, Serialize, Deserialize)]
649#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
650pub(crate) struct MemberStagedCommitState {
651    group_epoch_secrets: GroupEpochSecrets,
652    message_secrets: MessageSecrets,
653    staged_diff: StagedPublicGroupDiff,
654    new_keypairs: Vec<EncryptionKeyPair>,
655    new_leaf_keypair_option: Option<EncryptionKeyPair>,
656    update_path_leaf_node: Option<LeafNode>,
657    #[cfg(feature = "extensions-draft-08")]
658    #[serde(default)]
659    // This is `None` only if the group was stored using an older version of
660    // OpenMLS that did not support the application exporter.
661    application_export_tree: Option<ApplicationExportTree>,
662}
663
664impl MemberStagedCommitState {
665    pub(crate) fn new(
666        group_epoch_secrets: GroupEpochSecrets,
667        message_secrets: MessageSecrets,
668        staged_diff: StagedPublicGroupDiff,
669        new_keypairs: Vec<EncryptionKeyPair>,
670        new_leaf_keypair_option: Option<EncryptionKeyPair>,
671        update_path_leaf_node: Option<LeafNode>,
672        #[cfg(feature = "extensions-draft-08")] application_export_tree: ApplicationExportTree,
673    ) -> Self {
674        Self {
675            group_epoch_secrets,
676            message_secrets,
677            staged_diff,
678            new_keypairs,
679            new_leaf_keypair_option,
680            update_path_leaf_node,
681            #[cfg(feature = "extensions-draft-08")]
682            application_export_tree: Some(application_export_tree),
683        }
684    }
685
686    /// Get the staged [`GroupContext`].
687    pub(crate) fn group_context(&self) -> &GroupContext {
688        self.staged_diff.group_context()
689    }
690}