mirror of
https://github.com/matrix-org/matrix-rust-sdk.git
synced 2026-06-21 22:58:32 -04:00
Merge pull request #3759 from matrix-org/stefan/crypto-identity-reset
ffi: add high level method for resetting the user's identity and deleting all associated secrets
This commit is contained in:
@@ -9,7 +9,7 @@ use thiserror::Error;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use super::RUNTIME;
|
||||
use crate::{client::Client, error::ClientError, task_handle::TaskHandle};
|
||||
use crate::{client::Client, error::ClientError, ruma::AuthData, task_handle::TaskHandle};
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct Encryption {
|
||||
@@ -357,6 +357,22 @@ impl Encryption {
|
||||
Ok(result?)
|
||||
}
|
||||
|
||||
/// Completely reset the current user's crypto identity: reset the cross
|
||||
/// signing keys, delete the existing backup and recovery key.
|
||||
pub async fn reset_identity(&self) -> Result<Option<Arc<IdentityResetHandle>>, ClientError> {
|
||||
if let Some(reset_handle) = self
|
||||
.inner
|
||||
.recovery()
|
||||
.reset_identity()
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })?
|
||||
{
|
||||
return Ok(Some(Arc::new(IdentityResetHandle { inner: reset_handle })));
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub async fn recover(&self, mut recovery_key: String) -> Result<()> {
|
||||
let result = self.inner.recovery().recover(&recovery_key).await;
|
||||
|
||||
@@ -387,3 +403,70 @@ impl Encryption {
|
||||
self.inner.wait_for_e2ee_initialization_tasks().await;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Object)]
|
||||
pub struct IdentityResetHandle {
|
||||
pub(crate) inner: matrix_sdk::encryption::recovery::IdentityResetHandle,
|
||||
}
|
||||
|
||||
#[uniffi::export(async_runtime = "tokio")]
|
||||
impl IdentityResetHandle {
|
||||
/// Get the underlying [`CrossSigningResetAuthType`] this identity reset
|
||||
/// process is using.
|
||||
pub fn auth_type(&self) -> CrossSigningResetAuthType {
|
||||
self.inner.auth_type().into()
|
||||
}
|
||||
|
||||
/// 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<AuthData>) -> Result<(), ClientError> {
|
||||
if let Some(auth) = auth {
|
||||
self.inner
|
||||
.reset(Some(auth.into()))
|
||||
.await
|
||||
.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
} else {
|
||||
self.inner.reset(None).await.map_err(|e| ClientError::Generic { msg: e.to_string() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum CrossSigningResetAuthType {
|
||||
/// The homeserver requires user-interactive authentication.
|
||||
Uiaa,
|
||||
// /// OIDC is used for authentication and the user needs to open a URL to
|
||||
// /// approve the upload of cross-signing keys.
|
||||
Oidc {
|
||||
info: OidcCrossSigningResetInfo,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::CrossSigningResetAuthType> for CrossSigningResetAuthType {
|
||||
fn from(value: &matrix_sdk::encryption::CrossSigningResetAuthType) -> Self {
|
||||
match value {
|
||||
encryption::CrossSigningResetAuthType::Uiaa(_) => Self::Uiaa,
|
||||
encryption::CrossSigningResetAuthType::Oidc(info) => Self::Oidc { info: info.into() },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct OidcCrossSigningResetInfo {
|
||||
/// The error message we received from the homeserver after we attempted to
|
||||
/// reset the cross-signing keys.
|
||||
pub error: String,
|
||||
/// The URL where the user can approve the reset of the cross-signing keys.
|
||||
pub approval_url: String,
|
||||
}
|
||||
|
||||
impl From<&matrix_sdk::encryption::OidcCrossSigningResetInfo> for OidcCrossSigningResetInfo {
|
||||
fn from(value: &matrix_sdk::encryption::OidcCrossSigningResetInfo) -> Self {
|
||||
Self { error: value.error.to_owned(), approval_url: value.approval_url.to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,36 @@ use crate::{
|
||||
utils::u64_to_uint,
|
||||
};
|
||||
|
||||
#[derive(uniffi::Enum)]
|
||||
pub enum AuthData {
|
||||
/// Password-based authentication (`m.login.password`).
|
||||
Password { password_details: AuthDataPasswordDetails },
|
||||
}
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct AuthDataPasswordDetails {
|
||||
/// One of the user's identifiers.
|
||||
identifier: String,
|
||||
|
||||
/// The plaintext password.
|
||||
password: String,
|
||||
}
|
||||
|
||||
impl From<AuthData> for ruma::api::client::uiaa::AuthData {
|
||||
fn from(value: AuthData) -> ruma::api::client::uiaa::AuthData {
|
||||
match value {
|
||||
AuthData::Password { password_details } => {
|
||||
let user_id = ruma::UserId::parse(password_details.identifier).unwrap();
|
||||
|
||||
ruma::api::client::uiaa::AuthData::Password(ruma::api::client::uiaa::Password::new(
|
||||
user_id.into(),
|
||||
password_details.password,
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a matrix entity from a given URI, be it either
|
||||
/// a `matrix.to` link or a `matrix:` URI
|
||||
#[uniffi::export]
|
||||
|
||||
@@ -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,79 @@ impl Recovery {
|
||||
RecoverAndReset::new(self, old_key)
|
||||
}
|
||||
|
||||
/// Completely reset the current user's crypto identity.
|
||||
/// This method 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 already enabled)
|
||||
///
|
||||
/// Disclaimer: failures in this flow will potentially leave the user in
|
||||
/// an inconsistent state but they're expected to just run the reset flow
|
||||
/// again as presumably the reason they started it to begin with was
|
||||
/// that they no longer had access to any of their data.
|
||||
///
|
||||
/// # 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<Option<IdentityResetHandle>> {
|
||||
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?;
|
||||
|
||||
let cross_signing_reset_handle = self.client.encryption().reset_cross_signing().await?;
|
||||
|
||||
if let Some(handle) = cross_signing_reset_handle {
|
||||
// Authentication required, backups will be re-enabled after the reset
|
||||
Ok(Some(IdentityResetHandle {
|
||||
client: self.client.clone(),
|
||||
cross_signing_reset_handle: handle,
|
||||
}))
|
||||
} else {
|
||||
// No authentication required, re-enable backups
|
||||
if self.client.encryption().recovery().should_auto_enable_backups().await? {
|
||||
self.client.encryption().recovery().enable_backup().await?; // 4.
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
/// Recover all the secrets from the homeserver.
|
||||
///
|
||||
/// This method is a convenience method around the
|
||||
@@ -567,3 +641,32 @@ impl Recovery {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper struct that handles continues resetting a user's crypto identity
|
||||
/// after authentication was required and re-enabling backups (if necessary) at
|
||||
/// the end of it
|
||||
#[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 will retry to upload the device keys after the previous try
|
||||
/// failed due to required authentication
|
||||
pub async fn reset(&self, auth: Option<AuthData>) -> Result<()> {
|
||||
self.cross_signing_reset_handle.auth(auth).await?;
|
||||
|
||||
if self.client.encryption().recovery().should_auto_enable_backups().await? {
|
||||
self.client.encryption().recovery().enable_backup().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user