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#[derive(Debug)]
70#[cfg_attr(any(test, feature = "test-utils"), derive(PartialEq, Clone))]
71pub struct PublicGroup {
72 treesync: TreeSync,
73 proposal_store: ProposalStore,
74 group_context: GroupContext,
75 interim_transcript_hash: Vec<u8>,
76 confirmation_tag: ConfirmationTag,
78}
79
80#[derive(Debug, Serialize, Deserialize)]
82pub struct InterimTranscriptHash(pub Vec<u8>);
83
84impl PublicGroup {
85 pub(crate) fn new(
88 crypto: &impl OpenMlsCrypto,
89 treesync: TreeSync,
90 group_context: GroupContext,
91 initial_confirmation_tag: ConfirmationTag,
92 ) -> Result<Self, LibraryError> {
93 let interim_transcript_hash = {
94 let input = InterimTranscriptHashInput::from(&initial_confirmation_tag);
95
96 input.calculate_interim_transcript_hash(
97 crypto,
98 group_context.ciphersuite(),
99 group_context.confirmed_transcript_hash(),
100 )?
101 };
102
103 Ok(PublicGroup {
104 treesync,
105 proposal_store: ProposalStore::new(),
106 group_context,
107 interim_transcript_hash,
108 confirmation_tag: initial_confirmation_tag,
109 })
110 }
111
112 pub fn from_external<StorageProvider, StorageError>(
118 crypto: &impl OpenMlsCrypto,
119 storage: &StorageProvider,
120 ratchet_tree: RatchetTreeIn,
121 verifiable_group_info: VerifiableGroupInfo,
122 proposal_store: ProposalStore,
123 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>>
124 where
125 StorageProvider: PublicStorageProvider<Error = StorageError>,
126 {
127 let (public_group, group_info) = PublicGroup::from_ratchet_tree(
128 crypto,
129 ratchet_tree,
130 verifiable_group_info,
131 proposal_store,
132 LeafNodeLifetimePolicy::Verify,
133 )?;
134
135 public_group
136 .store(storage)
137 .map_err(CreationFromExternalError::WriteToStorageError)?;
138
139 Ok((public_group, group_info))
140 }
141
142 pub(crate) fn from_ratchet_tree<StorageError>(
143 crypto: &impl OpenMlsCrypto,
144 ratchet_tree: RatchetTreeIn,
145 verifiable_group_info: VerifiableGroupInfo,
146 proposal_store: ProposalStore,
147 validate_lifetimes: LeafNodeLifetimePolicy,
148 ) -> Result<(Self, GroupInfo), CreationFromExternalError<StorageError>> {
149 let ciphersuite = verifiable_group_info.ciphersuite();
150
151 let group_id = verifiable_group_info.group_id();
152 let ratchet_tree = ratchet_tree
153 .into_verified(ciphersuite, crypto, group_id)
154 .map_err(|e| {
155 CreationFromExternalError::TreeSyncError(TreeSyncFromNodesError::RatchetTreeError(
156 e,
157 ))
158 })?;
159
160 let treesync = TreeSync::from_ratchet_tree(crypto, ciphersuite, ratchet_tree)?;
164
165 let mut encryption_keys = HashSet::new();
166 let mut signature_keys = HashSet::new();
167
168 treesync.full_leaves().try_for_each(|(_, leaf_node)| {
173 leaf_node.validate_locally()?;
174
175 if !signature_keys.insert(leaf_node.signature_key()) {
178 return Err(CreationFromExternalError::DuplicateSignatureKey);
179 }
180
181 if !encryption_keys.insert(leaf_node.encryption_key()) {
184 return Err(CreationFromExternalError::DuplicateEncryptionKey);
185 }
186
187 Ok(())
188 })?;
189
190 treesync
192 .full_parents()
193 .try_for_each(|(parent_index, parent_node)| {
194 if !encryption_keys.insert(parent_node.encryption_key()) {
201 return Err(CreationFromExternalError::DuplicateEncryptionKey);
202 }
203
204 parent_node
205 .unmerged_leaves()
206 .iter()
207 .try_for_each(|leaf_index| {
208 let path = direct_path(*leaf_index, treesync.tree_size());
209
210 let this_parent_offset = path
214 .iter()
215 .position(|x| x == &parent_index)
216 .ok_or(
217 CreationFromExternalError::<StorageError>::UnmergedLeafNotADescendant,
218 )?;
219 let path_leaf_to_this = &path[..this_parent_offset];
220
221
222 path_leaf_to_this
226 .iter()
227 .try_for_each(|intermediate_index| {
228 if let Some(intermediate_node) = treesync
230 .parent(*intermediate_index) {
231 if !intermediate_node.unmerged_leaves().contains(leaf_index) {
232 return Err(CreationFromExternalError::<StorageError>::IntermediateNodeMissingUnmergedLeaf);
233 }
234 }
235
236 Ok(())
237 })
238 })
239 })?;
240
241 let group_info: GroupInfo = {
243 let signer_signature_key = treesync
244 .leaf(verifiable_group_info.signer())
245 .ok_or(CreationFromExternalError::UnknownSender)?
246 .signature_key()
247 .clone()
248 .into_signature_public_key_enriched(ciphersuite.signature_algorithm());
249
250 verifiable_group_info
251 .verify(crypto, &signer_signature_key)
252 .map_err(|_| CreationFromExternalError::InvalidGroupInfoSignature)?
253 };
254
255 if treesync.tree_hash() != group_info.group_context().tree_hash() {
257 return Err(CreationFromExternalError::TreeHashMismatch);
258 }
259
260 if group_info.group_context().protocol_version() != ProtocolVersion::Mls10 {
261 return Err(CreationFromExternalError::UnsupportedMlsVersion);
262 }
263
264 let group_context = group_info.group_context().clone();
265
266 let interim_transcript_hash = {
267 let input = InterimTranscriptHashInput::from(group_info.confirmation_tag());
268
269 input.calculate_interim_transcript_hash(
270 crypto,
271 group_context.ciphersuite(),
272 group_context.confirmed_transcript_hash(),
273 )?
274 };
275
276 let public_group = Self {
277 treesync,
278 group_context,
279 interim_transcript_hash,
280 confirmation_tag: group_info.confirmation_tag().clone(),
281 proposal_store,
282 };
283
284 public_group
287 .treesync
288 .full_leaves()
289 .try_for_each(|(_, leaf_node)| {
290 public_group.validate_leaf_node_inner(leaf_node, validate_lifetimes)
291 })?;
292
293 Ok((public_group, group_info))
294 }
295
296 pub fn ext_commit_sender_index(
298 &self,
299 commit: &StagedCommit,
300 ) -> Result<LeafNodeIndex, LibraryError> {
301 self.leftmost_free_index(commit.queued_proposals())
302 }
303
304 pub(crate) fn leftmost_free_index<'a>(
311 &self,
312 queued_proposals: impl Iterator<Item = &'a QueuedProposal>,
313 ) -> Result<LeafNodeIndex, LibraryError> {
314 let free_leaf_index = self.treesync().free_leaf_index();
316 let removed_indices = queued_proposals.filter_map(|proposal| {
319 match (proposal.proposal(), proposal.sender()) {
320 (Proposal::Remove(r), _) => Some(r.removed),
321 (Proposal::SelfRemove, Sender::Member(sender)) => Some(*sender),
322 _ => None, }
324 });
325 removed_indices
328 .into_iter()
329 .chain(std::iter::once(free_leaf_index))
330 .min()
331 .ok_or_else(|| LibraryError::custom("No free leaf index found"))
332 }
333
334 pub(crate) fn empty_diff(&self) -> PublicGroupDiff<'_> {
336 PublicGroupDiff::new(self)
337 }
338
339 pub(crate) fn merge_diff(&mut self, diff: StagedPublicGroupDiff) {
345 self.treesync.merge_diff(diff.staged_diff);
346 self.group_context = diff.group_context;
347 self.interim_transcript_hash = diff.interim_transcript_hash;
348 self.confirmation_tag = diff.confirmation_tag;
349 }
350
351 pub(crate) fn derive_path_secrets(
365 &self,
366 crypto: &impl OpenMlsCrypto,
367 ciphersuite: Ciphersuite,
368 path_secret: PathSecret,
369 sender_index: LeafNodeIndex,
370 leaf_index: LeafNodeIndex,
371 ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), DerivePathError> {
372 self.treesync.derive_path_secrets(
373 crypto,
374 ciphersuite,
375 path_secret,
376 sender_index,
377 leaf_index,
378 )
379 }
380
381 pub fn members(&self) -> impl Iterator<Item = Member> + '_ {
383 self.treesync().full_leaf_members()
384 }
385
386 pub fn export_ratchet_tree(&self) -> RatchetTree {
388 self.treesync().export_ratchet_tree()
389 }
390
391 pub fn add_proposal<Storage: PublicStorageProvider>(
393 &mut self,
394 storage: &Storage,
395 proposal: QueuedProposal,
396 ) -> Result<(), Storage::Error> {
397 storage.queue_proposal(self.group_id(), &proposal.proposal_reference(), &proposal)?;
398 self.proposal_store.add(proposal);
399 Ok(())
400 }
401
402 pub fn remove_proposal<Storage: PublicStorageProvider>(
404 &mut self,
405 storage: &Storage,
406 proposal_ref: &ProposalRef,
407 ) -> Result<(), Storage::Error> {
408 storage.remove_proposal(self.group_id(), proposal_ref)?;
409 self.proposal_store.remove(proposal_ref);
410 Ok(())
411 }
412
413 pub fn queued_proposals<Storage: PublicStorageProvider>(
415 &self,
416 storage: &Storage,
417 ) -> Result<Vec<(ProposalRef, QueuedProposal)>, Storage::Error> {
418 storage.queued_proposals(self.group_id())
419 }
420}
421
422impl PublicGroup {
424 pub fn ciphersuite(&self) -> Ciphersuite {
426 self.group_context.ciphersuite()
427 }
428
429 pub fn version(&self) -> ProtocolVersion {
431 self.group_context.protocol_version()
432 }
433
434 pub fn group_id(&self) -> &GroupId {
436 self.group_context.group_id()
437 }
438
439 pub fn group_context(&self) -> &GroupContext {
441 &self.group_context
442 }
443
444 pub fn required_capabilities(&self) -> Option<&RequiredCapabilitiesExtension> {
446 self.group_context.required_capabilities()
447 }
448
449 fn treesync(&self) -> &TreeSync {
451 &self.treesync
452 }
453
454 pub fn confirmation_tag(&self) -> &ConfirmationTag {
456 &self.confirmation_tag
457 }
458
459 pub fn leaf(&self, leaf_index: LeafNodeIndex) -> Option<&LeafNode> {
462 self.treesync().leaf(leaf_index)
463 }
464
465 pub(crate) fn tree_size(&self) -> TreeSize {
467 self.treesync().tree_size()
468 }
469
470 fn interim_transcript_hash(&self) -> &[u8] {
471 &self.interim_transcript_hash
472 }
473
474 pub(crate) fn owned_encryption_keys(&self, leaf_index: LeafNodeIndex) -> Vec<EncryptionKey> {
477 self.treesync().owned_encryption_keys(leaf_index)
478 }
479
480 pub(crate) fn store<Storage: PublicStorageProvider>(
485 &self,
486 storage: &Storage,
487 ) -> Result<(), Storage::Error> {
488 let group_id = self.group_context.group_id();
489 storage.write_tree(group_id, self.treesync())?;
490 storage.write_confirmation_tag(group_id, self.confirmation_tag())?;
491 storage.write_context(group_id, self.group_context())?;
492 storage.write_interim_transcript_hash(
493 group_id,
494 &InterimTranscriptHash(self.interim_transcript_hash.clone()),
495 )?;
496 Ok(())
497 }
498
499 pub fn delete<Storage: PublicStorageProvider>(
501 storage: &Storage,
502 group_id: &GroupId,
503 ) -> Result<(), Storage::Error> {
504 storage.delete_tree(group_id)?;
505 storage.delete_confirmation_tag(group_id)?;
506 storage.delete_context(group_id)?;
507 storage.delete_interim_transcript_hash(group_id)?;
508
509 Ok(())
510 }
511
512 pub fn load<Storage: PublicStorageProvider>(
514 storage: &Storage,
515 group_id: &GroupId,
516 ) -> Result<Option<Self>, Storage::Error> {
517 let treesync = storage.tree(group_id)?;
518 let proposals: Vec<(ProposalRef, QueuedProposal)> = storage.queued_proposals(group_id)?;
519 let group_context = storage.group_context(group_id)?;
520 let interim_transcript_hash: Option<InterimTranscriptHash> =
521 storage.interim_transcript_hash(group_id)?;
522 let confirmation_tag = storage.confirmation_tag(group_id)?;
523 let mut proposal_store = ProposalStore::new();
524
525 for (_ref, proposal) in proposals {
526 proposal_store.add(proposal);
527 }
528
529 let build = || -> Option<Self> {
530 Some(Self {
531 treesync: treesync?,
532 proposal_store,
533 group_context: group_context?,
534 interim_transcript_hash: interim_transcript_hash?.0,
535 confirmation_tag: confirmation_tag?,
536 })
537 };
538
539 Ok(build())
540 }
541
542 pub(crate) fn proposal_store(&self) -> &ProposalStore {
544 &self.proposal_store
545 }
546
547 pub(crate) fn proposal_store_mut(&mut self) -> &mut ProposalStore {
549 &mut self.proposal_store
550 }
551}
552
553#[cfg(any(feature = "test-utils", test))]
555impl PublicGroup {
556 pub(crate) fn context_mut(&mut self) -> &mut GroupContext {
557 &mut self.group_context
558 }
559
560 #[cfg(test)]
561 pub(crate) fn set_group_context(&mut self, group_context: GroupContext) {
562 self.group_context = group_context;
563 }
564
565 #[cfg(test)]
566 pub(crate) fn encrypt_path(
567 &self,
568 provider: &impl crate::storage::OpenMlsProvider,
569 ciphersuite: Ciphersuite,
570 path: &[PlainUpdatePathNode],
571 group_context: &[u8],
572 exclusion_list: &HashSet<&LeafNodeIndex>,
573 own_leaf_index: LeafNodeIndex,
574 ) -> Result<Vec<UpdatePathNode>, LibraryError> {
575 self.treesync().empty_diff().encrypt_path(
576 provider.crypto(),
577 ciphersuite,
578 path,
579 group_context,
580 exclusion_list,
581 own_leaf_index,
582 )
583 }
584}