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-08")]
136mod pprf;
137pub mod psk;
138
139#[cfg(feature = "extensions-draft-08")]
140pub use pprf::PprfError;
141
142// Crate
143#[cfg(feature = "extensions-draft-08")]
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))]
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-08")]
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-08")]
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    /// Derive a `Secret` from the exporter secret. We return `Vec<u8>` here, so
744    /// it can be used outside of OpenMLS. This function is made available for
745    /// use from the outside through [`MlsGroup::export_secret`].
746    pub(crate) fn derive_exported_secret(
747        &self,
748        ciphersuite: Ciphersuite,
749        crypto: &impl OpenMlsCrypto,
750        label: &str,
751        context: &[u8],
752        key_length: usize,
753    ) -> Result<Vec<u8>, CryptoError> {
754        let context_hash = &crypto.hash(ciphersuite.hash_algorithm(), context)?;
755        Ok(self
756            .secret
757            .derive_secret(crypto, ciphersuite, label)?
758            .kdf_expand_label(crypto, ciphersuite, "exported", context_hash, key_length)?
759            .as_slice()
760            .to_vec())
761    }
762}
763
764/// A secret that we can derive secrets from, that are used outside of OpenMLS.
765/// In contrast to `[ExporterSecret]`, the `[ApplicationExportSecret]` is not
766/// persisted. It can be deleted after use to achieve forward secrecy.
767#[cfg(feature = "extensions-draft-08")]
768#[derive(Debug, Serialize, Deserialize, Clone)]
769#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq))]
770pub struct ApplicationExportSecret {
771    secret: Secret,
772}
773
774#[cfg(feature = "extensions-draft-08")]
775impl ApplicationExportSecret {
776    /// Derive an `ExporterSecret` from an `EpochSecret`.
777    fn new(
778        crypto: &impl OpenMlsCrypto,
779        ciphersuite: Ciphersuite,
780        epoch_secret: &EpochSecret,
781    ) -> Result<Self, CryptoError> {
782        let secret =
783            epoch_secret
784                .secret
785                .derive_secret(crypto, ciphersuite, "application_export")?;
786        Ok(ApplicationExportSecret { secret })
787    }
788}
789
790/// A secret used when joining a group with an external Commit.
791#[derive(Debug, Serialize, Deserialize)]
792#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
793pub(crate) struct ExternalSecret {
794    secret: Secret,
795}
796
797impl ExternalSecret {
798    /// Derive an `ExternalSecret` from an `EpochSecret`.
799    fn new(
800        crypto: &impl OpenMlsCrypto,
801        ciphersuite: Ciphersuite,
802        epoch_secret: &EpochSecret,
803    ) -> Result<Self, CryptoError> {
804        let secret = epoch_secret
805            .secret
806            .derive_secret(crypto, ciphersuite, "external")?;
807        Ok(Self { secret })
808    }
809
810    /// Derive the external keypair for External Commits
811    pub(crate) fn derive_external_keypair(
812        &self,
813        crypto: &impl OpenMlsCrypto,
814        ciphersuite: Ciphersuite,
815    ) -> Result<HpkeKeyPair, CryptoError> {
816        crypto.derive_hpke_keypair(ciphersuite.hpke_config(), self.secret.as_slice())
817    }
818
819    #[cfg(any(feature = "test-utils", test))]
820    pub(crate) fn as_slice(&self) -> &[u8] {
821        self.secret.as_slice()
822    }
823}
824
825/// The confirmation key is used to calculate the `ConfirmationTag`.
826#[derive(Debug, Serialize, Deserialize)]
827#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
828pub(crate) struct ConfirmationKey {
829    secret: Secret,
830}
831
832impl ConfirmationKey {
833    /// Derive an `ConfirmationKey` from an `EpochSecret`.
834    fn new(
835        crypto: &impl OpenMlsCrypto,
836        ciphersuite: Ciphersuite,
837        epoch_secret: &EpochSecret,
838    ) -> Result<Self, CryptoError> {
839        log::debug!("Computing confirmation key.");
840        log_crypto!(
841            trace,
842            "  epoch_secret {:x?}",
843            epoch_secret.secret.as_slice()
844        );
845        let secret = epoch_secret
846            .secret
847            .derive_secret(crypto, ciphersuite, "confirm")?;
848        Ok(Self { secret })
849    }
850
851    /// Create a new confirmation tag.
852    ///
853    /// >  11.2. Commit
854    ///
855    /// ```text
856    /// PublicMessage.confirmation_tag =
857    ///     MAC(confirmation_key, GroupContext.confirmed_transcript_hash)
858    /// ```
859    pub(crate) fn tag(
860        &self,
861        crypto: &impl OpenMlsCrypto,
862        ciphersuite: Ciphersuite,
863        confirmed_transcript_hash: &[u8],
864    ) -> Result<ConfirmationTag, CryptoError> {
865        log::debug!("Computing confirmation tag.");
866        log_crypto!(trace, "  confirmation key {:x?}", self.secret.as_slice());
867        log_crypto!(trace, "  transcript hash  {:x?}", confirmed_transcript_hash);
868        Ok(ConfirmationTag(Mac::new(
869            crypto,
870            ciphersuite,
871            &self.secret,
872            confirmed_transcript_hash,
873        )?))
874    }
875}
876
877#[cfg(test)]
878impl ConfirmationKey {
879    pub(crate) fn from_secret(secret: Secret) -> Self {
880        Self { secret }
881    }
882}
883
884#[cfg(any(feature = "test-utils", test))]
885impl ConfirmationKey {
886    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
887        Self {
888            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
889        }
890    }
891
892    pub(crate) fn as_slice(&self) -> &[u8] {
893        self.secret.as_slice()
894    }
895}
896
897/// The membership key is used to calculate the `MembershipTag`.
898#[derive(Debug, Serialize, Deserialize)]
899#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
900pub(crate) struct MembershipKey {
901    secret: Secret,
902}
903
904impl MembershipKey {
905    /// Derive an `MembershipKey` from an `EpochSecret`.
906    fn new(
907        crypto: &impl OpenMlsCrypto,
908        ciphersuite: Ciphersuite,
909        epoch_secret: &EpochSecret,
910    ) -> Result<Self, CryptoError> {
911        let secret = epoch_secret
912            .secret
913            .derive_secret(crypto, ciphersuite, "membership")?;
914        Ok(Self { secret })
915    }
916
917    /// Create a new membership tag.
918    ///
919    /// 9.1 Content Authentication
920    ///
921    /// ```text
922    /// membership_tag = MAC(membership_key, MLSPlaintextTBM);
923    /// ```
924    pub(crate) fn tag_message(
925        &self,
926        crypto: &impl OpenMlsCrypto,
927        ciphersuite: Ciphersuite,
928        tbm_payload: AuthenticatedContentTbm,
929    ) -> Result<MembershipTag, LibraryError> {
930        Ok(MembershipTag(
931            Mac::new(
932                crypto,
933                ciphersuite,
934                &self.secret,
935                &tbm_payload
936                    .into_bytes()
937                    .map_err(LibraryError::missing_bound_check)?,
938            )
939            .map_err(LibraryError::unexpected_crypto_error)?,
940        ))
941    }
942
943    #[cfg(any(feature = "test-utils", test))]
944    pub(crate) fn from_secret(secret: Secret) -> Self {
945        Self { secret }
946    }
947
948    #[cfg(any(feature = "test-utils", test))]
949    pub(crate) fn as_slice(&self) -> &[u8] {
950        self.secret.as_slice()
951    }
952
953    #[cfg(any(feature = "test-utils", test))]
954    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
955        Self {
956            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
957        }
958    }
959}
960
961// Get a ciphertext sample of `hash_length` from the ciphertext.
962fn ciphertext_sample(ciphersuite: Ciphersuite, ciphertext: &[u8]) -> &[u8] {
963    let sample_length = ciphersuite.hash_length();
964    log::debug!("Getting ciphertext sample of length {sample_length:?}");
965    if ciphertext.len() <= sample_length {
966        ciphertext
967    } else {
968        &ciphertext[0..sample_length]
969    }
970}
971
972/// A key that can be used to derive an `AeadKey` and an `AeadNonce`.
973#[derive(Serialize, Deserialize)]
974#[cfg_attr(
975    any(feature = "test-utils", feature = "crypto-debug", test),
976    derive(Debug, Clone, PartialEq)
977)]
978pub(crate) struct SenderDataSecret {
979    secret: Secret,
980}
981
982impl SenderDataSecret {
983    /// Derive an `ExporterSecret` from an `EpochSecret`.
984    fn new(
985        crypto: &impl OpenMlsCrypto,
986        ciphersuite: Ciphersuite,
987        epoch_secret: &EpochSecret,
988    ) -> Result<Self, CryptoError> {
989        let secret = epoch_secret
990            .secret
991            .derive_secret(crypto, ciphersuite, "sender data")?;
992        Ok(SenderDataSecret { secret })
993    }
994
995    /// Derive a new AEAD key from a `SenderDataSecret`.
996    pub(crate) fn derive_aead_key(
997        &self,
998        crypto: &impl OpenMlsCrypto,
999        ciphersuite: Ciphersuite,
1000        ciphertext: &[u8],
1001    ) -> Result<AeadKey, CryptoError> {
1002        let ciphertext_sample = ciphertext_sample(ciphersuite, ciphertext);
1003        log::debug!("SenderDataSecret::derive_aead_key ciphertext sample: {ciphertext_sample:x?}");
1004        let secret = self.secret.kdf_expand_label(
1005            crypto,
1006            ciphersuite,
1007            "key",
1008            ciphertext_sample,
1009            ciphersuite.aead_key_length(),
1010        )?;
1011        Ok(AeadKey::from_secret(secret, ciphersuite))
1012    }
1013
1014    /// Derive a new AEAD nonce from a `SenderDataSecret`.
1015    pub(crate) fn derive_aead_nonce(
1016        &self,
1017        ciphersuite: Ciphersuite,
1018        crypto: &impl OpenMlsCrypto,
1019        ciphertext: &[u8],
1020    ) -> Result<AeadNonce, CryptoError> {
1021        let ciphertext_sample = ciphertext_sample(ciphersuite, ciphertext);
1022        log::debug!(
1023            "SenderDataSecret::derive_aead_nonce ciphertext sample: {ciphertext_sample:x?}"
1024        );
1025        let nonce_secret = self.secret.kdf_expand_label(
1026            crypto,
1027            ciphersuite,
1028            "nonce",
1029            ciphertext_sample,
1030            ciphersuite.aead_nonce_length(),
1031        )?;
1032        Ok(AeadNonce::from_secret(nonce_secret))
1033    }
1034
1035    #[cfg(any(feature = "test-utils", test))]
1036    pub(crate) fn random(ciphersuite: Ciphersuite, rng: &impl OpenMlsRand) -> Self {
1037        Self {
1038            secret: Secret::random(ciphersuite, rng).expect("Not enough randomness."),
1039        }
1040    }
1041
1042    #[cfg(any(feature = "test-utils", test))]
1043    pub(crate) fn as_slice(&self) -> &[u8] {
1044        self.secret.as_slice()
1045    }
1046
1047    #[cfg(any(feature = "test-utils", test))]
1048    /// Create a new secret from a byte vector.
1049    pub(crate) fn from_slice(bytes: &[u8]) -> Self {
1050        Self {
1051            secret: Secret::from_slice(bytes),
1052        }
1053    }
1054}
1055
1056/// The `EpochSecrets` contain keys (or secrets), which are accessible outside
1057/// of the `KeySchedule` and which don't get consumed immediately upon first
1058/// use.
1059///
1060/// | Secret                  | Label           |
1061/// |:------------------------|:----------------|
1062/// | `init_secret`           | "init"          |
1063/// | `sender_data_secret`    | "sender data"   |
1064/// | `encryption_secret`     | "encryption"    |
1065/// | `exporter_secret`       | "exporter"      |
1066/// | `epoch_authenticator`   | "authentication"|
1067/// | `external_secret`       | "external"      |
1068/// | `confirmation_key`      | "confirm"       |
1069/// | `membership_key`        | "membership"    |
1070/// | `resumption_psk`        | "resumption"    |
1071pub(crate) struct EpochSecrets {
1072    init_secret: InitSecret,
1073    sender_data_secret: SenderDataSecret,
1074    encryption_secret: EncryptionSecret,
1075    exporter_secret: ExporterSecret,
1076    epoch_authenticator: EpochAuthenticator,
1077    external_secret: ExternalSecret,
1078    confirmation_key: ConfirmationKey,
1079    membership_key: MembershipKey,
1080    resumption_psk: ResumptionPskSecret,
1081}
1082
1083impl std::fmt::Debug for EpochSecrets {
1084    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1085        f.write_str("EpochSecrets { *** }")
1086    }
1087}
1088
1089#[cfg(not(test))]
1090impl PartialEq for EpochSecrets {
1091    fn eq(&self, _other: &Self) -> bool {
1092        false
1093    }
1094}
1095
1096// In tests we allow comparing secrets.
1097#[cfg(test)]
1098impl PartialEq for EpochSecrets {
1099    fn eq(&self, other: &Self) -> bool {
1100        self.sender_data_secret == other.sender_data_secret
1101            && self.exporter_secret == other.exporter_secret
1102            && self.epoch_authenticator == other.epoch_authenticator
1103            && self.external_secret == other.external_secret
1104            && self.confirmation_key == other.confirmation_key
1105            && self.membership_key == other.membership_key
1106            && self.resumption_psk == other.resumption_psk
1107    }
1108}
1109
1110impl EpochSecrets {
1111    /// Get the sender_data secret.
1112    #[cfg(any(feature = "test-utils", test))]
1113    pub(crate) fn sender_data_secret(&self) -> &SenderDataSecret {
1114        &self.sender_data_secret
1115    }
1116
1117    /// Get the confirmation key.
1118    pub(crate) fn confirmation_key(&self) -> &ConfirmationKey {
1119        &self.confirmation_key
1120    }
1121
1122    /// Epoch authenticator
1123    #[cfg(any(feature = "test-utils", test))]
1124    pub(crate) fn epoch_authenticator(&self) -> &EpochAuthenticator {
1125        &self.epoch_authenticator
1126    }
1127
1128    /// Exporter secret
1129    #[cfg(any(feature = "test-utils", test))]
1130    pub(crate) fn exporter_secret(&self) -> &ExporterSecret {
1131        &self.exporter_secret
1132    }
1133
1134    /// Membership key
1135    #[cfg(any(feature = "test-utils", test))]
1136    pub(crate) fn membership_key(&self) -> &MembershipKey {
1137        &self.membership_key
1138    }
1139
1140    /// External secret
1141    pub(crate) fn external_secret(&self) -> &ExternalSecret {
1142        &self.external_secret
1143    }
1144
1145    /// External secret
1146    #[cfg(any(feature = "test-utils", test))]
1147    pub(crate) fn resumption_psk(&self) -> &ResumptionPskSecret {
1148        &self.resumption_psk
1149    }
1150
1151    /// Init secret
1152    #[cfg(any(feature = "test-utils", test))]
1153    pub(crate) fn init_secret(&self) -> &InitSecret {
1154        &self.init_secret
1155    }
1156
1157    /// Encryption secret
1158    #[cfg(any(feature = "test-utils", test))]
1159    pub(crate) fn encryption_secret(&self) -> &EncryptionSecret {
1160        &self.encryption_secret
1161    }
1162
1163    /// Derive `EpochSecrets` from an `EpochSecret`.
1164    /// If the `with_init_secret` argument is `true`, the init secret is derived and
1165    /// part of the `EpochSecrets`. Otherwise not.
1166    fn new(
1167        crypto: &impl OpenMlsCrypto,
1168        ciphersuite: Ciphersuite,
1169        epoch_secret: EpochSecret,
1170    ) -> Result<Self, CryptoError> {
1171        log::debug!("Computing EpochSecrets from epoch secret with {ciphersuite}");
1172        log_crypto!(
1173            trace,
1174            "  epoch_secret: {:x?}",
1175            epoch_secret.secret.as_slice()
1176        );
1177        let sender_data_secret = SenderDataSecret::new(crypto, ciphersuite, &epoch_secret)?;
1178        let encryption_secret = EncryptionSecret::new(crypto, ciphersuite, &epoch_secret)?;
1179        let exporter_secret = ExporterSecret::new(crypto, ciphersuite, &epoch_secret)?;
1180        let epoch_authenticator = EpochAuthenticator::new(crypto, ciphersuite, &epoch_secret)?;
1181        let external_secret = ExternalSecret::new(crypto, ciphersuite, &epoch_secret)?;
1182        let confirmation_key = ConfirmationKey::new(crypto, ciphersuite, &epoch_secret)?;
1183        let membership_key = MembershipKey::new(crypto, ciphersuite, &epoch_secret)?;
1184        let resumption_psk = ResumptionPskSecret::new(crypto, ciphersuite, &epoch_secret)?;
1185
1186        log::trace!("  Computing init secret.");
1187        let init_secret = InitSecret::new(crypto, ciphersuite, epoch_secret)?;
1188
1189        Ok(EpochSecrets {
1190            init_secret,
1191            sender_data_secret,
1192            encryption_secret,
1193            exporter_secret,
1194            epoch_authenticator,
1195            external_secret,
1196            confirmation_key,
1197            membership_key,
1198            resumption_psk,
1199        })
1200    }
1201
1202    /// This function initializes the `EpochSecrets` from an all-zero
1203    /// epoch-secret with the exception of the `init_secret`, which is populated
1204    /// with the given `InitSecret`. This is meant to be used in the case of an
1205    /// external init.
1206    pub(crate) fn with_init_secret(
1207        crypto: &impl OpenMlsCrypto,
1208        ciphersuite: Ciphersuite,
1209        init_secret: InitSecret,
1210    ) -> Result<Self, CryptoError> {
1211        let epoch_secret = EpochSecret {
1212            secret: Secret::zero(ciphersuite),
1213        };
1214        let mut epoch_secrets = Self::new(crypto, ciphersuite, epoch_secret)?;
1215        epoch_secrets.init_secret = init_secret;
1216        Ok(epoch_secrets)
1217    }
1218
1219    /// Splits `EpochSecrets` into two different categories:
1220    ///  - [`GroupEpochSecrets`]: These secrets are only used within the same epoch
1221    ///  - [`MessageSecrets`]: These secrets are potentially also used for past epochs
1222    ///    to decrypt and validate messages
1223    pub(crate) fn split_secrets(
1224        self,
1225        serialized_context: Vec<u8>,
1226        treesize: TreeSize,
1227        own_index: LeafNodeIndex,
1228    ) -> (GroupEpochSecrets, MessageSecrets) {
1229        let secret_tree = self
1230            .encryption_secret
1231            .create_secret_tree(treesize, own_index);
1232        (
1233            GroupEpochSecrets {
1234                init_secret: self.init_secret,
1235                exporter_secret: self.exporter_secret,
1236                epoch_authenticator: self.epoch_authenticator,
1237                external_secret: self.external_secret,
1238                resumption_psk: self.resumption_psk,
1239            },
1240            MessageSecrets::new(
1241                self.sender_data_secret,
1242                self.membership_key,
1243                self.confirmation_key,
1244                serialized_context,
1245                secret_tree,
1246            ),
1247        )
1248    }
1249}
1250
1251#[derive(Serialize, Deserialize)]
1252#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
1253pub(crate) struct GroupEpochSecrets {
1254    init_secret: InitSecret,
1255    exporter_secret: ExporterSecret,
1256    epoch_authenticator: EpochAuthenticator,
1257    external_secret: ExternalSecret,
1258    resumption_psk: ResumptionPskSecret,
1259}
1260
1261impl std::fmt::Debug for GroupEpochSecrets {
1262    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1263        f.write_str("GroupEpochSecrets { *** }")
1264    }
1265}
1266
1267#[cfg(not(any(test, feature = "test-utils")))]
1268impl PartialEq for GroupEpochSecrets {
1269    fn eq(&self, _other: &Self) -> bool {
1270        false
1271    }
1272}
1273
1274impl GroupEpochSecrets {
1275    /// Init secret
1276    pub(crate) fn init_secret(&self) -> &InitSecret {
1277        &self.init_secret
1278    }
1279
1280    /// Epoch authenticator
1281    pub(crate) fn epoch_authenticator(&self) -> &EpochAuthenticator {
1282        &self.epoch_authenticator
1283    }
1284
1285    /// Exporter secret
1286    pub(crate) fn exporter_secret(&self) -> &ExporterSecret {
1287        &self.exporter_secret
1288    }
1289
1290    /// External secret
1291    pub(crate) fn external_secret(&self) -> &ExternalSecret {
1292        &self.external_secret
1293    }
1294
1295    /// External secret
1296    pub(crate) fn resumption_psk(&self) -> &ResumptionPskSecret {
1297        &self.resumption_psk
1298    }
1299}