#[cfg(test)]
use std::collections::HashSet;
use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
use serde::{Deserialize, Serialize};
use self::{
diff::{PublicGroupDiff, StagedPublicGroupDiff},
errors::CreationFromExternalError,
};
use super::{GroupContext, GroupId, Member, ProposalStore, QueuedProposal, StagedCommit};
#[cfg(test)]
use crate::treesync::{node::parent_node::PlainUpdatePathNode, treekem::UpdatePathNode};
use crate::{
binary_tree::{array_representation::TreeSize, LeafNodeIndex},
ciphersuite::signable::Verifiable,
error::LibraryError,
extensions::RequiredCapabilitiesExtension,
framing::InterimTranscriptHashInput,
messages::{
group_info::{GroupInfo, VerifiableGroupInfo},
proposals::{Proposal, ProposalOrRefType, ProposalType},
ConfirmationTag, PathSecret,
},
schedule::CommitSecret,
storage::{OpenMlsProvider, StorageProvider},
treesync::{
errors::{DerivePathError, TreeSyncFromNodesError},
node::{
encryption_keys::{EncryptionKey, EncryptionKeyPair},
leaf_node::LeafNode,
},
RatchetTree, RatchetTreeIn, TreeSync,
},
versions::ProtocolVersion,
};
#[cfg(doc)]
use crate::{framing::PublicMessage, group::CoreGroup};
pub(crate) mod builder;
pub(crate) mod diff;
pub mod errors;
pub mod process;
pub(crate) mod staged_commit;
#[cfg(test)]
mod tests;
mod validation;
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(test, derive(PartialEq, Clone))]
pub struct PublicGroup {
treesync: TreeSync,
proposal_store: ProposalStore,
group_context: GroupContext,
interim_transcript_hash: Vec<u8>,
confirmation_tag: ConfirmationTag,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct InterimTranscriptHash(pub Vec<u8>);
impl PublicGroup {
pub(crate) fn new(
crypto: &impl OpenMlsCrypto,
treesync: TreeSync,
group_context: GroupContext,
initial_confirmation_tag: ConfirmationTag,
) -> Result<Self, LibraryError> {
let interim_transcript_hash = {
let input = InterimTranscriptHashInput::from(&initial_confirmation_tag);
input.calculate_interim_transcript_hash(
crypto,
group_context.ciphersuite(),
group_context.confirmed_transcript_hash(),
)?
};
Ok(PublicGroup {
treesync,
proposal_store: ProposalStore::new(),
group_context,
interim_transcript_hash,
confirmation_tag: initial_confirmation_tag,
})
}
pub fn from_external<Provider: OpenMlsProvider>(
provider: &Provider,
ratchet_tree: RatchetTreeIn,
verifiable_group_info: VerifiableGroupInfo,
proposal_store: ProposalStore,
) -> Result<(Self, GroupInfo), CreationFromExternalError<Provider::StorageError>> {
let ciphersuite = verifiable_group_info.ciphersuite();
let crypto = provider.crypto();
let group_id = verifiable_group_info.group_id();
let ratchet_tree = ratchet_tree
.into_verified(ciphersuite, crypto, group_id)
.map_err(|e| {
CreationFromExternalError::TreeSyncError(TreeSyncFromNodesError::RatchetTreeError(
e,
))
})?;
let treesync = TreeSync::from_ratchet_tree(crypto, ciphersuite, ratchet_tree)?;
let group_info: GroupInfo = {
let signer_signature_key = treesync
.leaf(verifiable_group_info.signer())
.ok_or(CreationFromExternalError::UnknownSender)?
.signature_key()
.clone()
.into_signature_public_key_enriched(ciphersuite.signature_algorithm());
verifiable_group_info
.verify(crypto, &signer_signature_key)
.map_err(|_| CreationFromExternalError::InvalidGroupInfoSignature)?
};
if treesync.tree_hash() != group_info.group_context().tree_hash() {
return Err(CreationFromExternalError::TreeHashMismatch);
}
if group_info.group_context().protocol_version() != ProtocolVersion::Mls10 {
return Err(CreationFromExternalError::UnsupportedMlsVersion);
}
let group_context = GroupContext::from(group_info.clone());
let interim_transcript_hash = {
let input = InterimTranscriptHashInput::from(group_info.confirmation_tag());
input.calculate_interim_transcript_hash(
crypto,
group_context.ciphersuite(),
group_context.confirmed_transcript_hash(),
)?
};
let public_group = Self {
treesync,
group_context,
interim_transcript_hash,
confirmation_tag: group_info.confirmation_tag().clone(),
proposal_store,
};
public_group
.store(provider.storage())
.map_err(CreationFromExternalError::WriteToStorageError)?;
Ok((public_group, group_info))
}
pub fn ext_commit_sender_index(
&self,
commit: &StagedCommit,
) -> Result<LeafNodeIndex, LibraryError> {
self.leftmost_free_index(commit.queued_proposals().filter_map(|p| {
if matches!(p.proposal_or_ref_type(), ProposalOrRefType::Proposal) {
Some(Some(p.proposal()))
} else {
None
}
}))
}
pub(crate) fn leftmost_free_index<'a>(
&self,
mut inline_proposals: impl Iterator<Item = Option<&'a Proposal>>,
) -> Result<LeafNodeIndex, LibraryError> {
let free_leaf_index = self.treesync().free_leaf_index();
let remove_proposal_option = inline_proposals
.find(|proposal| match proposal {
Some(p) => p.is_type(ProposalType::Remove),
None => false,
})
.flatten();
let leaf_index = if let Some(remove_proposal) = remove_proposal_option {
if let Proposal::Remove(remove_proposal) = remove_proposal {
let removed_index = remove_proposal.removed();
if removed_index < free_leaf_index {
removed_index
} else {
free_leaf_index
}
} else {
return Err(LibraryError::custom("missing key package"));
}
} else {
free_leaf_index
};
Ok(leaf_index)
}
pub(crate) fn empty_diff(&self) -> PublicGroupDiff {
PublicGroupDiff::new(self)
}
pub(crate) fn merge_diff(&mut self, diff: StagedPublicGroupDiff) {
self.treesync.merge_diff(diff.staged_diff);
self.group_context = diff.group_context;
self.interim_transcript_hash = diff.interim_transcript_hash;
self.confirmation_tag = diff.confirmation_tag;
}
pub(crate) fn derive_path_secrets(
&self,
crypto: &impl OpenMlsCrypto,
ciphersuite: Ciphersuite,
path_secret: PathSecret,
sender_index: LeafNodeIndex,
leaf_index: LeafNodeIndex,
) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), DerivePathError> {
self.treesync.derive_path_secrets(
crypto,
ciphersuite,
path_secret,
sender_index,
leaf_index,
)
}
pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
self.treesync().full_leave_members()
}
pub fn export_ratchet_tree(&self) -> RatchetTree {
self.treesync().export_ratchet_tree()
}
pub fn add_proposal(&mut self, proposal: QueuedProposal) {
self.proposal_store.add(proposal)
}
}
impl PublicGroup {
pub fn ciphersuite(&self) -> Ciphersuite {
self.group_context.ciphersuite()
}
pub fn version(&self) -> ProtocolVersion {
self.group_context.protocol_version()
}
pub fn group_id(&self) -> &GroupId {
self.group_context.group_id()
}
pub fn group_context(&self) -> &GroupContext {
&self.group_context
}
pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
self.group_context.required_capabilities()
}
fn treesync(&self) -> &TreeSync {
&self.treesync
}
pub fn confirmation_tag(&self) -> &ConfirmationTag {
&self.confirmation_tag
}
pub fn leaf(&self, leaf_index: LeafNodeIndex) -> Option<&LeafNode> {
self.treesync().leaf(leaf_index)
}
pub(crate) fn tree_size(&self) -> TreeSize {
self.treesync().tree_size()
}
fn interim_transcript_hash(&self) -> &[u8] {
&self.interim_transcript_hash
}
pub(crate) fn owned_encryption_keys(&self, leaf_index: LeafNodeIndex) -> Vec<EncryptionKey> {
self.treesync().owned_encryption_keys(leaf_index)
}
pub(crate) fn store<Storage: StorageProvider>(
&self,
storage: &Storage,
) -> Result<(), Storage::Error> {
let group_id = self.group_context.group_id();
storage.write_tree(group_id, self.treesync())?;
storage.write_confirmation_tag(group_id, self.confirmation_tag())?;
storage.write_context(group_id, self.group_context())?;
storage.write_interim_transcript_hash(
group_id,
&InterimTranscriptHash(self.interim_transcript_hash.clone()),
)?;
Ok(())
}
pub(crate) fn delete<Storage: StorageProvider>(
&self,
storage: &Storage,
) -> Result<(), Storage::Error> {
storage.delete_tree(self.group_id())?;
storage.delete_confirmation_tag(self.group_id())?;
storage.delete_context(self.group_id())?;
storage.delete_interim_transcript_hash(self.group_id())?;
Ok(())
}
pub(crate) fn load<Storage: StorageProvider>(
storage: &Storage,
group_id: &GroupId,
) -> Result<Option<Self>, Storage::Error> {
let treesync = storage.treesync(group_id)?;
let group_context = storage.group_context(group_id)?;
let interim_transcript_hash: Option<InterimTranscriptHash> =
storage.interim_transcript_hash(group_id)?;
let confirmation_tag = storage.confirmation_tag(group_id)?;
let build = || -> Option<Self> {
Some(Self {
treesync: treesync?,
proposal_store: ProposalStore::new(),
group_context: group_context?,
interim_transcript_hash: interim_transcript_hash?.0,
confirmation_tag: confirmation_tag?,
})
};
Ok(build())
}
}
#[cfg(any(feature = "test-utils", test))]
impl PublicGroup {
pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
&mut self.group_context
}
#[cfg(test)]
pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
self.group_context = group_context;
}
#[cfg(test)]
pub(crate) fn encrypt_path(
&self,
provider: &impl OpenMlsProvider,
ciphersuite: Ciphersuite,
path: &[PlainUpdatePathNode],
group_context: &[u8],
exclusion_list: &HashSet<&LeafNodeIndex>,
own_leaf_index: LeafNodeIndex,
) -> Result<Vec<UpdatePathNode>, LibraryError> {
self.treesync().empty_diff().encrypt_path(
provider.crypto(),
ciphersuite,
path,
group_context,
exclusion_list,
own_leaf_index,
)
}
}