Skip to content

Commit

Permalink
sdk: Add high level method for resetting the user's identity and dele…
Browse files Browse the repository at this point in the history
…ting all associated secrets
  • Loading branch information
stefanceriu committed Jul 24, 2024
1 parent f0ef37e commit 107f2e9
Show file tree
Hide file tree
Showing 2 changed files with 275 additions and 2 deletions.
84 changes: 84 additions & 0 deletions crates/matrix-sdk/src/encryption/recovery/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ use self::{
futures::{Enable, RecoverAndReset, Reset},
types::{BackupDisabledContent, SecretStorageDisabledContent},
};
use crate::encryption::{AuthData, CrossSigningResetAuthType, CrossSigningResetHandle};

/// The recovery manager for the [`Client`].
#[derive(Debug)]
Expand Down Expand Up @@ -344,6 +345,48 @@ impl Recovery {
RecoverAndReset::new(self, old_key)
}

/// Completely reset the current user's crypto identity: reset the cross
/// signing keys, delete the existing backup and recovery key. # Example
///
/// ```no_run
/// # use matrix_sdk::{ruma::api::client::uiaa, Client, encryption::recovery};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// # let user_id = unimplemented!();
/// let encryption = client.encryption();
///
/// if let Some(handle) = encryption.recovery().reset_identity().await? {
/// match handle.auth_type() {
/// CrossSigningResetAuthType::Uiaa(uiaa) => {
/// let password = "1234".to_owned();
/// let mut password = uiaa::Password::new(user_id, password);
/// password.session = uiaa.session;
///
/// handle.reset(Some(uiaa::AuthData::Password(password))).await?;
/// }
/// CrossSigningResetAuthType::Oidc(o) => {
/// println!("To reset your end-to-end encryption cross-signing identity, you first need to approve it at {}", o.approval_url);
/// handle.reset(None).await?;
/// }
/// }
/// }
/// # anyhow::Ok(()) };
/// ```
pub async fn reset_identity(&self) -> Result<Option<IdentityResetHandle>> {
let cross_signing_reset_handle = self.client.encryption().reset_cross_signing().await?;

if let Some(handle) = cross_signing_reset_handle {
Ok(Some(IdentityResetHandle {
client: self.client.clone(),
cross_signing_reset_handle: handle,
}))
} else {
Ok(None)
}
}

/// Recover all the secrets from the homeserver.
///
/// This method is a convenience method around the
Expand Down Expand Up @@ -567,3 +610,44 @@ impl Recovery {
}
}
}

/// A helper struct that handles resetting a user's crypto identity as well as
/// deleting their key backup, recovery key and store secrets.
#[derive(Debug)]
pub struct IdentityResetHandle {
client: Client,
cross_signing_reset_handle: CrossSigningResetHandle,
}

impl IdentityResetHandle {
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
/// process is using.
pub fn auth_type(&self) -> &CrossSigningResetAuthType {
&self.cross_signing_reset_handle.auth_type
}

/// This method starts the identity reset process and
/// will go through the following steps:
///
/// 1. Disable backing up room keys and delete the active backup
/// 2. Disable recovery (We can't delete account data events)
/// 3. Remove previously known secrets by creating a new store (?)
/// 4. Go through the cross-signing key reset flow
/// 5. Finally, re-enable key backups only if they were enabled before
pub async fn reset(&self, auth: Option<AuthData>) -> Result<()> {
self.client.encryption().backups().disable().await?; // 1.

self.client.account().set_account_data(SecretStorageDisabledContent {}).await?; // 2.
self.client.encryption().recovery().update_recovery_state().await?;

self.client.encryption().secret_storage().create_secret_store().await?; // .3

self.cross_signing_reset_handle.auth(auth).await?; // 4.

if self.client.encryption().recovery().should_auto_enable_backups().await? {
self.client.encryption().recovery().enable_backup().await?; // 5.
}

Ok(())
}
}
193 changes: 191 additions & 2 deletions crates/matrix-sdk/tests/integration/encryption/recovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,22 @@

use std::sync::{Arc, Mutex};

use assert_matches2::assert_let;
use futures_util::StreamExt;
use matrix_sdk::{
config::RequestConfig,
encryption::{
backups::BackupState,
recovery::{EnableProgress, RecoveryState},
BackupDownloadStrategy,
BackupDownloadStrategy, CrossSigningResetAuthType,
},
matrix_auth::{MatrixSession, MatrixSessionTokens},
test_utils::{no_retry_test_client_with_server, test_client_builder_with_server},
Client,
};
use matrix_sdk_base::SessionMeta;
use matrix_sdk_test::async_test;
use ruma::{device_id, user_id, UserId};
use ruma::{api::client::uiaa, device_id, user_id, UserId};
use serde::Deserialize;
use serde_json::{json, Value};
use tokio::spawn;
Expand Down Expand Up @@ -766,3 +767,191 @@ async fn recover_and_reset() {

server.verify().await
}

#[async_test]
async fn test_reset_identity() {
let user_id = user_id!("@example:morpheus.localhost");
let (client, server) = test_client(user_id).await;

enable(user_id, &client, &server, true).await;

// At this point both backups and recovery should be enabled
assert_eq!(client.encryption().backups().state(), BackupState::Enabled);
assert_eq!(client.encryption().recovery().state(), RecoveryState::Enabled);

// Disabling backups
Mock::given(method("DELETE"))
.and(path("_matrix/client/r0/room_keys/version/1"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.mount(&server)
.await;

// Disabling recovery
Mock::given(method("PUT"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
)))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(2) // One for the secret deletion and one for the new key
.named("m.secret_storage.default_key PUT")
.mount(&server)
.await;

Mock::given(method("GET"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.default_key"
)))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.named("m.secret_storage.default_key account data GET")
.mount(&server)
.await;

// Create a new secrets store
Mock::given(method("PUT"))
.and(path_regex(format!(
r"_matrix/client/r0/user/{user_id}/account_data/m.secret_storage.key.[A-Za-z0-9]"
)))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.named("m.secret_storage.key.[A-Za-z0-9] PUT")
.mount(&server)
.await;

Mock::given(method("GET"))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.megolm_backup.v1")))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.named("m.megolm_backup.v1 GET")
.mount(&server)
.await;

Mock::given(method("PUT"))
.and(path(format!("_matrix/client/r0/user/{user_id}/account_data/m.megolm_backup.v1")))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.named("m.megolm_backup.v1 PUT")
.mount(&server)
.await;

// Resetting cross-signing keys
let reset_handle = {
let _guard = Mock::given(method("POST"))
.and(path("/_matrix/client/unstable/keys/device_signing/upload"))
.respond_with(ResponseTemplate::new(401).set_body_json(json!({
"flows": [
{
"stages": [
"m.login.password"
]
}
],
"params": {},
"session": "oFIJVvtEOCKmRUTYKTYIIPHL"
})))
.expect(1)
.named("Initial cross-signing upload attempt")
.mount_as_scoped(&server)
.await;

client
.encryption()
.recovery()
.reset_identity()
.await
.unwrap()
.expect("We should have received a reset handle")
};

Mock::given(method("POST"))
.and(path("/_matrix/client/unstable/keys/device_signing/upload"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.named("Retrying to upload the cross-signing keys")
.expect(1)
.mount(&server)
.await;

Mock::given(method("POST"))
.and(path("/_matrix/client/unstable/keys/signatures/upload"))
.respond_with(move |_: &wiremock::Request| {
ResponseTemplate::new(200).set_body_json(json!({}))
})
.named("Final signatures upload")
.expect(1)
.mount(&server)
.await;

// Re-enable backups
Mock::given(method("GET"))
.and(path("_matrix/client/r0/room_keys/version"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(404).set_body_json(json!({
"errcode": "M_NOT_FOUND",
"error": "No current backup version"
})))
.expect(2)
.named("room_keys/version GET")
.mount(&server)
.await;

Mock::given(method("PUT"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.org.matrix.custom.backup_disabled"
)))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({})))
.expect(1)
.named("m.org.matrix.custom.backup_disabled PUT")
.mount(&server)
.await;

Mock::given(method("GET"))
.and(path(format!(
"_matrix/client/r0/user/{user_id}/account_data/m.org.matrix.custom.backup_disabled"
)))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(
json!({"type": "m.org.matrix.custom.backup_disabled",
"content": {
"disabled": false
}}),
))
.expect(1)
.named("m.org.matrix.custom.backup_disabled GET")
.mount(&server)
.await;

Mock::given(method("POST"))
.and(path("_matrix/client/unstable/room_keys/version"))
.and(header("authorization", "Bearer 1234"))
.respond_with(ResponseTemplate::new(200).set_body_json(json!({ "version": "1" })))
.expect(1)
.named("room_keys/version POST")
.mount(&server)
.await;

assert_let!(CrossSigningResetAuthType::Uiaa(uiaa_info) = reset_handle.auth_type());

let mut password = uiaa::Password::new(user_id.to_owned().into(), "1234".to_owned());
password.session = uiaa_info.session.clone();
reset_handle
.reset(Some(uiaa::AuthData::Password(password)))
.await
.expect("Failed retrieving identity reset handle");

assert!(
client.encryption().cross_signing_status().await.unwrap().is_complete(),
"After the reset we have the cross-signing available.",
);

// After reset backups should get renabled but recovery needs setting up again
assert_eq!(client.encryption().backups().state(), BackupState::Enabled);
assert_eq!(client.encryption().recovery().state(), RecoveryState::Disabled);

server.verify().await;
}

0 comments on commit 107f2e9

Please sign in to comment.