use past_secrets::MessageSecretsStore;
use proposal_store::ProposalQueue;
use serde::{Deserialize, Serialize};
use tls_codec::Serialize as _;
#[cfg(test)]
use crate::treesync::node::leaf_node::TreePosition;
use super::proposal_store::{ProposalStore, QueuedProposal};
use crate::{
binary_tree::array_representation::LeafNodeIndex,
ciphersuite::{hash_ref::ProposalRef, signable::Signable},
credentials::Credential,
error::LibraryError,
extensions::Extensions,
framing::{mls_auth_content::AuthenticatedContent, *},
group::{
CreateGroupContextExtProposalError, DeletePastEpochSecretsError, Extension, ExtensionType,
ExternalPubExtension, GroupContext, GroupEpoch, GroupId, MlsGroupJoinConfig,
MlsGroupStateError, OutgoingWireFormatPolicy, PublicGroup, RatchetTreeExtension,
RequiredCapabilitiesExtension, SetPastEpochDeletionPolicyError, StagedCommit,
},
key_packages::KeyPackageBundle,
messages::{
group_info::{GroupInfo, GroupInfoTBS, VerifiableGroupInfo},
proposals::*,
ConfirmationTag, GroupSecrets, Welcome,
},
schedule::{
message_secrets::MessageSecrets,
psk::{load_psks, store::ResumptionPskStore, PskSecret},
GroupEpochSecrets, JoinerSecret, KeySchedule,
},
storage::{OpenMlsProvider, StorageProvider},
treesync::{
node::{encryption_keys::EncryptionKeyPair, leaf_node::LeafNode},
RatchetTree, TreeSync,
},
versions::ProtocolVersion,
};
use openmls_traits::{
crypto::OpenMlsCrypto, signatures::Signer, storage::StorageProvider as _, types::Ciphersuite,
};
#[cfg(feature = "extensions-draft")]
use crate::schedule::{application_export_tree::ApplicationExportTree, ApplicationExportSecret};
mod application;
mod exporting;
mod updates;
#[cfg(feature = "migration-import")]
pub(crate) mod migration_import;
#[cfg(feature = "virtual-clients-draft")]
pub use application::UnconfirmedMessage;
pub use proposal::Propose;
use config::*;
pub(crate) mod builder;
pub(crate) mod commit_builder;
pub(crate) mod config;
pub(crate) mod creation;
pub(crate) mod errors;
pub(crate) mod membership;
pub(crate) mod past_secrets;
pub(crate) mod processing;
pub(crate) mod proposal;
pub(crate) mod proposal_store;
pub(crate) mod staged_commit;
#[cfg(feature = "extensions-draft")]
pub(crate) mod app_ephemeral;
#[cfg(feature = "targeted-messages-draft")]
mod targeted_messages;
#[cfg(test)]
pub(crate) mod tests_and_kats;
#[derive(Debug)]
pub(crate) struct CreateCommitResult {
pub(crate) commit: AuthenticatedContent,
pub(crate) welcome_option: Option<Welcome>,
pub(crate) staged_commit: StagedCommit,
pub(crate) group_info: Option<GroupInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Member {
pub index: LeafNodeIndex,
pub credential: Credential,
pub encryption_key: Vec<u8>,
pub signature_key: Vec<u8>,
}
impl Member {
pub fn new(
index: LeafNodeIndex,
encryption_key: Vec<u8>,
signature_key: Vec<u8>,
credential: Credential,
) -> Self {
Self {
index,
encryption_key,
signature_key,
credential,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
pub enum PendingCommitState {
Member(StagedCommit),
External(StagedCommit),
}
impl PendingCommitState {
pub(crate) fn staged_commit(&self) -> &StagedCommit {
match self {
PendingCommitState::Member(pc) => pc,
PendingCommitState::External(pc) => pc,
}
}
}
impl From<PendingCommitState> for StagedCommit {
fn from(pcs: PendingCommitState) -> Self {
match pcs {
PendingCommitState::Member(pc) => pc,
PendingCommitState::External(pc) => pc,
}
}
}
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
pub enum MlsGroupState {
PendingCommit(Box<PendingCommitState>),
Operational,
Inactive,
}
#[derive(Debug)]
#[cfg_attr(feature = "migration-import", derive(serde::Deserialize))]
#[cfg_attr(
all(feature = "migration-import", feature = "test-utils"),
derive(serde::Serialize)
)]
#[cfg_attr(feature = "test-utils", derive(Clone, PartialEq))]
pub struct MlsGroup {
mls_group_config: MlsGroupJoinConfig,
public_group: PublicGroup,
group_epoch_secrets: GroupEpochSecrets,
own_leaf_index: LeafNodeIndex,
message_secrets_store: MessageSecretsStore,
resumption_psk_store: ResumptionPskStore,
own_leaf_nodes: Vec<LeafNode>,
aad: Vec<u8>,
#[cfg(feature = "extensions-draft")]
#[cfg_attr(
feature = "migration-import",
serde(default = "crate::framing::SafeAad::empty")
)]
safe_aad: SafeAad,
group_state: MlsGroupState,
#[cfg(feature = "extensions-draft")]
#[cfg_attr(feature = "migration-import", serde(default))]
application_export_tree: Option<ApplicationExportTree>,
}
impl MlsGroup {
pub fn configuration(&self) -> &MlsGroupJoinConfig {
&self.mls_group_config
}
pub fn set_configuration<Storage: StorageProvider>(
&mut self,
storage: &Storage,
mls_group_config: &MlsGroupJoinConfig,
) -> Result<(), Storage::Error> {
let policy_changed = self.mls_group_config.past_epoch_deletion_policy()
!= mls_group_config.past_epoch_deletion_policy();
self.mls_group_config = mls_group_config.clone();
storage.write_mls_join_config(self.group_id(), mls_group_config)?;
if policy_changed {
self.resize_message_secrets_store(mls_group_config.past_epoch_deletion_policy());
storage.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
}
Ok(())
}
pub fn set_aad(&mut self, aad: Vec<u8>) {
self.aad = aad;
}
pub fn aad(&self) -> &[u8] {
&self.aad
}
#[cfg(feature = "extensions-draft")]
pub fn set_safe_aad(&mut self, items: Vec<SafeAadItem>) -> Result<(), SafeAadError> {
self.safe_aad = SafeAad::from_items(items)?;
Ok(())
}
#[cfg(feature = "extensions-draft")]
pub fn safe_aad_items(&self) -> &[SafeAadItem] {
self.safe_aad.items()
}
pub fn ciphersuite(&self) -> Ciphersuite {
self.public_group.ciphersuite()
}
pub fn confirmation_tag(&self) -> &ConfirmationTag {
self.public_group.confirmation_tag()
}
pub fn is_active(&self) -> bool {
!matches!(self.group_state, MlsGroupState::Inactive)
}
pub fn credential(&self) -> Result<&Credential, MlsGroupStateError> {
if !self.is_active() {
return Err(MlsGroupStateError::UseAfterEviction);
}
self.public_group
.leaf(self.own_leaf_index())
.map(|node| node.credential())
.ok_or_else(|| LibraryError::custom("Own leaf node missing").into())
}
pub fn own_leaf_index(&self) -> LeafNodeIndex {
self.own_leaf_index
}
pub fn own_leaf_node(&self) -> Option<&LeafNode> {
self.public_group().leaf(self.own_leaf_index())
}
pub fn group_id(&self) -> &GroupId {
self.public_group.group_id()
}
pub fn epoch(&self) -> GroupEpoch {
self.public_group.group_context().epoch()
}
pub fn pending_proposals(&self) -> impl Iterator<Item = &QueuedProposal> {
self.proposal_store().proposals()
}
pub fn treesync(&self) -> &TreeSync {
self.public_group.treesync()
}
pub fn pending_commit(&self) -> Option<&StagedCommit> {
match self.group_state {
MlsGroupState::PendingCommit(ref pending_commit_state) => {
Some(pending_commit_state.staged_commit())
}
MlsGroupState::Operational => None,
MlsGroupState::Inactive => None,
}
}
pub fn clear_pending_commit<Storage: StorageProvider>(
&mut self,
storage: &Storage,
) -> Result<(), Storage::Error> {
match self.group_state {
MlsGroupState::PendingCommit(ref pending_commit_state) => {
if let PendingCommitState::Member(_) = **pending_commit_state {
self.group_state = MlsGroupState::Operational;
storage.write_group_state(self.group_id(), &self.group_state)
} else {
Ok(())
}
}
MlsGroupState::Operational | MlsGroupState::Inactive => Ok(()),
}
}
pub fn clear_pending_proposals<Storage: StorageProvider>(
&mut self,
storage: &Storage,
) -> Result<(), Storage::Error> {
if !self.proposal_store().is_empty() {
self.proposal_store_mut().empty();
storage.clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())?;
}
Ok(())
}
pub fn extensions(&self) -> &Extensions<GroupContext> {
self.public_group().group_context().extensions()
}
pub fn ext_commit_sender_index(
&self,
commit: &StagedCommit,
) -> Result<LeafNodeIndex, LibraryError> {
self.public_group().ext_commit_sender_index(commit)
}
pub fn load<Storage: crate::storage::StorageProvider>(
storage: &Storage,
group_id: &GroupId,
) -> Result<Option<MlsGroup>, Storage::Error> {
let public_group = PublicGroup::load(storage, group_id)?;
let group_epoch_secrets = storage.group_epoch_secrets(group_id)?;
let own_leaf_index = storage.own_leaf_index(group_id)?;
let message_secrets_store = storage.message_secrets(group_id)?;
let resumption_psk_store = storage.resumption_psk_store(group_id)?;
let mls_group_config = storage.mls_group_join_config(group_id)?;
let own_leaf_nodes = storage.own_leaf_nodes(group_id)?;
let group_state = storage.group_state(group_id)?;
#[cfg(feature = "extensions-draft")]
let application_export_tree = storage.application_export_tree(group_id)?;
let build = || -> Option<Self> {
Some(Self {
public_group: public_group?,
group_epoch_secrets: group_epoch_secrets?,
own_leaf_index: own_leaf_index?,
message_secrets_store: message_secrets_store?,
resumption_psk_store: resumption_psk_store?,
mls_group_config: mls_group_config?,
own_leaf_nodes,
aad: vec![],
#[cfg(feature = "extensions-draft")]
safe_aad: SafeAad::empty(),
group_state: group_state?,
#[cfg(feature = "extensions-draft")]
application_export_tree,
})
};
Ok(build())
}
pub fn delete<Storage: crate::storage::StorageProvider>(
&mut self,
storage: &Storage,
) -> Result<(), Storage::Error> {
PublicGroup::delete(storage, self.group_id())?;
storage.delete_own_leaf_index(self.group_id())?;
storage.delete_group_epoch_secrets(self.group_id())?;
storage.delete_message_secrets(self.group_id())?;
storage.delete_all_resumption_psk_secrets(self.group_id())?;
storage.delete_group_config(self.group_id())?;
storage.delete_own_leaf_nodes(self.group_id())?;
storage.delete_group_state(self.group_id())?;
storage.clear_proposal_queue::<GroupId, ProposalRef>(self.group_id())?;
#[cfg(feature = "extensions-draft")]
storage.delete_application_export_tree::<_, ApplicationExportTree>(self.group_id())?;
#[cfg(feature = "virtual-clients-draft")]
{
storage.delete_vc_emulation_bindings(self.group_id())?;
storage.delete_registered_vc_emulation_epoch(self.group_id())?;
}
self.proposal_store_mut().empty();
storage.delete_encryption_epoch_key_pairs(
self.group_id(),
&self.epoch(),
self.own_leaf_index().u32(),
)?;
Ok(())
}
pub fn export_ratchet_tree(&self) -> RatchetTree {
self.public_group().export_ratchet_tree()
}
}
#[cfg(feature = "virtual-clients-draft")]
#[derive(thiserror::Error, Debug, PartialEq, Clone)]
pub(crate) enum VcEmulationStateError<StorageError> {
#[error("Error reading the binding or emulation-epoch state from storage: {0}")]
Storage(StorageError),
#[error("The group is bound to an emulation epoch, but its state is missing.")]
MissingEmulationEpochState,
}
impl MlsGroup {
pub(crate) fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
self.public_group.required_capabilities()
}
pub(crate) fn group_epoch_secrets(&self) -> &GroupEpochSecrets {
&self.group_epoch_secrets
}
pub(crate) fn message_secrets(&self) -> &MessageSecrets {
self.message_secrets_store.message_secrets()
}
pub(crate) fn resize_message_secrets_store(&mut self, policy: &PastEpochDeletionPolicy) {
self.message_secrets_store.resize(policy);
}
pub fn past_epoch_deletion_policy(&self) -> &PastEpochDeletionPolicy {
self.mls_group_config.past_epoch_deletion_policy()
}
pub fn set_past_epoch_deletion_policy<Provider: OpenMlsProvider>(
&mut self,
provider: &Provider,
policy: PastEpochDeletionPolicy,
) -> Result<(), SetPastEpochDeletionPolicyError<Provider::StorageError>> {
self.resize_message_secrets_store(&policy);
self.mls_group_config.past_epoch_deletion_policy = policy;
provider
.storage()
.write_mls_join_config(self.group_id(), &self.mls_group_config)?;
provider
.storage()
.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
Ok(())
}
pub(crate) fn message_secrets_for_epoch_mut(
&mut self,
epoch: GroupEpoch,
) -> Result<&mut MessageSecrets, SecretTreeError> {
if epoch < self.context().epoch() {
self.message_secrets_store
.secrets_for_epoch_mut(epoch)
.ok_or(SecretTreeError::TooDistantInThePast)
} else {
Ok(self.message_secrets_store.message_secrets_mut())
}
}
pub(crate) fn message_secrets_for_epoch(
&self,
epoch: GroupEpoch,
) -> Result<&MessageSecrets, SecretTreeError> {
if epoch < self.context().epoch() {
self.message_secrets_store
.secrets_for_epoch(epoch)
.ok_or(SecretTreeError::TooDistantInThePast)
} else {
Ok(self.message_secrets_store.message_secrets())
}
}
pub(crate) fn message_secrets_and_leaves(
&self,
epoch: GroupEpoch,
) -> Result<(&MessageSecrets, &[Member]), SecretTreeError> {
if epoch < self.context().epoch() {
self.message_secrets_store
.secrets_and_leaves_for_epoch(epoch)
.ok_or(SecretTreeError::TooDistantInThePast)
} else {
Ok((self.message_secrets_store.message_secrets(), &[]))
}
}
pub(crate) fn create_group_context_ext_proposal<Provider: OpenMlsProvider>(
&self,
framing_parameters: FramingParameters,
extensions: Extensions<GroupContext>,
signer: &impl Signer,
) -> Result<AuthenticatedContent, CreateGroupContextExtProposalError<Provider::StorageError>>
{
let required_extension = extensions
.iter()
.find(|extension| extension.extension_type() == ExtensionType::RequiredCapabilities);
if let Some(required_extension) = required_extension {
let required_capabilities = required_extension.as_required_capabilities_extension()?;
self.own_leaf_node()
.ok_or_else(|| LibraryError::custom("Tree has no own leaf."))?
.capabilities()
.supports_required_capabilities(required_capabilities)?;
self.public_group()
.check_extension_support(required_capabilities.extension_types())?;
}
let proposal = GroupContextExtensionProposal::new(extensions);
let proposal = Proposal::GroupContextExtensions(Box::new(proposal));
AuthenticatedContent::member_proposal(
framing_parameters,
self.own_leaf_index(),
proposal,
self.context(),
signer,
)
.map_err(|e| e.into())
}
#[cfg(feature = "virtual-clients-draft")]
pub(crate) fn vc_emulation_state_at_epoch<Storage: StorageProvider>(
&self,
storage: &Storage,
epoch: GroupEpoch,
) -> Result<
Option<crate::components::vc_derivation_info::EmulationEpochState>,
VcEmulationStateError<Storage::Error>,
> {
let bindings: Option<crate::components::vc_derivation_info::VcEmulationBindings> = storage
.vc_emulation_bindings(self.group_id())
.map_err(VcEmulationStateError::Storage)?;
let Some(epoch_id) = bindings.and_then(|bindings| bindings.get(epoch).cloned()) else {
return Ok(None);
};
let state = storage
.vc_emulation_epoch_state(&epoch_id)
.map_err(VcEmulationStateError::Storage)?
.ok_or_else(|| {
log::error!("vc: group is bound to emulation epoch, but state is missing");
VcEmulationStateError::MissingEmulationEpochState
})?;
Ok(Some(state))
}
pub(crate) fn encrypt<Provider: OpenMlsProvider>(
&mut self,
public_message: AuthenticatedContent,
provider: &Provider,
) -> Result<EncryptionOutput, MessageEncryptionError<Provider::StorageError>> {
let padding_size = self.configuration().padding_size();
#[cfg(feature = "virtual-clients-draft")]
let emulation_state = self
.vc_emulation_state_at_epoch(provider.storage(), self.epoch())
.map_err(|e| match e {
VcEmulationStateError::Storage(e) => MessageEncryptionError::StorageError(e),
VcEmulationStateError::MissingEmulationEpochState => {
MessageEncryptionError::VirtualClientsError(
crate::components::vc_derivation_info::VirtualClientsError::MissingEmulationEpochState,
)
}
})?;
#[cfg(feature = "virtual-clients-draft")]
let emulator_ctx: Option<crate::framing::EmulatorReuseGuardCtx<'_>> = emulation_state
.as_ref()
.map(|state| state.reuse_guard_inputs());
let msg = PrivateMessage::try_from_authenticated_content(
provider.crypto(),
provider.rand(),
&public_message,
self.ciphersuite(),
self.message_secrets_store.message_secrets_mut(),
padding_size,
#[cfg(feature = "virtual-clients-draft")]
emulator_ctx.as_ref(),
)?;
#[cfg(feature = "virtual-clients-draft")]
let msg = {
use crate::components::vc_derivation_info::RatchetType;
let mut msg = msg;
if let Some(state) = &emulation_state {
let ratchet_type = match public_message.content().content_type() {
ContentType::Application => RatchetType::Application,
ContentType::Proposal | ContentType::Commit => RatchetType::Handshake,
};
let generation_id = state
.derive_generation_id(
provider.crypto(),
self.group_id(),
self.epoch(),
msg.generation,
ratchet_type,
)
.map_err(MessageEncryptionError::VirtualClientsError)?;
msg.generation_id = Some(generation_id);
}
msg
};
provider
.storage()
.write_message_secrets(self.group_id(), &self.message_secrets_store)
.map_err(MessageEncryptionError::StorageError)?;
Ok(msg)
}
pub(crate) fn outgoing_wire_format(&self) -> WireFormat {
self.mls_group_config.wire_format_policy().outgoing().into()
}
pub(crate) fn outgoing_authenticated_data(&self) -> Result<Vec<u8>, LibraryError> {
#[cfg(feature = "extensions-draft")]
{
self.assembled_authenticated_data()
}
#[cfg(not(feature = "extensions-draft"))]
{
Ok(self.aad.clone())
}
}
#[cfg(feature = "extensions-draft")]
pub(crate) fn assembled_authenticated_data(&self) -> Result<Vec<u8>, LibraryError> {
if !self.context().safe_aad_required() {
return Ok(self.aad.clone());
}
crate::framing::safe_aad::assemble_authenticated_data(&self.safe_aad, &self.aad)
.map_err(|_| LibraryError::custom("SafeAad serialization failed"))
}
pub fn delete_past_epoch_secrets<Provider: OpenMlsProvider>(
&mut self,
provider: &Provider,
policy: PastEpochDeletion,
) -> Result<(), DeletePastEpochSecretsError<Provider::StorageError>> {
self.message_secrets_store.delete_past_epoch_secrets(policy);
provider
.storage()
.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
Ok(())
}
pub fn proposal_store(&self) -> &ProposalStore {
self.public_group.proposal_store()
}
pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
self.public_group.proposal_store_mut()
}
pub(crate) fn context(&self) -> &GroupContext {
self.public_group.group_context()
}
pub(crate) fn version(&self) -> ProtocolVersion {
self.public_group.version()
}
#[inline]
pub(crate) fn reset_aad(&mut self) {
self.aad.clear();
#[cfg(feature = "extensions-draft")]
{
self.safe_aad = SafeAad::empty();
}
}
pub fn public_group(&self) -> &PublicGroup {
&self.public_group
}
}
#[cfg(feature = "virtual-clients-draft")]
#[derive(Debug, Clone)]
pub struct HandshakeConfirmationData {
pub epoch: GroupEpoch,
pub generation: u32,
pub generation_id: Option<crate::components::vc_derivation_info::GenerationId>,
}
pub(crate) struct HandshakeFramingOutput {
pub(crate) message: MlsMessageOut,
#[cfg(feature = "virtual-clients-draft")]
pub(crate) confirmation: Option<HandshakeConfirmationData>,
}
impl MlsGroup {
pub(super) fn store_epoch_keypairs<Storage: StorageProvider>(
&self,
store: &Storage,
keypair_references: &[EncryptionKeyPair],
) -> Result<(), Storage::Error> {
store.write_encryption_epoch_key_pairs(
self.group_id(),
&self.context().epoch(),
self.own_leaf_index().u32(),
keypair_references,
)
}
pub(super) fn read_epoch_keypairs<Storage: StorageProvider>(
&self,
store: &Storage,
) -> Result<Vec<EncryptionKeyPair>, Storage::Error> {
store.encryption_epoch_key_pairs(
self.group_id(),
&self.context().epoch(),
self.own_leaf_index().u32(),
)
}
#[cfg(not(feature = "virtual-clients-draft"))]
pub(super) fn delete_previous_epoch_keypairs<Storage: StorageProvider>(
&self,
store: &Storage,
) -> Result<(), Storage::Error> {
store.delete_encryption_epoch_key_pairs(
self.group_id(),
&GroupEpoch::from(self.context().epoch().as_u64() - 1),
self.own_leaf_index().u32(),
)
}
#[cfg(feature = "virtual-clients-draft")]
pub(super) fn delete_previous_epoch_keypairs<Storage: StorageProvider>(
&self,
store: &Storage,
previous_own_leaf_index: LeafNodeIndex,
) -> Result<(), Storage::Error> {
store.delete_encryption_epoch_key_pairs(
self.group_id(),
&GroupEpoch::from(self.context().epoch().as_u64() - 1),
previous_own_leaf_index.u32(),
)
}
pub(super) fn store<Storage: crate::storage::StorageProvider>(
&self,
storage: &Storage,
) -> Result<(), Storage::Error> {
self.public_group.store(storage)?;
storage.write_group_epoch_secrets(self.group_id(), &self.group_epoch_secrets)?;
storage.write_own_leaf_index(self.group_id(), &self.own_leaf_index)?;
storage.write_message_secrets(self.group_id(), &self.message_secrets_store)?;
storage.write_resumption_psk_store(self.group_id(), &self.resumption_psk_store)?;
storage.write_mls_join_config(self.group_id(), &self.mls_group_config)?;
storage.write_group_state(self.group_id(), &self.group_state)?;
#[cfg(feature = "extensions-draft")]
if let Some(application_export_tree) = &self.application_export_tree {
storage.write_application_export_tree(self.group_id(), application_export_tree)?;
}
Ok(())
}
fn content_to_mls_message(
&mut self,
mls_auth_content: AuthenticatedContent,
provider: &impl OpenMlsProvider,
) -> Result<HandshakeFramingOutput, LibraryError> {
let output = match self.configuration().wire_format_policy().outgoing() {
OutgoingWireFormatPolicy::AlwaysPlaintext => {
let mut plaintext: PublicMessage = mls_auth_content.into();
if plaintext.sender().is_member() {
plaintext.set_membership_tag(
provider.crypto(),
self.ciphersuite(),
self.message_secrets().membership_key(),
self.message_secrets().serialized_context(),
)?;
}
HandshakeFramingOutput {
message: plaintext.into(),
#[cfg(feature = "virtual-clients-draft")]
confirmation: None,
}
}
OutgoingWireFormatPolicy::AlwaysCiphertext => {
#[cfg(feature = "virtual-clients-draft")]
let epoch = self.epoch();
let encryption_output = self
.encrypt(mls_auth_content, provider)
.map_err(|_| LibraryError::custom("Malformed plaintext"))?;
let message = MlsMessageOut::from_private_message(
encryption_output.private_message,
self.version(),
);
HandshakeFramingOutput {
message,
#[cfg(feature = "virtual-clients-draft")]
confirmation: Some(HandshakeConfirmationData {
epoch,
generation: encryption_output.generation,
generation_id: encryption_output.generation_id,
}),
}
}
};
Ok(output)
}
fn is_operational(&self) -> Result<(), MlsGroupStateError> {
match self.group_state {
MlsGroupState::PendingCommit(_) => Err(MlsGroupStateError::PendingCommit),
MlsGroupState::Inactive => Err(MlsGroupStateError::UseAfterEviction),
MlsGroupState::Operational => Ok(()),
}
}
}
impl MlsGroup {
#[cfg(any(feature = "test-utils", test))]
pub fn export_group_context(&self) -> &GroupContext {
self.context()
}
#[cfg(any(feature = "test-utils", test))]
pub fn tree_hash(&self) -> &[u8] {
self.public_group().group_context().tree_hash()
}
#[cfg(any(feature = "test-utils", test))]
pub(crate) fn message_secrets_test_mut(&mut self) -> &mut MessageSecrets {
self.message_secrets_store.message_secrets_mut()
}
#[cfg(any(feature = "test-utils", test))]
pub fn print_ratchet_tree(&self, message: &str) {
println!("{}: {}", message, self.public_group().export_ratchet_tree());
}
#[cfg(any(feature = "test-utils", test))]
pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
self.public_group.context_mut()
}
#[cfg(test)]
pub(crate) fn set_own_leaf_index(&mut self, own_leaf_index: LeafNodeIndex) {
self.own_leaf_index = own_leaf_index;
}
#[cfg(test)]
pub(crate) fn own_tree_position(&self) -> TreePosition {
TreePosition::new(self.group_id().clone(), self.own_leaf_index())
}
#[cfg(test)]
pub(crate) fn message_secrets_store(&self) -> &MessageSecretsStore {
&self.message_secrets_store
}
#[cfg(test)]
pub(crate) fn resumption_psk_store(&self) -> &ResumptionPskStore {
&self.resumption_psk_store
}
#[cfg(test)]
pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
self.public_group.set_group_context(group_context)
}
#[cfg(any(test, feature = "test-utils"))]
pub fn ensure_persistence(&self, storage: &impl StorageProvider) -> Result<(), LibraryError> {
let loaded = MlsGroup::load(storage, self.group_id())
.map_err(|_| LibraryError::custom("Failed to load group from storage"))?;
let other = loaded.ok_or_else(|| LibraryError::custom("Group not found in storage"))?;
if self != &other {
let mut diagnostics = Vec::new();
if self.mls_group_config != other.mls_group_config {
diagnostics.push(format!(
"mls_group_config:\n Current: {:?}\n Loaded: {:?}",
self.mls_group_config, other.mls_group_config
));
}
if self.public_group != other.public_group {
diagnostics.push(format!(
"public_group:\n Current: {:?}\n Loaded: {:?}",
self.public_group, other.public_group
));
}
if self.group_epoch_secrets != other.group_epoch_secrets {
diagnostics.push(format!(
"group_epoch_secrets:\n Current: {:?}\n Loaded: {:?}",
self.group_epoch_secrets, other.group_epoch_secrets
));
}
if self.own_leaf_index != other.own_leaf_index {
diagnostics.push(format!(
"own_leaf_index:\n Current: {:?}\n Loaded: {:?}",
self.own_leaf_index, other.own_leaf_index
));
}
if self.message_secrets_store != other.message_secrets_store {
diagnostics.push(format!(
"message_secrets_store:\n Current: {:?}\n Loaded: {:?}",
self.message_secrets_store, other.message_secrets_store
));
}
if self.resumption_psk_store != other.resumption_psk_store {
diagnostics.push(format!(
"resumption_psk_store:\n Current: {:?}\n Loaded: {:?}",
self.resumption_psk_store, other.resumption_psk_store
));
}
if self.own_leaf_nodes != other.own_leaf_nodes {
diagnostics.push(format!(
"own_leaf_nodes:\n Current: {:?}\n Loaded: {:?}",
self.own_leaf_nodes, other.own_leaf_nodes
));
}
if self.aad != other.aad {
diagnostics.push(format!(
"aad:\n Current: {:?}\n Loaded: {:?}",
self.aad, other.aad
));
}
if self.group_state != other.group_state {
diagnostics.push(format!(
"group_state:\n Current: {:?}\n Loaded: {:?}",
self.group_state, other.group_state
));
}
#[cfg(feature = "extensions-draft")]
if self.application_export_tree != other.application_export_tree {
diagnostics.push(format!(
"application_export_tree:\n Current: {:?}\n Loaded: {:?}",
self.application_export_tree, other.application_export_tree
));
}
log::error!(
"Loaded group does not match current group! Differing fields ({}):\n\n{}",
diagnostics.len(),
diagnostics.join("\n\n")
);
return Err(LibraryError::custom(
"Loaded group does not match current group",
));
}
Ok(())
}
}
#[derive(Debug)]
pub struct StagedWelcome {
mls_group_config: MlsGroupJoinConfig,
public_group: PublicGroup,
group_epoch_secrets: GroupEpochSecrets,
own_leaf_index: LeafNodeIndex,
message_secrets_store: MessageSecretsStore,
#[cfg(feature = "extensions-draft")]
application_export_secret: ApplicationExportSecret,
resumption_psk_store: ResumptionPskStore,
verifiable_group_info: VerifiableGroupInfo,
key_material: WelcomeKeyMaterial,
path_keypairs: Option<Vec<EncryptionKeyPair>>,
}
pub struct ProcessedWelcome {
mls_group_config: MlsGroupJoinConfig,
ciphersuite: Ciphersuite,
group_secrets: GroupSecrets,
epoch_secrets: crate::schedule::EpochSecretsResult,
verifiable_group_info: crate::messages::group_info::VerifiableGroupInfo,
resumption_psk_store: crate::schedule::psk::store::ResumptionPskStore,
key_material: WelcomeKeyMaterial,
}
#[derive(Debug)]
pub(crate) enum WelcomeKeyMaterial {
KeyPackage(Box<KeyPackageBundle>),
#[cfg(feature = "virtual-clients-draft")]
VirtualClient(crate::components::vc_derivation_info::VcWelcomeMaterial),
}
impl WelcomeKeyMaterial {
fn key_package_ref(
&self,
crypto: &impl OpenMlsCrypto,
) -> Result<crate::ciphersuite::hash_ref::KeyPackageRef, LibraryError> {
match self {
WelcomeKeyMaterial::KeyPackage(bundle) => bundle.key_package().hash_ref(crypto),
#[cfg(feature = "virtual-clients-draft")]
WelcomeKeyMaterial::VirtualClient(material) => Ok(material.key_package_ref.clone()),
}
}
fn init_private_key(&self) -> &crate::ciphersuite::HpkePrivateKey {
match self {
WelcomeKeyMaterial::KeyPackage(bundle) => bundle.init_private_key(),
#[cfg(feature = "virtual-clients-draft")]
WelcomeKeyMaterial::VirtualClient(material) => &material.init_private_key,
}
}
fn key_package_bundle(&self) -> Option<&KeyPackageBundle> {
match self {
WelcomeKeyMaterial::KeyPackage(bundle) => Some(bundle),
#[cfg(feature = "virtual-clients-draft")]
WelcomeKeyMaterial::VirtualClient(_) => None,
}
}
fn encryption_key_pair(&self) -> EncryptionKeyPair {
match self {
WelcomeKeyMaterial::KeyPackage(bundle) => bundle.encryption_key_pair(),
#[cfg(feature = "virtual-clients-draft")]
WelcomeKeyMaterial::VirtualClient(material) => material.encryption_keypair.clone(),
}
}
}