Skip to content

Commit c444bc9

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

File tree

6 files changed

+214
-25
lines changed

6 files changed

+214
-25
lines changed

Diff for: crates/matrix-sdk/CHANGELOG.md

+8
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

Diff for: crates/matrix-sdk/src/encryption/recovery/mod.rs

+62-3
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> {

Diff for: crates/matrix-sdk/src/encryption/secret_storage/mod.rs

+22-15
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
}

Diff for: crates/matrix-sdk/src/test_utils/mod.rs

+3-3
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.

Diff for: crates/matrix-sdk/tests/integration/encryption/recovery.rs

+9-1
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);

Diff for: testing/matrix-sdk-integration-testing/src/tests/e2ee.rs

+110-3
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,15 @@ use matrix_sdk::{
2223
MessageType, OriginalSyncRoomMessageEvent, RoomMessageEventContent,
2324
SyncRoomMessageEvent,
2425
},
25-
OriginalSyncMessageLikeEvent,
26+
secret_storage::{key::SecretStorageKeyEventContent, secret::SecretEventContent},
27+
EventContentFromType, GlobalAccountDataEventType, OriginalSyncMessageLikeEvent,
2628
},
2729
},
2830
Client,
2931
};
32+
use serde_json::value::to_raw_value;
3033
use similar_asserts::assert_eq;
31-
use tracing::warn;
34+
use tracing::{debug, warn};
3235

3336
use crate::helpers::{SyncTokenAwareClient, TestClientBuilder};
3437

@@ -974,3 +977,107 @@ async fn test_secret_gossip_after_interactive_verification() -> Result<()> {
974977

975978
Ok(())
976979
}
980+
981+
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
982+
async fn test_recovery_disabling_deletes_secret_storage_secrets() -> Result<()> {
983+
let encryption_settings = EncryptionSettings {
984+
auto_enable_cross_signing: true,
985+
auto_enable_backups: true,
986+
..Default::default()
987+
};
988+
let client = SyncTokenAwareClient::new(
989+
TestClientBuilder::new("alice_recovery_deletion_test")
990+
.encryption_settings(encryption_settings)
991+
.build()
992+
.await?,
993+
);
994+
995+
debug!("Enabling recovery");
996+
997+
client.encryption().wait_for_e2ee_initialization_tasks().await;
998+
client.encryption().recovery().enable().await?;
999+
1000+
// Let's wait for recovery to become enabled.
1001+
let mut recovery_state = client.encryption().recovery().state_stream();
1002+
1003+
debug!("Checking that recovery has been enabled");
1004+
1005+
assert_next_eq_with_timeout!(
1006+
recovery_state,
1007+
RecoveryState::Enabled,
1008+
"Recovery should have been enabled"
1009+
);
1010+
1011+
debug!("Checking that the secrets have been stored on the server");
1012+
1013+
let default_event = client
1014+
.encryption()
1015+
.secret_storage()
1016+
.fetch_default_key_id()
1017+
.await?
1018+
.expect("The default key event should have been uploaded to the server")
1019+
.deserialize()
1020+
.expect("We should be able to deserialize the default event content");
1021+
1022+
let key_id = default_event.key_id;
1023+
1024+
for event_type in Recovery::KNOWN_SECRETS {
1025+
let event_type = GlobalAccountDataEventType::from(event_type.clone());
1026+
let event = client
1027+
.account()
1028+
.fetch_account_data(event_type.clone())
1029+
.await?
1030+
.expect("The secret event should still exist");
1031+
1032+
let event = event
1033+
.deserialize_as::<SecretEventContent>()
1034+
.expect("We should be able to deserialize the content of known secrets");
1035+
1036+
assert!(
1037+
!event.encrypted.is_empty(),
1038+
"The known secret {event_type} should exist on the server"
1039+
);
1040+
}
1041+
1042+
debug!("Disabling recovery");
1043+
1044+
client.encryption().recovery().disable().await?;
1045+
1046+
debug!("Checking that the secrets have been removed from the server");
1047+
1048+
for event_type in Recovery::KNOWN_SECRETS {
1049+
let event_type = GlobalAccountDataEventType::from(event_type.clone());
1050+
let event = client
1051+
.account()
1052+
.fetch_account_data(event_type.clone())
1053+
.await?
1054+
.expect("The secret event should still exist");
1055+
1056+
let event = event
1057+
.deserialize_as::<SecretEventContent>()
1058+
.expect("We should be able to deserialize the content since that's what we uploaded");
1059+
1060+
assert!(
1061+
event.encrypted.is_empty(),
1062+
"The known secret {event_type} should have been deleted from the server"
1063+
);
1064+
}
1065+
1066+
debug!("Checking that the info about the default key has been deleted");
1067+
let event_type = GlobalAccountDataEventType::SecretStorageKey(key_id);
1068+
let content = client
1069+
.account()
1070+
.fetch_account_data(event_type.clone())
1071+
.await?
1072+
.expect("The key event should be available on the server");
1073+
1074+
SecretStorageKeyEventContent::from_parts(
1075+
&event_type.to_string(),
1076+
&to_raw_value(&content).unwrap(),
1077+
)
1078+
.expect_err(
1079+
"The key event should have been “deleted” so we shouldn't be able to deserialize it",
1080+
);
1081+
1082+
Ok(())
1083+
}

0 commit comments

Comments
 (0)