From db064626faa98cd73b1a42eb4a6a7aa3bd5a3257 Mon Sep 17 00:00:00 2001 From: Stefan Ceriu Date: Wed, 24 Jul 2024 10:53:50 +0300 Subject: [PATCH] sdk: Add high level method for resetting the user's identity and deleting all associated secrets --- .../matrix-sdk/src/encryption/recovery/mod.rs | 91 ++++++++++ .../tests/integration/encryption/recovery.rs | 164 +++++++++++++++++- 2 files changed, 253 insertions(+), 2 deletions(-) diff --git a/crates/matrix-sdk/src/encryption/recovery/mod.rs b/crates/matrix-sdk/src/encryption/recovery/mod.rs index 3c37c495e..33b1cb473 100644 --- a/crates/matrix-sdk/src/encryption/recovery/mod.rs +++ b/crates/matrix-sdk/src/encryption/recovery/mod.rs @@ -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)] @@ -344,6 +345,57 @@ 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. + /// + /// # Examples + /// + /// ```no_run + /// # use matrix_sdk::{ + /// encryption::recovery, encryption::CrossSigningResetAuthType, ruma::api::client::uiaa, + /// Client, + /// }; + /// # 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> { + 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 @@ -567,3 +619,42 @@ 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 and delete secret storage + /// 3. Go through the cross-signing key reset flow + /// 4. Finally, re-enable key backups only if they were enabled before + pub async fn reset(&self, auth: Option) -> Result<()> { + self.client.encryption().backups().disable().await?; // 1. + + // 2. (We can't delete account data events) + self.client.account().set_account_data(SecretStorageDisabledContent {}).await?; + self.client.encryption().recovery().update_recovery_state().await?; + + self.cross_signing_reset_handle.auth(auth).await?; // 3. + + if self.client.encryption().recovery().should_auto_enable_backups().await? { + self.client.encryption().recovery().enable_backup().await?; // 4. + } + + Ok(()) + } +} diff --git a/crates/matrix-sdk/tests/integration/encryption/recovery.rs b/crates/matrix-sdk/tests/integration/encryption/recovery.rs index 3d7138657..3f0ca63e1 100644 --- a/crates/matrix-sdk/tests/integration/encryption/recovery.rs +++ b/crates/matrix-sdk/tests/integration/encryption/recovery.rs @@ -14,13 +14,14 @@ 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}, @@ -28,7 +29,7 @@ use matrix_sdk::{ }; 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; @@ -766,3 +767,162 @@ 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(1) + .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; + + // 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; +}