1use std::collections::HashSet;
15
16use openmls_traits::{crypto::OpenMlsCrypto, types::Ciphersuite};
17use serde::{Deserialize, Serialize};
18
19use self::{
20 diff::{PublicGroupDiff, StagedPublicGroupDiff},
21 errors::CreationFromExternalError,
22};
23use super::{
24 proposal_store::{ProposalStore, QueuedProposal},
25 GroupContext, GroupId, Member, StagedCommit,
26};
27#[cfg(test)]
28use crate::treesync::{node::parent_node::PlainUpdatePathNode, treekem::UpdatePathNode};
29use crate::{
30 binary_tree::{
31 array_representation::{direct_path, TreeSize},
32 LeafNodeIndex,
33 },
34 ciphersuite::{hash_ref::ProposalRef, signable::Verifiable},
35 error::LibraryError,
36 extensions::RequiredCapabilitiesExtension,
37 framing::InterimTranscriptHashInput,
38 messages::{
39 group_info::{GroupInfo, VerifiableGroupInfo},
40 proposals::{Proposal, ProposalOrRefType, ProposalType},
41 ConfirmationTag, PathSecret,
42 },
43 schedule::CommitSecret,
44 storage::PublicStorageProvider,
45 treesync::{
46 errors::{DerivePathError, TreeSyncFromNodesError},
47 node::{
48 encryption_keys::{EncryptionKey, EncryptionKeyPair},
49 leaf_node::LeafNode,
50 },
51 RatchetTree, RatchetTreeIn, TreeSync,
52 },
53 versions::ProtocolVersion,
54};
55#[cfg(doc)]
56use crate::{framing::PublicMessage, group::MlsGroup};
57
58pub(crate) mod builder;
59pub(crate) mod diff;
60pub mod errors;
61pub mod process;
62pub(crate) mod staged_commit;
63#[cfg(test)]
64mod tests;
65mod validation;
66
67#[derive(Debug)]
69#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
70pub struct PublicGroup {
71 treesync: TreeSync,
72 proposal_store: ProposalStore,
73 group_context: GroupContext,
74 interim_transcript_hash: Vec<u8>,
75 confirmation_tag: ConfirmationTag,
77}
78
79#[derive(Debug, Serialize, Deserialize)]
81pub struct InterimTranscriptHash(pub Vec<u8>);
82
83impl PublicGroup {
84 pub(crate) fn new(
87 crypto: &impl OpenMlsCrypto,
88 treesync: TreeSync,
89 group_context: GroupContext,
90 initial_confirmation_tag: ConfirmationTag,
91 ) -> Result<Self, LibraryError> {
92 let interim_transcript_hash = {
93 let input = InterimTranscriptHashInput::from(&initial_confirmation_tag);
94
95 input.calculate_interim_transcript_hash(
96 crypto,
97 group_context.ciphersuite(),
98 group_context.confirmed_transcript_hash(),
99 )?
100 };
101
102 Ok(PublicGroup {
103 treesync,
104 proposal_store: ProposalStore::new(),
105 group_context,
106 interim_transcript_hash,
107 confirmation_tag: initial_confirmation_tag,
108 })
109 }
110
111 pub fn from_external<StorageProvider, StorageError>(
117 crypto: &impl OpenMlsCrypto,
118 storage: &StorageProvider,
119 ratchet_tree: RatchetTreeIn,
120 verifiable_group_info: VerifiableGroupInfo,
121 proposal_store: ProposalStore,
122 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>>
123 where
124 StorageProvider: PublicStorageProvider<Error = StorageError>,
125 {
126 let (public_group, group_info) = PublicGroup::from_external_internal(
127 crypto,
128 ratchet_tree,
129 verifiable_group_info,
130 proposal_store,
131 )?;
132
133 public_group
134 .store(storage)
135 .map_err(CreationFromExternalError::WriteToStorageError)?;
136
137 Ok((public_group, group_info))
138 }
139 pub(crate) fn from_external_internal<StorageError>(
140 crypto: &impl OpenMlsCrypto,
141 ratchet_tree: RatchetTreeIn,
142 verifiable_group_info: VerifiableGroupInfo,
143 proposal_store: ProposalStore,
144 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>> {
145 let ciphersuite = verifiable_group_info.ciphersuite();
146
147 let group_id = verifiable_group_info.group_id();
148 let ratchet_tree = ratchet_tree
149 .into_verified(ciphersuite, crypto, group_id)
150 .map_err(|e| {
151 CreationFromExternalError::TreeSyncError(TreeSyncFromNodesError::RatchetTreeError(
152 e,
153 ))
154 })?;
155
156 let treesync = TreeSync::from_ratchet_tree(crypto, ciphersuite, ratchet_tree)?;
160
161 let mut encryption_keys = HashSet::new();
162
163 treesync.full_leaves().try_for_each(|leaf_node| {
167 leaf_node.validate_locally()?;
168
169 if !encryption_keys.insert(leaf_node.encryption_key()) {
176 return Err(CreationFromExternalError::DuplicateEncryptionKey);
177 }
178
179 Ok(())
180 })?;
181
182 treesync
184 .full_parents()
185 .try_for_each(|(parent_index, parent_node)| {
186 if !encryption_keys.insert(parent_node.encryption_key()) {
193 return Err(CreationFromExternalError::DuplicateEncryptionKey);
194 }
195
196 parent_node
197 .unmerged_leaves()
198 .iter()
199 .try_for_each(|leaf_index| {
200 let path = direct_path(*leaf_index, treesync.tree_size());
201
202 let this_parent_offset = path
206 .iter()
207 .position(|x| x == &parent_index)
208 .ok_or(
209 CreationFromExternalError::<StorageError>::UnmergedLeafNotADescendant,
210 )?;
211 let path_leaf_to_this = &path[..this_parent_offset];
212
213
214 path_leaf_to_this
218 .iter()
219 .try_for_each(|intermediate_index| {
220 if let Some(intermediate_node) = treesync
222 .parent(*intermediate_index) {
223 if !intermediate_node.unmerged_leaves().contains(leaf_index) {
224 return Err(CreationFromExternalError::<StorageError>::IntermediateNodeMissingUnmergedLeaf);
225 }
226 }
227
228 Ok(())
229 })
230 })
231 })?;
232
233 let group_info: GroupInfo = {
235 let signer_signature_key = treesync
236 .leaf(verifiable_group_info.signer())
237 .ok_or(CreationFromExternalError::UnknownSender)?
238 .signature_key()
239 .clone()
240 .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
241
242 verifiable_group_info
243 .verify(crypto, &signer_signature_key)
244 .map_err(|_| CreationFromExternalError::InvalidGroupInfoSignature)?
245 };
246
247 if treesync.tree_hash() != group_info.group_context().tree_hash() {
249 return Err(CreationFromExternalError::TreeHashMismatch);
250 }
251
252 if group_info.group_context().protocol_version() != ProtocolVersion::Mls10 {
253 return Err(CreationFromExternalError::UnsupportedMlsVersion);
254 }
255
256 let group_context = group_info.group_context().clone();
257
258 let interim_transcript_hash = {
259 let input = InterimTranscriptHashInput::from(group_info.confirmation_tag());
260
261 input.calculate_interim_transcript_hash(
262 crypto,
263 group_context.ciphersuite(),
264 group_context.confirmed_transcript_hash(),
265 )?
266 };
267
268 let public_group = Self {
269 treesync,
270 group_context,
271 interim_transcript_hash,
272 confirmation_tag: group_info.confirmation_tag().clone(),
273 proposal_store,
274 };
275
276 public_group
279 .treesync
280 .full_leaves()
281 .try_for_each(|leaf_node| public_group.validate_leaf_node(leaf_node))?;
282
283 Ok((public_group, group_info))
284 }
285
286 pub fn ext_commit_sender_index(
288 &self,
289 commit: &StagedCommit,
290 ) -> Result<LeafNodeIndex, LibraryError> {
291 self.leftmost_free_index(commit.queued_proposals().filter_map(|p| {
292 if matches!(p.proposal_or_ref_type(), ProposalOrRefType::Proposal) {
293 Some(Some(p.proposal()))
294 } else {
295 None
296 }
297 }))
298 }
299
300 pub(crate) fn leftmost_free_index<'a>(
307 &self,
308 mut inline_proposals: impl Iterator<Item = Option<&'a Proposal>>,
309 ) -> Result<LeafNodeIndex, LibraryError> {
310 let free_leaf_index = self.treesync().free_leaf_index();
312 let remove_proposal_option = inline_proposals
314 .find(|proposal| match proposal {
315 Some(p) => p.is_type(ProposalType::Remove),
316 None => false,
317 })
318 .flatten();
319 let leaf_index = if let Some(remove_proposal) = remove_proposal_option {
320 if let Proposal::Remove(remove_proposal) = remove_proposal {
321 let removed_index = remove_proposal.removed();
322 if removed_index < free_leaf_index {
324 removed_index
325 } else {
326 free_leaf_index
327 }
328 } else {
329 return Err(LibraryError::custom("missing key package"));
330 }
331 } else {
332 free_leaf_index
333 };
334 Ok(leaf_index)
335 }
336
337 pub(crate) fn empty_diff(&self) -> PublicGroupDiff {
339 PublicGroupDiff::new(self)
340 }
341
342 pub(crate) fn merge_diff(&mut self, diff: StagedPublicGroupDiff) {
348 self.treesync.merge_diff(diff.staged_diff);
349 self.group_context = diff.group_context;
350 self.interim_transcript_hash = diff.interim_transcript_hash;
351 self.confirmation_tag = diff.confirmation_tag;
352 }
353
354 pub(crate) fn derive_path_secrets(
368 &self,
369 crypto: &impl OpenMlsCrypto,
370 ciphersuite: Ciphersuite,
371 path_secret: PathSecret,
372 sender_index: LeafNodeIndex,
373 leaf_index: LeafNodeIndex,
374 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), DerivePathError> {
375 self.treesync.derive_path_secrets(
376 crypto,
377 ciphersuite,
378 path_secret,
379 sender_index,
380 leaf_index,
381 )
382 }
383
384 pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
386 self.treesync().full_leave_members()
387 }
388
389 pub fn export_ratchet_tree(&self) -> RatchetTree {
391 self.treesync().export_ratchet_tree()
392 }
393
394 pub fn add_proposal<Storage: PublicStorageProvider>(
396 &mut self,
397 storage: &Storage,
398 proposal: QueuedProposal,
399 ) -> Result<(), Storage::Error> {
400 storage.queue_proposal(self.group_id(), &proposal.proposal_reference(), &proposal)?;
401 self.proposal_store.add(proposal);
402 Ok(())
403 }
404
405 pub fn remove_proposal<Storage: PublicStorageProvider>(
407 &mut self,
408 storage: &Storage,
409 proposal_ref: &ProposalRef,
410 ) -> Result<(), Storage::Error> {
411 storage.remove_proposal(self.group_id(), proposal_ref)?;
412 self.proposal_store.remove(proposal_ref);
413 Ok(())
414 }
415
416 pub fn queued_proposals<Storage: PublicStorageProvider>(
418 &self,
419 storage: &Storage,
420 ) -> Result<Vec<(ProposalRef, QueuedProposal)>, Storage::Error> {
421 storage.queued_proposals(self.group_id())
422 }
423}
424
425impl PublicGroup {
427 pub fn ciphersuite(&self) -> Ciphersuite {
429 self.group_context.ciphersuite()
430 }
431
432 pub fn version(&self) -> ProtocolVersion {
434 self.group_context.protocol_version()
435 }
436
437 pub fn group_id(&self) -> &GroupId {
439 self.group_context.group_id()
440 }
441
442 pub fn group_context(&self) -> &GroupContext {
444 &self.group_context
445 }
446
447 pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
449 self.group_context.required_capabilities()
450 }
451
452 fn treesync(&self) -> &TreeSync {
454 &self.treesync
455 }
456
457 pub fn confirmation_tag(&self) -> &ConfirmationTag {
459 &self.confirmation_tag
460 }
461
462 pub fn leaf(&self, leaf_index: LeafNodeIndex) -> Option<&LeafNode> {
465 self.treesync().leaf(leaf_index)
466 }
467
468 pub(crate) fn tree_size(&self) -> TreeSize {
470 self.treesync().tree_size()
471 }
472
473 fn interim_transcript_hash(&self) -> &[u8] {
474 &self.interim_transcript_hash
475 }
476
477 pub(crate) fn owned_encryption_keys(&self, leaf_index: LeafNodeIndex) -> Vec<EncryptionKey> {
480 self.treesync().owned_encryption_keys(leaf_index)
481 }
482
483 pub(crate) fn store<Storage: PublicStorageProvider>(
488 &self,
489 storage: &Storage,
490 ) -> Result<(), Storage::Error> {
491 let group_id = self.group_context.group_id();
492 storage.write_tree(group_id, self.treesync())?;
493 storage.write_confirmation_tag(group_id, self.confirmation_tag())?;
494 storage.write_context(group_id, self.group_context())?;
495 storage.write_interim_transcript_hash(
496 group_id,
497 &InterimTranscriptHash(self.interim_transcript_hash.clone()),
498 )?;
499 Ok(())
500 }
501
502 pub fn delete<Storage: PublicStorageProvider>(
504 storage: &Storage,
505 group_id: &GroupId,
506 ) -> Result<(), Storage::Error> {
507 storage.delete_tree(group_id)?;
508 storage.delete_confirmation_tag(group_id)?;
509 storage.delete_context(group_id)?;
510 storage.delete_interim_transcript_hash(group_id)?;
511
512 Ok(())
513 }
514
515 pub fn load<Storage: PublicStorageProvider>(
517 storage: &Storage,
518 group_id: &GroupId,
519 ) -> Result<Option<Self>, Storage::Error> {
520 let treesync = storage.tree(group_id)?;
521 let proposals: Vec<(ProposalRef, QueuedProposal)> = storage.queued_proposals(group_id)?;
522 let group_context = storage.group_context(group_id)?;
523 let interim_transcript_hash: Option<InterimTranscriptHash> =
524 storage.interim_transcript_hash(group_id)?;
525 let confirmation_tag = storage.confirmation_tag(group_id)?;
526 let mut proposal_store = ProposalStore::new();
527
528 for (_ref, proposal) in proposals {
529 proposal_store.add(proposal);
530 }
531
532 let build = || -> Option<Self> {
533 Some(Self {
534 treesync: treesync?,
535 proposal_store,
536 group_context: group_context?,
537 interim_transcript_hash: interim_transcript_hash?.0,
538 confirmation_tag: confirmation_tag?,
539 })
540 };
541
542 Ok(build())
543 }
544
545 pub(crate) fn proposal_store(&self) -> &ProposalStore {
547 &self.proposal_store
548 }
549
550 pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
552 &mut self.proposal_store
553 }
554}
555
556#[cfg(any(feature = "test-utils", test))]
558impl PublicGroup {
559 pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
560 &mut self.group_context
561 }
562
563 #[cfg(test)]
564 pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
565 self.group_context = group_context;
566 }
567
568 #[cfg(test)]
569 pub(crate) fn encrypt_path(
570 &self,
571 provider: &impl crate::storage::OpenMlsProvider,
572 ciphersuite: Ciphersuite,
573 path: &[PlainUpdatePathNode],
574 group_context: &[u8],
575 exclusion_list: &HashSet<&LeafNodeIndex>,
576 own_leaf_index: LeafNodeIndex,
577 ) -> Result<Vec<UpdatePathNode>, LibraryError> {
578 self.treesync().empty_diff().encrypt_path(
579 provider.crypto(),
580 ciphersuite,
581 path,
582 group_context,
583 exclusion_list,
584 own_leaf_index,
585 )
586 }
587}