// 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; use mls_rs_core::{ crypto::{CipherSuite, SignatureSecretKey}, extension::ExtensionList, identity::SigningIdentity, protocol_version::ProtocolVersion, }; use crate::{client::MlsError, Client, Group, MlsMessage}; use super::{ proposal::ReInitProposal, ClientConfig, ExportedTree, JustPreSharedKeyID, MessageProcessor, NewMemberInfo, PreSharedKeyID, PskGroupId, PskSecretInput, ResumptionPSKUsage, ResumptionPsk, }; struct ResumptionGroupParameters<'a> { group_id: &'a [u8], cipher_suite: CipherSuite, version: ProtocolVersion, extensions: &'a ExtensionList, } pub struct ReinitClient { client: Client, reinit: ReInitProposal, psk_input: PskSecretInput, } impl Group where C: ClientConfig + Clone, { /// Create a sub-group from a subset of the current group members. /// /// Membership within the resulting sub-group is indicated by providing a /// key package that produces the same /// [identity](crate::IdentityProvider::identity) value /// as an existing group member. The identity value of each key package /// is determined using the /// [`IdentityProvider`](crate::IdentityProvider) /// that is currently in use by this group instance. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn branch( &self, sub_group_id: Vec, new_key_packages: Vec, ) -> Result<(Group, Vec), MlsError> { let new_group_params = ResumptionGroupParameters { group_id: &sub_group_id, cipher_suite: self.cipher_suite(), version: self.protocol_version(), extensions: &self.group_state().context.extensions, }; resumption_create_group( self.config.clone(), new_key_packages, &new_group_params, // TODO investigate if it's worth updating your own signing identity here self.current_member_signing_identity()?.clone(), self.signer.clone(), #[cfg(any(feature = "private_message", feature = "psk"))] self.resumption_psk_input(ResumptionPSKUsage::Branch)?, ) .await } /// Join a subgroup that was created by [`Group::branch`]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn join_subgroup( &self, welcome: &MlsMessage, tree_data: Option>, ) -> Result<(Group, NewMemberInfo), MlsError> { let expected_new_group_prams = ResumptionGroupParameters { group_id: &[], cipher_suite: self.cipher_suite(), version: self.protocol_version(), extensions: &self.group_state().context.extensions, }; resumption_join_group( self.config.clone(), self.signer.clone(), welcome, tree_data, expected_new_group_prams, false, self.resumption_psk_input(ResumptionPSKUsage::Branch)?, ) .await } /// Generate a [`ReinitClient`] that can be used to create or join a new group /// that is based on properties defined by a [`ReInitProposal`] /// committed in a previously accepted commit. This is the only action available /// after accepting such a commit. The old group can no longer be used according to the RFC. /// /// If the [`ReInitProposal`] changes the ciphersuite, then `new_signer` /// and `new_signer_identity` must be set and match the new ciphersuite, as indicated by /// [`pending_reinit_ciphersuite`](crate::group::StateUpdate::pending_reinit_ciphersuite) /// of the [`StateUpdate`](crate::group::StateUpdate) outputted after processing the /// commit to the reinit proposal. The value of [identity](crate::IdentityProvider::identity) /// must be the same for `new_signing_identity` and the current identity in use by this /// group instance. pub fn get_reinit_client( self, new_signer: Option, new_signing_identity: Option, ) -> Result, MlsError> { let psk_input = self.resumption_psk_input(ResumptionPSKUsage::Reinit)?; let new_signing_identity = new_signing_identity .map(Ok) .unwrap_or_else(|| self.current_member_signing_identity().cloned())?; let reinit = self .state .pending_reinit .ok_or(MlsError::PendingReInitNotFound)?; let new_signer = match new_signer { Some(signer) => signer, None => self.signer, }; let client = Client::new( self.config, Some(new_signer), Some((new_signing_identity, reinit.new_cipher_suite())), reinit.new_version(), ); Ok(ReinitClient { client, reinit, psk_input, }) } fn resumption_psk_input(&self, usage: ResumptionPSKUsage) -> Result { let psk = self.epoch_secrets.resumption_secret.clone(); let id = JustPreSharedKeyID::Resumption(ResumptionPsk { usage, psk_group_id: PskGroupId(self.group_id().to_vec()), psk_epoch: self.current_epoch(), }); let id = PreSharedKeyID::new(id, self.cipher_suite_provider())?; Ok(PskSecretInput { id, psk }) } } /// A [`Client`] that can be used to create or join a new group /// that is based on properties defined by a [`ReInitProposal`] /// committed in a previously accepted commit. impl ReinitClient { /// Generate a key package for the new group. The key package can /// be used in [`ReinitClient::commit`]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn generate_key_package(&self) -> Result { self.client.generate_key_package_message().await } /// Create the new group using new key packages of all group members, possibly /// generated by [`ReinitClient::generate_key_package`]. /// /// # Warning /// /// This function will fail if the number of members in the reinitialized /// group is not the same as the prior group roster. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn commit( self, new_key_packages: Vec, ) -> Result<(Group, Vec), MlsError> { let new_group_params = ResumptionGroupParameters { group_id: self.reinit.group_id(), cipher_suite: self.reinit.new_cipher_suite(), version: self.reinit.new_version(), extensions: self.reinit.new_group_context_extensions(), }; resumption_create_group( self.client.config.clone(), new_key_packages, &new_group_params, // These private fields are created with `Some(x)` by `get_reinit_client` self.client.signing_identity.unwrap().0, self.client.signer.unwrap(), #[cfg(any(feature = "private_message", feature = "psk"))] self.psk_input, ) .await } /// Join a reinitialized group that was created by [`ReinitClient::commit`]. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn join( self, welcome: &MlsMessage, tree_data: Option>, ) -> Result<(Group, NewMemberInfo), MlsError> { let reinit = self.reinit; let expected_group_params = ResumptionGroupParameters { group_id: reinit.group_id(), cipher_suite: reinit.new_cipher_suite(), version: reinit.new_version(), extensions: reinit.new_group_context_extensions(), }; resumption_join_group( self.client.config, // This private field is created with `Some(x)` by `get_reinit_client` self.client.signer.unwrap(), welcome, tree_data, expected_group_params, true, self.psk_input, ) .await } } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn resumption_create_group( config: C, new_key_packages: Vec, new_group_params: &ResumptionGroupParameters<'_>, signing_identity: SigningIdentity, signer: SignatureSecretKey, psk_input: PskSecretInput, ) -> Result<(Group, Vec), MlsError> { // Create a new group with new parameters let mut group = Group::new( config, Some(new_group_params.group_id.to_vec()), new_group_params.cipher_suite, new_group_params.version, signing_identity, new_group_params.extensions.clone(), signer, ) .await?; // Install the resumption psk in the new group group.previous_psk = Some(psk_input); // Create a commit that adds new key packages and uses the resumption PSK let mut commit = group.commit_builder(); for kp in new_key_packages.into_iter() { commit = commit.add_member(kp)?; } let commit = commit.build().await?; group.apply_pending_commit().await?; // Uninstall the resumption psk on success (in case of failure, the new group is discarded anyway) group.previous_psk = None; Ok((group, commit.welcome_messages)) } #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] async fn resumption_join_group( config: C, signer: SignatureSecretKey, welcome: &MlsMessage, tree_data: Option>, expected_new_group_params: ResumptionGroupParameters<'_>, verify_group_id: bool, psk_input: PskSecretInput, ) -> Result<(Group, NewMemberInfo), MlsError> { let psk_input = Some(psk_input); let (group, new_member_info) = Group::::from_welcome_message(welcome, tree_data, config, signer, psk_input).await?; if group.protocol_version() != expected_new_group_params.version { Err(MlsError::ProtocolVersionMismatch) } else if group.cipher_suite() != expected_new_group_params.cipher_suite { Err(MlsError::CipherSuiteMismatch) } else if verify_group_id && group.group_id() != expected_new_group_params.group_id { Err(MlsError::GroupIdMismatch) } else if &group.group_state().context.extensions != expected_new_group_params.extensions { Err(MlsError::ReInitExtensionsMismatch) } else { Ok((group, new_member_info)) } }