openmls/group/public_group/
diff.rs

1//! # Public group diffs
2//!
3//! This module contains the [`PublicGroupDiff`] struct, as well as the
4//! [`StagedPublicGroupDiff`] and associated functions and types.
5use std::collections::HashSet;
6
7use openmls_traits::crypto::OpenMlsCrypto;
8use openmls_traits::types::Ciphersuite;
9use serde::{Deserialize, Serialize};
10use tls_codec::Serialize as TlsSerialize;
11
12use super::PublicGroup;
13use crate::{
14    binary_tree::{array_representation::TreeSize, LeafNodeIndex},
15    error::LibraryError,
16    extensions::Extensions,
17    framing::{mls_auth_content::AuthenticatedContent, public_message::InterimTranscriptHashInput},
18    group::GroupContext,
19    messages::{proposals::AddProposal, ConfirmationTag, EncryptedGroupSecrets},
20    schedule::{psk::PreSharedKeyId, CommitSecret, JoinerSecret},
21    treesync::{
22        diff::{StagedTreeSyncDiff, TreeSyncDiff},
23        errors::ApplyUpdatePathError,
24        node::{
25            encryption_keys::EncryptionKeyPair, leaf_node::LeafNode,
26            parent_node::PlainUpdatePathNode,
27        },
28        treekem::{DecryptPathParams, UpdatePath, UpdatePathNode},
29        RatchetTree,
30    },
31};
32
33pub(crate) mod apply_proposals;
34pub(crate) mod compute_path;
35
36pub(crate) struct PublicGroupDiff<'a> {
37    diff: TreeSyncDiff<'a>,
38    group_context: GroupContext,
39    interim_transcript_hash: Vec<u8>,
40    // Most recent confirmation tag. Kept here for verification purposes.
41    confirmation_tag: ConfirmationTag,
42}
43
44impl<'a> PublicGroupDiff<'a> {
45    /// Create a new [`PublicGroupDiff`] based on the given [`PublicGroup`].
46    pub(super) fn new(public_group: &'a PublicGroup) -> PublicGroupDiff<'a> {
47        Self {
48            diff: public_group.treesync().empty_diff(),
49            group_context: public_group.group_context().clone(),
50            interim_transcript_hash: public_group.interim_transcript_hash().to_vec(),
51            confirmation_tag: public_group.confirmation_tag().clone(),
52        }
53    }
54
55    /// Turn this [`PublicGroupDiff`] into a [`StagedPublicGroupDiff`], thus
56    /// freezing it until it is merged with the original [`PublicGroup`].
57    pub(crate) fn into_staged_diff(
58        self,
59        crypto: &impl OpenMlsCrypto,
60        ciphersuite: Ciphersuite,
61    ) -> Result<StagedPublicGroupDiff, LibraryError> {
62        let staged_diff = self.diff.into_staged_diff(crypto, ciphersuite)?;
63        Ok(StagedPublicGroupDiff {
64            staged_diff,
65            group_context: self.group_context,
66            interim_transcript_hash: self.interim_transcript_hash,
67            confirmation_tag: self.confirmation_tag,
68        })
69    }
70
71    /// Prepare the [`EncryptedGroupSecrets`] for a number of `invited_members`
72    /// based on this [`PublicGroupDiff`]. If a slice of [`PlainUpdatePathNode`]
73    /// is given, they are included in the [`GroupSecrets`] of the path.
74    ///
75    /// Returns an error if
76    ///  - the own node is outside the tree
77    ///  - the invited members are not part of the tree yet
78    ///  - the leaf index of a new member is identical to the own leaf index
79    ///  - the plain path does not contain the correct secrets
80    #[allow(clippy::too_many_arguments)]
81    pub(crate) fn encrypt_group_secrets(
82        &self,
83        joiner_secret: &JoinerSecret,
84        invited_members: Vec<(LeafNodeIndex, AddProposal)>,
85        plain_path_option: Option<&[PlainUpdatePathNode]>,
86        presharedkeys: &[PreSharedKeyId],
87        encrypted_group_info: &[u8],
88        crypto: &impl OpenMlsCrypto,
89        leaf_index: LeafNodeIndex,
90    ) -> Result<Vec<EncryptedGroupSecrets>, LibraryError> {
91        self.diff.encrypt_group_secrets(
92            joiner_secret,
93            invited_members,
94            plain_path_option,
95            presharedkeys,
96            encrypted_group_info,
97            crypto,
98            leaf_index,
99        )
100    }
101
102    /// Returns the tree size
103    pub(crate) fn tree_size(&self) -> TreeSize {
104        self.diff.tree_size()
105    }
106
107    /// Returns a vector of all nodes in the tree resulting from merging this
108    /// diff.
109    pub(crate) fn export_ratchet_tree(&self) -> RatchetTree {
110        self.diff.export_ratchet_tree()
111    }
112
113    /// Decrypt an [`UpdatePath`] originating from the given
114    /// `sender_leaf_index`. The `group_context` is used in the decryption
115    /// process and the `exclusion_list` is used to determine the position of
116    /// the ciphertext in the `UpdatePath` that we can decrypt.
117    ///
118    /// Returns the [`CommitSecret`] resulting from their derivation. Returns an
119    /// error if the `sender_leaf_index` is outside of the tree.
120    ///
121    /// ValSem203: Path secrets must decrypt correctly
122    /// ValSem204: Public keys from Path must be verified and match the private keys from the direct path
123    /// TODO #804
124    pub(crate) fn decrypt_path(
125        &self,
126        crypto: &impl OpenMlsCrypto,
127        owned_keys: &[&EncryptionKeyPair],
128        own_leaf_index: LeafNodeIndex,
129        sender_leaf_index: LeafNodeIndex,
130        update_path: &[UpdatePathNode],
131        exclusion_list: &HashSet<&LeafNodeIndex>,
132    ) -> Result<(Vec<EncryptionKeyPair>, CommitSecret), ApplyUpdatePathError> {
133        let params = DecryptPathParams {
134            update_path,
135            sender_leaf_index,
136            exclusion_list,
137            group_context: &self
138                .group_context()
139                .tls_serialize_detached()
140                .map_err(LibraryError::missing_bound_check)?,
141        };
142        self.diff.decrypt_path(
143            crypto,
144            self.group_context().ciphersuite(),
145            params,
146            owned_keys,
147            own_leaf_index,
148        )
149    }
150
151    /// Return a reference to the leaf with the given index.
152    pub(crate) fn leaf(&self, index: LeafNodeIndex) -> Option<&LeafNode> {
153        self.diff.leaf(index)
154    }
155
156    /// Set the given path as the direct path of the `sender_leaf_index` and
157    /// replace the [`LeafNode`] in the corresponding leaf with the given one.
158    ///
159    /// Returns an error if the `sender_leaf_index` is outside of the tree.
160    /// ValSem202: Path must be the right length
161    /// TODO #804
162    pub(crate) fn apply_received_update_path(
163        &mut self,
164        crypto: &impl OpenMlsCrypto,
165        ciphersuite: Ciphersuite,
166        sender_leaf_index: LeafNodeIndex,
167        update_path: &UpdatePath,
168    ) -> Result<(), ApplyUpdatePathError> {
169        self.diff
170            .apply_received_update_path(crypto, ciphersuite, sender_leaf_index, update_path)
171    }
172
173    /// Update the interim transcript hash of the diff and store the
174    /// confirmation tag s.t. it can later be merged back into the original
175    /// group.
176    pub(crate) fn update_interim_transcript_hash(
177        &mut self,
178        ciphersuite: Ciphersuite,
179        crypto: &impl OpenMlsCrypto,
180        confirmation_tag: ConfirmationTag,
181    ) -> Result<(), LibraryError> {
182        let interim_transcript_hash = {
183            let input = InterimTranscriptHashInput::from(&confirmation_tag);
184
185            input.calculate_interim_transcript_hash(
186                crypto,
187                ciphersuite,
188                self.group_context.confirmed_transcript_hash(),
189            )?
190        };
191
192        self.confirmation_tag = confirmation_tag;
193        self.interim_transcript_hash = interim_transcript_hash;
194
195        Ok(())
196    }
197
198    /// Update the [`GroupContext`] of the diff. This includes tree hash
199    /// computation and epoch incrementation, but this does _not_ update the
200    /// confirmed transcript hash.
201    pub(crate) fn update_group_context(
202        &mut self,
203        crypto: &impl OpenMlsCrypto,
204        extensions: Option<Extensions>,
205    ) -> Result<(), LibraryError> {
206        // Calculate tree hash
207        let new_tree_hash = self
208            .diff
209            .compute_tree_hashes(crypto, self.group_context().ciphersuite())?;
210        self.group_context.update_tree_hash(new_tree_hash);
211        self.group_context.increment_epoch();
212        if let Some(extensions) = extensions {
213            self.group_context.set_extensions(extensions);
214        }
215        Ok(())
216    }
217
218    /// Update the confirmed transcript hash of the diff's [`GroupContext`]
219    /// using the given `interim_transcript_hash`, as well as the
220    /// `commit_content`.
221    pub(crate) fn update_confirmed_transcript_hash(
222        &mut self,
223        crypto: &impl OpenMlsCrypto,
224        commit_content: &AuthenticatedContent,
225    ) -> Result<(), LibraryError> {
226        self.group_context.update_confirmed_transcript_hash(
227            crypto,
228            &self.interim_transcript_hash,
229            commit_content,
230        )
231    }
232
233    pub(crate) fn group_context(&self) -> &GroupContext {
234        &self.group_context
235    }
236}
237
238/// The staged version of a [`PublicGroupDiff`], which means it can no longer be
239/// modified. Its only use is to merge it into the original [`PublicGroup`].
240#[derive(Debug, Serialize, Deserialize)]
241#[cfg_attr(any(test, feature = "test-utils"), derive(Clone, PartialEq))]
242pub(crate) struct StagedPublicGroupDiff {
243    pub(super) staged_diff: StagedTreeSyncDiff,
244    pub(super) group_context: GroupContext,
245    pub(super) interim_transcript_hash: Vec<u8>,
246    pub(super) confirmation_tag: ConfirmationTag,
247}
248
249impl StagedPublicGroupDiff {
250    /// Get the staged [`GroupContext`].
251    pub(crate) fn group_context(&self) -> &GroupContext {
252        &self.group_context
253    }
254}