Skip to content

Commit

Permalink
Merge pull request #3759 from matrix-org/stefan/crypto-identity-reset
Browse files Browse the repository at this point in the history
ffi: add high level method for resetting the user's identity and deleting all associated secrets
  • Loading branch information
stefanceriu authored Jul 30, 2024
2 parents 40e3a96 + 8895e53 commit f51eebb
Show file tree
Hide file tree
Showing 4 changed files with 379 additions and 3 deletions.
85 changes: 84 additions & 1 deletion bindings/matrix-sdk-ffi/src/encryption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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() }
}
}
30 changes: 30 additions & 0 deletions bindings/matrix-sdk-ffi/src/ruma.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
103 changes: 103 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,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
Expand Down Expand Up @@ -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(())
}
}
Loading

0 comments on commit f51eebb

Please sign in to comment.