Skip to main content

openmls/group/fork_resolution/
reboot.rs

1//! The [`reboot`] module contains helpers to set up a new group and add all members of the current
2//! group. The application needs to determine who should set that new group up and how to migrate
3//! the group context extensions from the old group. This is the more expensive mechanism.
4
5use openmls_traits::signatures::Signer;
6
7use crate::{
8    credentials::CredentialWithKey,
9    extensions::errors::InvalidExtensionError,
10    group::{
11        commit_builder::{CommitBuilder, CommitMessageBundle, Initial},
12        mls_group::builder::MlsGroupBuilder,
13        CommitBuilderStageError, CreateCommitError, Extensions, GroupId, Member, MlsGroup,
14        NewGroupError,
15    },
16    prelude::GroupContext,
17    prelude::KeyPackage,
18    storage::OpenMlsProvider,
19};
20
21impl MlsGroup {
22    /// The first step towards creating a new group based on the parameters and membership list of
23    /// the current one.
24    pub fn reboot(&'_ self, group_id: GroupId) -> RebootBuilder<'_> {
25        let group_builder = MlsGroup::builder()
26            .with_wire_format_policy(self.configuration().wire_format_policy)
27            .padding_size(self.configuration().padding_size)
28            .set_past_epoch_deletion_policy(self.configuration().past_epoch_deletion_policy.clone())
29            .number_of_resumption_psks(self.configuration().number_of_resumption_psks)
30            .use_ratchet_tree_extension(self.configuration().use_ratchet_tree_extension)
31            .sender_ratchet_configuration(self.configuration().sender_ratchet_configuration)
32            .ciphersuite(self.ciphersuite())
33            .with_group_id(group_id);
34
35        RebootBuilder {
36            group: self,
37            group_builder,
38        }
39    }
40}
41
42/// The builder type for a group reboot.
43pub struct RebootBuilder<'a> {
44    group: &'a MlsGroup,
45    group_builder: MlsGroupBuilder,
46}
47
48impl<'a> RebootBuilder<'a> {
49    /// Returns the group context extensions of the old group, so they can be updated and passed
50    /// into the new group.
51    pub fn old_group_context_extensions(&self) -> &Extensions<GroupContext> {
52        self.group.context().extensions()
53    }
54
55    /// The members of the old group, so new key packages for other members can be retrieved.
56    pub fn old_members(&self) -> impl Iterator<Item = Member> + 'a {
57        self.group
58            .members()
59            .filter(|member| member.index != self.group.own_leaf_index())
60    }
61
62    /// Lets the caller make changes to the [`MlsGroupBuilder`] before the group is created.
63    pub fn refine_group_builder(
64        self,
65        mut f: impl FnMut(MlsGroupBuilder) -> MlsGroupBuilder,
66    ) -> Self {
67        Self {
68            group_builder: f(self.group_builder),
69            ..self
70        }
71    }
72
73    /// Creates the group and commit using the provided `extensions` and `new_members`. The caller
74    /// can also make further changes to the [`CommitBuilder`] using the `refine_commit_builder`
75    /// argument. If that is not desired, provide the identity function (`|b| b`).
76    #[openmls_traits::maybe_async]
77    pub async fn finish<Provider: OpenMlsProvider>(
78        self,
79        extensions: Extensions<GroupContext>,
80        new_members: Vec<KeyPackage>,
81        refine_commit_builder: impl FnMut(CommitBuilder<Initial>) -> CommitBuilder<Initial>,
82        provider: &Provider,
83        signer: &impl Signer,
84        credential_with_key: CredentialWithKey,
85    ) -> Result<(MlsGroup, CommitMessageBundle), RebootError<Provider::StorageError>> {
86        let group_builder = self.group_builder.with_group_context_extensions(extensions);
87
88        let mut new_group = group_builder
89            .build(provider, signer, credential_with_key)
90            .await?;
91
92        new_group
93            .commit_builder()
94            .propose_adds(new_members)
95            .pipe_through(refine_commit_builder)
96            .load_psks(provider.storage())
97            .await?
98            .build(provider.rand(), provider.crypto(), signer, |_| true)?
99            .stage_commit(provider)
100            .await
101            .map_err(RebootError::CommitBuilderStage)
102            .map(|message_bundle| (new_group, message_bundle))
103    }
104}
105
106/// Indicates an error occurred during reboot.
107#[derive(Debug, thiserror::Error)]
108pub enum RebootError<StorageError> {
109    /// An invalid extension was provided.
110    #[error(transparent)]
111    InvalidExtension(#[from] InvalidExtensionError),
112    /// An error occurred while creating the new group.
113    #[error(transparent)]
114    NewGroup(#[from] NewGroupError<StorageError>),
115    /// An error occurred while creating the commit.
116    #[error(transparent)]
117    CreateCommit(#[from] CreateCommitError),
118    /// An error occurred while staging the commit.
119    #[error(transparent)]
120    CommitBuilderStage(#[from] CommitBuilderStageError<StorageError>),
121}
122
123/// Defines a method that consumes self, passes it into a closure and returns the output of the
124/// closure. Comes in handy in long builder chains.
125trait PipeThrough: Sized {
126    fn pipe_through<T: Sized, F: FnMut(Self) -> T>(self, mut f: F) -> T {
127        f(self)
128    }
129}
130
131impl<T> PipeThrough for T {}
132
133#[cfg(test)]
134mod test {
135    use crate::{
136        framing::MlsMessageIn,
137        group::{
138            mls_group::tests_and_kats::utils::{setup_alice_bob_group, setup_client},
139            tests_and_kats::utils::{generate_key_package, CredentialWithKeyAndSigner},
140            Extensions, GroupId, StagedWelcome,
141        },
142    };
143
144    #[openmls_test::openmls_test]
145    fn example_reboot() {
146        let alice_provider = &Provider::default();
147        let bob_provider = &Provider::default();
148        let charlie_provider = &Provider::default();
149        let dave_provider = &Provider::default();
150
151        // Create group with alice and bob
152        let (mut alice_group, alice_signer, mut bob_group, bob_signer, alice_cwk, bob_cwk) =
153            setup_alice_bob_group(ciphersuite, alice_provider, bob_provider);
154
155        let (charlie_cwk, charlie_kpb, charlie_signer, _charlie_sig_pk) =
156            setup_client("Charlie", ciphersuite, charlie_provider);
157
158        let (_dave_cwk, dave_kpb, _dave_signer, _dave_sig_pk) =
159            setup_client("Dave", ciphersuite, dave_provider);
160
161        let bob_cwkas = CredentialWithKeyAndSigner {
162            credential_with_key: bob_cwk.clone(),
163            signer: bob_signer.clone(),
164        };
165
166        let charlie_cwkas = CredentialWithKeyAndSigner {
167            credential_with_key: charlie_cwk.clone(),
168            signer: charlie_signer.clone(),
169        };
170
171        // Alice and Bob concurrently invite someone and merge for whatever reason
172        alice_group
173            .commit_builder()
174            .propose_adds(Some(charlie_kpb.key_package().clone()))
175            .load_psks(alice_provider.storage())
176            .unwrap()
177            .build(
178                alice_provider.rand(),
179                alice_provider.crypto(),
180                &alice_signer,
181                |_| true,
182            )
183            .unwrap()
184            .stage_commit(alice_provider)
185            .unwrap();
186
187        bob_group
188            .commit_builder()
189            .propose_adds(Some(dave_kpb.key_package().clone()))
190            .load_psks(bob_provider.storage())
191            .unwrap()
192            .build(
193                bob_provider.rand(),
194                bob_provider.crypto(),
195                &bob_signer,
196                |_| true,
197            )
198            .unwrap()
199            .stage_commit(bob_provider)
200            .unwrap();
201
202        alice_group.merge_pending_commit(alice_provider).unwrap();
203        bob_group.merge_pending_commit(bob_provider).unwrap();
204
205        // We are forked now! Let's try to recover by rebooting. first get new key packages
206        let bob_new_kpb =
207            generate_key_package(ciphersuite, Extensions::empty(), bob_provider, bob_cwkas);
208
209        let charlie_new_kpb = generate_key_package(
210            ciphersuite,
211            Extensions::empty(),
212            charlie_provider,
213            charlie_cwkas,
214        );
215
216        // Now, reboot the group
217        let (mut new_alice_group, message_bundle) = alice_group
218            .reboot(GroupId::from_slice(b"new group id"))
219            .finish(
220                Extensions::empty(),
221                vec![
222                    bob_new_kpb.key_package().clone(),
223                    charlie_new_kpb.key_package().clone(),
224                ],
225                |builder| builder,
226                alice_provider,
227                &alice_signer,
228                alice_cwk.clone(),
229            )
230            .unwrap();
231
232        let (_commit, welcome, _group_info) = message_bundle.into_messages();
233        new_alice_group
234            .merge_pending_commit(alice_provider)
235            .unwrap();
236
237        // Invite everyone
238        let welcome = welcome.unwrap();
239        let welcome: MlsMessageIn = welcome.into();
240        let welcome = welcome.into_welcome().unwrap();
241        let ratchet_tree = new_alice_group.export_ratchet_tree();
242
243        let new_bob_group = StagedWelcome::new_from_welcome(
244            bob_provider,
245            alice_group.configuration(),
246            welcome.clone(),
247            Some(ratchet_tree.clone().into()),
248        )
249        .unwrap()
250        .into_group(bob_provider)
251        .unwrap();
252
253        let new_group_charlie = StagedWelcome::new_from_welcome(
254            charlie_provider,
255            alice_group.configuration(),
256            welcome.clone(),
257            Some(ratchet_tree.clone().into()),
258        )
259        .unwrap()
260        .into_group(bob_provider)
261        .unwrap();
262
263        let alice_comparison = new_alice_group
264            .export_secret(alice_provider.crypto(), "comparison", b"", 32)
265            .unwrap();
266
267        let bob_comparison = new_bob_group
268            .export_secret(bob_provider.crypto(), "comparison", b"", 32)
269            .unwrap();
270
271        let charlie_comparison = new_group_charlie
272            .export_secret(charlie_provider.crypto(), "comparison", b"", 32)
273            .unwrap();
274
275        assert_eq!(alice_comparison, bob_comparison);
276        assert_eq!(alice_comparison, charlie_comparison);
277    }
278}