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, Sender},
38 group::mls_group::creation::LeafNodeLifetimePolicy,
39 messages::{
40 group_info::{GroupInfo, VerifiableGroupInfo},
41 proposals::Proposal,
42 ConfirmationTag, PathSecret,
43 },
44 schedule::CommitSecret,
45 storage::PublicStorageProvider,
46 treesync::{
47 errors::{DerivePathError, TreeSyncFromNodesError},
48 node::{
49 encryption_keys::{EncryptionKey, EncryptionKeyPair},
50 leaf_node::LeafNode,
51 },
52 RatchetTree, RatchetTreeIn, TreeSync,
53 },
54 versions::ProtocolVersion,
55};
56#[cfg(doc)]
57use crate::{framing::PublicMessage, group::MlsGroup};
58
59pub(crate) mod builder;
60pub(crate) mod diff;
61pub mod errors;
62pub mod process;
63pub(crate) mod staged_commit;
64#[cfg(test)]
65mod tests;
66mod validation;
67
68#[cfg(feature = "migration-import")]
69mod migration_import;
70
71#[derive(Debug)]
73#[cfg_attr(feature = "migration-import", derive(serde::Deserialize))]
74#[cfg_attr(
75 all(feature = "test-utils", feature = "migration-import"),
76 derive(serde::Serialize)
77)]
78#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
79pub struct PublicGroup {
80 treesync: TreeSync,
81 proposal_store: ProposalStore,
82 group_context: GroupContext,
83 interim_transcript_hash: Vec<u8>,
84 confirmation_tag: ConfirmationTag,
86}
87
88#[derive(Debug, Serialize, Deserialize)]
90pub struct InterimTranscriptHash(pub Vec<u8>);
91
92impl PublicGroup {
93 pub(crate) fn new(
96 crypto: &impl OpenMlsCrypto,
97 treesync: TreeSync,
98 group_context: GroupContext,
99 initial_confirmation_tag: ConfirmationTag,
100 ) -> Result<Self, LibraryError> {
101 let interim_transcript_hash = {
102 let input = InterimTranscriptHashInput::from(&initial_confirmation_tag);
103
104 input.calculate_interim_transcript_hash(
105 crypto,
106 group_context.ciphersuite(),
107 group_context.confirmed_transcript_hash(),
108 )?
109 };
110
111 Ok(PublicGroup {
112 treesync,
113 proposal_store: ProposalStore::new(),
114 group_context,
115 interim_transcript_hash,
116 confirmation_tag: initial_confirmation_tag,
117 })
118 }
119
120 #[openmls_traits::maybe_async]
126 pub async fn from_external<StorageProvider, StorageError>(
127 crypto: &impl OpenMlsCrypto,
128 storage: &StorageProvider,
129 ratchet_tree: RatchetTreeIn,
130 verifiable_group_info: VerifiableGroupInfo,
131 proposal_store: ProposalStore,
132 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>>
133 where
134 StorageProvider: PublicStorageProvider<Error = StorageError>,
135 {
136 Self::from_external_with_lifetime_policy(
137 crypto,
138 storage,
139 ratchet_tree,
140 verifiable_group_info,
141 proposal_store,
142 LeafNodeLifetimePolicy::Verify,
143 )
144 .await
145 }
146
147 #[openmls_traits::maybe_async]
150 pub async fn from_external_with_lifetime_policy<StorageProvider, StorageError>(
151 crypto: &impl OpenMlsCrypto,
152 storage: &StorageProvider,
153 ratchet_tree: RatchetTreeIn,
154 verifiable_group_info: VerifiableGroupInfo,
155 proposal_store: ProposalStore,
156 lifetime_policy: LeafNodeLifetimePolicy,
157 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>>
158 where
159 StorageProvider: PublicStorageProvider<Error = StorageError>,
160 {
161 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
162 crypto,
163 ratchet_tree,
164 verifiable_group_info,
165 proposal_store,
166 lifetime_policy,
167 )?;
168
169 public_group
170 .store(storage)
171 .await
172 .map_err(CreationFromExternalError::WriteToStorageError)?;
173
174 Ok((public_group, group_info))
175 }
176
177 pub(crate) fn from_ratchet_tree<StorageError>(
178 crypto: &impl OpenMlsCrypto,
179 ratchet_tree: RatchetTreeIn,
180 verifiable_group_info: VerifiableGroupInfo,
181 proposal_store: ProposalStore,
182 validate_lifetimes: LeafNodeLifetimePolicy,
183 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>> {
184 let ciphersuite = verifiable_group_info.ciphersuite();
185 crypto
186 .supports(ciphersuite)
187 .map_err(|_| CreationFromExternalError::UnsupportedCiphersuite(ciphersuite))?;
188
189 let group_id = verifiable_group_info.group_id();
190 let ratchet_tree = ratchet_tree
191 .into_verified(ciphersuite, crypto, group_id)
192 .map_err(|e| {
193 CreationFromExternalError::TreeSyncError(TreeSyncFromNodesError::RatchetTreeError(
194 e,
195 ))
196 })?;
197
198 let treesync = TreeSync::from_ratchet_tree(crypto, ciphersuite, ratchet_tree)?;
202
203 let mut encryption_keys = HashSet::new();
204 let mut signature_keys = HashSet::new();
205
206 treesync.full_leaves().try_for_each(|(_, leaf_node)| {
211 leaf_node.validate_locally()?;
212
213 if !signature_keys.insert(leaf_node.signature_key()) {
216 return Err(CreationFromExternalError::DuplicateSignatureKey);
217 }
218
219 if !encryption_keys.insert(leaf_node.encryption_key()) {
222 return Err(CreationFromExternalError::DuplicateEncryptionKey);
223 }
224
225 Ok(())
226 })?;
227
228 treesync
230 .full_parents()
231 .try_for_each(|(parent_index, parent_node)| {
232 if !encryption_keys.insert(parent_node.encryption_key()) {
239 return Err(CreationFromExternalError::DuplicateEncryptionKey);
240 }
241
242 parent_node
243 .unmerged_leaves()
244 .iter()
245 .try_for_each(|leaf_index| {
246 let path = direct_path(*leaf_index, treesync.tree_size());
247
248 let this_parent_offset = path
252 .iter()
253 .position(|x| x == &parent_index)
254 .ok_or(
255 CreationFromExternalError::<StorageError>::UnmergedLeafNotADescendant,
256 )?;
257 let path_leaf_to_this = &path[..this_parent_offset];
258
259
260 path_leaf_to_this
264 .iter()
265 .try_for_each(|intermediate_index| {
266 if let Some(intermediate_node) = treesync
268 .parent(*intermediate_index) {
269 if intermediate_node
273 .unmerged_leaves()
274 .binary_search(leaf_index)
275 .is_err()
276 {
277 return Err(CreationFromExternalError::<StorageError>::IntermediateNodeMissingUnmergedLeaf);
278 }
279 }
280
281 Ok(())
282 })
283 })
284 })?;
285
286 let group_info: GroupInfo = {
288 let signer_signature_key = treesync
289 .leaf(verifiable_group_info.signer())
290 .ok_or(CreationFromExternalError::UnknownSender)?
291 .signature_key()
292 .clone()
293 .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
294
295 verifiable_group_info
296 .verify(crypto, &signer_signature_key)
297 .map_err(|_| CreationFromExternalError::InvalidGroupInfoSignature)?
298 };
299
300 if treesync.tree_hash() != group_info.group_context().tree_hash() {
302 return Err(CreationFromExternalError::TreeHashMismatch);
303 }
304
305 if group_info.group_context().protocol_version() != ProtocolVersion::Mls10 {
306 return Err(CreationFromExternalError::UnsupportedMlsVersion);
307 }
308
309 let group_context = group_info.group_context().clone();
310
311 let interim_transcript_hash = {
312 let input = InterimTranscriptHashInput::from(group_info.confirmation_tag());
313
314 input.calculate_interim_transcript_hash(
315 crypto,
316 group_context.ciphersuite(),
317 group_context.confirmed_transcript_hash(),
318 )?
319 };
320
321 let public_group = Self {
322 treesync,
323 group_context,
324 interim_transcript_hash,
325 confirmation_tag: group_info.confirmation_tag().clone(),
326 proposal_store,
327 };
328
329 public_group
332 .treesync
333 .full_leaves()
334 .try_for_each(|(_, leaf_node)| {
335 public_group.validate_leaf_node(leaf_node, validate_lifetimes)
336 })?;
337
338 Ok((public_group, group_info))
339 }
340
341 pub fn ext_commit_sender_index(
343 &self,
344 commit: &StagedCommit,
345 ) -> Result<LeafNodeIndex, LibraryError> {
346 self.leftmost_free_index(commit.queued_proposals())
347 }
348
349 pub(crate) fn leftmost_free_index<'a>(
356 &self,
357 queued_proposals: impl Iterator<Item = &'a QueuedProposal>,
358 ) -> Result<LeafNodeIndex, LibraryError> {
359 let free_leaf_index = self.treesync().free_leaf_index();
361 let removed_indices = queued_proposals.filter_map(|proposal| {
364 match (proposal.proposal(), proposal.sender()) {
365 (Proposal::Remove(r), _) => Some(r.removed),
366 (Proposal::SelfRemove, Sender::Member(sender)) => Some(*sender),
367 _ => None, }
369 });
370 removed_indices
373 .into_iter()
374 .chain(std::iter::once(free_leaf_index))
375 .min()
376 .ok_or_else(|| LibraryError::custom("No free leaf index found"))
377 }
378
379 pub(crate) fn empty_diff(&self) -> PublicGroupDiff<'_> {
381 PublicGroupDiff::new(self)
382 }
383
384 pub(crate) fn merge_diff(&mut self, diff: StagedPublicGroupDiff) {
390 self.treesync.merge_diff(diff.staged_diff);
391 self.group_context = diff.group_context;
392 self.interim_transcript_hash = diff.interim_transcript_hash;
393 self.confirmation_tag = diff.confirmation_tag;
394 }
395
396 pub(crate) fn derive_path_secrets(
410 &self,
411 crypto: &impl OpenMlsCrypto,
412 ciphersuite: Ciphersuite,
413 path_secret: PathSecret,
414 sender_index: LeafNodeIndex,
415 leaf_index: LeafNodeIndex,
416 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), DerivePathError> {
417 self.treesync.derive_path_secrets(
418 crypto,
419 ciphersuite,
420 path_secret,
421 sender_index,
422 leaf_index,
423 )
424 }
425
426 pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
428 self.treesync().full_leaf_members()
429 }
430
431 pub fn export_ratchet_tree(&self) -> RatchetTree {
433 self.treesync().export_ratchet_tree()
434 }
435
436 #[openmls_traits::maybe_async]
438 pub async fn add_proposal<Storage: PublicStorageProvider>(
439 &mut self,
440 storage: &Storage,
441 proposal: QueuedProposal,
442 ) -> Result<(), Storage::Error> {
443 storage
444 .queue_proposal(self.group_id(), &proposal.proposal_reference(), &proposal)
445 .await?;
446 self.proposal_store.add(proposal);
447 Ok(())
448 }
449
450 #[openmls_traits::maybe_async]
452 pub async fn remove_proposal<Storage: PublicStorageProvider>(
453 &mut self,
454 storage: &Storage,
455 proposal_ref: &ProposalRef,
456 ) -> Result<(), Storage::Error> {
457 storage
458 .remove_proposal(self.group_id(), proposal_ref)
459 .await?;
460 self.proposal_store.remove(proposal_ref);
461 Ok(())
462 }
463
464 #[openmls_traits::maybe_async]
466 pub async fn queued_proposals<Storage: PublicStorageProvider>(
467 &self,
468 storage: &Storage,
469 ) -> Result<Vec<(ProposalRef, QueuedProposal)>, Storage::Error> {
470 storage.queued_proposals(self.group_id()).await
471 }
472}
473
474impl PublicGroup {
476 pub fn ciphersuite(&self) -> Ciphersuite {
478 self.group_context.ciphersuite()
479 }
480
481 pub fn version(&self) -> ProtocolVersion {
483 self.group_context.protocol_version()
484 }
485
486 pub fn group_id(&self) -> &GroupId {
488 self.group_context.group_id()
489 }
490
491 pub fn group_context(&self) -> &GroupContext {
493 &self.group_context
494 }
495
496 pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
498 self.group_context.required_capabilities()
499 }
500
501 pub fn treesync(&self) -> &TreeSync {
503 &self.treesync
504 }
505
506 pub fn confirmation_tag(&self) -> &ConfirmationTag {
508 &self.confirmation_tag
509 }
510
511 pub fn leaf(&self, leaf_index: LeafNodeIndex) -> Option<&LeafNode> {
514 self.treesync().leaf(leaf_index)
515 }
516
517 pub(crate) fn tree_size(&self) -> TreeSize {
519 self.treesync().tree_size()
520 }
521
522 fn interim_transcript_hash(&self) -> &[u8] {
523 &self.interim_transcript_hash
524 }
525
526 pub(crate) fn owned_encryption_keys(&self, leaf_index: LeafNodeIndex) -> Vec<EncryptionKey> {
529 self.treesync().owned_encryption_keys(leaf_index)
530 }
531
532 #[openmls_traits::maybe_async]
537 pub(crate) async fn store<Storage: PublicStorageProvider>(
538 &self,
539 storage: &Storage,
540 ) -> Result<(), Storage::Error> {
541 let group_id = self.group_context.group_id();
542 storage.write_tree(group_id, self.treesync()).await?;
543 storage
544 .write_confirmation_tag(group_id, self.confirmation_tag())
545 .await?;
546 storage
547 .write_context(group_id, self.group_context())
548 .await?;
549 storage
550 .write_interim_transcript_hash(
551 group_id,
552 &InterimTranscriptHash(self.interim_transcript_hash.clone()),
553 )
554 .await?;
555 Ok(())
556 }
557
558 #[openmls_traits::maybe_async]
560 pub async fn delete<Storage: PublicStorageProvider>(
561 storage: &Storage,
562 group_id: &GroupId,
563 ) -> Result<(), Storage::Error> {
564 storage.delete_tree(group_id).await?;
565 storage.delete_confirmation_tag(group_id).await?;
566 storage.delete_context(group_id).await?;
567 storage.delete_interim_transcript_hash(group_id).await?;
568
569 Ok(())
570 }
571
572 #[openmls_traits::maybe_async]
574 pub async fn load<Storage: PublicStorageProvider>(
575 storage: &Storage,
576 group_id: &GroupId,
577 ) -> Result<Option<Self>, Storage::Error> {
578 let treesync = storage.tree(group_id).await?;
579 let proposals: Vec<(ProposalRef, QueuedProposal)> =
580 storage.queued_proposals(group_id).await?;
581 let group_context = storage.group_context(group_id).await?;
582 let interim_transcript_hash: Option<InterimTranscriptHash> =
583 storage.interim_transcript_hash(group_id).await?;
584 let confirmation_tag = storage.confirmation_tag(group_id).await?;
585 let mut proposal_store = ProposalStore::new();
586
587 for (_ref, proposal) in proposals {
588 proposal_store.add(proposal);
589 }
590
591 let build = || -> Option<Self> {
592 Some(Self {
593 treesync: treesync?,
594 proposal_store,
595 group_context: group_context?,
596 interim_transcript_hash: interim_transcript_hash?.0,
597 confirmation_tag: confirmation_tag?,
598 })
599 };
600
601 Ok(build())
602 }
603
604 pub(crate) fn proposal_store(&self) -> &ProposalStore {
606 &self.proposal_store
607 }
608
609 pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
611 &mut self.proposal_store
612 }
613}
614
615#[cfg(any(feature = "test-utils", test))]
617impl PublicGroup {
618 pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
619 &mut self.group_context
620 }
621
622 #[cfg(test)]
623 pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
624 self.group_context = group_context;
625 }
626
627 #[cfg(test)]
628 pub(crate) fn encrypt_path(
629 &self,
630 provider: &impl crate::storage::OpenMlsProvider,
631 ciphersuite: Ciphersuite,
632 path: &[PlainUpdatePathNode],
633 group_context: &[u8],
634 exclusion_list: &HashSet<&LeafNodeIndex>,
635 own_leaf_index: LeafNodeIndex,
636 ) -> Result<Vec<UpdatePathNode>, LibraryError> {
637 self.treesync().empty_diff().encrypt_path(
638 provider.crypto(),
639 ciphersuite,
640 path,
641 group_context,
642 exclusion_list,
643 own_leaf_index,
644 )
645 }
646}