// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // Copyright by contributors to this project. // SPDX-License-Identifier: (Apache-2.0 OR MIT) use alloc::{vec, vec::Vec}; use mls_rs_core::{ crypto::{CipherSuite, CipherSuiteProvider, SignatureSecretKey}, error::IntoAnyError, extension::{Extension, ExtensionList}, group::GroupContext, identity::{MemberValidationContext, SigningIdentity}, protocol_version::ProtocolVersion, time::MlsTime, }; use crate::{ client_config::ClientConfig, error::MlsError, group::{ cipher_suite_provider, confirmation_tag::ConfirmationTag, key_schedule::KeySchedule, state_repo::GroupStateRepository, transcript_hash::InterimTranscriptHash, GroupState, LeafIndex, LeafNode, }, tree_kem::{ leaf_node_validator::{LeafNodeValidator, ValidationContext}, TreeKemPublic, }, Group, }; /// Builder for creating a new MLS [`Group`] with customizable parameters. /// /// A `GroupBuilder` is typically obtained via [`Client::group_builder`](crate::Client::group_builder), /// which pre-fills the cipher suite, signing identity, and signer from the client configuration. /// Optional parameters such as group ID, extensions, and protocol version can then be set /// using the `with_*` methods before calling [`build`](GroupBuilder::build) to finalize the group. /// /// If no group ID is provided, a random one is generated. The protocol version defaults /// to MLS 1.0. pub struct GroupBuilder { pub(crate) group_id: Option>, pub(crate) protocol_version: ProtocolVersion, pub(crate) group_context_extensions: ExtensionList, pub(crate) leaf_node_extensions: ExtensionList, pub(crate) now_time: Option, #[cfg(feature = "custom_start_epoch")] pub(crate) start_epoch: u64, pub(crate) config: C, pub(crate) cipher_suite: CipherSuite, pub(crate) signing_identity: SigningIdentity, pub(crate) signer: SignatureSecretKey, } impl GroupBuilder { /// Create a new `GroupBuilder` with the required parameters. /// /// Most callers should use [`Client::group_builder`](crate::Client::group_builder) instead, /// which fills these from the client's configuration. pub(crate) fn new( config: C, cipher_suite: CipherSuite, signing_identity: SigningIdentity, signer: SignatureSecretKey, ) -> Self { Self { group_id: None, protocol_version: ProtocolVersion::MLS_10, group_context_extensions: Default::default(), leaf_node_extensions: Default::default(), now_time: None, #[cfg(feature = "custom_start_epoch")] start_epoch: 0, config, cipher_suite, signing_identity, signer, } } /// Set a specific group ID. If not called, a random group ID is generated /// during [`build`](GroupBuilder::build). pub fn with_group_id(mut self, group_id: Vec) -> Self { self.group_id = Some(group_id); self } /// Set the MLS protocol version. Defaults to [`ProtocolVersion::MLS_10`]. pub fn with_protocol_version(mut self, protocol_version: ProtocolVersion) -> Self { self.protocol_version = protocol_version; self } /// Set extensions to include in the group context. pub fn with_group_context_extensions(mut self, extensions: ExtensionList) -> Self { self.group_context_extensions = extensions; self } /// Add extension to include in the group context. pub fn with_group_context_extension(mut self, extension: Extension) -> Self { self.group_context_extensions.set(extension); self } /// Set extensions to include in the creator's leaf node. pub fn with_leaf_node_extensions(mut self, extensions: ExtensionList) -> Self { self.leaf_node_extensions = extensions; self } /// Add extension to include in the creator's leaf node. pub fn with_leaf_node_extension(mut self, extension: Extension) -> Self { self.leaf_node_extensions.set(extension); self } /// Set the current time used for leaf node lifetime validation. pub fn with_now_time(mut self, now_time: MlsTime) -> Self { self.now_time = Some(now_time); self } /// Set the epoch the newly created group starts from. /// /// By default a group starts at epoch 0, as mandated by RFC 9420. Setting a /// non-zero value here makes the group start at the given epoch instead, which /// allows an application to keep epoch numbers non-overlapping across group /// resets (see mls-rs issue #327). /// /// This is **not** RFC 9420 compliant and only affects the group created /// locally by this builder; it does not apply to groups joined via a Welcome, /// external commit, or to branch/re-init subgroups (whose Welcome epoch must /// be 1). #[cfg(feature = "custom_start_epoch")] pub fn with_start_epoch(mut self, start_epoch: u64) -> Self { self.start_epoch = start_epoch; self } } impl GroupBuilder { /// Consume the builder and create the new [`Group`]. /// /// This generates the initial ratchet tree, key schedule, and group context /// for the starting epoch of the group. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn build(self) -> Result, MlsError> { let cipher_suite_provider = cipher_suite_provider(self.config.crypto_provider(), self.cipher_suite)?; let (leaf_node, leaf_node_secret) = LeafNode::generate( &cipher_suite_provider, self.config.leaf_properties(self.leaf_node_extensions), self.signing_identity, &self.signer, self.config.lifetime(self.now_time), ) .await?; let (mut public_tree, private_tree) = TreeKemPublic::derive( leaf_node, leaf_node_secret, &self.config.identity_provider(), &self.group_context_extensions, ) .await?; let tree_hash = public_tree.tree_hash(&cipher_suite_provider).await?; let group_id = self.group_id.map(Ok).unwrap_or_else(|| { cipher_suite_provider .random_bytes_vec(cipher_suite_provider.kdf_extract_size()) .map_err(|e| MlsError::CryptoProviderError(e.into_any_error())) })?; let context = GroupContext::new( self.protocol_version, self.cipher_suite, group_id, tree_hash, self.group_context_extensions, ); #[cfg(feature = "custom_start_epoch")] let context = GroupContext { epoch: self.start_epoch, ..context }; let identity_provider = self.config.identity_provider(); let member_validation_context = MemberValidationContext::ForNewGroup { current_context: &context, }; let leaf_node_validator = LeafNodeValidator::new( &cipher_suite_provider, &identity_provider, member_validation_context, ); leaf_node_validator .check_if_valid( public_tree.get_leaf_node(LeafIndex::unchecked(0))?, ValidationContext::Add(self.now_time), ) .await?; let state_repo = GroupStateRepository::new( #[cfg(feature = "prior_epoch")] context.group_id.clone(), self.config.group_state_storage(), self.config.key_package_repo(), None, )?; let key_schedule_result = KeySchedule::from_random_epoch_secret( &cipher_suite_provider, #[cfg(any(feature = "secret_tree_access", feature = "private_message"))] public_tree.total_leaf_count(), ) .await?; let confirmation_tag = ConfirmationTag::create( &key_schedule_result.confirmation_key, &vec![].into(), &cipher_suite_provider, ) .await?; let interim_hash = InterimTranscriptHash::create( &cipher_suite_provider, &vec![].into(), &confirmation_tag, ) .await?; Ok(Group { config: self.config, state: GroupState::new(context, public_tree, interim_hash, confirmation_tag), private_tree, key_schedule: key_schedule_result.key_schedule, #[cfg(feature = "by_ref_proposal")] pending_updates: Default::default(), pending_commit: Default::default(), epoch_secrets: key_schedule_result.epoch_secrets, state_repo, cipher_suite_provider, #[cfg(feature = "psk")] previous_psk: None, signer: self.signer, #[cfg(test)] commit_modifiers: Default::default(), }) } }