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