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!("Adding context to key schedule. {serialized_group_context:?}");
469        if self.state != State::Initial || self.intermediate_secret.is_none() {
470            log::error!(
471                "Trying to add context to the key schedule while not in the initial state."
472            );
473            return Err(KeyScheduleError::InvalidState(ErrorState::Init));
474        }
475        self.state = State::Context;
476
477        // We can return a library error here, because there must be a mistake in the state machine
478        let intermediate_secret = self
479            .intermediate_secret
480            .take()
481            .ok_or_else(|| LibraryError::custom("state machine error"))?;
482
483        log_crypto!(
484            trace,
485            "  intermediate_secret: {:x?}",
486            intermediate_secret.secret.as_slice()
487        );
488
489        self.epoch_secret = Some(EpochSecret::new(
490            self.ciphersuite,
491            crypto,
492            intermediate_secret,
493            serialized_group_context,
494        )?);
495        self.intermediate_secret = None;
496        Ok(())
497    }
498
499    /// Derive the epoch secrets.
500    /// If the `with_init_secret` argument is `true`, the init secret is derived and
501    /// part of the `EpochSecrets`. Otherwise not.
502    pub(crate) fn epoch_secrets(
503        &mut self,
504        crypto: &impl OpenMlsCrypto,
505        ciphersuite: Ciphersuite,
506    ) -> Result<EpochSecrets, KeyScheduleError> {
507        if self.state != State::Context || self.epoch_secret.is_none() {
508            log::error!("Trying to derive the epoch secrets while not in the right state.");
509            return Err(KeyScheduleError::InvalidState(ErrorState::Context));
510        }
511        self.state = State::Done;
512
513        let epoch_secret = match self.epoch_secret.take() {
514            Some(epoch_secret) => epoch_secret,
515            // We can return a library error here, because there must be a mistake in the state machine
516            None => return Err(LibraryError::custom("state machine error").into()),
517        };
518
519        Ok(EpochSecrets::new(crypto, ciphersuite, epoch_secret)?)
520    }
521}
522
523/// The intermediate secret includes the optional PSK and is used to later
524/// derive the welcome secret and epoch secret
525struct IntermediateSecret {
526    secret: Secret,
527}
528
529impl IntermediateSecret {
530    /// Derive an `IntermediateSecret` from a `JoinerSecret` and an optional
531    /// PSK.
532    fn new(
533        crypto: &impl OpenMlsCrypto,
534        ciphersuite: Ciphersuite,
535        joiner_secret: &JoinerSecret,
536        psk: PskSecret,
537    ) -> Result<Self, CryptoError> {
538        log_crypto!(trace, "PSK input: {:x?}", psk.as_slice());
539        let secret = joiner_secret
540            .secret
541            .hkdf_extract(crypto, ciphersuite, psk.secret())?;
542        log_crypto!(trace, "Intermediate secret: {:x?}", secret);
543        Ok(Self { secret })
544    }
545}
546
547pub(crate) struct WelcomeSecret {
548    secret: Secret,
549}
550
551impl WelcomeSecret {
552    /// Derive a `WelcomeSecret` from to decrypt a `Welcome` message.
553    fn new(
554        crypto: &impl OpenMlsCrypto,
555        ciphersuite: Ciphersuite,
556        intermediate_secret: &IntermediateSecret,
557    ) -> Result<Self, CryptoError> {
558        let secret = intermediate_secret
559            .secret
560            .derive_secret(crypto, ciphersuite, "welcome")?;
561        log_crypto!(trace, "Welcome secret: {:x?}", secret);
562        Ok(WelcomeSecret { secret })
563    }
564
565    /// Derive an `AeadKey` and an `AeadNonce` from the `WelcomeSecret`,
566    /// consuming it in the process.
567    pub(crate) fn derive_welcome_key_nonce(
568        self,
569        crypto: &impl OpenMlsCrypto,
570        ciphersuite: Ciphersuite,
571    ) -> Result<(AeadKey, AeadNonce), CryptoError> {
572        let welcome_nonce = self.derive_aead_nonce(crypto, ciphersuite)?;
573        let welcome_key = self.derive_aead_key(crypto, ciphersuite)?;
574        Ok((welcome_key, welcome_nonce))
575    }
576
577    /// Derive a new AEAD key from a `WelcomeSecret`.
578    fn derive_aead_key(
579        &self,
580        crypto: &impl OpenMlsCrypto,
581        ciphersuite: Ciphersuite,
582    ) -> Result<AeadKey, CryptoError> {
583        log::trace!("WelcomeSecret.derive_aead_key with {ciphersuite}");
584        let aead_secret = self.secret.kdf_expand_label(
585            crypto,
586            ciphersuite,
587            "key",
588            b"",
589            ciphersuite.aead_key_length(),
590        )?;
591        Ok(AeadKey::from_secret(aead_secret, ciphersuite))
592    }
593
594    /// Derive a new AEAD nonce from a `WelcomeSecret`.
595    fn derive_aead_nonce(
596        &self,
597        crypto: &impl OpenMlsCrypto,
598        ciphersuite: Ciphersuite,
599    ) -> Result<AeadNonce, CryptoError> {
600        let nonce_secret = self.secret.kdf_expand_label(
601            crypto,
602            ciphersuite,
603            "nonce",
604            b"",
605            ciphersuite.aead_nonce_length(),
606        )?;
607        Ok(AeadNonce::from_secret(nonce_secret))
608    }
609
610    #[cfg(any(feature = "test-utils", test))]
611    pub(crate) fn as_slice(&self) -> &[u8] {
612        self.secret.as_slice()
613    }
614}
615
616/// An intermediate secret in the key schedule, the `EpochSecret` is used to
617/// create an `EpochSecrets` object and is finally consumed when creating that
618/// epoch's `InitSecret`.
619struct EpochSecret {
620    secret: Secret,
621}
622
623impl EpochSecret {
624    /// Derive an `EpochSecret` from a `JoinerSecret`
625    fn new(
626        ciphersuite: Ciphersuite,
627        crypto: &impl OpenMlsCrypto,
628        intermediate_secret: IntermediateSecret,
629        serialized_group_context: &[u8],
630    ) -> Result<Self, CryptoError> {
631        let secret = intermediate_secret.secret.kdf_expand_label(
632            crypto,
633            ciphersuite,
634            "epoch",
635            serialized_group_context,
636            ciphersuite.hash_length(),
637        )?;
638        log_crypto!(trace, "Epoch secret: {:x?}", secret);
639        Ok(EpochSecret { secret })
640    }
641}
642
643/// The `EncryptionSecret` is used to create a `SecretTree`.
644#[cfg_attr(test, derive(Clone))]
645pub(crate) struct EncryptionSecret {
646    secret: Secret,
647}
648
649impl EncryptionSecret {
650    /// Derive an encryption secret from a reference to an `EpochSecret`.
651    fn new(
652        crypto: &impl OpenMlsCrypto,
653        ciphersuite: Ciphersuite,
654        epoch_secret: &EpochSecret,
655    ) -> Result<Self, CryptoError> {
656        Ok(EncryptionSecret {
657            secret: epoch_secret
658                .secret
659                .derive_secret(crypto, ciphersuite, "encryption")?,
660        })
661    }
662
663    /// Create a `SecretTree` from the `encryption_secret` contained in the
664    /// `EpochSecrets`. The `encryption_secret` is consumed, allowing us to achieve FS.
665    pub(crate) fn create_secret_tree(
666        self,
667        treesize: TreeSize,
668        own_index: LeafNodeIndex,
669    ) -> SecretTree {
670        SecretTree::new(self, treesize, own_index)
671    }
672
673    pub(crate) fn consume_secret(self) -> Secret {
674        self.secret
675    }
676
677    /// Create a random `EncryptionSecret`. For testing purposes only.
678    #[cfg(test)]
679    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
680        EncryptionSecret {
681            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
682        }
683    }
684
685    #[cfg(any(feature = "test-utils", test))]
686    pub(crate) fn as_slice(&self) -> &[u8] {
687        self.secret.as_slice()
688    }
689
690    #[cfg(any(feature = "test-utils", test))]
691    /// Create a new secret from a byte vector.
692    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
693        Self {
694            secret: Secret::from_slice(bytes),
695        }
696    }
697}
698
699/// A secret that we can derive secrets from, that are used outside of OpenMLS.
700#[derive(Debug, Serialize, Deserialize)]
701#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
702pub(crate) struct ExporterSecret {
703    secret: Secret,
704}
705
706impl ExporterSecret {
707    /// Derive an `ExporterSecret` from an `EpochSecret`.
708    fn new(
709        crypto: &impl OpenMlsCrypto,
710        ciphersuite: Ciphersuite,
711        epoch_secret: &EpochSecret,
712    ) -> Result<Self, CryptoError> {
713        let secret = epoch_secret
714            .secret
715            .derive_secret(crypto, ciphersuite, "exporter")?;
716        Ok(ExporterSecret { secret })
717    }
718
719    #[cfg(any(feature = "test-utils", test))]
720    pub(crate) fn as_slice(&self) -> &[u8] {
721        self.secret.as_slice()
722    }
723
724    /// Derive a `Secret` from the exporter secret. We return `Vec<u8>` here, so
725    /// it can be used outside of OpenMLS. This function is made available for
726    /// use from the outside through [`MlsGroup::export_secret`].
727    pub(crate) fn derive_exported_secret(
728        &self,
729        ciphersuite: Ciphersuite,
730        crypto: &impl OpenMlsCrypto,
731        label: &str,
732        context: &[u8],
733        key_length: usize,
734    ) -> Result<Vec<u8>, CryptoError> {
735        let context_hash = &crypto.hash(ciphersuite.hash_algorithm(), context)?;
736        Ok(self
737            .secret
738            .derive_secret(crypto, ciphersuite, label)?
739            .kdf_expand_label(crypto, ciphersuite, "exported", context_hash, key_length)?
740            .as_slice()
741            .to_vec())
742    }
743}
744
745/// A secret used when joining a group with an external Commit.
746#[derive(Debug, Serialize, Deserialize)]
747#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
748pub(crate) struct ExternalSecret {
749    secret: Secret,
750}
751
752impl ExternalSecret {
753    /// Derive an `ExternalSecret` from an `EpochSecret`.
754    fn new(
755        crypto: &impl OpenMlsCrypto,
756        ciphersuite: Ciphersuite,
757        epoch_secret: &EpochSecret,
758    ) -> Result<Self, CryptoError> {
759        let secret = epoch_secret
760            .secret
761            .derive_secret(crypto, ciphersuite, "external")?;
762        Ok(Self { secret })
763    }
764
765    /// Derive the external keypair for External Commits
766    pub(crate) fn derive_external_keypair(
767        &self,
768        crypto: &impl OpenMlsCrypto,
769        ciphersuite: Ciphersuite,
770    ) -> Result<HpkeKeyPair, CryptoError> {
771        crypto.derive_hpke_keypair(ciphersuite.hpke_config(), self.secret.as_slice())
772    }
773
774    #[cfg(any(feature = "test-utils", test))]
775    pub(crate) fn as_slice(&self) -> &[u8] {
776        self.secret.as_slice()
777    }
778}
779
780/// The confirmation key is used to calculate the `ConfirmationTag`.
781#[derive(Debug, Serialize, Deserialize)]
782#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
783pub(crate) struct ConfirmationKey {
784    secret: Secret,
785}
786
787impl ConfirmationKey {
788    /// Derive an `ConfirmationKey` from an `EpochSecret`.
789    fn new(
790        crypto: &impl OpenMlsCrypto,
791        ciphersuite: Ciphersuite,
792        epoch_secret: &EpochSecret,
793    ) -> Result<Self, CryptoError> {
794        log::debug!("Computing confirmation key.");
795        log_crypto!(
796            trace,
797            "  epoch_secret {:x?}",
798            epoch_secret.secret.as_slice()
799        );
800        let secret = epoch_secret
801            .secret
802            .derive_secret(crypto, ciphersuite, "confirm")?;
803        Ok(Self { secret })
804    }
805
806    /// Create a new confirmation tag.
807    ///
808    /// >  11.2. Commit
809    ///
810    /// ```text
811    /// PublicMessage.confirmation_tag =
812    ///     MAC(confirmation_key, GroupContext.confirmed_transcript_hash)
813    /// ```
814    pub(crate) fn tag(
815        &self,
816        crypto: &impl OpenMlsCrypto,
817        ciphersuite: Ciphersuite,
818        confirmed_transcript_hash: &[u8],
819    ) -> Result<ConfirmationTag, CryptoError> {
820        log::debug!("Computing confirmation tag.");
821        log_crypto!(trace, "  confirmation key {:x?}", self.secret.as_slice());
822        log_crypto!(trace, "  transcript hash  {:x?}", confirmed_transcript_hash);
823        Ok(ConfirmationTag(Mac::new(
824            crypto,
825            ciphersuite,
826            &self.secret,
827            confirmed_transcript_hash,
828        )?))
829    }
830}
831
832#[cfg(test)]
833impl ConfirmationKey {
834    pub(crate) fn from_secret(secret: Secret) -> Self {
835        Self { secret }
836    }
837}
838
839#[cfg(any(feature = "test-utils", test))]
840impl ConfirmationKey {
841    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
842        Self {
843            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
844        }
845    }
846
847    pub(crate) fn as_slice(&self) -> &[u8] {
848        self.secret.as_slice()
849    }
850}
851
852/// The membership key is used to calculate the `MembershipTag`.
853#[derive(Debug, Serialize, Deserialize)]
854#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
855pub(crate) struct MembershipKey {
856    secret: Secret,
857}
858
859impl MembershipKey {
860    /// Derive an `MembershipKey` from an `EpochSecret`.
861    fn new(
862        crypto: &impl OpenMlsCrypto,
863        ciphersuite: Ciphersuite,
864        epoch_secret: &EpochSecret,
865    ) -> Result<Self, CryptoError> {
866        let secret = epoch_secret
867            .secret
868            .derive_secret(crypto, ciphersuite, "membership")?;
869        Ok(Self { secret })
870    }
871
872    /// Create a new membership tag.
873    ///
874    /// 9.1 Content Authentication
875    ///
876    /// ```text
877    /// membership_tag = MAC(membership_key, MLSPlaintextTBM);
878    /// ```
879    pub(crate) fn tag_message(
880        &self,
881        crypto: &impl OpenMlsCrypto,
882        ciphersuite: Ciphersuite,
883        tbm_payload: AuthenticatedContentTbm,
884    ) -> Result<MembershipTag, LibraryError> {
885        Ok(MembershipTag(
886            Mac::new(
887                crypto,
888                ciphersuite,
889                &self.secret,
890                &tbm_payload
891                    .into_bytes()
892                    .map_err(LibraryError::missing_bound_check)?,
893            )
894            .map_err(LibraryError::unexpected_crypto_error)?,
895        ))
896    }
897
898    #[cfg(any(feature = "test-utils", test))]
899    pub(crate) fn from_secret(secret: Secret) -> Self {
900        Self { secret }
901    }
902
903    #[cfg(any(feature = "test-utils", test))]
904    pub(crate) fn as_slice(&self) -> &[u8] {
905        self.secret.as_slice()
906    }
907
908    #[cfg(any(feature = "test-utils", test))]
909    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
910        Self {
911            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
912        }
913    }
914}
915
916// Get a ciphertext sample of `hash_length` from the ciphertext.
917fn ciphertext_sample(ciphersuite: Ciphersuite, ciphertext: &[u8]) -> &[u8] {
918    let sample_length = ciphersuite.hash_length();
919    log::debug!("Getting ciphertext sample of length {sample_length:?}");
920    if ciphertext.len() <= sample_length {
921        ciphertext
922    } else {
923        &ciphertext[0..sample_length]
924    }
925}
926
927/// A key that can be used to derive an `AeadKey` and an `AeadNonce`.
928#[derive(Serialize, Deserialize)]
929#[cfg_attr(
930    any(feature = "test-utils", feature = "crypto-debug", test),
931    derive(Debug, Clone, PartialEq)
932)]
933pub(crate) struct SenderDataSecret {
934    secret: Secret,
935}
936
937impl SenderDataSecret {
938    /// Derive an `ExporterSecret` from an `EpochSecret`.
939    fn new(
940        crypto: &impl OpenMlsCrypto,
941        ciphersuite: Ciphersuite,
942        epoch_secret: &EpochSecret,
943    ) -> Result<Self, CryptoError> {
944        let secret = epoch_secret
945            .secret
946            .derive_secret(crypto, ciphersuite, "sender data")?;
947        Ok(SenderDataSecret { secret })
948    }
949
950    /// Derive a new AEAD key from a `SenderDataSecret`.
951    pub(crate) fn derive_aead_key(
952        &self,
953        crypto: &impl OpenMlsCrypto,
954        ciphersuite: Ciphersuite,
955        ciphertext: &[u8],
956    ) -> Result<AeadKey, CryptoError> {
957        let ciphertext_sample = ciphertext_sample(ciphersuite, ciphertext);
958        log::debug!("SenderDataSecret::derive_aead_key ciphertext sample: {ciphertext_sample:x?}");
959        let secret = self.secret.kdf_expand_label(
960            crypto,
961            ciphersuite,
962            "key",
963            ciphertext_sample,
964            ciphersuite.aead_key_length(),
965        )?;
966        Ok(AeadKey::from_secret(secret, ciphersuite))
967    }
968
969    /// Derive a new AEAD nonce from a `SenderDataSecret`.
970    pub(crate) fn derive_aead_nonce(
971        &self,
972        ciphersuite: Ciphersuite,
973        crypto: &impl OpenMlsCrypto,
974        ciphertext: &[u8],
975    ) -> Result<AeadNonce, CryptoError> {
976        let ciphertext_sample = ciphertext_sample(ciphersuite, ciphertext);
977        log::debug!(
978            "SenderDataSecret::derive_aead_nonce ciphertext sample: {ciphertext_sample:x?}"
979        );
980        let nonce_secret = self.secret.kdf_expand_label(
981            crypto,
982            ciphersuite,
983            "nonce",
984            ciphertext_sample,
985            ciphersuite.aead_nonce_length(),
986        )?;
987        Ok(AeadNonce::from_secret(nonce_secret))
988    }
989
990    #[cfg(any(feature = "test-utils", test))]
991    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
992        Self {
993            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
994        }
995    }
996
997    #[cfg(any(feature = "test-utils", test))]
998    pub(crate) fn as_slice(&self) -> &[u8] {
999        self.secret.as_slice()
1000    }
1001
1002    #[cfg(any(feature = "test-utils", test))]
1003    /// Create a new secret from a byte vector.
1004    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
1005        Self {
1006            secret: Secret::from_slice(bytes),
1007        }
1008    }
1009}
1010
1011/// The `EpochSecrets` contain keys (or secrets), which are accessible outside
1012/// of the `KeySchedule` and which don't get consumed immediately upon first
1013/// use.
1014///
1015/// | Secret                  | Label           |
1016/// |:------------------------|:----------------|
1017/// | `init_secret`           | "init"          |
1018/// | `sender_data_secret`    | "sender data"   |
1019/// | `encryption_secret`     | "encryption"    |
1020/// | `exporter_secret`       | "exporter"      |
1021/// | `epoch_authenticator`   | "authentication"|
1022/// | `external_secret`       | "external"      |
1023/// | `confirmation_key`      | "confirm"       |
1024/// | `membership_key`        | "membership"    |
1025/// | `resumption_psk`        | "resumption"    |
1026pub(crate) struct EpochSecrets {
1027    init_secret: InitSecret,
1028    sender_data_secret: SenderDataSecret,
1029    encryption_secret: EncryptionSecret,
1030    exporter_secret: ExporterSecret,
1031    epoch_authenticator: EpochAuthenticator,
1032    external_secret: ExternalSecret,
1033    confirmation_key: ConfirmationKey,
1034    membership_key: MembershipKey,
1035    resumption_psk: ResumptionPskSecret,
1036}
1037
1038impl std::fmt::Debug for EpochSecrets {
1039    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1040        f.write_str("EpochSecrets { *** }")
1041    }
1042}
1043
1044#[cfg(not(test))]
1045impl PartialEq for EpochSecrets {
1046    fn eq(&self, _other: &Self) -> bool {
1047        false
1048    }
1049}
1050
1051// In tests we allow comparing secrets.
1052#[cfg(test)]
1053impl PartialEq for EpochSecrets {
1054    fn eq(&self, other: &Self) -> bool {
1055        self.sender_data_secret == other.sender_data_secret
1056            && self.exporter_secret == other.exporter_secret
1057            && self.epoch_authenticator == other.epoch_authenticator
1058            && self.external_secret == other.external_secret
1059            && self.confirmation_key == other.confirmation_key
1060            && self.membership_key == other.membership_key
1061            && self.resumption_psk == other.resumption_psk
1062    }
1063}
1064
1065impl EpochSecrets {
1066    /// Get the sender_data secret.
1067    #[cfg(any(feature = "test-utils", test))]
1068    pub(crate) fn sender_data_secret(&self) -> &SenderDataSecret {
1069        &self.sender_data_secret
1070    }
1071
1072    /// Get the confirmation key.
1073    pub(crate) fn confirmation_key(&self) -> &ConfirmationKey {
1074        &self.confirmation_key
1075    }
1076
1077    /// Epoch authenticator
1078    #[cfg(any(feature = "test-utils", test))]
1079    pub(crate) fn epoch_authenticator(&self) -> &EpochAuthenticator {
1080        &self.epoch_authenticator
1081    }
1082
1083    /// Exporter secret
1084    #[cfg(any(feature = "test-utils", test))]
1085    pub(crate) fn exporter_secret(&self) -> &ExporterSecret {
1086        &self.exporter_secret
1087    }
1088
1089    /// Membership key
1090    #[cfg(any(feature = "test-utils", test))]
1091    pub(crate) fn membership_key(&self) -> &MembershipKey {
1092        &self.membership_key
1093    }
1094
1095    /// External secret
1096    pub(crate) fn external_secret(&self) -> &ExternalSecret {
1097        &self.external_secret
1098    }
1099
1100    /// External secret
1101    #[cfg(any(feature = "test-utils", test))]
1102    pub(crate) fn resumption_psk(&self) -> &ResumptionPskSecret {
1103        &self.resumption_psk
1104    }
1105
1106    /// Init secret
1107    #[cfg(any(feature = "test-utils", test))]
1108    pub(crate) fn init_secret(&self) -> &InitSecret {
1109        &self.init_secret
1110    }
1111
1112    /// Encryption secret
1113    #[cfg(any(feature = "test-utils", test))]
1114    pub(crate) fn encryption_secret(&self) -> &EncryptionSecret {
1115        &self.encryption_secret
1116    }
1117
1118    /// Derive `EpochSecrets` from an `EpochSecret`.
1119    /// If the `with_init_secret` argument is `true`, the init secret is derived and
1120    /// part of the `EpochSecrets`. Otherwise not.
1121    fn new(
1122        crypto: &impl OpenMlsCrypto,
1123        ciphersuite: Ciphersuite,
1124        epoch_secret: EpochSecret,
1125    ) -> Result<Self, CryptoError> {
1126        log::debug!("Computing EpochSecrets from epoch secret with {ciphersuite}");
1127        log_crypto!(
1128            trace,
1129            "  epoch_secret: {:x?}",
1130            epoch_secret.secret.as_slice()
1131        );
1132        let sender_data_secret = SenderDataSecret::new(crypto, ciphersuite, &epoch_secret)?;
1133        let encryption_secret = EncryptionSecret::new(crypto, ciphersuite, &epoch_secret)?;
1134        let exporter_secret = ExporterSecret::new(crypto, ciphersuite, &epoch_secret)?;
1135        let epoch_authenticator = EpochAuthenticator::new(crypto, ciphersuite, &epoch_secret)?;
1136        let external_secret = ExternalSecret::new(crypto, ciphersuite, &epoch_secret)?;
1137        let confirmation_key = ConfirmationKey::new(crypto, ciphersuite, &epoch_secret)?;
1138        let membership_key = MembershipKey::new(crypto, ciphersuite, &epoch_secret)?;
1139        let resumption_psk = ResumptionPskSecret::new(crypto, ciphersuite, &epoch_secret)?;
1140
1141        log::trace!("  Computing init secret.");
1142        let init_secret = InitSecret::new(crypto, ciphersuite, epoch_secret)?;
1143
1144        Ok(EpochSecrets {
1145            init_secret,
1146            sender_data_secret,
1147            encryption_secret,
1148            exporter_secret,
1149            epoch_authenticator,
1150            external_secret,
1151            confirmation_key,
1152            membership_key,
1153            resumption_psk,
1154        })
1155    }
1156
1157    /// This function initializes the `EpochSecrets` from an all-zero
1158    /// epoch-secret with the exception of the `init_secret`, which is populated
1159    /// with the given `InitSecret`. This is meant to be used in the case of an
1160    /// external init.
1161    pub(crate) fn with_init_secret(
1162        crypto: &impl OpenMlsCrypto,
1163        ciphersuite: Ciphersuite,
1164        init_secret: InitSecret,
1165    ) -> Result<Self, CryptoError> {
1166        let epoch_secret = EpochSecret {
1167            secret: Secret::zero(ciphersuite),
1168        };
1169        let mut epoch_secrets = Self::new(crypto, ciphersuite, epoch_secret)?;
1170        epoch_secrets.init_secret = init_secret;
1171        Ok(epoch_secrets)
1172    }
1173
1174    /// Splits `EpochSecrets` into two different categories:
1175    ///  - [`GroupEpochSecrets`]: These secrets are only used within the same epoch
1176    ///  - [`MessageSecrets`]: These secrets are potentially also used for past epochs
1177    ///    to decrypt and validate messages
1178    pub(crate) fn split_secrets(
1179        self,
1180        serialized_context: Vec<u8>,
1181        treesize: TreeSize,
1182        own_index: LeafNodeIndex,
1183    ) -> (GroupEpochSecrets, MessageSecrets) {
1184        let secret_tree = self
1185            .encryption_secret
1186            .create_secret_tree(treesize, own_index);
1187        (
1188            GroupEpochSecrets {
1189                init_secret: self.init_secret,
1190                exporter_secret: self.exporter_secret,
1191                epoch_authenticator: self.epoch_authenticator,
1192                external_secret: self.external_secret,
1193                resumption_psk: self.resumption_psk,
1194            },
1195            MessageSecrets::new(
1196                self.sender_data_secret,
1197                self.membership_key,
1198                self.confirmation_key,
1199                serialized_context,
1200                secret_tree,
1201            ),
1202        )
1203    }
1204}
1205
1206#[derive(Serialize, Deserialize)]
1207#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1208pub(crate) struct GroupEpochSecrets {
1209    init_secret: InitSecret,
1210    exporter_secret: ExporterSecret,
1211    epoch_authenticator: EpochAuthenticator,
1212    external_secret: ExternalSecret,
1213    resumption_psk: ResumptionPskSecret,
1214}
1215
1216impl std::fmt::Debug for GroupEpochSecrets {
1217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1218        f.write_str("GroupEpochSecrets { *** }")
1219    }
1220}
1221
1222#[cfg(not(any(test, feature = "test-utils")))]
1223impl PartialEq for GroupEpochSecrets {
1224    fn eq(&self, _other: &Self) -> bool {
1225        false
1226    }
1227}
1228
1229impl GroupEpochSecrets {
1230    /// Init secret
1231    pub(crate) fn init_secret(&self) -> &InitSecret {
1232        &self.init_secret
1233    }
1234
1235    /// Epoch authenticator
1236    pub(crate) fn epoch_authenticator(&self) -> &EpochAuthenticator {
1237        &self.epoch_authenticator
1238    }
1239
1240    /// Exporter secret
1241    pub(crate) fn exporter_secret(&self) -> &ExporterSecret {
1242        &self.exporter_secret
1243    }
1244
1245    /// External secret
1246    pub(crate) fn external_secret(&self) -> &ExternalSecret {
1247        &self.external_secret
1248    }
1249
1250    /// External secret
1251    pub(crate) fn resumption_psk(&self) -> &ResumptionPskSecret {
1252        &self.resumption_psk
1253    }
1254}