Skip to main content

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