Recovery support

Co-authored-by: Jonas Platte <jplatte@matrix.org>
This commit is contained in:
Damir Jelić
2023-11-11 18:02:37 +01:00
parent 1fcd5af526
commit 97026fc3a6
7 changed files with 925 additions and 5 deletions
+7 -2
View File
@@ -85,8 +85,9 @@ use crate::{
};
#[cfg(feature = "e2e-encryption")]
use crate::{
encryption::backups::types::BackupClientState,
encryption::{Encryption, EncryptionSettings},
encryption::{
backups::types::BackupClientState, recovery::RecoveryState, Encryption, EncryptionSettings,
},
store_locks::CrossProcessStoreLock,
};
@@ -251,6 +252,8 @@ pub(crate) struct ClientInner {
pub(crate) encryption_settings: EncryptionSettings,
#[cfg(feature = "e2e-encryption")]
pub(crate) backup_state: BackupClientState,
#[cfg(feature = "e2e-encryption")]
pub(crate) recovery_state: SharedObservable<RecoveryState>,
}
impl ClientInner {
@@ -291,6 +294,8 @@ impl ClientInner {
encryption_settings,
#[cfg(feature = "e2e-encryption")]
backup_state: Default::default(),
#[cfg(feature = "e2e-encryption")]
recovery_state: Default::default(),
};
#[allow(clippy::let_and_return)]
@@ -862,6 +862,7 @@ impl Backups {
/// removed on the homeserver.
async fn handle_deleted_backup_version(&self, olm_machine: &OlmMachine) -> Result<(), Error> {
olm_machine.backup_machine().disable_backup().await?;
self.client.encryption().recovery().update_state_after_backup_disabling().await;
self.set_state(BackupState::Unknown);
Ok(())
+14
View File
@@ -62,6 +62,7 @@ use self::{
backups::Backups,
futures::PrepareEncryptedFile,
identities::{DeviceUpdates, IdentityUpdates},
recovery::Recovery,
secret_storage::SecretStorage,
};
use crate::{
@@ -78,6 +79,7 @@ use crate::{
pub mod backups;
pub mod futures;
pub mod identities;
pub mod recovery;
pub mod secret_storage;
pub mod verification;
@@ -114,6 +116,9 @@ pub struct EncryptionSettings {
///
/// [`SecretStore::import_secrets()`]: crate::encryption::secret_storage::SecretStore::import_secrets
pub auto_download_from_backup: bool,
/// Automatically create a backup version if no backup exists.
pub auto_enable_backups: bool,
}
impl Client {
@@ -152,6 +157,7 @@ impl Client {
let response = self.send(request, None).await?;
self.mark_request_as_sent(request_id, &response).await?;
self.encryption().recovery().update_state_after_keys_query(&response).await;
Ok(response)
}
@@ -1086,6 +1092,11 @@ impl Encryption {
Backups { client: self.client.to_owned() }
}
/// Get the recovery manager of the client.
pub fn recovery(&self) -> Recovery {
Recovery { client: self.client.to_owned() }
}
/// Enables the crypto-store cross-process lock.
///
/// This may be required if there are multiple processes that may do writes
@@ -1212,6 +1223,9 @@ impl Encryption {
if let Err(e) = this.backups().setup_and_resume().await {
error!("Couldn't setup and resume backups {e:?}");
}
if let Err(e) = this.recovery().setup().await {
error!("Couldn't setup and resume recovery {e:?}");
}
}));
Ok(())
@@ -0,0 +1,252 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Named futures for the recovery support.
use std::future::IntoFuture;
use futures_core::Stream;
use futures_util::{pin_mut, StreamExt};
use matrix_sdk_common::boxed_into_future;
use tokio_stream::wrappers::errors::BroadcastStreamRecvError;
use tracing::{warn, Instrument, Span};
use super::{EnableProgress, Recovery, RecoveryError, Result};
use crate::{
encryption::{backups::UploadState, secret_storage::SecretStore},
utils::ChannelObservable,
};
/// Named future for the [`Recovery::enable()`] method.
#[derive(Debug)]
pub struct Enable<'a> {
pub(super) recovery: &'a Recovery,
pub(super) progress: ChannelObservable<EnableProgress>,
pub(super) wait_for_backups_upload: bool,
pub(super) passphrase: Option<&'a str>,
tracing_span: Span,
}
impl<'a> Enable<'a> {
pub(super) fn new(recovery: &'a Recovery) -> Self {
Self {
recovery,
progress: Default::default(),
wait_for_backups_upload: false,
passphrase: None,
tracing_span: Span::current(),
}
}
/// Subscribe to updates to the recovery enabling progress.
pub fn subscribe_to_progress(
&self,
) -> impl Stream<Item = Result<EnableProgress, BroadcastStreamRecvError>> {
self.progress.subscribe()
}
/// Should the enabling of the recovery also wait for *all* room keys to be
/// uploaded to the server-side key backup?
///
/// This is useful if the user is enabling recovery and the room key backup
/// just before logging out. Otherwise the logout might finish before
/// all room keys have been backed up and thus historic messages will
/// fail to decrypt once the user logs back in again.
pub fn wait_for_backups_to_upload(mut self) -> Self {
self.wait_for_backups_upload = true;
self
}
/// In addition to the recovery key the [`Recovery::enable()`] method
/// returns, allow this passphrase to be used for the
/// [`Recovery::recover()`] method.
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
self.passphrase = Some(passphrase);
self
}
}
impl<'a> IntoFuture for Enable<'a> {
type Output = Result<String>;
boxed_into_future!(extra_bounds: 'a);
fn into_future(self) -> Self::IntoFuture {
let Self { recovery, progress, wait_for_backups_upload, passphrase, tracing_span } = self;
let future = async move {
if !recovery.client.encryption().backups().are_enabled().await {
if recovery.client.encryption().backups().exists_on_server().await? {
return Err(RecoveryError::BackupExistsOnServer);
} else {
progress.set(EnableProgress::CreatingBackup);
recovery.mark_backup_as_enabled().await?;
recovery.client.encryption().backups().create().await?;
}
}
progress.set(EnableProgress::CreatingRecoveryKey);
let secret_storage = recovery.client.encryption().secret_storage();
let create_store = if let Some(passphrase) = passphrase {
secret_storage.create_secret_store().with_passphrase(passphrase)
} else {
secret_storage.create_secret_store()
};
let store: SecretStore = create_store.await?;
if wait_for_backups_upload {
let backups = recovery.client.encryption().backups();
let upload_future = backups.wait_for_steady_state();
let upload_progress = upload_future.subscribe_to_progress();
#[allow(unused_variables)]
let progress_task = matrix_sdk_common::executor::spawn({
let progress = progress.clone();
async move {
pin_mut!(upload_progress);
while let Some(update) = upload_progress.next().await {
match update {
Ok(UploadState::Uploading(count)) => {
progress.set(EnableProgress::BackingUp(count));
}
Ok(UploadState::Done | UploadState::Error) | Err(_) => break,
_ => (),
}
}
}
});
if let Err(e) = upload_future.await {
warn!("Couldn't upload all the room keys to the backup: {e:?}");
progress.set(EnableProgress::RoomKeyUploadError);
}
#[cfg(not(target_arch = "wasm32"))]
progress_task.abort();
} else {
recovery.client.encryption().backups().maybe_trigger_backup();
}
let key = store.secret_storage_key();
progress.set(EnableProgress::Done { recovery_key: key });
recovery.update_recovery_state().await?;
Ok(store.secret_storage_key())
};
Box::pin(future.instrument(tracing_span))
}
}
/// Named future for the [`Recovery::reset_key()`] method.
#[derive(Debug)]
pub struct Reset<'a> {
pub(super) recovery: &'a Recovery,
pub(super) passphrase: Option<&'a str>,
tracing_span: Span,
}
impl<'a> Reset<'a> {
pub(super) fn new(recovery: &'a Recovery) -> Self {
Self { recovery, passphrase: None, tracing_span: Span::current() }
}
/// In addition to the recovery key the [`Recovery::reset_key()`] method
/// returns, allow this passphrase to be used for the
/// [`Recovery::recover()`] method.
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
self.passphrase = Some(passphrase);
self
}
}
impl<'a> IntoFuture for Reset<'a> {
type Output = Result<String>;
boxed_into_future!(extra_bounds: 'a);
fn into_future(self) -> Self::IntoFuture {
let Self { recovery, passphrase, tracing_span } = self;
let future = async move {
let secret_storage = recovery.client.encryption().secret_storage();
let create_store = if let Some(passphrase) = passphrase {
secret_storage.create_secret_store().with_passphrase(passphrase)
} else {
secret_storage.create_secret_store()
};
let store: SecretStore = create_store.await?;
recovery.update_recovery_state().await?;
Ok(store.secret_storage_key())
};
Box::pin(future.instrument(tracing_span))
}
}
/// Named future for the [`Recovery::recover_and_reset()`] method.
#[derive(Debug)]
pub struct RecoverAndReset<'a> {
pub(super) recovery: &'a Recovery,
pub(super) old_recovery_key: &'a str,
pub(super) passphrase: Option<&'a str>,
tracing_span: Span,
}
impl<'a> RecoverAndReset<'a> {
pub(super) fn new(recovery: &'a Recovery, old_recovery_key: &'a str) -> Self {
Self { recovery, old_recovery_key, passphrase: None, tracing_span: Span::current() }
}
/// In addition to the new recovery key the
/// [`Recovery::recover_and_reset()`] method returns, allow this
/// passphrase to be used for the [`Recovery::recover()`] method.
pub fn with_passphrase(mut self, passphrase: &'a str) -> Self {
self.passphrase = Some(passphrase);
self
}
}
impl<'a> IntoFuture for RecoverAndReset<'a> {
type Output = Result<String>;
boxed_into_future!(extra_bounds: 'a);
fn into_future(self) -> Self::IntoFuture {
let Self { recovery, old_recovery_key, passphrase, tracing_span } = self;
let future = async move {
recovery.recover(old_recovery_key).await?;
let reset = if let Some(passphrase) = passphrase {
recovery.reset_key().with_passphrase(passphrase)
} else {
recovery.reset_key()
};
reset.await
};
Box::pin(future.instrument(tracing_span))
}
}
@@ -0,0 +1,522 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! The recovery module
//!
//! The recovery module attempts to provide a unified and simplified view over
//! the secret storage and backup subsystems.
//!
//! **Note**: If you are using this module, do not use the [`SecretStorage`] and
//! [`Backups`] subsystems directly. This module makes assumptions that might be
//! broken by the direct usage of the respective lower level modules.
//!
//! **Note**: The term Recovery used in this submodule is not the same as the
//! [`Recovery key`] mentioned in the spec. The recovery key from the spec is
//! solely about backups, while the term recovery in this file includes both the
//! backups and the secret storage subsystems. The recovery key mentioned in
//! this file is the secret storage key.
//!
//! You should configure your client to bootstrap cross-signing automatically
//! and may chose to let your client automatically create a backup, if it
//! doesn't exist, as well:
//!
//! ```no_run
//! use matrix_sdk::{encryption::EncryptionSettings, Client};
//!
//! # async {
//! # let homeserver = "http://example.org";
//! let client = Client::builder()
//! .homeserver_url(homeserver)
//! .with_encryption_settings(EncryptionSettings {
//! auto_enable_cross_signing: true,
//! auto_enable_backups: true,
//! ..Default::default()
//! })
//! .build()
//! .await?;
//! # anyhow::Ok(()) };
//! ```
//!
//! # Examples
//!
//! For a newly registered user you will want to enable recovery, either
//! immediately or before the user logs out.
//!
//! ```no_run
//! # use matrix_sdk::{Client, encryption::recovery::EnableProgress};
//! # use url::Url;
//! # async {
//! # let homeserver = Url::parse("http://example.com")?;
//! # let client = Client::new(homeserver).await?;
//! let recovery = client.encryption().recovery();
//!
//! // Create a new recovery key, you can use the provided passphrase, or the returned recovery key
//! // to recover.
//! let recovery_key = recovery
//! .enable()
//! .wait_for_backups_to_upload()
//! .with_passphrase("my passphrase")
//! .await;
//! # anyhow::Ok(()) };
//! ```
//!
//! If the user logs in with another device, you'll want to let the user recover
//! its secrets by entering the recovery key or recovery passphrase.
//!
//! ```no_run
//! # use matrix_sdk::{Client, encryption::recovery::EnableProgress};
//! # use url::Url;
//! # async {
//! # let homeserver = Url::parse("http://example.com")?;
//! # let client = Client::new(homeserver).await?;
//! let recovery = client.encryption().recovery();
//!
//! // Create a new recovery key, you can use the provided passphrase, or the returned recovery key
//! // to recover.
//! recovery.recover("my recovery key or passphrase").await;
//! # anyhow::Ok(()) };
//! ```
//!
//! [`Recovery key`]: https://spec.matrix.org/v1.8/client-server-api/#recovery-key
use futures_core::Stream;
use ruma::{
api::client::keys::get_keys,
events::{
secret::send::ToDeviceSecretSendEvent,
secret_storage::default_key::SecretStorageDefaultKeyEvent,
},
};
use tracing::{error, info, instrument};
#[cfg(doc)]
use crate::encryption::{
backups::Backups,
secret_storage::{SecretStorage, SecretStore},
};
use crate::Client;
pub mod futures;
mod types;
pub use self::types::{EnableProgress, RecoveryError, RecoveryState, Result};
use self::{
futures::{Enable, RecoverAndReset, Reset},
types::{BackupDisabledContent, SecretStorageDisabledContent},
};
/// The recovery manager for the [`Client`].
#[derive(Debug)]
pub struct Recovery {
pub(super) client: Client,
}
impl Recovery {
/// Get the current [`RecoveryState`] for this [`Client`].
pub fn state(&self) -> RecoveryState {
self.client.inner.recovery_state.get()
}
/// Get a stream of updates to the [`RecoveryState`].
///
/// This method will send out the current state as the first update.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// use futures_util::StreamExt;
///
/// let recovery = client.encryption().recovery();
///
/// let mut state_stream = recovery.state_stream();
///
/// while let Some(update) = state_stream.next().await {
/// match update {
/// RecoveryState::Enabled => {
/// println!("Recovery has been enabled");
/// }
/// _ => (),
/// }
/// }
/// # anyhow::Ok(()) };
/// ```
pub fn state_stream(&self) -> impl Stream<Item = RecoveryState> {
self.client.inner.recovery_state.subscribe_reset()
}
/// Enable secret storage *and* backups.
///
/// This method will create a new secret storage key and a new backup if one
/// doesn't already exist. It will then upload all the locally cached
/// secrets, including the backup recovery key, to the new secret store.
///
/// This method will throw an error if a backup already exists on the
/// homeserver but this [`Client`] isn't connected to the existing backup.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::recovery::EnableProgress};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// use futures_util::StreamExt;
///
/// let recovery = client.encryption().recovery();
///
/// let enable = recovery
/// .enable()
/// .wait_for_backups_to_upload()
/// .with_passphrase("my passphrase");
///
/// let mut progress_stream = enable.subscribe_to_progress();
///
/// tokio::spawn(async move {
/// while let Some(update) = progress_stream.next().await {
/// let Ok(update) = update else {
/// panic!("Update to the enable progress lagged")
/// };
///
/// match update {
/// EnableProgress::CreatingBackup => {
/// println!("Creating a new backup");
/// }
/// EnableProgress::CreatingRecoveryKey => {
/// println!("Creating a new recovery key");
/// }
/// EnableProgress::Done { .. } => {
/// println!("Recovery has been enabled");
/// break;
/// }
/// _ => (),
/// }
/// }
/// });
///
/// let recovery_key = enable.await?;
///
/// # anyhow::Ok(()) };
/// ```
#[instrument(skip_all)]
pub fn enable(&self) -> Enable<'_> {
Enable::new(self)
}
/// Create a new backup if one does not exist yet.
///
/// This method will throw an error if a backup already exists on the
/// homeserver but this [`Client`] isn't connected to the existing backup.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::backups::BackupState};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let recovery = client.encryption().recovery();
///
/// recovery.enable_backup().await?;
///
/// assert_eq!(client.encryption().backups().state(), BackupState::Enabled);
///
/// # anyhow::Ok(()) };
/// ```
#[instrument(skip_all)]
pub async fn enable_backup(&self) -> Result<()> {
if !self.client.encryption().backups().exists_on_server().await? {
self.mark_backup_as_enabled().await?;
self.client.encryption().backups().create().await?;
self.client.encryption().backups().maybe_trigger_backup();
Ok(())
} else {
Err(RecoveryError::BackupExistsOnServer)
}
}
/// Disable recovery completely.
///
/// This method will do the following steps:
///
/// 1. Disable the uploading of room keys to a currently active backup.
/// 2. Delete the currently active backup.
/// 3. Set the `m.secret_storage.default_key` global account data event to
/// an empty JSON content.
/// 4. Set a global account data event so clients won't attempt to
/// automatically re-enable a backup.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let recovery = client.encryption().recovery();
///
/// recovery.disable().await?;
///
/// assert_eq!(recovery.state(), RecoveryState::Disabled);
///
/// # anyhow::Ok(()) };
/// ```
#[instrument(skip_all)]
pub async fn disable(&self) -> Result<()> {
self.client.encryption().backups().disable().await?;
// Why oh why, can't we delete account data events?
self.client.account().set_account_data(SecretStorageDisabledContent {}).await?;
self.client.account().set_account_data(BackupDisabledContent { disabled: true }).await?;
self.update_recovery_state().await?;
// TODO: Do we want to "delete" the known secrets as well?
Ok(())
}
/// Reset the recovery key.
///
/// This will rotate the secret storage key and re-upload all the secrets to
/// the [`SecretStore`].
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let recovery = client.encryption().recovery();
///
/// let new_recovery_key =
/// recovery.reset_key().with_passphrase("my passphrase").await;
/// # anyhow::Ok(()) };
/// ```
#[instrument(skip_all)]
pub fn reset_key(&self) -> Reset<'_> {
// TODO: Should this only be possible if we're in the RecoveryState::Enabled
// state? Otherwise we'll create a new secret store but won't be able to
// upload all the secrets.
Reset::new(self)
}
/// Reset the recovery key but first import all the secrets from secret
/// storage.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let recovery = client.encryption().recovery();
///
/// let new_recovery_key = recovery
/// .recover_and_reset("my old passphrase or key")
/// .with_passphrase("my new passphrase")
/// .await?;
/// # anyhow::Ok(()) };
/// ```
#[instrument(skip_all)]
pub fn recover_and_reset<'a>(&'a self, old_key: &'a str) -> RecoverAndReset<'_> {
RecoverAndReset::new(self, old_key)
}
/// Recover all the secrets from the homeserver.
///
/// This method is a convenience method around the
/// [`SecretStore::import_secrets()`] method, please read the documentation
/// of this method for more information about what happens if you call
/// this method.
///
/// In short, this method will turn a newly created [`Client`] into a fully
/// end-to-end encryption enabled client.
///
/// # Examples
///
/// ```no_run
/// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
/// # use url::Url;
/// # async {
/// # let homeserver = Url::parse("http://example.com")?;
/// # let client = Client::new(homeserver).await?;
/// let recovery = client.encryption().recovery();
///
/// recovery.recover("my recovery key or passphrase").await;
///
/// assert_eq!(recovery.state(), RecoveryState::Enabled);
/// # anyhow::Ok(()) };
/// ```
#[instrument(skip_all)]
pub async fn recover(&self, recovery_key: &str) -> Result<()> {
let store =
self.client.encryption().secret_storage().open_secret_store(recovery_key).await?;
store.import_secrets().await?;
self.update_recovery_state().await?;
Ok(())
}
/// Is this device the last device the user has?
///
/// This method is useful to check if we should recommend to the user that
/// they should enable recovery, typically done before logging out.
///
/// If the user does not enable recovery before logging out of their last
/// device, they will not be able to decrypt historic messages once they
/// create a new device.
pub async fn are_we_the_last_man_standing(&self) -> Result<bool> {
let olm_machine = self.client.olm_machine().await;
let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
let user_id = olm_machine.user_id();
self.client.encryption().ensure_initial_key_query().await?;
let devices = self.client.encryption().get_user_devices(user_id).await?;
Ok(devices.devices().count() == 1)
}
async fn all_known_secrets_available(&self) -> Result<bool> {
let cross_signing_complete = self
.client
.encryption()
.cross_signing_status()
.await
.map(|status| status.is_complete())
.unwrap_or_default();
// The backup state is fine if we have backups enabled locally, or if backups
// have been marked as disabled.
let backup_state_ok = if self.client.encryption().backups().are_enabled().await {
true
} else {
self.are_backups_marked_as_disabled().await?
};
Ok(cross_signing_complete && backup_state_ok)
}
async fn should_auto_enable_backups(&self) -> Result<bool> {
// If we didn't already enable backups, we don't see a backup version on the
// server, and finally if backups have not been marked to be explicitly
// disabled, then we can automatically enable them.
Ok(self.client.inner.encryption_settings.auto_enable_backups
&& !self.client.encryption().backups().are_enabled().await
&& !self.client.encryption().backups().exists_on_server().await?
&& !self.are_backups_marked_as_disabled().await?)
}
pub(crate) async fn setup(&self) -> Result<()> {
info!("Setting up account data listeners and trying to setup recovery");
self.update_recovery_state().await?;
if self.should_auto_enable_backups().await? {
self.enable_backup().await?;
}
self.client.add_event_handler(Self::default_key_event_handler);
self.client.add_event_handler(Self::secret_send_event_handler);
Ok(())
}
async fn are_backups_marked_as_disabled(&self) -> Result<bool> {
Ok(self
.client
.account()
.fetch_account_data(BackupDisabledContent::event_type())
.await?
.map(|event| {
event
.deserialize_as::<BackupDisabledContent>()
.map(|event| event.disabled)
.unwrap_or(false)
})
.unwrap_or(false))
}
async fn mark_backup_as_enabled(&self) -> Result<()> {
self.client.account().set_account_data(BackupDisabledContent { disabled: false }).await?;
Ok(())
}
async fn check_recovery_state(&self) -> Result<RecoveryState> {
Ok(if self.client.encryption().secret_storage().is_enabled().await? {
if self.all_known_secrets_available().await? {
RecoveryState::Enabled
} else {
RecoveryState::Incomplete
}
} else {
RecoveryState::Disabled
})
}
async fn update_recovery_state(&self) -> Result<()> {
let new_state = self.check_recovery_state().await?;
self.client.inner.recovery_state.set(new_state);
Ok(())
}
async fn update_recovery_state_no_fail(&self) {
if let Err(e) = self.update_recovery_state().await {
error!("Coulnd't update the recovery state: {e:?}");
}
}
#[instrument]
async fn secret_send_event_handler(_: ToDeviceSecretSendEvent, client: Client) {
client.encryption().recovery().update_recovery_state_no_fail().await;
}
#[instrument]
async fn default_key_event_handler(_: SecretStorageDefaultKeyEvent, client: Client) {
client.encryption().recovery().update_recovery_state_no_fail().await;
}
#[instrument]
pub(crate) async fn update_state_after_backup_disabling(&self) {
// TODO: This is quite ugly, this method is called by the backups subsystem.
// Backups shouldn't depend on recovery, recovery should listen to the
// backup state change.
self.update_recovery_state_no_fail().await;
}
#[instrument]
pub(crate) async fn update_state_after_keys_query(&self, response: &get_keys::v3::Response) {
if let Some(user_id) = self.client.user_id() {
if response.master_keys.contains_key(user_id) {
// TODO: This is unnecessarily expensive, we could let the crypto crate notify
// us that our private keys got erased... But, the OlmMachine
// gets recreated and... You know the drill by now...
self.update_recovery_state_no_fail().await;
}
}
}
}
@@ -0,0 +1,126 @@
// Copyright 2023 The Matrix.org Foundation C.I.C.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use matrix_sdk_base::crypto::store::RoomKeyCounts;
use ruma::{
events::{EventContent, GlobalAccountDataEventType},
exports::ruma_macros::EventContent,
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
#[cfg(doc)]
use crate::encryption::{
backups::Backups,
recovery::{futures::Enable, Recovery},
};
/// Result type alias for the [`Recovery`] subsystem.
pub type Result<A, E = RecoveryError> = std::result::Result<A, E>;
/// Error type for the [`Recovery`] subsystem.
#[derive(Debug, Error)]
pub enum RecoveryError {
/// A backup already exists on the homeserver, the recovery subsystem does
/// not allow backups to be overwritten, disable recovery first.
#[error(
"A backup already exists on the homeserver and the method does not allow to overwrite it"
)]
BackupExistsOnServer,
/// A typical SDK error.
#[error(transparent)]
Sdk(#[from] crate::Error),
/// Error in the secret storage subsystem.
#[error(transparent)]
SecretStorage(#[from] crate::encryption::secret_storage::SecretStorageError),
}
/// Enum describing the states the [`Recovery::enable()`] method can be in.
#[derive(Debug, Default, Clone, Zeroize, ZeroizeOnDrop)]
pub enum EnableProgress {
/// The client is just starting the process of enabling recovery, this is
/// the initial state.
#[default]
Starting,
/// The client is creating a new server-side key backup.
CreatingBackup,
/// The client is creating a new recovery key and uploading all the locally
/// cached secrets to the homeserver.
CreatingRecoveryKey,
/// The client is currently backing up room keys to the server-side key
/// backup. This state may be emitted multiple times until all room keys
/// have been backed up.
#[zeroize(skip)]
BackingUp(RoomKeyCounts),
/// The client encountered an error while trying to upload all room keys to
/// the server-side key backup.
///
/// Not all room keys may have been backed up, the client will try to back
/// them up again at a later point. If you'd like to wait for the backup
/// to finish again you can use the [`Backups::wait_for_steady_state()`]
/// method.
RoomKeyUploadError,
/// Recovery has been successfully enabled, this is the final state.
Done {
/// The newly created recovery key.
// TODO: Can I remove this from here? It seems a bit dumb.
recovery_key: String,
},
}
/// The states the recovery subsystem can be in.
///
/// You can listen on the state of the recovery mechanism using the
/// [`Recovery::state_stream()`] method.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum RecoveryState {
/// We didn't yet inform ourselves about the state of things.
#[default]
Unknown,
/// Secret storage is setup and we have all the secrets locally.
Enabled,
/// No default secret storage key exists or it is disabled explicitly using
/// the account data event.
Disabled,
/// Secret storage is setup but we're missing some secrets.
Incomplete,
}
/// A hack to allow the `m.secret_storage.default_key` event to be "deleted".
///
/// This allows us to set the `m.secret_storage.default_key` event to an empty
/// JSON object, which means that the event will be invalid.
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.secret_storage.default_key", kind = GlobalAccountData)]
pub(super) struct SecretStorageDisabledContent {}
/// A custom global account data event which tells us that a new backup should
/// not be automatically created.
#[derive(Clone, Debug, Default, Deserialize, Serialize, EventContent)]
#[ruma_event(type = "m.org.matrix.custom.backup_disabled", kind = GlobalAccountData)]
pub(super) struct BackupDisabledContent {
pub disabled: bool,
}
impl BackupDisabledContent {
/// Get the event type of the [`BackupDisabledContent`] global account data
/// event.
pub(super) fn event_type() -> GlobalAccountDataEventType {
// This is dumb, there's got to be a better way to get to the event type?
Self { disabled: false }.event_type()
}
}
@@ -153,7 +153,7 @@ async fn secret_store_missing_key_info() {
.respond_with(ResponseTemplate::new(200).set_body_json(json!({
"key": key_id
})))
.expect(1)
.expect(1..)
.named("default_key account data GET")
.mount(&server)
.await;
@@ -203,7 +203,7 @@ async fn secret_store_not_setup() {
"errcode": "M_NOT_FOUND",
"error": "Account data not found"
})))
.expect(1)
.expect(1..)
.named("default_key account data GET")
.mount(&server)
.await;
@@ -589,7 +589,7 @@ async fn is_secret_storage_enabled() {
"errcode": "M_NOT_FOUND",
"error": "Account data not found"
})))
.expect(1)
.expect(1..)
.named("default_key account data GET")
.mount_as_scoped(&server)
.await;