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 pub fn from_external<StorageProvider, StorageError>(
126 crypto: &impl OpenMlsCrypto,
127 storage: &StorageProvider,
128 ratchet_tree: RatchetTreeIn,
129 verifiable_group_info: VerifiableGroupInfo,
130 proposal_store: ProposalStore,
131 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>>
132 where
133 StorageProvider: PublicStorageProvider<Error = StorageError>,
134 {
135 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
136 crypto,
137 ratchet_tree,
138 verifiable_group_info,
139 proposal_store,
140 LeafNodeLifetimePolicy::Verify,
141 )?;
142
143 public_group
144 .store(storage)
145 .map_err(CreationFromExternalError::WriteToStorageError)?;
146
147 Ok((public_group, group_info))
148 }
149
150 pub(crate) fn from_ratchet_tree<StorageError>(
151 crypto: &impl OpenMlsCrypto,
152 ratchet_tree: RatchetTreeIn,
153 verifiable_group_info: VerifiableGroupInfo,
154 proposal_store: ProposalStore,
155 validate_lifetimes: LeafNodeLifetimePolicy,
156 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>> {
157 let ciphersuite = verifiable_group_info.ciphersuite();
158 crypto
159 .supports(ciphersuite)
160 .map_err(|_| CreationFromExternalError::UnsupportedCiphersuite(ciphersuite))?;
161
162 let group_id = verifiable_group_info.group_id();
163 let ratchet_tree = ratchet_tree
164 .into_verified(ciphersuite, crypto, group_id)
165 .map_err(|e| {
166 CreationFromExternalError::TreeSyncError(TreeSyncFromNodesError::RatchetTreeError(
167 e,
168 ))
169 })?;
170
171 let treesync = TreeSync::from_ratchet_tree(crypto, ciphersuite, ratchet_tree)?;
175
176 let mut encryption_keys = HashSet::new();
177 let mut signature_keys = HashSet::new();
178
179 treesync.full_leaves().try_for_each(|(_, leaf_node)| {
184 leaf_node.validate_locally()?;
185
186 if !signature_keys.insert(leaf_node.signature_key()) {
189 return Err(CreationFromExternalError::DuplicateSignatureKey);
190 }
191
192 if !encryption_keys.insert(leaf_node.encryption_key()) {
195 return Err(CreationFromExternalError::DuplicateEncryptionKey);
196 }
197
198 Ok(())
199 })?;
200
201 treesync
203 .full_parents()
204 .try_for_each(|(parent_index, parent_node)| {
205 if !encryption_keys.insert(parent_node.encryption_key()) {
212 return Err(CreationFromExternalError::DuplicateEncryptionKey);
213 }
214
215 parent_node
216 .unmerged_leaves()
217 .iter()
218 .try_for_each(|leaf_index| {
219 let path = direct_path(*leaf_index, treesync.tree_size());
220
221 let this_parent_offset = path
225 .iter()
226 .position(|x| x == &parent_index)
227 .ok_or(
228 CreationFromExternalError::<StorageError>::UnmergedLeafNotADescendant,
229 )?;
230 let path_leaf_to_this = &path[..this_parent_offset];
231
232
233 path_leaf_to_this
237 .iter()
238 .try_for_each(|intermediate_index| {
239 if let Some(intermediate_node) = treesync
241 .parent(*intermediate_index) {
242 if intermediate_node
246 .unmerged_leaves()
247 .binary_search(leaf_index)
248 .is_err()
249 {
250 return Err(CreationFromExternalError::<StorageError>::IntermediateNodeMissingUnmergedLeaf);
251 }
252 }
253
254 Ok(())
255 })
256 })
257 })?;
258
259 let group_info: GroupInfo = {
261 let signer_signature_key = treesync
262 .leaf(verifiable_group_info.signer())
263 .ok_or(CreationFromExternalError::UnknownSender)?
264 .signature_key()
265 .clone()
266 .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
267
268 verifiable_group_info
269 .verify(crypto, &signer_signature_key)
270 .map_err(|_| CreationFromExternalError::InvalidGroupInfoSignature)?
271 };
272
273 if treesync.tree_hash() != group_info.group_context().tree_hash() {
275 return Err(CreationFromExternalError::TreeHashMismatch);
276 }
277
278 if group_info.group_context().protocol_version() != ProtocolVersion::Mls10 {
279 return Err(CreationFromExternalError::UnsupportedMlsVersion);
280 }
281
282 let group_context = group_info.group_context().clone();
283
284 let interim_transcript_hash = {
285 let input = InterimTranscriptHashInput::from(group_info.confirmation_tag());
286
287 input.calculate_interim_transcript_hash(
288 crypto,
289 group_context.ciphersuite(),
290 group_context.confirmed_transcript_hash(),
291 )?
292 };
293
294 let public_group = Self {
295 treesync,
296 group_context,
297 interim_transcript_hash,
298 confirmation_tag: group_info.confirmation_tag().clone(),
299 proposal_store,
300 };
301
302 public_group
305 .treesync
306 .full_leaves()
307 .try_for_each(|(_, leaf_node)| {
308 public_group.validate_leaf_node_inner(leaf_node, validate_lifetimes)
309 })?;
310
311 Ok((public_group, group_info))
312 }
313
314 pub fn ext_commit_sender_index(
316 &self,
317 commit: &StagedCommit,
318 ) -> Result<LeafNodeIndex, LibraryError> {
319 self.leftmost_free_index(commit.queued_proposals())
320 }
321
322 pub(crate) fn leftmost_free_index<'a>(
329 &self,
330 queued_proposals: impl Iterator<Item = &'a QueuedProposal>,
331 ) -> Result<LeafNodeIndex, LibraryError> {
332 let free_leaf_index = self.treesync().free_leaf_index();
334 let removed_indices = queued_proposals.filter_map(|proposal| {
337 match (proposal.proposal(), proposal.sender()) {
338 (Proposal::Remove(r), _) => Some(r.removed),
339 (Proposal::SelfRemove, Sender::Member(sender)) => Some(*sender),
340 _ => None, }
342 });
343 removed_indices
346 .into_iter()
347 .chain(std::iter::once(free_leaf_index))
348 .min()
349 .ok_or_else(|| LibraryError::custom("No free leaf index found"))
350 }
351
352 pub(crate) fn empty_diff(&self) -> PublicGroupDiff<'_> {
354 PublicGroupDiff::new(self)
355 }
356
357 pub(crate) fn merge_diff(&mut self, diff: StagedPublicGroupDiff) {
363 self.treesync.merge_diff(diff.staged_diff);
364 self.group_context = diff.group_context;
365 self.interim_transcript_hash = diff.interim_transcript_hash;
366 self.confirmation_tag = diff.confirmation_tag;
367 }
368
369 pub(crate) fn derive_path_secrets(
383 &self,
384 crypto: &impl OpenMlsCrypto,
385 ciphersuite: Ciphersuite,
386 path_secret: PathSecret,
387 sender_index: LeafNodeIndex,
388 leaf_index: LeafNodeIndex,
389 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), DerivePathError> {
390 self.treesync.derive_path_secrets(
391 crypto,
392 ciphersuite,
393 path_secret,
394 sender_index,
395 leaf_index,
396 )
397 }
398
399 pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
401 self.treesync().full_leaf_members()
402 }
403
404 pub fn export_ratchet_tree(&self) -> RatchetTree {
406 self.treesync().export_ratchet_tree()
407 }
408
409 pub fn add_proposal<Storage: PublicStorageProvider>(
411 &mut self,
412 storage: &Storage,
413 proposal: QueuedProposal,
414 ) -> Result<(), Storage::Error> {
415 storage.queue_proposal(self.group_id(), &proposal.proposal_reference(), &proposal)?;
416 self.proposal_store.add(proposal);
417 Ok(())
418 }
419
420 pub fn remove_proposal<Storage: PublicStorageProvider>(
422 &mut self,
423 storage: &Storage,
424 proposal_ref: &ProposalRef,
425 ) -> Result<(), Storage::Error> {
426 storage.remove_proposal(self.group_id(), proposal_ref)?;
427 self.proposal_store.remove(proposal_ref);
428 Ok(())
429 }
430
431 pub fn queued_proposals<Storage: PublicStorageProvider>(
433 &self,
434 storage: &Storage,
435 ) -> Result<Vec<(ProposalRef, QueuedProposal)>, Storage::Error> {
436 storage.queued_proposals(self.group_id())
437 }
438}
439
440impl PublicGroup {
442 pub fn ciphersuite(&self) -> Ciphersuite {
444 self.group_context.ciphersuite()
445 }
446
447 pub fn version(&self) -> ProtocolVersion {
449 self.group_context.protocol_version()
450 }
451
452 pub fn group_id(&self) -> &GroupId {
454 self.group_context.group_id()
455 }
456
457 pub fn group_context(&self) -> &GroupContext {
459 &self.group_context
460 }
461
462 pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
464 self.group_context.required_capabilities()
465 }
466
467 pub fn treesync(&self) -> &TreeSync {
469 &self.treesync
470 }
471
472 pub fn confirmation_tag(&self) -> &ConfirmationTag {
474 &self.confirmation_tag
475 }
476
477 pub fn leaf(&self, leaf_index: LeafNodeIndex) -> Option<&LeafNode> {
480 self.treesync().leaf(leaf_index)
481 }
482
483 pub(crate) fn tree_size(&self) -> TreeSize {
485 self.treesync().tree_size()
486 }
487
488 fn interim_transcript_hash(&self) -> &[u8] {
489 &self.interim_transcript_hash
490 }
491
492 pub(crate) fn owned_encryption_keys(&self, leaf_index: LeafNodeIndex) -> Vec<EncryptionKey> {
495 self.treesync().owned_encryption_keys(leaf_index)
496 }
497
498 pub(crate) fn store<Storage: PublicStorageProvider>(
503 &self,
504 storage: &Storage,
505 ) -> Result<(), Storage::Error> {
506 let group_id = self.group_context.group_id();
507 storage.write_tree(group_id, self.treesync())?;
508 storage.write_confirmation_tag(group_id, self.confirmation_tag())?;
509 storage.write_context(group_id, self.group_context())?;
510 storage.write_interim_transcript_hash(
511 group_id,
512 &InterimTranscriptHash(self.interim_transcript_hash.clone()),
513 )?;
514 Ok(())
515 }
516
517 pub fn delete<Storage: PublicStorageProvider>(
519 storage: &Storage,
520 group_id: &GroupId,
521 ) -> Result<(), Storage::Error> {
522 storage.delete_tree(group_id)?;
523 storage.delete_confirmation_tag(group_id)?;
524 storage.delete_context(group_id)?;
525 storage.delete_interim_transcript_hash(group_id)?;
526
527 Ok(())
528 }
529
530 pub fn load<Storage: PublicStorageProvider>(
532 storage: &Storage,
533 group_id: &GroupId,
534 ) -> Result<Option<Self>, Storage::Error> {
535 let treesync = storage.tree(group_id)?;
536 let proposals: Vec<(ProposalRef, QueuedProposal)> = storage.queued_proposals(group_id)?;
537 let group_context = storage.group_context(group_id)?;
538 let interim_transcript_hash: Option<InterimTranscriptHash> =
539 storage.interim_transcript_hash(group_id)?;
540 let confirmation_tag = storage.confirmation_tag(group_id)?;
541 let mut proposal_store = ProposalStore::new();
542
543 for (_ref, proposal) in proposals {
544 proposal_store.add(proposal);
545 }
546
547 let build = || -> Option<Self> {
548 Some(Self {
549 treesync: treesync?,
550 proposal_store,
551 group_context: group_context?,
552 interim_transcript_hash: interim_transcript_hash?.0,
553 confirmation_tag: confirmation_tag?,
554 })
555 };
556
557 Ok(build())
558 }
559
560 pub(crate) fn proposal_store(&self) -> &ProposalStore {
562 &self.proposal_store
563 }
564
565 pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
567 &mut self.proposal_store
568 }
569}
570
571#[cfg(any(feature = "test-utils", test))]
573impl PublicGroup {
574 pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
575 &mut self.group_context
576 }
577
578 #[cfg(test)]
579 pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
580 self.group_context = group_context;
581 }
582
583 #[cfg(test)]
584 pub(crate) fn encrypt_path(
585 &self,
586 provider: &impl crate::storage::OpenMlsProvider,
587 ciphersuite: Ciphersuite,
588 path: &[PlainUpdatePathNode],
589 group_context: &[u8],
590 exclusion_list: &HashSet<&LeafNodeIndex>,
591 own_leaf_index: LeafNodeIndex,
592 ) -> Result<Vec<UpdatePathNode>, LibraryError> {
593 self.treesync().empty_diff().encrypt_path(
594 provider.crypto(),
595 ciphersuite,
596 path,
597 group_context,
598 exclusion_list,
599 own_leaf_index,
600 )
601 }
602}