matrix_sdk/encryption/recovery/mod.rs
1// Copyright 2023 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! The recovery module
16//!
17//! The recovery module attempts to provide a unified and simplified view over
18//! the secret storage and backup subsystems.
19//!
20//! **Note**: If you are using this module, do not use the [`SecretStorage`] and
21//! [`Backups`] subsystems directly. This module makes assumptions that might be
22//! broken by the direct usage of the respective lower level modules.
23//!
24//! **Note**: The term Recovery used in this submodule is not the same as the
25//! [`Recovery key`] mentioned in the spec. The recovery key from the spec is
26//! solely about backups, while the term recovery in this file includes both the
27//! backups and the secret storage subsystems. The recovery key mentioned in
28//! this file is the secret storage key.
29//!
30//! You should configure your client to bootstrap cross-signing automatically
31//! and may choose to let your client automatically create a backup, if it
32//! doesn't exist, as well:
33//!
34//! ```no_run
35//! use matrix_sdk::{Client, encryption::EncryptionSettings};
36//!
37//! # async {
38//! # let homeserver = "http://example.org";
39//! let client = Client::builder()
40//! .homeserver_url(homeserver)
41//! .with_encryption_settings(EncryptionSettings {
42//! auto_enable_cross_signing: true,
43//! auto_enable_backups: true,
44//! ..Default::default()
45//! })
46//! .build()
47//! .await?;
48//! # anyhow::Ok(()) };
49//! ```
50//!
51//! # Examples
52//!
53//! For a newly registered user you will want to enable recovery, either
54//! immediately or before the user logs out.
55//!
56//! ```no_run
57//! # use matrix_sdk::{Client, encryption::recovery::EnableProgress};
58//! # use url::Url;
59//! # async {
60//! # let homeserver = Url::parse("http://example.com")?;
61//! # let client = Client::new(homeserver).await?;
62//! let recovery = client.encryption().recovery();
63//!
64//! // Create a new recovery key, you can use the provided passphrase, or the returned recovery key
65//! // to recover.
66//! let recovery_key = recovery
67//! .enable()
68//! .wait_for_backups_to_upload()
69//! .with_passphrase("my passphrase")
70//! .await;
71//! # anyhow::Ok(()) };
72//! ```
73//!
74//! If the user logs in with another device, you'll want to let the user recover
75//! its secrets by entering the recovery key or recovery passphrase.
76//!
77//! ```no_run
78//! # use matrix_sdk::{Client, encryption::recovery::EnableProgress};
79//! # use url::Url;
80//! # async {
81//! # let homeserver = Url::parse("http://example.com")?;
82//! # let client = Client::new(homeserver).await?;
83//! let recovery = client.encryption().recovery();
84//!
85//! // Create a new recovery key, you can use the provided passphrase, or the returned recovery key
86//! // to recover.
87//! recovery.recover("my recovery key or passphrase").await;
88//! # anyhow::Ok(()) };
89//! ```
90//!
91//! [`Recovery key`]: https://spec.matrix.org/v1.8/client-server-api/#recovery-key
92
93use futures_core::{Future, Stream};
94use futures_util::StreamExt as _;
95use ruma::{
96 api::client::keys::get_keys,
97 events::{
98 GlobalAccountDataEventType,
99 secret::{request::SecretName, send::ToDeviceSecretSendEvent},
100 secret_storage::{default_key::SecretStorageDefaultKeyEvent, secret::SecretEventContent},
101 },
102 serde::Raw,
103};
104use serde_json::{json, value::to_raw_value};
105use tracing::{error, info, instrument, warn};
106
107#[cfg(doc)]
108use crate::encryption::{
109 backups::Backups,
110 secret_storage::{SecretStorage, SecretStore},
111};
112use crate::{
113 Client,
114 client::WeakClient,
115 encryption::{backups::BackupState, secret_storage::SecretStorageError},
116};
117
118pub mod futures;
119mod types;
120pub use self::types::{EnableProgress, RecoveryError, RecoveryState, Result};
121use self::{
122 futures::{Enable, RecoverAndReset, Reset},
123 types::{BackupDisabledContent, KeyBackupContent, SecretStorageDisabledContent},
124};
125use crate::encryption::{AuthData, CrossSigningResetAuthType, CrossSigningResetHandle};
126
127/// The recovery manager for the [`Client`].
128#[derive(Debug)]
129pub struct Recovery {
130 pub(super) client: Client,
131}
132
133impl Recovery {
134 /// The list of known secrets that are contained in secret storage once
135 /// recover is enabled.
136 pub const KNOWN_SECRETS: &[SecretName] = &[
137 SecretName::CrossSigningMasterKey,
138 SecretName::CrossSigningUserSigningKey,
139 SecretName::CrossSigningSelfSigningKey,
140 SecretName::RecoveryKey,
141 ];
142
143 /// Get the current [`RecoveryState`] for this [`Client`].
144 pub fn state(&self) -> RecoveryState {
145 self.client.inner.e2ee.recovery_state.get()
146 }
147
148 /// Get a stream of updates to the [`RecoveryState`].
149 ///
150 /// This method will send out the current state as the first update.
151 ///
152 /// # Examples
153 ///
154 /// ```no_run
155 /// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
156 /// # use url::Url;
157 /// # async {
158 /// # let homeserver = Url::parse("http://example.com")?;
159 /// # let client = Client::new(homeserver).await?;
160 /// use futures_util::StreamExt;
161 ///
162 /// let recovery = client.encryption().recovery();
163 ///
164 /// let mut state_stream = recovery.state_stream();
165 ///
166 /// while let Some(update) = state_stream.next().await {
167 /// match update {
168 /// RecoveryState::Enabled => {
169 /// println!("Recovery has been enabled");
170 /// }
171 /// _ => (),
172 /// }
173 /// }
174 /// # anyhow::Ok(()) };
175 /// ```
176 pub fn state_stream(&self) -> impl Stream<Item = RecoveryState> + use<> {
177 self.client.inner.e2ee.recovery_state.subscribe_reset()
178 }
179
180 /// Enable secret storage _and_ backups.
181 ///
182 /// This method will create a new secret storage key and a new backup if one
183 /// doesn't already exist. It will then upload all the locally cached
184 /// secrets, including the backup recovery key, to the new secret store.
185 ///
186 /// This method will throw an error if a backup already exists on the
187 /// homeserver but this [`Client`] isn't connected to the existing backup.
188 ///
189 /// # Examples
190 ///
191 /// ```no_run
192 /// # use matrix_sdk::{Client, encryption::recovery::EnableProgress};
193 /// # use url::Url;
194 /// # async {
195 /// # let homeserver = Url::parse("http://example.com")?;
196 /// # let client = Client::new(homeserver).await?;
197 /// use futures_util::StreamExt;
198 ///
199 /// let recovery = client.encryption().recovery();
200 ///
201 /// let enable = recovery
202 /// .enable()
203 /// .wait_for_backups_to_upload()
204 /// .with_passphrase("my passphrase");
205 ///
206 /// let mut progress_stream = enable.subscribe_to_progress();
207 ///
208 /// tokio::spawn(async move {
209 /// while let Some(update) = progress_stream.next().await {
210 /// let Ok(update) = update else {
211 /// panic!("Update to the enable progress lagged")
212 /// };
213 ///
214 /// match update {
215 /// EnableProgress::CreatingBackup => {
216 /// println!("Creating a new backup");
217 /// }
218 /// EnableProgress::CreatingRecoveryKey => {
219 /// println!("Creating a new recovery key");
220 /// }
221 /// EnableProgress::Done { .. } => {
222 /// println!("Recovery has been enabled");
223 /// break;
224 /// }
225 /// _ => (),
226 /// }
227 /// }
228 /// });
229 ///
230 /// let recovery_key = enable.await?;
231 ///
232 /// # anyhow::Ok(()) };
233 /// ```
234 #[instrument(skip_all)]
235 pub fn enable(&self) -> Enable<'_> {
236 Enable::new(self)
237 }
238
239 /// Create a new backup if one does not exist yet.
240 ///
241 /// This method will throw an error if a backup already exists on the
242 /// homeserver but this [`Client`] isn't connected to the existing backup.
243 ///
244 /// # Examples
245 ///
246 /// ```no_run
247 /// # use matrix_sdk::{Client, encryption::backups::BackupState};
248 /// # use url::Url;
249 /// # async {
250 /// # let homeserver = Url::parse("http://example.com")?;
251 /// # let client = Client::new(homeserver).await?;
252 /// let recovery = client.encryption().recovery();
253 ///
254 /// recovery.enable_backup().await?;
255 ///
256 /// assert_eq!(client.encryption().backups().state(), BackupState::Enabled);
257 ///
258 /// # anyhow::Ok(()) };
259 /// ```
260 #[instrument(skip_all)]
261 pub async fn enable_backup(&self) -> Result<()> {
262 if !self.client.encryption().backups().fetch_exists_on_server().await? {
263 self.mark_backup_as_enabled().await?;
264
265 self.client.encryption().backups().create().await?;
266 self.client.encryption().backups().maybe_trigger_backup();
267
268 Ok(())
269 } else {
270 Err(RecoveryError::BackupExistsOnServer)
271 }
272 }
273
274 /// Disable recovery completely.
275 ///
276 /// This method will do the following steps:
277 ///
278 /// 1. Disable the uploading of room keys to a currently active backup.
279 /// 2. Delete the currently active backup.
280 /// 3. Set the `m.secret_storage.default_key` global account data event to
281 /// an empty JSON content.
282 /// 4. Set a global account data event so clients won't attempt to
283 /// automatically re-enable a backup.
284 ///
285 /// # Examples
286 ///
287 /// ```no_run
288 /// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
289 /// # use url::Url;
290 /// # async {
291 /// # let homeserver = Url::parse("http://example.com")?;
292 /// # let client = Client::new(homeserver).await?;
293 /// let recovery = client.encryption().recovery();
294 ///
295 /// recovery.disable().await?;
296 ///
297 /// assert_eq!(recovery.state(), RecoveryState::Disabled);
298 ///
299 /// # anyhow::Ok(()) };
300 /// ```
301 #[instrument(skip_all)]
302 pub async fn disable(&self) -> Result<()> {
303 self.client.encryption().backups().disable().await?;
304
305 // Why oh why, can't we delete account data events?
306 //
307 // Alright, let's attempt to "delete" the content of our current default
308 // key, for this we first need to check if there is a default key, then
309 // deserialize the content and find out the key ID.
310 //
311 // Then we finally set the event to an empty JSON content.
312 if let Ok(Some(default_event)) =
313 self.client.encryption().secret_storage().fetch_default_key_id().await
314 && let Ok(default_event) = default_event.deserialize()
315 {
316 let key_id = default_event.key_id;
317 let event_type = GlobalAccountDataEventType::SecretStorageKey(key_id);
318
319 self.client
320 .account()
321 .set_account_data_raw(event_type, Raw::new(&json!({})).expect("").cast_unchecked())
322 .await?;
323 }
324
325 // Now let's "delete" the actual `m.secret.storage.default_key` event.
326 self.client.account().set_account_data(SecretStorageDisabledContent {}).await?;
327 // Make sure that we don't re-enable backups automatically.
328 self.client.account().set_account_data(KeyBackupContent { enabled: false }).await?;
329 // (Unstable prefix version of KeyBackupContent)
330 self.client.account().set_account_data(BackupDisabledContent { disabled: true }).await?;
331 // Finally, "delete" all the known secrets we have in the account data.
332 self.delete_all_known_secrets().await?;
333
334 self.update_recovery_state().await?;
335
336 Ok(())
337 }
338
339 /// Reset the recovery key.
340 ///
341 /// This will rotate the secret storage key and re-upload all the secrets to
342 /// the [`SecretStore`].
343 ///
344 /// # Examples
345 ///
346 /// ```no_run
347 /// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
348 /// # use url::Url;
349 /// # async {
350 /// # let homeserver = Url::parse("http://example.com")?;
351 /// # let client = Client::new(homeserver).await?;
352 /// let recovery = client.encryption().recovery();
353 ///
354 /// let new_recovery_key =
355 /// recovery.reset_key().with_passphrase("my passphrase").await;
356 /// # anyhow::Ok(()) };
357 /// ```
358 #[instrument(skip_all)]
359 pub fn reset_key(&self) -> Reset<'_> {
360 // TODO: Should this only be possible if we're in the
361 // RecoveryState::Enabled state? Otherwise we'll create a new secret
362 // store but won't be able to upload all the secrets.
363 Reset::new(self)
364 }
365
366 /// Reset the recovery key but first import all the secrets from secret
367 /// storage.
368 ///
369 /// # Examples
370 ///
371 /// ```no_run
372 /// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
373 /// # use url::Url;
374 /// # async {
375 /// # let homeserver = Url::parse("http://example.com")?;
376 /// # let client = Client::new(homeserver).await?;
377 /// let recovery = client.encryption().recovery();
378 ///
379 /// let new_recovery_key = recovery
380 /// .recover_and_reset("my old passphrase or key")
381 /// .with_passphrase("my new passphrase")
382 /// .await?;
383 /// # anyhow::Ok(()) };
384 /// ```
385 #[instrument(skip_all)]
386 pub fn recover_and_reset<'a>(&'a self, old_key: &'a str) -> RecoverAndReset<'a> {
387 RecoverAndReset::new(self, old_key)
388 }
389
390 /// Completely reset the current user's crypto identity. This method will go
391 /// through the following steps:
392 ///
393 /// 1. Disable backing up room keys and delete the active backup
394 /// 2. Disable recovery and delete secret storage
395 /// 3. Go through the cross-signing key reset flow
396 /// 4. Finally, re-enable key backups (only if they were already enabled)
397 ///
398 /// Disclaimer: failures in this flow will potentially leave the user in an
399 /// inconsistent state but they're expected to just run the reset flow again
400 /// as presumably the reason they started it to begin with was that they no
401 /// longer had access to any of their data.
402 ///
403 /// # Examples
404 ///
405 /// ```no_run
406 /// # use matrix_sdk::{
407 /// encryption::recovery, encryption::CrossSigningResetAuthType, ruma::api::client::uiaa,
408 /// Client,
409 /// };
410 /// # use url::Url;
411 /// # async {
412 /// # let homeserver = Url::parse("http://example.com")?;
413 /// # let client = Client::new(homeserver).await?;
414 /// # let user_id = unimplemented!();
415 /// let encryption = client.encryption();
416 ///
417 /// if let Some(handle) = encryption.recovery().reset_identity().await? {
418 /// match handle.auth_type() {
419 /// CrossSigningResetAuthType::Uiaa(uiaa) => {
420 /// let password = "1234".to_owned();
421 /// let mut password = uiaa::Password::new(user_id, password);
422 /// password.session = uiaa.session;
423 ///
424 /// handle.reset(Some(uiaa::AuthData::Password(password))).await?;
425 /// }
426 /// CrossSigningResetAuthType::OAuth(o) => {
427 /// println!(
428 /// "To reset your end-to-end encryption cross-signing identity, \
429 /// you first need to approve it at {}",
430 /// o.approval_url
431 /// );
432 /// handle.reset(None).await?;
433 /// }
434 /// }
435 /// }
436 /// # anyhow::Ok(()) };
437 /// ```
438 pub async fn reset_identity(&self) -> Result<Option<IdentityResetHandle>> {
439 self.client.encryption().backups().disable_and_delete().await?; // 1.
440
441 // 2. (We can't delete account data events)
442 self.client.account().set_account_data(SecretStorageDisabledContent {}).await?;
443 self.client.encryption().recovery().update_recovery_state().await?;
444
445 let cross_signing_reset_handle = self.client.encryption().reset_cross_signing().await?;
446
447 if let Some(handle) = cross_signing_reset_handle {
448 // Authentication required, backups will be re-enabled after the
449 // reset
450 Ok(Some(IdentityResetHandle {
451 client: self.client.clone(),
452 cross_signing_reset_handle: handle,
453 }))
454 } else {
455 // No authentication required, re-enable backups
456 if self.client.encryption().recovery().should_auto_enable_backups().await? {
457 self.client.encryption().recovery().enable_backup().await?; // 4.
458 }
459
460 Ok(None)
461 }
462 }
463
464 /// Recover all the secrets from the homeserver.
465 ///
466 /// This method is a convenience method around the
467 /// [`SecretStore::import_secrets()`] method, please read the documentation
468 /// of this method for more information about what happens if you call this
469 /// method.
470 ///
471 /// In short, this method will turn a newly created [`Client`] into a fully
472 /// end-to-end encryption enabled client.
473 ///
474 /// # Examples
475 ///
476 /// ```no_run
477 /// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
478 /// # use url::Url;
479 /// # async {
480 /// # let homeserver = Url::parse("http://example.com")?;
481 /// # let client = Client::new(homeserver).await?;
482 /// let recovery = client.encryption().recovery();
483 ///
484 /// recovery.recover("my recovery key or passphrase").await;
485 ///
486 /// assert_eq!(recovery.state(), RecoveryState::Enabled);
487 /// # anyhow::Ok(()) };
488 /// ```
489 #[instrument(skip_all)]
490 pub async fn recover(&self, recovery_key: &str) -> Result<()> {
491 let store =
492 self.client.encryption().secret_storage().open_secret_store(recovery_key).await?;
493
494 store.import_secrets().await?;
495 self.update_recovery_state().await?;
496
497 Ok(())
498 }
499
500 /// Recover all the secrets from the homeserver, and, if the key backup
501 /// information is inconsistent, create a new key backup.
502 ///
503 /// Please read the documentation for [`SecretStore::import_secrets()`] for
504 /// more information about the recovery of identity information.
505 ///
506 /// This will create a new key backup if:
507 ///
508 /// - Key backup is enabled and the backup decryption key is missing from
509 /// Recovery, or
510 /// - Key backup is enabled and the backup decryption key does not match the
511 /// public key
512 ///
513 /// # Examples
514 ///
515 /// ```no_run
516 /// # use matrix_sdk::{Client, encryption::recovery::RecoveryState};
517 /// # use url::Url;
518 /// # async {
519 /// # let homeserver = Url::parse("http://example.com")?;
520 /// # let client = Client::new(homeserver).await?;
521 /// let recovery = client.encryption().recovery();
522 ///
523 /// recovery.recover_and_fix_backup("my recovery key or passphrase").await;
524 ///
525 /// assert_eq!(recovery.state(), RecoveryState::Enabled);
526 /// # anyhow::Ok(()) };
527 /// ```
528 #[instrument(skip_all)]
529 pub async fn recover_and_fix_backup(&self, recovery_key: &str) -> Result<()> {
530 let store =
531 self.client.encryption().secret_storage().open_secret_store(recovery_key).await?;
532
533 let delete_and_recreate_backup = match store.import_secrets().await {
534 Ok(()) => false,
535 Err(SecretStorageError::InconsistentBackupDecryptionKey) => {
536 warn!(
537 "Key storage decryption key does not match the current backup - creating a new key backup"
538 );
539 true
540 }
541 Err(SecretStorageError::MissingOrInvalidBackupDecryptionKey) => {
542 warn!("Missing or invalid backup decryption key - creating a new key backup");
543 true
544 }
545 Err(e) => return Err(e.into()),
546 };
547
548 if delete_and_recreate_backup {
549 self.client.encryption().backups().disable_and_delete().await?;
550 self.enable_backup().await?;
551 store.export_secrets().await?;
552 }
553
554 self.update_recovery_state().await?;
555
556 Ok(())
557 }
558
559 /// Is this device the last device the user has?
560 ///
561 /// This method is useful to check if we should recommend to the user that
562 /// they should enable recovery, typically done before logging out.
563 ///
564 /// If the user does not enable recovery before logging out of their last
565 /// device, they will not be able to decrypt historic messages once they
566 /// create a new device.
567 pub async fn is_last_device(&self) -> Result<bool> {
568 let olm_machine = self.client.olm_machine().await;
569 let olm_machine = olm_machine.as_ref().ok_or(crate::Error::NoOlmMachine)?;
570 let user_id = olm_machine.user_id();
571
572 self.client.encryption().ensure_initial_key_query().await?;
573
574 let devices = self.client.encryption().get_user_devices(user_id).await?;
575
576 Ok(devices.devices().count() == 1)
577 }
578
579 /// Did we correctly set up cross-signing and backups?
580 async fn all_known_secrets_available(&self) -> Result<bool> {
581 // Cross-signing state is fine if we have all the private cross-signing
582 // keys, as indicated in the status.
583 let cross_signing_complete = self
584 .client
585 .encryption()
586 .cross_signing_status()
587 .await
588 .map(|status| status.is_complete());
589 if !cross_signing_complete.unwrap_or_default() {
590 return Ok(false);
591 }
592
593 // The backup state is fine if we have backups enabled locally, or if
594 // backups have been marked as disabled.
595 if self.client.encryption().backups().are_enabled().await {
596 Ok(true)
597 } else {
598 self.are_backups_marked_as_disabled().await
599 }
600 }
601
602 async fn should_auto_enable_backups(&self) -> Result<bool> {
603 // If we didn't already enable backups, we don't see a backup version on
604 // the server, and finally if backups have not been marked to be
605 // explicitly disabled, then we can automatically enable them.
606 Ok(self.client.inner.e2ee.encryption_settings.auto_enable_backups
607 && !self.client.encryption().backups().are_enabled().await
608 && !self.client.encryption().backups().fetch_exists_on_server().await?
609 && !self.are_backups_marked_as_disabled().await?)
610 }
611
612 pub(crate) async fn setup(&self) -> Result<()> {
613 info!("Setting up account data listeners and trying to setup recovery");
614
615 self.client.add_event_handler(Self::default_key_event_handler);
616 self.client.add_event_handler(Self::secret_send_event_handler);
617 self.client.inner.e2ee.initialize_recovery_state_update_task(&self.client);
618
619 self.update_recovery_state().await?;
620
621 if self.should_auto_enable_backups().await? {
622 info!("Trying to automatically enable backups");
623
624 if let Err(e) = self.enable_backup().await {
625 warn!("Could not automatically enable backups: {e:?}");
626 }
627 }
628
629 Ok(())
630 }
631
632 /// Delete all the known secrets we are keeping in secret storage.
633 ///
634 /// The exact list of secrets is defined in [`Recovery::KNOWN_SECRETS`] and
635 /// might change over time.
636 ///
637 /// Since account data events can't actually be deleted, due to a missing
638 /// DELETE API, we're replacing the events with an empty
639 /// [`SecretEventContent`].
640 async fn delete_all_known_secrets(&self) -> Result<()> {
641 for secret_name in Self::KNOWN_SECRETS {
642 let event_type = GlobalAccountDataEventType::from(secret_name.to_owned());
643 let content = SecretEventContent::new(Default::default());
644 let secret_content = Raw::from_json(
645 to_raw_value(&content)
646 .expect("We should be able to serialize a raw empty secret event content"),
647 );
648 self.client.account().set_account_data_raw(event_type, secret_content).await?;
649 }
650
651 Ok(())
652 }
653
654 /// Run a network request to figure whether backups have been disabled at
655 /// the account level.
656 async fn are_backups_marked_as_disabled(&self) -> Result<bool> {
657 if let Some(key_backup_content) =
658 self.client.account().fetch_account_data_static::<KeyBackupContent>().await?
659 {
660 Ok(key_backup_content.deserialize().map(|event| !event.enabled).unwrap_or(false))
661 } else {
662 Ok(self
663 .client
664 .account()
665 .fetch_account_data_static::<BackupDisabledContent>()
666 .await?
667 .map(|event| event.deserialize().map(|event| event.disabled).unwrap_or(false))
668 .unwrap_or(false))
669 }
670 }
671
672 async fn mark_backup_as_enabled(&self) -> Result<()> {
673 self.client.account().set_account_data(KeyBackupContent { enabled: true }).await?;
674
675 // Unstable prefix: will be removed when sufficient time has passed for
676 // clients to use the stable prefix.
677 self.client.account().set_account_data(BackupDisabledContent { disabled: false }).await?;
678
679 Ok(())
680 }
681
682 async fn check_recovery_state(&self) -> Result<RecoveryState> {
683 Ok(if self.client.encryption().secret_storage().is_enabled().await? {
684 if self.all_known_secrets_available().await? {
685 RecoveryState::Enabled
686 } else {
687 RecoveryState::Incomplete
688 }
689 } else {
690 RecoveryState::Disabled
691 })
692 }
693
694 async fn update_recovery_state(&self) -> Result<()> {
695 let new_state = self.check_recovery_state().await?;
696 let old_state = self.client.inner.e2ee.recovery_state.set(new_state);
697
698 if new_state != old_state {
699 info!("Recovery state changed from {old_state:?} to {new_state:?}");
700 }
701
702 Ok(())
703 }
704
705 async fn update_recovery_state_no_fail(&self) {
706 if let Err(e) = self.update_recovery_state().await {
707 error!("Couldn't update the recovery state: {e:?}");
708 }
709 }
710
711 #[instrument]
712 async fn secret_send_event_handler(_: ToDeviceSecretSendEvent, client: Client) {
713 client.encryption().recovery().update_recovery_state_no_fail().await;
714 }
715
716 #[instrument]
717 async fn default_key_event_handler(_: SecretStorageDefaultKeyEvent, client: Client) {
718 client.encryption().recovery().update_recovery_state_no_fail().await;
719 }
720
721 /// Listen for changes in the [`BackupState`] and, if necessary, update the
722 /// [`RecoveryState`] accordingly.
723 ///
724 /// This should not be called directly, this method is put into a background
725 /// task which is always listening for updates in the [`BackupState`].
726 pub(crate) fn update_state_after_backup_state_change(
727 client: &Client,
728 ) -> impl Future<Output = ()> + use<> {
729 let mut stream = client.encryption().backups().state_stream();
730 let weak = WeakClient::from_client(client);
731
732 async move {
733 while let Some(update) = stream.next().await {
734 if let Some(client) = weak.get() {
735 match update {
736 Ok(update) => {
737 // The recovery state only cares about these two
738 // states, the intermediate states that tell us that
739 // we're creating a backup are not interesting.
740 if matches!(update, BackupState::Unknown | BackupState::Enabled) {
741 client
742 .encryption()
743 .recovery()
744 .update_recovery_state_no_fail()
745 .await;
746 }
747 }
748 Err(_) => {
749 // We missed some updates, let's update our state in
750 // case something changed.
751 client.encryption().recovery().update_recovery_state_no_fail().await;
752 }
753 }
754 } else {
755 break;
756 }
757 }
758 }
759 }
760
761 #[instrument(skip_all)]
762 pub(crate) async fn update_state_after_keys_query(&self, response: &get_keys::v3::Response) {
763 if let Some(user_id) = self.client.user_id()
764 && response.master_keys.contains_key(user_id)
765 {
766 // TODO: This is unnecessarily expensive, we could let the crypto
767 // crate notify us that our private keys got erased... But, the
768 // OlmMachine gets recreated and... You know the drill by now...
769 self.update_recovery_state_no_fail().await;
770 }
771 }
772}
773
774/// A helper struct that handles continues resetting a user's crypto identity
775/// after authentication was required and re-enabling backups (if necessary) at
776/// the end of it
777#[derive(Debug)]
778pub struct IdentityResetHandle {
779 client: Client,
780 cross_signing_reset_handle: CrossSigningResetHandle,
781}
782
783impl IdentityResetHandle {
784 /// Get the underlying [`CrossSigningResetAuthType`] this identity reset
785 /// process is using.
786 pub fn auth_type(&self) -> &CrossSigningResetAuthType {
787 &self.cross_signing_reset_handle.auth_type
788 }
789
790 /// This method will retry to upload the device keys after the previous try
791 /// failed due to required authentication
792 pub async fn reset(&self, auth: Option<AuthData>) -> Result<()> {
793 self.cross_signing_reset_handle.auth(auth).await?;
794
795 if self.client.encryption().recovery().should_auto_enable_backups().await? {
796 self.client.encryption().recovery().enable_backup().await?;
797 }
798
799 Ok(())
800 }
801
802 /// Cancel the ongoing identity reset process
803 pub async fn cancel(&self) {
804 self.cross_signing_reset_handle.cancel().await;
805 }
806}
807
808// The http mocking library is not supported for wasm32
809#[cfg(all(test, not(target_family = "wasm")))]
810pub(crate) mod tests {
811 use assert_matches::assert_matches;
812 use matrix_sdk_test::async_test;
813 use ruma::{
814 events::{secret::request::SecretName, secret_storage::key},
815 serde::Base64,
816 };
817 use serde_json::json;
818
819 use super::Recovery;
820 use crate::{
821 encryption::{recovery::types::RecoveryError, secret_storage::SecretStorageError},
822 test_utils::mocks::MatrixMockServer,
823 };
824
825 // If recovery fails due when importing a secret from secret storage, we
826 // should get the `ImportError` variant of `SecretStorageError`. The
827 // following tests test different import failures.
828 #[async_test]
829 async fn test_recover_with_no_cross_signing_key() {
830 let server = MatrixMockServer::new().await;
831 let client = server.client_builder().build().await;
832
833 server
834 .mock_get_secret_storage_key()
835 .ok(
836 client.user_id().unwrap(),
837 &key::SecretStorageKeyEventContent::new(
838 "abc".into(),
839 key::SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
840 key::SecretStorageV1AesHmacSha2Properties::new(
841 Some(Base64::parse("xv5b6/p3ExEw++wTyfSHEg==").unwrap()),
842 Some(
843 Base64::parse("ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=")
844 .unwrap(),
845 ),
846 ),
847 ),
848 ),
849 )
850 .mount()
851 .await;
852 server
853 .mock_get_default_secret_storage_key()
854 .ok(client.user_id().unwrap(), "abc")
855 .mount()
856 .await;
857
858 let recovery = Recovery { client };
859
860 let ret =
861 recovery.recover("EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd").await;
862
863 assert_matches!(
864 ret,
865 Err(RecoveryError::SecretStorage(SecretStorageError::ImportError {
866 name: SecretName::CrossSigningMasterKey,
867 error: _
868 }))
869 );
870 }
871
872 #[async_test]
873 async fn test_recover_with_invalid_cross_signing_key() {
874 let server = MatrixMockServer::new().await;
875 let client = server.client_builder().build().await;
876
877 server
878 .mock_get_secret_storage_key()
879 .ok(
880 client.user_id().unwrap(),
881 &key::SecretStorageKeyEventContent::new(
882 "abc".into(),
883 key::SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
884 key::SecretStorageV1AesHmacSha2Properties::new(
885 Some(Base64::parse("xv5b6/p3ExEw++wTyfSHEg==").unwrap()),
886 Some(
887 Base64::parse("ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=")
888 .unwrap(),
889 ),
890 ),
891 ),
892 ),
893 )
894 .mount()
895 .await;
896 server
897 .mock_get_default_secret_storage_key()
898 .ok(client.user_id().unwrap(), "abc")
899 .mount()
900 .await;
901 server.mock_get_master_signing_key().ok(client.user_id().unwrap(), json!({})).mount().await;
902
903 let recovery = Recovery { client };
904
905 let ret =
906 recovery.recover("EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd").await;
907
908 assert_matches!(
909 ret,
910 Err(RecoveryError::SecretStorage(SecretStorageError::ImportError {
911 name: SecretName::CrossSigningMasterKey,
912 error: _
913 }))
914 );
915 }
916
917 #[async_test]
918 async fn test_recover_with_undecryptable_cross_signing_key() {
919 let server = MatrixMockServer::new().await;
920 let client = server.client_builder().build().await;
921
922 server
923 .mock_get_secret_storage_key()
924 .ok(
925 client.user_id().unwrap(),
926 &key::SecretStorageKeyEventContent::new(
927 "abc".into(),
928 key::SecretStorageEncryptionAlgorithm::V1AesHmacSha2(
929 key::SecretStorageV1AesHmacSha2Properties::new(
930 Some(Base64::parse("xv5b6/p3ExEw++wTyfSHEg==").unwrap()),
931 Some(
932 Base64::parse("ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=")
933 .unwrap(),
934 ),
935 ),
936 ),
937 ),
938 )
939 .mount()
940 .await;
941 server
942 .mock_get_default_secret_storage_key()
943 .ok(client.user_id().unwrap(), "abc")
944 .mount()
945 .await;
946 server
947 .mock_get_master_signing_key()
948 .ok(
949 client.user_id().unwrap(),
950 json!({
951 "encrypted": {
952 "abc": {
953 "iv": "xv5b6/p3ExEw++wTyfSHEg==",
954 "mac": "ujBBbXahnTAMkmPUX2/0+VTfUh63pGyVRuBcDMgmJC8=",
955 "ciphertext": "abcd"
956 }
957 }
958 }),
959 )
960 .mount()
961 .await;
962
963 let recovery = Recovery { client };
964
965 let ret =
966 recovery.recover("EsTj 3yST y93F SLpB jJsz eAXc 2XzA ygD3 w69H fGaN TKBj jXEd").await;
967
968 assert_matches!(
969 ret,
970 Err(RecoveryError::SecretStorage(SecretStorageError::ImportError {
971 name: SecretName::CrossSigningMasterKey,
972 error: _
973 }))
974 );
975 }
976}