diff --git a/mls-rs/src/client.rs b/mls-rs/src/client.rs index def0b0d1..4b85ef89 100644 --- a/mls-rs/src/client.rs +++ b/mls-rs/src/client.rs @@ -338,6 +338,8 @@ pub enum MlsError { InvalidGroupInfo, #[cfg_attr(feature = "std", error("Invalid welcome message"))] InvalidWelcomeMessage, + #[cfg_attr(feature = "std", error("Exporter deleted"))] + ExporterDeleted, } impl IntoAnyError for MlsError { diff --git a/mls-rs/src/group/key_schedule.rs b/mls-rs/src/group/key_schedule.rs index 77c1d65e..77c53352 100644 --- a/mls-rs/src/group/key_schedule.rs +++ b/mls-rs/src/group/key_schedule.rs @@ -83,6 +83,10 @@ impl KeySchedule { } } + pub fn delete_exporter(&mut self) { + self.exporter_secret = Default::default(); + } + #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn derive_for_external( &self, @@ -234,6 +238,10 @@ impl KeySchedule { len: usize, cipher_suite: &P, ) -> Result>, MlsError> { + if self.exporter_secret.is_empty() { + return Err(MlsError::ExporterDeleted); + } + let secret = kdf_derive_secret(cipher_suite, &self.exporter_secret, label).await?; let context_hash = cipher_suite diff --git a/mls-rs/src/group/mod.rs b/mls-rs/src/group/mod.rs index 57ff5628..f66db9db 100644 --- a/mls-rs/src/group/mod.rs +++ b/mls-rs/src/group/mod.rs @@ -1438,6 +1438,11 @@ where Ok(self.key_schedule.authentication_secret.clone().into()) } + /// Export a secret for use outside of MLS. Each epoch, label, context + /// combination has a unique and independent secret. Secrets for all + /// epochs, labels and contexts can be derived until either the epoch + /// changes, i.e. a commit is received (or own commit is applied), or + /// [Group::delete_exporter] is called. #[cfg_attr(not(mls_build_async), maybe_async::must_be_sync)] pub async fn export_secret( &self, @@ -1451,6 +1456,15 @@ where .map(Into::into) } + /// Delete the exporter secret. Afterwards the state contains no information + /// about any secrets outputted by [Group::export_secret] (for the current or + /// past epochs). This means that after calling this function, [Group::export_secret] + /// can no longer be used and we get forward secrecy for all secrets derived using + /// [Group::export_secret]. + pub fn delete_exporter(&mut self) { + self.key_schedule.delete_exporter(); + } + /// Export the current epoch's ratchet tree in serialized format. /// /// This function is used to provide the current group tree to new members @@ -4412,4 +4426,19 @@ mod tests { assert_eq!(restored.group_state(), group.group_state()); } + + #[maybe_async::test(not(mls_build_async), async(mls_build_async, crate::futures_test))] + async fn delete_exporter() { + let mut group = test_group(TEST_PROTOCOL_VERSION, TEST_CIPHER_SUITE).await; + + group.export_secret(b"123", b"", 15).await.unwrap(); + + group.delete_exporter(); + let res = group.export_secret(b"123", b"", 15).await; + assert_matches!(res, Err(MlsError::ExporterDeleted)); + + group.commit(vec![]).await.unwrap(); + group.apply_pending_commit().await.unwrap(); + group.export_secret(b"123", b"", 15).await.unwrap(); + } }