openmls/schedule/
mod.rs

1//! # Key Schedule
2//!
3//! This module defines types and implementations for key schedule operations.
4//! It provides the [`EpochAuthenticator`] and [`ResumptionPskSecret`] types.
5//!
6//! ## Internal Documentation
7//!
8//! The key schedule is described in Section 9 of the MLS specification. It
9//! evolves in epochs, with new key material injected in each epoch.
10//!
11//! The key schedule flow (from Section 9 of the MLS specification) is as follows:
12//
13// ```text
14//                  init_secret_[n-1]
15//                         |
16//                         V
17//    commit_secret -> KDF.Extract
18//                         |
19//                         V
20//                   DeriveSecret(., "joiner")
21//                         |
22//                         V
23//                    joiner_secret
24//                         |
25//                         V
26// psk_secret (or 0) -> KDF.Extract (= intermediary_secret)
27//                         |
28//                         +--> DeriveSecret(., "welcome")
29//                         |    = welcome_secret
30//                         |
31//                         V
32//                   ExpandWithLabel(., "epoch", GroupContext_[n], KDF.Nh)
33//                         |
34//                         V
35//                    epoch_secret
36//                         |
37//                         +--> DeriveSecret(., <label>)
38//                         |    = <secret>
39//                         |
40//                         V
41//                   DeriveSecret(., "init")
42//                         |
43//                         V
44//                   init_secret_[n]
45// ```
46//
47// Each secret in the key schedule (except welcome_secret) has its own struct to
48// prevent confusion or out-of-order derivation. This ensures clarity and safety
49// in the key schedule operations.
50//
51// ## Key schedule structure
52// The spec's key schedule isn't a single linear process. The `joiner_secret`
53// serves as both an intermediate and output value, which violates key schedule
54// principles. The actual key schedule begins with the `joiner_secret`, as seen
55// when initializing a group from a welcome message.
56//
57// The `joiner_secret` is computed as
58//
59// ```text
60//     DeriveSecret(KDF.Extract(init_secret_[n-1], commit_secret), "joiner")
61// ```
62//
63// or
64//
65// ```text
66//                  init_secret_[n-1]
67//                         |
68//                         V
69//    commit_secret -> KDF.Extract
70//                         |
71//                         V
72//                   DeriveSecret(., "joiner")
73//                         |
74//                         V
75//                    joiner_secret
76// ```
77//
78// The key schedule continues with `joiner_secret` and `psk_secret`. The graph
79// below includes `GroupContext_[n]` as input, which is omitted in the spec. The
80// derivation of secrets from `epoch_secret` is simplified for clarity.
81//
82// ```text
83//                    joiner_secret
84//                         |
85//                         V
86// psk_secret (or 0) -> KDF.Extract
87//                         |
88//                         +--> DeriveSecret(., "welcome")
89//                         |    = welcome_secret
90//                         |
91//                         V
92// GroupContext_[n] -> ExpandWithLabel(., "epoch", GroupContext_[n], KDF.Nh)
93//                         |
94//                         V
95//                    epoch_secret
96//                         |
97//                         v
98//                 DeriveSecret(., <label>)
99//                     = <secret>
100// ```
101//
102// with
103//
104// ```text
105// | secret                  | label           |
106// |:------------------------|:----------------|
107// | `init_secret`           | "init"          |
108// | `sender_data_secret`    | "sender data"   |
109// | `encryption_secret`     | "encryption"    |
110// | `exporter_secret`       | "exporter"      |
111// | `epoch_authenticator`   | "authentication"|
112// | `external_secret`       | "external"      |
113// | `confirmation_key`      | "confirm"       |
114// | `membership_key`        | "membership"    |
115// | `resumption_psk`        | "resumption"    |
116// ```
117
118use openmls_traits::{crypto::OpenMlsCrypto, types::*};
119use serde::{Deserialize, Serialize};
120use tls_codec::{TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize};
121
122use crate::{
123    binary_tree::array_representation::{LeafNodeIndex, TreeSize},
124    ciphersuite::{AeadKey, AeadNonce, HpkePrivateKey, Mac, Secret},
125    error::LibraryError,
126    framing::{mls_content::AuthenticatedContentTbm, MembershipTag},
127    group::GroupContext,
128    messages::{ConfirmationTag, PathSecret},
129    tree::secret_tree::SecretTree,
130    versions::ProtocolVersion,
131};
132
133// Public
134pub mod errors;
135pub mod psk;
136
137// Crate
138pub(crate) mod message_secrets;
139
140// Private
141use errors::*;
142use message_secrets::MessageSecrets;
143use openmls_traits::random::OpenMlsRand;
144use psk::PskSecret;
145
146// Tests and kats
147#[cfg(any(feature = "test-utils", test))]
148pub mod tests_and_kats;
149
150// Public types
151pub use psk::{ExternalPsk, PreSharedKeyId, Psk};
152
153/// A group secret that can be used among members to prove that a member was
154/// part of a group in a given epoch.
155#[derive(Clone, Debug, Serialize, Deserialize)]
156#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq))]
157pub struct ResumptionPskSecret {
158    secret: Secret,
159}
160
161impl ResumptionPskSecret {
162    /// Derive an `ResumptionPsk` from an `EpochSecret`.
163    fn new(
164        crypto: &impl OpenMlsCrypto,
165        ciphersuite: Ciphersuite,
166        epoch_secret: &EpochSecret,
167    ) -> Result<Self, CryptoError> {
168        let secret = epoch_secret
169            .secret
170            .derive_secret(crypto, ciphersuite, "resumption")?;
171        Ok(Self { secret })
172    }
173
174    /// Returns the secret as a slice.
175    pub fn as_slice(&self) -> &[u8] {
176        self.secret.as_slice()
177    }
178}
179
180/// A secret that can be used among members to make sure everyone has the same
181/// group state.
182#[derive(Debug, Serialize, Deserialize)]
183#[cfg_attr(any(test, feature = "test-utils"), derive(Eq, PartialEq, Clone))]
184pub struct EpochAuthenticator {
185    secret: Secret,
186}
187
188impl EpochAuthenticator {
189    /// Derive an `EpochAuthenticator` from an `EpochSecret`.
190    fn new(
191        crypto: &impl OpenMlsCrypto,
192        ciphersuite: Ciphersuite,
193        epoch_secret: &EpochSecret,
194    ) -> Result<Self, CryptoError> {
195        let secret = epoch_secret
196            .secret
197            .derive_secret(crypto, ciphersuite, "authentication")?;
198        Ok(Self { secret })
199    }
200
201    /// Returns the secret as a slice.
202    pub fn as_slice(&self) -> &[u8] {
203        self.secret.as_slice()
204    }
205}
206
207// Crate-only types
208
209#[derive(Debug, Default, Serialize, Deserialize)]
210#[cfg_attr(test, derive(PartialEq))]
211#[cfg_attr(any(feature = "test-utils", test), derive(Clone))]
212pub(crate) struct CommitSecret {
213    secret: Secret,
214}
215
216impl From<PathSecret> for CommitSecret {
217    fn from(path_secret: PathSecret) -> Self {
218        CommitSecret {
219            secret: path_secret.secret(),
220        }
221    }
222}
223
224impl CommitSecret {
225    /// Create a CommitSecret consisting of an all-zero string of length
226    /// `hash_length`.
227    pub(crate) fn zero_secret(ciphersuite: Ciphersuite) -> Self {
228        CommitSecret {
229            secret: Secret::zero(ciphersuite),
230        }
231    }
232
233    #[cfg(any(feature = "test-utils", test))]
234    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
235        Self {
236            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
237        }
238    }
239
240    #[cfg(any(feature = "test-utils", test))]
241    pub(crate) fn as_slice(&self) -> &[u8] {
242        self.secret.as_slice()
243    }
244}
245
246/// The `InitSecret` is used to connect the next epoch to the current one.
247#[derive(Debug, Serialize, Deserialize)]
248#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
249pub(crate) struct InitSecret {
250    secret: Secret,
251}
252
253impl From<Secret> for InitSecret {
254    fn from(secret: Secret) -> Self {
255        Self { secret }
256    }
257}
258
259/// Creates a string from the given MLS `ProtocolVersion` for the computation of
260/// the `init_secret` when creating or processing a commit with an external init
261/// proposal.
262fn hpke_info_from_version(version: ProtocolVersion) -> &'static str {
263    match version {
264        ProtocolVersion::Mls10 => "MLS 1.0 external init secret",
265        _ => "<OpenMLS reserved; Don't use this.>",
266    }
267}
268
269impl InitSecret {
270    /// Derive an `InitSecret` from an `EpochSecret`.
271    fn new(
272        crypto: &impl OpenMlsCrypto,
273        ciphersuite: Ciphersuite,
274        epoch_secret: EpochSecret,
275    ) -> Result<Self, CryptoError> {
276        let secret = epoch_secret
277            .secret
278            .derive_secret(crypto, ciphersuite, "init")?;
279        log_crypto!(trace, "Init secret: {:x?}", secret);
280        Ok(InitSecret { secret })
281    }
282
283    /// Sample a fresh, random `InitSecret` for the creation of a new group.
284    pub(crate) fn random(
285        ciphersuite: Ciphersuite,
286        rand: &impl OpenMlsRand,
287    ) -> Result<Self, CryptoError> {
288        Ok(InitSecret {
289            secret: Secret::random(ciphersuite, rand)?,
290        })
291    }
292
293    /// Create an `InitSecret` and the corresponding `kem_output` from a group info.
294    pub(crate) fn from_group_context(
295        crypto: &impl OpenMlsCrypto,
296        group_context: &GroupContext,
297        external_pub: &[u8],
298    ) -> Result<(Self, Vec<u8>), KeyScheduleError> {
299        let ciphersuite = group_context.ciphersuite();
300        let version = group_context.protocol_version();
301        let (kem_output, raw_init_secret) = crypto.hpke_setup_sender_and_export(
302            ciphersuite.hpke_config(),
303            external_pub,
304            &[],
305            hpke_info_from_version(version).as_bytes(),
306            ciphersuite.hash_length(),
307        )?;
308        Ok((
309            InitSecret {
310                secret: Secret::from_slice(&raw_init_secret),
311            },
312            kem_output,
313        ))
314    }
315
316    /// Create an `InitSecret` from a `kem_output`.
317    pub(crate) fn from_kem_output(
318        crypto: &impl OpenMlsCrypto,
319        ciphersuite: Ciphersuite,
320        version: ProtocolVersion,
321        external_priv: &HpkePrivateKey,
322        kem_output: &[u8],
323    ) -> Result<Self, LibraryError> {
324        let raw_init_secret = crypto
325            .hpke_setup_receiver_and_export(
326                ciphersuite.hpke_config(),
327                kem_output,
328                external_priv,
329                &[],
330                hpke_info_from_version(version).as_bytes(),
331                ciphersuite.hash_length(),
332            )
333            .map_err(LibraryError::unexpected_crypto_error)?;
334        Ok(InitSecret {
335            secret: Secret::from_slice(&raw_init_secret),
336        })
337    }
338
339    #[cfg(any(feature = "test-utils", test))]
340    pub(crate) fn clone(&self) -> Self {
341        Self {
342            secret: self.secret.clone(),
343        }
344    }
345
346    #[cfg(any(feature = "test-utils", test))]
347    pub(crate) fn as_slice(&self) -> &[u8] {
348        self.secret.as_slice()
349    }
350}
351
352#[derive(Debug, TlsDeserialize, TlsDeserializeBytes, TlsSerialize, TlsSize)]
353pub(crate) struct JoinerSecret {
354    secret: Secret,
355}
356
357impl JoinerSecret {
358    /// Derive a `JoinerSecret` from an optional `CommitSecret` and an
359    /// `EpochSecrets` object, which contains the necessary `InitSecret`. The
360    /// `CommitSecret` needs to be present if the current commit is not a
361    /// partial commit.
362    pub(crate) fn new(
363        crypto: &impl OpenMlsCrypto,
364        ciphersuite: Ciphersuite,
365        commit_secret_option: impl Into<Option<CommitSecret>>,
366        init_secret: &InitSecret,
367        serialized_group_context: &[u8],
368    ) -> Result<Self, CryptoError> {
369        let intermediate_secret = init_secret.secret.hkdf_extract(
370            crypto,
371            ciphersuite,
372            commit_secret_option.into().as_ref().map(|cs| &cs.secret),
373        )?;
374        let secret = intermediate_secret.kdf_expand_label(
375            crypto,
376            ciphersuite,
377            "joiner",
378            serialized_group_context,
379            ciphersuite.hash_length(),
380        )?;
381        log_crypto!(trace, "Joiner secret: {:x?}", secret);
382        Ok(JoinerSecret { secret })
383    }
384
385    #[cfg(any(feature = "test-utils", test))]
386    pub(crate) fn as_slice(&self) -> &[u8] {
387        self.secret.as_slice()
388    }
389
390    #[cfg(test)]
391    pub(crate) fn random(ciphersuite: Ciphersuite, rand: &impl OpenMlsRand) -> Self {
392        Self {
393            secret: Secret::random(ciphersuite, rand).expect("Not enough randomness."),
394        }
395    }
396}
397
398// Different states of the key schedule
399#[derive(Debug, PartialEq)]
400enum State {
401    Initial,
402    Context,
403    Done,
404}
405
406pub(crate) struct KeySchedule {
407    ciphersuite: Ciphersuite,
408    intermediate_secret: Option<IntermediateSecret>,
409    epoch_secret: Option<EpochSecret>,
410    state: State,
411}
412
413impl KeySchedule {
414    /// Initialize the key schedule and return it.
415    pub(crate) fn init(
416        ciphersuite: Ciphersuite,
417        crypto: &impl OpenMlsCrypto,
418        joiner_secret: &JoinerSecret,
419        psk: PskSecret,
420    ) -> Result<Self, LibraryError> {
421        log::debug!("Initializing the key schedule with {:?} ...", ciphersuite);
422        log_crypto!(
423            trace,
424            "  joiner_secret: {:x?}",
425            joiner_secret.secret.as_slice()
426        );
427        let intermediate_secret = IntermediateSecret::new(crypto, ciphersuite, joiner_secret, psk)
428            .map_err(LibraryError::unexpected_crypto_error)?;
429        Ok(Self {
430            ciphersuite,
431            intermediate_secret: Some(intermediate_secret),
432            epoch_secret: None,
433            state: State::Initial,
434        })
435    }
436
437    /// Derive the welcome secret.
438    /// Note that this has to be called before the context is added.
439    pub(crate) fn welcome(
440        &self,
441        crypto: &impl OpenMlsCrypto,
442        ciphersuite: Ciphersuite,
443    ) -> Result<WelcomeSecret, KeyScheduleError> {
444        if self.state != State::Initial || self.intermediate_secret.is_none() {
445            log::error!("Trying to derive a welcome secret while not in the initial state.");
446            return Err(KeyScheduleError::InvalidState(ErrorState::Init));
447        }
448
449        // We can return a library error here, because there must be a mistake in the state machine
450        let intermediate_secret = self
451            .intermediate_secret
452            .as_ref()
453            .ok_or_else(|| LibraryError::custom("state machine error"))?;
454
455        Ok(WelcomeSecret::new(
456            crypto,
457            ciphersuite,
458            intermediate_secret,
459        )?)
460    }
461
462    /// Add the group context to the key schedule.
463    pub(crate) fn add_context(
464        &mut self,
465        crypto: &impl OpenMlsCrypto,
466        serialized_group_context: &[u8],
467    ) -> Result<(), KeyScheduleError> {
468        log::trace!(
469            "Adding context to key schedule. {:?}",
470            serialized_group_context
471        );
472        if self.state != State::Initial || self.intermediate_secret.is_none() {
473            log::error!(
474                "Trying to add context to the key schedule while not in the initial state."
475            );
476            return Err(KeyScheduleError::InvalidState(ErrorState::Init));
477        }
478        self.state = State::Context;
479
480        // We can return a library error here, because there must be a mistake in the state machine
481        let intermediate_secret = self
482            .intermediate_secret
483            .take()
484            .ok_or_else(|| LibraryError::custom("state machine error"))?;
485
486        log_crypto!(
487            trace,
488            "  intermediate_secret: {:x?}",
489            intermediate_secret.secret.as_slice()
490        );
491
492        self.epoch_secret = Some(EpochSecret::new(
493            self.ciphersuite,
494            crypto,
495            intermediate_secret,
496            serialized_group_context,
497        )?);
498        self.intermediate_secret = None;
499        Ok(())
500    }
501
502    /// Derive the epoch secrets.
503    /// If the `with_init_secret` argument is `true`, the init secret is derived and
504    /// part of the `EpochSecrets`. Otherwise not.
505    pub(crate) fn epoch_secrets(
506        &mut self,
507        crypto: &impl OpenMlsCrypto,
508        ciphersuite: Ciphersuite,
509    ) -> Result<EpochSecrets, KeyScheduleError> {
510        if self.state != State::Context || self.epoch_secret.is_none() {
511            log::error!("Trying to derive the epoch secrets while not in the right state.");
512            return Err(KeyScheduleError::InvalidState(ErrorState::Context));
513        }
514        self.state = State::Done;
515
516        let epoch_secret = match self.epoch_secret.take() {
517            Some(epoch_secret) => epoch_secret,
518            // We can return a library error here, because there must be a mistake in the state machine
519            None => return Err(LibraryError::custom("state machine error").into()),
520        };
521
522        Ok(EpochSecrets::new(crypto, ciphersuite, epoch_secret)?)
523    }
524}
525
526/// The intermediate secret includes the optional PSK and is used to later
527/// derive the welcome secret and epoch secret
528struct IntermediateSecret {
529    secret: Secret,
530}
531
532impl IntermediateSecret {
533    /// Derive an `IntermediateSecret` from a `JoinerSecret` and an optional
534    /// PSK.
535    fn new(
536        crypto: &impl OpenMlsCrypto,
537        ciphersuite: Ciphersuite,
538        joiner_secret: &JoinerSecret,
539        psk: PskSecret,
540    ) -> Result<Self, CryptoError> {
541        log_crypto!(trace, "PSK input: {:x?}", psk.as_slice());
542        let secret = joiner_secret
543            .secret
544            .hkdf_extract(crypto, ciphersuite, psk.secret())?;
545        log_crypto!(trace, "Intermediate secret: {:x?}", secret);
546        Ok(Self { secret })
547    }
548}
549
550pub(crate) struct WelcomeSecret {
551    secret: Secret,
552}
553
554impl WelcomeSecret {
555    /// Derive a `WelcomeSecret` from to decrypt a `Welcome` message.
556    fn new(
557        crypto: &impl OpenMlsCrypto,
558        ciphersuite: Ciphersuite,
559        intermediate_secret: &IntermediateSecret,
560    ) -> Result<Self, CryptoError> {
561        let secret = intermediate_secret
562            .secret
563            .derive_secret(crypto, ciphersuite, "welcome")?;
564        log_crypto!(trace, "Welcome secret: {:x?}", secret);
565        Ok(WelcomeSecret { secret })
566    }
567
568    /// Derive an `AeadKey` and an `AeadNonce` from the `WelcomeSecret`,
569    /// consuming it in the process.
570    pub(crate) fn derive_welcome_key_nonce(
571        self,
572        crypto: &impl OpenMlsCrypto,
573        ciphersuite: Ciphersuite,
574    ) -> Result<(AeadKey, AeadNonce), CryptoError> {
575        let welcome_nonce = self.derive_aead_nonce(crypto, ciphersuite)?;
576        let welcome_key = self.derive_aead_key(crypto, ciphersuite)?;
577        Ok((welcome_key, welcome_nonce))
578    }
579
580    /// Derive a new AEAD key from a `WelcomeSecret`.
581    fn derive_aead_key(
582        &self,
583        crypto: &impl OpenMlsCrypto,
584        ciphersuite: Ciphersuite,
585    ) -> Result<AeadKey, CryptoError> {
586        log::trace!("WelcomeSecret.derive_aead_key with {}", ciphersuite);
587        let aead_secret = self.secret.kdf_expand_label(
588            crypto,
589            ciphersuite,
590            "key",
591            b"",
592            ciphersuite.aead_key_length(),
593        )?;
594        Ok(AeadKey::from_secret(aead_secret, ciphersuite))
595    }
596
597    /// Derive a new AEAD nonce from a `WelcomeSecret`.
598    fn derive_aead_nonce(
599        &self,
600        crypto: &impl OpenMlsCrypto,
601        ciphersuite: Ciphersuite,
602    ) -> Result<AeadNonce, CryptoError> {
603        let nonce_secret = self.secret.kdf_expand_label(
604            crypto,
605            ciphersuite,
606            "nonce",
607            b"",
608            ciphersuite.aead_nonce_length(),
609        )?;
610        Ok(AeadNonce::from_secret(nonce_secret))
611    }
612
613    #[cfg(any(feature = "test-utils", test))]
614    pub(crate) fn as_slice(&self) -> &[u8] {
615        self.secret.as_slice()
616    }
617}
618
619/// An intermediate secret in the key schedule, the `EpochSecret` is used to
620/// create an `EpochSecrets` object and is finally consumed when creating that
621/// epoch's `InitSecret`.
622struct EpochSecret {
623    secret: Secret,
624}
625
626impl EpochSecret {
627    /// Derive an `EpochSecret` from a `JoinerSecret`
628    fn new(
629        ciphersuite: Ciphersuite,
630        crypto: &impl OpenMlsCrypto,
631        intermediate_secret: IntermediateSecret,
632        serialized_group_context: &[u8],
633    ) -> Result<Self, CryptoError> {
634        let secret = intermediate_secret.secret.kdf_expand_label(
635            crypto,
636            ciphersuite,
637            "epoch",
638            serialized_group_context,
639            ciphersuite.hash_length(),
640        )?;
641        log_crypto!(trace, "Epoch secret: {:x?}", secret);
642        Ok(EpochSecret { secret })
643    }
644}
645
646/// The `EncryptionSecret` is used to create a `SecretTree`.
647#[cfg_attr(test, derive(Clone))]
648pub(crate) struct EncryptionSecret {
649    secret: Secret,
650}
651
652impl EncryptionSecret {
653    /// Derive an encryption secret from a reference to an `EpochSecret`.
654    fn new(
655        crypto: &impl OpenMlsCrypto,
656        ciphersuite: Ciphersuite,
657        epoch_secret: &EpochSecret,
658    ) -> Result<Self, CryptoError> {
659        Ok(EncryptionSecret {
660            secret: epoch_secret
661                .secret
662                .derive_secret(crypto, ciphersuite, "encryption")?,
663        })
664    }
665
666    /// Create a `SecretTree` from the `encryption_secret` contained in the
667    /// `EpochSecrets`. The `encryption_secret` is consumed, allowing us to achieve FS.
668    pub(crate) fn create_secret_tree(
669        self,
670        treesize: TreeSize,
671        own_index: LeafNodeIndex,
672    ) -> SecretTree {
673        SecretTree::new(self, treesize, own_index)
674    }
675
676    pub(crate) fn consume_secret(self) -> Secret {
677        self.secret
678    }
679
680    /// Create a random `EncryptionSecret`. For testing purposes only.
681    #[cfg(test)]
682    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
683        EncryptionSecret {
684            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
685        }
686    }
687
688    #[cfg(any(feature = "test-utils", test))]
689    pub(crate) fn as_slice(&self) -> &[u8] {
690        self.secret.as_slice()
691    }
692
693    #[cfg(any(feature = "test-utils", test))]
694    /// Create a new secret from a byte vector.
695    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
696        Self {
697            secret: Secret::from_slice(bytes),
698        }
699    }
700}
701
702/// A secret that we can derive secrets from, that are used outside of OpenMLS.
703#[derive(Debug, Serialize, Deserialize)]
704#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
705pub(crate) struct ExporterSecret {
706    secret: Secret,
707}
708
709impl ExporterSecret {
710    /// Derive an `ExporterSecret` from an `EpochSecret`.
711    fn new(
712        crypto: &impl OpenMlsCrypto,
713        ciphersuite: Ciphersuite,
714        epoch_secret: &EpochSecret,
715    ) -> Result<Self, CryptoError> {
716        let secret = epoch_secret
717            .secret
718            .derive_secret(crypto, ciphersuite, "exporter")?;
719        Ok(ExporterSecret { secret })
720    }
721
722    #[cfg(any(feature = "test-utils", test))]
723    pub(crate) fn as_slice(&self) -> &[u8] {
724        self.secret.as_slice()
725    }
726
727    /// Derive a `Secret` from the exporter secret. We return `Vec<u8>` here, so
728    /// it can be used outside of OpenMLS. This function is made available for
729    /// use from the outside through [`MlsGroup::export_secret`].
730    pub(crate) fn derive_exported_secret(
731        &self,
732        ciphersuite: Ciphersuite,
733        crypto: &impl OpenMlsCrypto,
734        label: &str,
735        context: &[u8],
736        key_length: usize,
737    ) -> Result<Vec<u8>, CryptoError> {
738        let context_hash = &crypto.hash(ciphersuite.hash_algorithm(), context)?;
739        Ok(self
740            .secret
741            .derive_secret(crypto, ciphersuite, label)?
742            .kdf_expand_label(crypto, ciphersuite, "exported", context_hash, key_length)?
743            .as_slice()
744            .to_vec())
745    }
746}
747
748/// A secret used when joining a group with an external Commit.
749#[derive(Debug, Serialize, Deserialize)]
750#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
751pub(crate) struct ExternalSecret {
752    secret: Secret,
753}
754
755impl ExternalSecret {
756    /// Derive an `ExternalSecret` from an `EpochSecret`.
757    fn new(
758        crypto: &impl OpenMlsCrypto,
759        ciphersuite: Ciphersuite,
760        epoch_secret: &EpochSecret,
761    ) -> Result<Self, CryptoError> {
762        let secret = epoch_secret
763            .secret
764            .derive_secret(crypto, ciphersuite, "external")?;
765        Ok(Self { secret })
766    }
767
768    /// Derive the external keypair for External Commits
769    pub(crate) fn derive_external_keypair(
770        &self,
771        crypto: &impl OpenMlsCrypto,
772        ciphersuite: Ciphersuite,
773    ) -> Result<HpkeKeyPair, CryptoError> {
774        crypto.derive_hpke_keypair(ciphersuite.hpke_config(), self.secret.as_slice())
775    }
776
777    #[cfg(any(feature = "test-utils", test))]
778    pub(crate) fn as_slice(&self) -> &[u8] {
779        self.secret.as_slice()
780    }
781}
782
783/// The confirmation key is used to calculate the `ConfirmationTag`.
784#[derive(Debug, Serialize, Deserialize)]
785#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
786pub(crate) struct ConfirmationKey {
787    secret: Secret,
788}
789
790impl ConfirmationKey {
791    /// Derive an `ConfirmationKey` from an `EpochSecret`.
792    fn new(
793        crypto: &impl OpenMlsCrypto,
794        ciphersuite: Ciphersuite,
795        epoch_secret: &EpochSecret,
796    ) -> Result<Self, CryptoError> {
797        log::debug!("Computing confirmation key.");
798        log_crypto!(
799            trace,
800            "  epoch_secret {:x?}",
801            epoch_secret.secret.as_slice()
802        );
803        let secret = epoch_secret
804            .secret
805            .derive_secret(crypto, ciphersuite, "confirm")?;
806        Ok(Self { secret })
807    }
808
809    /// Create a new confirmation tag.
810    ///
811    /// >  11.2. Commit
812    ///
813    /// ```text
814    /// PublicMessage.confirmation_tag =
815    ///     MAC(confirmation_key, GroupContext.confirmed_transcript_hash)
816    /// ```
817    pub(crate) fn tag(
818        &self,
819        crypto: &impl OpenMlsCrypto,
820        ciphersuite: Ciphersuite,
821        confirmed_transcript_hash: &[u8],
822    ) -> Result<ConfirmationTag, CryptoError> {
823        log::debug!("Computing confirmation tag.");
824        log_crypto!(trace, "  confirmation key {:x?}", self.secret.as_slice());
825        log_crypto!(trace, "  transcript hash  {:x?}", confirmed_transcript_hash);
826        Ok(ConfirmationTag(Mac::new(
827            crypto,
828            ciphersuite,
829            &self.secret,
830            confirmed_transcript_hash,
831        )?))
832    }
833}
834
835#[cfg(test)]
836impl ConfirmationKey {
837    pub(crate) fn from_secret(secret: Secret) -> Self {
838        Self { secret }
839    }
840}
841
842#[cfg(any(feature = "test-utils", test))]
843impl ConfirmationKey {
844    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
845        Self {
846            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
847        }
848    }
849
850    pub(crate) fn as_slice(&self) -> &[u8] {
851        self.secret.as_slice()
852    }
853}
854
855/// The membership key is used to calculate the `MembershipTag`.
856#[derive(Debug, Serialize, Deserialize)]
857#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
858pub(crate) struct MembershipKey {
859    secret: Secret,
860}
861
862impl MembershipKey {
863    /// Derive an `MembershipKey` from an `EpochSecret`.
864    fn new(
865        crypto: &impl OpenMlsCrypto,
866        ciphersuite: Ciphersuite,
867        epoch_secret: &EpochSecret,
868    ) -> Result<Self, CryptoError> {
869        let secret = epoch_secret
870            .secret
871            .derive_secret(crypto, ciphersuite, "membership")?;
872        Ok(Self { secret })
873    }
874
875    /// Create a new membership tag.
876    ///
877    /// 9.1 Content Authentication
878    ///
879    /// ```text
880    /// membership_tag = MAC(membership_key, MLSPlaintextTBM);
881    /// ```
882    pub(crate) fn tag_message(
883        &self,
884        crypto: &impl OpenMlsCrypto,
885        ciphersuite: Ciphersuite,
886        tbm_payload: AuthenticatedContentTbm,
887    ) -> Result<MembershipTag, LibraryError> {
888        Ok(MembershipTag(
889            Mac::new(
890                crypto,
891                ciphersuite,
892                &self.secret,
893                &tbm_payload
894                    .into_bytes()
895                    .map_err(LibraryError::missing_bound_check)?,
896            )
897            .map_err(LibraryError::unexpected_crypto_error)?,
898        ))
899    }
900
901    #[cfg(any(feature = "test-utils", test))]
902    pub(crate) fn from_secret(secret: Secret) -> Self {
903        Self { secret }
904    }
905
906    #[cfg(any(feature = "test-utils", test))]
907    pub(crate) fn as_slice(&self) -> &[u8] {
908        self.secret.as_slice()
909    }
910
911    #[cfg(any(feature = "test-utils", test))]
912    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
913        Self {
914            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
915        }
916    }
917}
918
919// Get a ciphertext sample of `hash_length` from the ciphertext.
920fn ciphertext_sample(ciphersuite: Ciphersuite, ciphertext: &[u8]) -> &[u8] {
921    let sample_length = ciphersuite.hash_length();
922    log::debug!("Getting ciphertext sample of length {:?}", sample_length);
923    if ciphertext.len() <= sample_length {
924        ciphertext
925    } else {
926        &ciphertext[0..sample_length]
927    }
928}
929
930/// A key that can be used to derive an `AeadKey` and an `AeadNonce`.
931#[derive(Serialize, Deserialize)]
932#[cfg_attr(
933    any(feature = "test-utils", feature = "crypto-debug", test),
934    derive(Debug, Clone, PartialEq)
935)]
936pub(crate) struct SenderDataSecret {
937    secret: Secret,
938}
939
940impl SenderDataSecret {
941    /// Derive an `ExporterSecret` from an `EpochSecret`.
942    fn new(
943        crypto: &impl OpenMlsCrypto,
944        ciphersuite: Ciphersuite,
945        epoch_secret: &EpochSecret,
946    ) -> Result<Self, CryptoError> {
947        let secret = epoch_secret
948            .secret
949            .derive_secret(crypto, ciphersuite, "sender data")?;
950        Ok(SenderDataSecret { secret })
951    }
952
953    /// Derive a new AEAD key from a `SenderDataSecret`.
954    pub(crate) fn derive_aead_key(
955        &self,
956        crypto: &impl OpenMlsCrypto,
957        ciphersuite: Ciphersuite,
958        ciphertext: &[u8],
959    ) -> Result<AeadKey, CryptoError> {
960        let ciphertext_sample = ciphertext_sample(ciphersuite, ciphertext);
961        log::debug!(
962            "SenderDataSecret::derive_aead_key ciphertext sample: {:x?}",
963            ciphertext_sample
964        );
965        let secret = self.secret.kdf_expand_label(
966            crypto,
967            ciphersuite,
968            "key",
969            ciphertext_sample,
970            ciphersuite.aead_key_length(),
971        )?;
972        Ok(AeadKey::from_secret(secret, ciphersuite))
973    }
974
975    /// Derive a new AEAD nonce from a `SenderDataSecret`.
976    pub(crate) fn derive_aead_nonce(
977        &self,
978        ciphersuite: Ciphersuite,
979        crypto: &impl OpenMlsCrypto,
980        ciphertext: &[u8],
981    ) -> Result<AeadNonce, CryptoError> {
982        let ciphertext_sample = ciphertext_sample(ciphersuite, ciphertext);
983        log::debug!(
984            "SenderDataSecret::derive_aead_nonce ciphertext sample: {:x?}",
985            ciphertext_sample
986        );
987        let nonce_secret = self.secret.kdf_expand_label(
988            crypto,
989            ciphersuite,
990            "nonce",
991            ciphertext_sample,
992            ciphersuite.aead_nonce_length(),
993        )?;
994        Ok(AeadNonce::from_secret(nonce_secret))
995    }
996
997    #[cfg(any(feature = "test-utils", test))]
998    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
999        Self {
1000            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
1001        }
1002    }
1003
1004    #[cfg(any(feature = "test-utils", test))]
1005    pub(crate) fn as_slice(&self) -> &[u8] {
1006        self.secret.as_slice()
1007    }
1008
1009    #[cfg(any(feature = "test-utils", test))]
1010    /// Create a new secret from a byte vector.
1011    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
1012        Self {
1013            secret: Secret::from_slice(bytes),
1014        }
1015    }
1016}
1017
1018/// The `EpochSecrets` contain keys (or secrets), which are accessible outside
1019/// of the `KeySchedule` and which don't get consumed immediately upon first
1020/// use.
1021///
1022/// | Secret                  | Label           |
1023/// |:------------------------|:----------------|
1024/// | `init_secret`           | "init"          |
1025/// | `sender_data_secret`    | "sender data"   |
1026/// | `encryption_secret`     | "encryption"    |
1027/// | `exporter_secret`       | "exporter"      |
1028/// | `epoch_authenticator`   | "authentication"|
1029/// | `external_secret`       | "external"      |
1030/// | `confirmation_key`      | "confirm"       |
1031/// | `membership_key`        | "membership"    |
1032/// | `resumption_psk`        | "resumption"    |
1033pub(crate) struct EpochSecrets {
1034    init_secret: InitSecret,
1035    sender_data_secret: SenderDataSecret,
1036    encryption_secret: EncryptionSecret,
1037    exporter_secret: ExporterSecret,
1038    epoch_authenticator: EpochAuthenticator,
1039    external_secret: ExternalSecret,
1040    confirmation_key: ConfirmationKey,
1041    membership_key: MembershipKey,
1042    resumption_psk: ResumptionPskSecret,
1043}
1044
1045impl std::fmt::Debug for EpochSecrets {
1046    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1047        f.write_str("EpochSecrets { *** }")
1048    }
1049}
1050
1051#[cfg(not(test))]
1052impl PartialEq for EpochSecrets {
1053    fn eq(&self, _other: &Self) -> bool {
1054        false
1055    }
1056}
1057
1058// In tests we allow comparing secrets.
1059#[cfg(test)]
1060impl PartialEq for EpochSecrets {
1061    fn eq(&self, other: &Self) -> bool {
1062        self.sender_data_secret == other.sender_data_secret
1063            && self.exporter_secret == other.exporter_secret
1064            && self.epoch_authenticator == other.epoch_authenticator
1065            && self.external_secret == other.external_secret
1066            && self.confirmation_key == other.confirmation_key
1067            && self.membership_key == other.membership_key
1068            && self.resumption_psk == other.resumption_psk
1069    }
1070}
1071
1072impl EpochSecrets {
1073    /// Get the sender_data secret.
1074    #[cfg(any(feature = "test-utils", test))]
1075    pub(crate) fn sender_data_secret(&self) -> &SenderDataSecret {
1076        &self.sender_data_secret
1077    }
1078
1079    /// Get the confirmation key.
1080    pub(crate) fn confirmation_key(&self) -> &ConfirmationKey {
1081        &self.confirmation_key
1082    }
1083
1084    /// Epoch authenticator
1085    #[cfg(any(feature = "test-utils", test))]
1086    pub(crate) fn epoch_authenticator(&self) -> &EpochAuthenticator {
1087        &self.epoch_authenticator
1088    }
1089
1090    /// Exporter secret
1091    #[cfg(any(feature = "test-utils", test))]
1092    pub(crate) fn exporter_secret(&self) -> &ExporterSecret {
1093        &self.exporter_secret
1094    }
1095
1096    /// Membership key
1097    #[cfg(any(feature = "test-utils", test))]
1098    pub(crate) fn membership_key(&self) -> &MembershipKey {
1099        &self.membership_key
1100    }
1101
1102    /// External secret
1103    pub(crate) fn external_secret(&self) -> &ExternalSecret {
1104        &self.external_secret
1105    }
1106
1107    /// External secret
1108    #[cfg(any(feature = "test-utils", test))]
1109    pub(crate) fn resumption_psk(&self) -> &ResumptionPskSecret {
1110        &self.resumption_psk
1111    }
1112
1113    /// Init secret
1114    #[cfg(any(feature = "test-utils", test))]
1115    pub(crate) fn init_secret(&self) -> &InitSecret {
1116        &self.init_secret
1117    }
1118
1119    /// Encryption secret
1120    #[cfg(any(feature = "test-utils", test))]
1121    pub(crate) fn encryption_secret(&self) -> &EncryptionSecret {
1122        &self.encryption_secret
1123    }
1124
1125    /// Derive `EpochSecrets` from an `EpochSecret`.
1126    /// If the `with_init_secret` argument is `true`, the init secret is derived and
1127    /// part of the `EpochSecrets`. Otherwise not.
1128    fn new(
1129        crypto: &impl OpenMlsCrypto,
1130        ciphersuite: Ciphersuite,
1131        epoch_secret: EpochSecret,
1132    ) -> Result<Self, CryptoError> {
1133        log::debug!(
1134            "Computing EpochSecrets from epoch secret with {}",
1135            ciphersuite
1136        );
1137        log_crypto!(
1138            trace,
1139            "  epoch_secret: {:x?}",
1140            epoch_secret.secret.as_slice()
1141        );
1142        let sender_data_secret = SenderDataSecret::new(crypto, ciphersuite, &epoch_secret)?;
1143        let encryption_secret = EncryptionSecret::new(crypto, ciphersuite, &epoch_secret)?;
1144        let exporter_secret = ExporterSecret::new(crypto, ciphersuite, &epoch_secret)?;
1145        let epoch_authenticator = EpochAuthenticator::new(crypto, ciphersuite, &epoch_secret)?;
1146        let external_secret = ExternalSecret::new(crypto, ciphersuite, &epoch_secret)?;
1147        let confirmation_key = ConfirmationKey::new(crypto, ciphersuite, &epoch_secret)?;
1148        let membership_key = MembershipKey::new(crypto, ciphersuite, &epoch_secret)?;
1149        let resumption_psk = ResumptionPskSecret::new(crypto, ciphersuite, &epoch_secret)?;
1150
1151        log::trace!("  Computing init secret.");
1152        let init_secret = InitSecret::new(crypto, ciphersuite, epoch_secret)?;
1153
1154        Ok(EpochSecrets {
1155            init_secret,
1156            sender_data_secret,
1157            encryption_secret,
1158            exporter_secret,
1159            epoch_authenticator,
1160            external_secret,
1161            confirmation_key,
1162            membership_key,
1163            resumption_psk,
1164        })
1165    }
1166
1167    /// This function initializes the `EpochSecrets` from an all-zero
1168    /// epoch-secret with the exception of the `init_secret`, which is populated
1169    /// with the given `InitSecret`. This is meant to be used in the case of an
1170    /// external init.
1171    pub(crate) fn with_init_secret(
1172        crypto: &impl OpenMlsCrypto,
1173        ciphersuite: Ciphersuite,
1174        init_secret: InitSecret,
1175    ) -> Result<Self, CryptoError> {
1176        let epoch_secret = EpochSecret {
1177            secret: Secret::zero(ciphersuite),
1178        };
1179        let mut epoch_secrets = Self::new(crypto, ciphersuite, epoch_secret)?;
1180        epoch_secrets.init_secret = init_secret;
1181        Ok(epoch_secrets)
1182    }
1183
1184    /// Splits `EpochSecrets` into two different categories:
1185    ///  - [`GroupEpochSecrets`]: These secrets are only used within the same epoch
1186    ///  - [`MessageSecrets`]: These secrets are potentially also used for past epochs
1187    ///    to decrypt and validate messages
1188    pub(crate) fn split_secrets(
1189        self,
1190        serialized_context: Vec<u8>,
1191        treesize: TreeSize,
1192        own_index: LeafNodeIndex,
1193    ) -> (GroupEpochSecrets, MessageSecrets) {
1194        let secret_tree = self
1195            .encryption_secret
1196            .create_secret_tree(treesize, own_index);
1197        (
1198            GroupEpochSecrets {
1199                init_secret: self.init_secret,
1200                exporter_secret: self.exporter_secret,
1201                epoch_authenticator: self.epoch_authenticator,
1202                external_secret: self.external_secret,
1203                resumption_psk: self.resumption_psk,
1204            },
1205            MessageSecrets::new(
1206                self.sender_data_secret,
1207                self.membership_key,
1208                self.confirmation_key,
1209                serialized_context,
1210                secret_tree,
1211            ),
1212        )
1213    }
1214}
1215
1216#[derive(Serialize, Deserialize)]
1217#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1218pub(crate) struct GroupEpochSecrets {
1219    init_secret: InitSecret,
1220    exporter_secret: ExporterSecret,
1221    epoch_authenticator: EpochAuthenticator,
1222    external_secret: ExternalSecret,
1223    resumption_psk: ResumptionPskSecret,
1224}
1225
1226impl std::fmt::Debug for GroupEpochSecrets {
1227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1228        f.write_str("GroupEpochSecrets { *** }")
1229    }
1230}
1231
1232#[cfg(not(any(test, feature = "test-utils")))]
1233impl PartialEq for GroupEpochSecrets {
1234    fn eq(&self, _other: &Self) -> bool {
1235        false
1236    }
1237}
1238
1239impl GroupEpochSecrets {
1240    /// Init secret
1241    pub(crate) fn init_secret(&self) -> &InitSecret {
1242        &self.init_secret
1243    }
1244
1245    /// Epoch authenticator
1246    pub(crate) fn epoch_authenticator(&self) -> &EpochAuthenticator {
1247        &self.epoch_authenticator
1248    }
1249
1250    /// Exporter secret
1251    pub(crate) fn exporter_secret(&self) -> &ExporterSecret {
1252        &self.exporter_secret
1253    }
1254
1255    /// External secret
1256    pub(crate) fn external_secret(&self) -> &ExternalSecret {
1257        &self.external_secret
1258    }
1259
1260    /// External secret
1261    pub(crate) fn resumption_psk(&self) -> &ResumptionPskSecret {
1262        &self.resumption_psk
1263    }
1264}