Skip to content

Commit 56c25f3

Browse files
committed
fix(recovery): Delete the known secrets from 4s when disabling recovery
1 parent ed8c1d5 commit 56c25f3

File tree

6 files changed

+186
-25
lines changed

6 files changed

+186
-25
lines changed

crates/matrix-sdk/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ All notable changes to this project will be documented in this file.
1212
`cleanup_frequency` setting.
1313
([#4603](https://github.com/matrix-org/matrix-rust-sdk/pull/4603))
1414

15+
### Bug Fixes
16+
17+
- Ensure all known secrets are removed from secret storage when invoking the
18+
`Recovery::disable()` method. While the server is not guaranteed to delete
19+
these secrets, making an attempt to remove them is considered good practice.
20+
Note that all secrets are uploaded to the server in an encrypted form.
21+
([#4629](https://github.com/matrix-org/matrix-rust-sdk/pull/4629))
22+
1523
## [0.10.0] - 2025-02-04
1624

1725
### Features

crates/matrix-sdk/src/encryption/recovery/mod.rs

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,13 @@ use futures_util::StreamExt as _;
9595
use ruma::{
9696
api::client::keys::get_keys,
9797
events::{
98-
secret::send::ToDeviceSecretSendEvent,
99-
secret_storage::default_key::SecretStorageDefaultKeyEvent,
98+
secret::{request::SecretName, send::ToDeviceSecretSendEvent},
99+
secret_storage::{default_key::SecretStorageDefaultKeyEvent, secret::SecretEventContent},
100+
GlobalAccountDataEventType,
100101
},
102+
serde::Raw,
101103
};
104+
use serde_json::{json, value::to_raw_value};
102105
use tracing::{error, info, instrument, warn};
103106

104107
#[cfg(doc)]
@@ -124,6 +127,15 @@ pub struct Recovery {
124127
}
125128

126129
impl Recovery {
130+
/// The list of known secrets that are contained in secret storage once
131+
/// recover is enabled.
132+
pub const KNOWN_SECRETS: &[SecretName] = &[
133+
SecretName::CrossSigningMasterKey,
134+
SecretName::CrossSigningUserSigningKey,
135+
SecretName::CrossSigningSelfSigningKey,
136+
SecretName::RecoveryKey,
137+
];
138+
127139
/// Get the current [`RecoveryState`] for this [`Client`].
128140
pub fn state(&self) -> RecoveryState {
129141
self.client.inner.e2ee.recovery_state.get()
@@ -285,11 +297,36 @@ impl Recovery {
285297
#[instrument(skip_all)]
286298
pub async fn disable(&self) -> Result<()> {
287299
self.client.encryption().backups().disable().await?;
300+
288301
// Why oh why, can't we delete account data events?
302+
//
303+
// Alright, let's attempt to "delete" the content of our current default key,
304+
// for this we first need to check if there is a default key, then
305+
// deserialize the content and find out the key ID.
306+
//
307+
// Then we finally set the event to an empty JSON content.
308+
if let Ok(Some(default_event)) =
309+
self.client.encryption().secret_storage().fetch_default_key_id().await
310+
{
311+
if let Ok(default_event) = default_event.deserialize() {
312+
let key_id = default_event.key_id;
313+
let event_type = GlobalAccountDataEventType::SecretStorageKey(key_id);
314+
315+
self.client
316+
.account()
317+
.set_account_data_raw(event_type, Raw::new(&json!({})).expect("").cast())
318+
.await?;
319+
}
320+
}
321+
322+
// Now let's "delete" the actual `m.secret.storage.default_key` event.
289323
self.client.account().set_account_data(SecretStorageDisabledContent {}).await?;
324+
// Make sure that we don't re-enable backups automatically.
290325
self.client.account().set_account_data(BackupDisabledContent { disabled: true }).await?;
326+
// Finally, "delete" all the known secrets we have in the account data.
327+
self.delete_all_known_secrets().await?;
328+
291329
self.update_recovery_state().await?;
292-
// TODO: Do we want to "delete" the known secrets as well?
293330

294331
Ok(())
295332
}
@@ -527,6 +564,28 @@ impl Recovery {
527564
Ok(())
528565
}
529566

567+
/// Delete all the known secrets we are keeping in secret storage.
568+
///
569+
/// The exact list of secrets is defined in [`Recovery::KNOWN_SECRETS`] and
570+
/// might change over time.
571+
///
572+
/// Since account data events can't actually be deleted, due to a missing
573+
/// DELETE API, we're replacing the events with an empty
574+
/// [`SecretEventContent`].
575+
async fn delete_all_known_secrets(&self) -> Result<()> {
576+
for secret_name in Self::KNOWN_SECRETS {
577+
let event_type = GlobalAccountDataEventType::from(secret_name.to_owned());
578+
let content = SecretEventContent::new(Default::default());
579+
let secret_content = Raw::from_json(
580+
to_raw_value(&content)
581+
.expect("We should be able to serialize a raw empty secret event content"),
582+
);
583+
self.client.account().set_account_data_raw(event_type, secret_content).await?;
584+
}
585+
586+
Ok(())
587+
}
588+
530589
/// Run a network request to figure whether backups have been disabled at
531590
/// the account level.
532591
async fn are_backups_marked_as_disabled(&self) -> Result<bool> {

crates/matrix-sdk/src/encryption/secret_storage/mod.rs

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -66,11 +66,14 @@ use matrix_sdk_base::crypto::{
6666
secret_storage::{DecodeError, MacError, SecretStorageKey},
6767
CryptoStoreError, SecretImportError,
6868
};
69-
use ruma::events::{
70-
secret_storage::{
71-
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
69+
use ruma::{
70+
events::{
71+
secret_storage::{
72+
default_key::SecretStorageDefaultKeyEventContent, key::SecretStorageKeyEventContent,
73+
},
74+
EventContentFromType, GlobalAccountDataEventType,
7275
},
73-
EventContentFromType, GlobalAccountDataEventType,
76+
serde::Raw,
7477
};
7578
use serde_json::value::to_raw_value;
7679
use thiserror::Error;
@@ -186,11 +189,7 @@ impl SecretStorage {
186189
/// # anyhow::Ok(()) };
187190
/// ```
188191
pub async fn open_secret_store(&self, secret_storage_key: &str) -> Result<SecretStore> {
189-
let maybe_default_key_id = self
190-
.client
191-
.account()
192-
.fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey)
193-
.await?;
192+
let maybe_default_key_id = self.fetch_default_key_id().await?;
194193

195194
if let Some(default_key_id) = maybe_default_key_id {
196195
let default_key_id =
@@ -271,12 +270,7 @@ impl SecretStorage {
271270

272271
/// Run a network request to find if secret storage is set up for this user.
273272
pub async fn is_enabled(&self) -> crate::Result<bool> {
274-
if let Some(content) = self
275-
.client
276-
.account()
277-
.fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey)
278-
.await?
279-
{
273+
if let Some(content) = self.fetch_default_key_id().await? {
280274
// Since we can't delete account data events, we're going to treat
281275
// deserialization failures as secret storage being disabled.
282276
Ok(content.deserialize_as::<SecretStorageDefaultKeyEventContent>().is_ok())
@@ -285,4 +279,17 @@ impl SecretStorage {
285279
Ok(false)
286280
}
287281
}
282+
283+
/// Fetch the `m.secret_storage.default_key` event from the server.
284+
pub async fn fetch_default_key_id(
285+
&self,
286+
) -> crate::Result<Option<Raw<SecretStorageDefaultKeyEventContent>>> {
287+
let maybe_default_key_id = self
288+
.client
289+
.account()
290+
.fetch_account_data(GlobalAccountDataEventType::SecretStorageDefaultKey)
291+
.await?;
292+
293+
Ok(maybe_default_key_id.map(|event| event.cast()))
294+
}
288295
}

crates/matrix-sdk/src/test_utils/mod.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,17 +247,17 @@ macro_rules! assert_next_eq_with_timeout_impl {
247247

248248
assert_eq!(next_value, $expected);
249249
};
250-
($stream:expr, $expected:expr, $timeout:expr, $($msg:tt)*) => {
250+
($stream:expr, $expected:expr, $timeout:expr, $($msg:tt)*) => {{
251251
let next_value = tokio::time::timeout(
252252
$timeout,
253-
$stream.next()
253+
futures_util::StreamExt::next(&mut $stream)
254254
)
255255
.await
256256
.expect("We should be able to get the next value out of the stream by now")
257257
.expect("The stream should have given us a new value instead of None");
258258

259259
assert_eq!(next_value, $expected, $($msg)*);
260-
};
260+
}};
261261
}
262262

263263
/// Like `assert_let`, but with the possibility to add an optional timeout.

crates/matrix-sdk/tests/integration/encryption/recovery.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -643,11 +643,19 @@ async fn test_recovery_disabling() {
643643
ResponseTemplate::new(404)
644644
}
645645
})
646-
.expect(2)
646+
.expect(3)
647647
.named("m.secret_storage.default_key account data GET")
648648
.mount(&server)
649649
.await;
650650

651+
Mock::given(method("PUT"))
652+
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.megolm_backup.v1",)))
653+
.and(header("authorization", "Bearer 1234"))
654+
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
655+
.expect(1..)
656+
.mount(&server)
657+
.await;
658+
651659
recovery.disable().await.expect("We should be able to disable recovery again.");
652660
assert_eq!(client.encryption().backups().state(), BackupState::Unknown);
653661
assert_eq!(recovery.state(), RecoveryState::Disabled);

testing/matrix-sdk-integration-testing/src/tests/e2ee.rs

Lines changed: 82 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,11 @@ use assert_matches::assert_matches;
55
use assert_matches2::assert_let;
66
use assign::assign;
77
use matrix_sdk::{
8+
assert_next_eq_with_timeout,
89
crypto::{format_emojis, SasState},
910
encryption::{
1011
backups::BackupState,
11-
recovery::RecoveryState,
12+
recovery::{Recovery, RecoveryState},
1213
verification::{
1314
QrVerificationData, QrVerificationState, Verification, VerificationRequestState,
1415
},
@@ -22,13 +23,14 @@ use matrix_sdk::{
2223
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
2324
SyncRoomMessageEvent,
2425
},
25-
OriginalSyncMessageLikeEvent,
26+
secret_storage::secret::SecretEventContent,
27+
GlobalAccountDataEventType, OriginalSyncMessageLikeEvent,
2628
},
2729
},
2830
Client,
2931
};
3032
use similar_asserts::assert_eq;
31-
use tracing::warn;
33+
use tracing::{debug, warn};
3234

3335
use crate::helpers::{SyncTokenAwareClient, TestClientBuilder};
3436

@@ -974,3 +976,80 @@ async fn test_secret_gossip_after_interactive_verification() -> Result<()> {
974976

975977
Ok(())
976978
}
979+
980+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
981+
async fn test_recovery_disabling_deletes_secret_storage_secrets() -> Result<()> {
982+
let encryption_settings = EncryptionSettings {
983+
auto_enable_cross_signing: true,
984+
auto_enable_backups: true,
985+
..Default::default()
986+
};
987+
let client = SyncTokenAwareClient::new(
988+
TestClientBuilder::new("alice_recovery_deletion_test")
989+
.encryption_settings(encryption_settings)
990+
.build()
991+
.await?,
992+
);
993+
994+
debug!("Enabling recovery");
995+
996+
client.encryption().wait_for_e2ee_initialization_tasks().await;
997+
client.encryption().recovery().enable().await?;
998+
999+
// Let's wait for recovery to become enabled.
1000+
let mut recovery_state = client.encryption().recovery().state_stream();
1001+
1002+
debug!("Checking that recovery has been enabled");
1003+
1004+
assert_next_eq_with_timeout!(
1005+
recovery_state,
1006+
RecoveryState::Enabled,
1007+
"Recovery should have been enabled"
1008+
);
1009+
1010+
debug!("Checking that the secrets have been stored on the server");
1011+
1012+
for event_type in Recovery::KNOWN_SECRETS {
1013+
let event_type = GlobalAccountDataEventType::from(event_type.clone());
1014+
let event = client
1015+
.account()
1016+
.fetch_account_data(event_type.clone())
1017+
.await?
1018+
.expect("The secret event should still exist");
1019+
1020+
let event = event
1021+
.deserialize_as::<SecretEventContent>()
1022+
.expect("We should be able to deserialize the content of known secrets");
1023+
1024+
assert!(
1025+
!event.encrypted.is_empty(),
1026+
"The known secret {event_type} should exist on the server"
1027+
);
1028+
}
1029+
1030+
debug!("Disabling recovery");
1031+
1032+
client.encryption().recovery().disable().await?;
1033+
1034+
debug!("Checking that the secrets have been removed from the server");
1035+
1036+
for event_type in Recovery::KNOWN_SECRETS {
1037+
let event_type = GlobalAccountDataEventType::from(event_type.clone());
1038+
let event = client
1039+
.account()
1040+
.fetch_account_data(event_type.clone())
1041+
.await?
1042+
.expect("The secret event should still exist");
1043+
1044+
let event = event
1045+
.deserialize_as::<SecretEventContent>()
1046+
.expect("We should be able to deserialize the content since that's what we uploaded");
1047+
1048+
assert!(
1049+
event.encrypted.is_empty(),
1050+
"The known secret {event_type} should have been deleted from the server"
1051+
);
1052+
}
1053+
1054+
Ok(())
1055+
}

0 commit comments

Comments
 (0)