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, Extension, ExtensionType, ExternalPubExtension,
GroupContext, GroupEpoch, GroupId, MlsGroupJoinConfig, MlsGroupStateError,
OutgoingWireFormatPolicy, PublicGroup, RatchetTreeExtension, RequiredCapabilitiesExtension,
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,
},
versions::ProtocolVersion,
};
use openmls_traits::{signatures::Signer, storage::StorageProvider as _, types::Ciphersuite};
#[cfg(feature = "extensions-draft-08")]
use crate::schedule::{application_export_tree::ApplicationExportTree, ApplicationExportSecret};
mod application;
mod exporting;
mod updates;
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-08")]
pub(crate) mod app_ephemeral;
#[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 = "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>,
group_state: MlsGroupState,
#[cfg(feature = "extensions-draft-08")]
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> {
self.mls_group_config = mls_group_config.clone();
storage.write_mls_join_config(self.group_id(), mls_group_config)
}
pub fn set_aad(&mut self, aad: Vec<u8>) {
self.aad = aad;
}
pub fn aad(&self) -> &[u8] {
&self.aad
}
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 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-08")]
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![],
group_state: group_state?,
#[cfg(feature = "extensions-draft-08")]
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-08")]
storage.delete_application_export_tree::<_, ApplicationExportTree>(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()
}
}
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 set_max_past_epochs(&mut self, max_past_epochs: usize) {
self.message_secrets_store.resize(max_past_epochs);
}
pub(crate) fn message_secrets_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_mut(
&mut self,
epoch: GroupEpoch,
) -> Result<(&mut MessageSecrets, &[Member]), SecretTreeError> {
if epoch < self.context().epoch() {
self.message_secrets_store
.secrets_and_leaves_for_epoch_mut(epoch)
.ok_or(SecretTreeError::TooDistantInThePast)
} else {
Ok((self.message_secrets_store.message_secrets_mut(), &[]))
}
}
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())
}
pub(crate) fn encrypt<Provider: OpenMlsProvider>(
&mut self,
public_message: AuthenticatedContent,
provider: &Provider,
) -> Result<PrivateMessage, MessageEncryptionError<Provider::StorageError>> {
let padding_size = self.configuration().padding_size();
let msg = PrivateMessage::try_from_authenticated_content(
provider.crypto(),
provider.rand(),
&public_message,
self.ciphersuite(),
self.message_secrets_store.message_secrets_mut(),
padding_size,
)?;
provider
.storage()
.write_message_secrets(self.group_id(), &self.message_secrets_store)
.map_err(MessageEncryptionError::StorageError)?;
Ok(msg)
}
pub(crate) fn framing_parameters(&self) -> FramingParameters<'_> {
FramingParameters::new(
&self.aad,
self.mls_group_config.wire_format_policy().outgoing(),
)
}
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();
}
pub(crate) fn public_group(&self) -> &PublicGroup {
&self.public_group
}
}
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(),
)
}
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(),
)
}
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-08")]
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<MlsMessageOut, LibraryError> {
let msg = 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(),
)?;
}
plaintext.into()
}
OutgoingWireFormatPolicy::AlwaysCiphertext => {
let ciphertext = self
.encrypt(mls_auth_content, provider)
.map_err(|_| LibraryError::custom("Malformed plaintext"))?;
MlsMessageOut::from_private_message(ciphertext, self.version())
}
};
Ok(msg)
}
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-08")]
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-08")]
application_export_secret: ApplicationExportSecret,
resumption_psk_store: ResumptionPskStore,
verifiable_group_info: VerifiableGroupInfo,
key_package_bundle: KeyPackageBundle,
path_keypairs: Option<Vec<EncryptionKeyPair>>,
}
pub struct ProcessedWelcome {
mls_group_config: MlsGroupJoinConfig,
ciphersuite: Ciphersuite,
group_secrets: GroupSecrets,
key_schedule: crate::schedule::KeySchedule,
verifiable_group_info: crate::messages::group_info::VerifiableGroupInfo,
resumption_psk_store: crate::schedule::psk::store::ResumptionPskStore,
key_package_bundle: KeyPackageBundle,
}