matrix_sdk/encryption/backups/
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//! Room key backup support
16//!
17//! This module implements support for server-side key backups[[1]]. The module
18//! allows you to connect to an existing backup, create or delete backups from
19//! the homeserver, and download room keys from a backup.
20//!
21//! [1]: https://spec.matrix.org/unstable/client-server-api/#server-side-key-backups
22
23use std::collections::{BTreeMap, BTreeSet};
24
25use futures_core::Stream;
26use futures_util::StreamExt;
27use matrix_sdk_base::crypto::{
28    backups::MegolmV1BackupKey,
29    store::types::BackupDecryptionKey,
30    types::{requests::KeysBackupRequest, RoomKeyBackupInfo},
31    OlmMachine, RoomKeyImportResult,
32};
33use ruma::{
34    api::client::{
35        backup::{
36            add_backup_keys, create_backup_version, get_backup_keys, get_backup_keys_for_room,
37            get_backup_keys_for_session, get_latest_backup_info, RoomKeyBackup,
38        },
39        error::ErrorKind,
40    },
41    events::{
42        room::encrypted::OriginalSyncRoomEncryptedEvent,
43        secret::{request::SecretName, send::ToDeviceSecretSendEvent},
44    },
45    serde::Raw,
46    OwnedRoomId, RoomId, TransactionId,
47};
48use tokio_stream::wrappers::{errors::BroadcastStreamRecvError, BroadcastStream};
49use tracing::{error, info, instrument, trace, warn, Span};
50
51pub mod futures;
52pub(crate) mod types;
53
54pub use types::{BackupState, UploadState};
55
56use self::futures::WaitForSteadyState;
57use crate::{
58    crypto::olm::ExportedRoomKey, encryption::BackupDownloadStrategy, Client, Error, Room,
59};
60
61/// The backups manager for the [`Client`].
62#[derive(Debug, Clone)]
63pub struct Backups {
64    pub(super) client: Client,
65}
66
67impl Backups {
68    /// Create a new backup version, encrypted with a new backup recovery key.
69    ///
70    /// The backup recovery key will be persisted locally and shared with
71    /// trusted devices as `m.secret.send` to-device messages.
72    ///
73    /// After the backup has been created, all room keys will be uploaded to the
74    /// homeserver.
75    ///
76    /// *Warning*: This will overwrite any existing backup.
77    ///
78    /// # Examples
79    ///
80    /// ```no_run
81    /// # use matrix_sdk::{Client, encryption::backups::BackupState};
82    /// # use url::Url;
83    /// # async {
84    /// # let homeserver = Url::parse("http://example.com")?;
85    /// # let client = Client::new(homeserver).await?;
86    /// let backups = client.encryption().backups();
87    /// backups.create().await?;
88    ///
89    /// assert_eq!(backups.state(), BackupState::Enabled);
90    /// # anyhow::Ok(()) };
91    /// ```
92    pub async fn create(&self) -> Result<(), Error> {
93        self.client.inner.e2ee.backup_state.clear_backup_exists_on_server();
94        let _guard = self.client.locks().backup_modify_lock.lock().await;
95
96        self.set_state(BackupState::Creating);
97
98        // Create a future so we can catch errors and go back to the `Unknown`
99        // state. This is a hack to get around the lack of `try` blocks in Rust.
100        let future = async {
101            let olm_machine = self.client.olm_machine().await;
102            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
103
104            // Create a new backup recovery key.
105            let decryption_key = BackupDecryptionKey::new().expect(
106                "We should be able to generate enough randomness to create a new backup recovery \
107                 key",
108            );
109
110            // Get the info about the new backup key, this needs to be uploaded to the
111            // homeserver[1].
112            //
113            // We need to sign the `RoomKeyBackupInfo` so other clients which might want
114            // to start using the backup without having access to the
115            // `BackupDecryptionKey` can do so, as per [spec]:
116            //
117            // Clients must only store keys in backups after they have ensured that the
118            // `auth_data` has not been tampered with. This can be done either by:
119            //
120            //  * checking that it is signed by the user's master cross-signing key or by a
121            //    verified device belonging to the same user, or
122            //  * by deriving the public key from a private key that it obtained from a
123            //    trusted source. Trusted sources for the private key include the user
124            //    entering the key, retrieving the key stored in secret storage, or
125            //    obtaining the key via secret sharing from a verified device belonging to
126            //    the same user.
127            //
128            //
129            // [1]: https://spec.matrix.org/v1.8/client-server-api/#post_matrixclientv3room_keysversion
130            // [spec]: https://spec.matrix.org/v1.8/client-server-api/#server-side-key-backups
131            let mut backup_info = decryption_key.to_backup_info();
132
133            if let Err(e) = olm_machine.backup_machine().sign_backup(&mut backup_info).await {
134                warn!("Unable to sign the newly created backup version: {e:?}");
135            }
136
137            let algorithm = Raw::new(&backup_info)?.cast();
138            let request = create_backup_version::v3::Request::new(algorithm);
139            let response = self.client.send(request).await?;
140            let version = response.version;
141
142            // Reset any state we might have had before the new backup was created.
143            // TODO: This should remove the old stored key and version.
144            olm_machine.backup_machine().disable_backup().await?;
145
146            let backup_key = decryption_key.megolm_v1_public_key();
147
148            // Save the newly created keys and the version we received from the server.
149            olm_machine
150                .backup_machine()
151                .save_decryption_key(Some(decryption_key), Some(version.to_owned()))
152                .await?;
153
154            // Enable the backup and start the upload of room keys.
155            self.enable(olm_machine, backup_key, version).await?;
156
157            Ok(())
158        };
159
160        let result = future.await;
161
162        if result.is_err() {
163            self.set_state(BackupState::Unknown);
164        }
165
166        result
167    }
168
169    /// Disable and delete the currently active backup only if previously
170    /// enabled before, otherwise an error will be returned.
171    ///
172    /// For a more aggressive variant see [`Backups::disable_and_delete`] which
173    /// will delete the remote backup without checking the local state.
174    ///
175    /// # Examples
176    ///
177    /// ```no_run
178    /// # use matrix_sdk::{Client, encryption::backups::BackupState};
179    /// # use url::Url;
180    /// # async {
181    /// # let homeserver = Url::parse("http://example.com")?;
182    /// # let client = Client::new(homeserver).await?;
183    /// let backups = client.encryption().backups();
184    /// backups.disable().await?;
185    ///
186    /// assert_eq!(backups.state(), BackupState::Unknown);
187    /// # anyhow::Ok(()) };
188    /// ```
189    #[instrument(skip_all, fields(version))]
190    pub async fn disable(&self) -> Result<(), Error> {
191        let _guard = self.client.locks().backup_modify_lock.lock().await;
192
193        self.set_state(BackupState::Disabling);
194
195        // Create a future so we can catch errors and go back to the `Unknown` state.
196        let future = async {
197            let olm_machine = self.client.olm_machine().await;
198            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
199
200            let backup_keys = olm_machine.backup_machine().get_backup_keys().await?;
201
202            if let Some(version) = backup_keys.backup_version {
203                Span::current().record("version", &version);
204                info!("Deleting and disabling backup");
205
206                self.delete_backup_from_server(version).await?;
207                info!("Backup successfully deleted");
208
209                olm_machine.backup_machine().disable_backup().await?;
210
211                info!("Backup successfully disabled and deleted");
212
213                Ok(())
214            } else {
215                info!("Backup is not enabled, can't disable it");
216                Err(Error::BackupNotEnabled)
217            }
218        };
219
220        let result = future.await;
221
222        self.set_state(BackupState::Unknown);
223
224        result
225    }
226
227    /// Completely disable and delete the active backup both locally
228    /// and from the backend no matter if previously setup locally
229    /// or not.
230    ///
231    /// ⚠️ This method is mainly used when resetting the crypto identity
232    /// and for most other use cases its safer [`Backups::disable`] counterpart
233    /// should be used.
234    ///
235    /// It will fetch the current backup version from the backend and delete it
236    /// before proceeding to disabling local backups as well
237    ///
238    /// # Examples
239    ///
240    /// ```no_run
241    /// # use matrix_sdk::{Client, encryption::backups::BackupState};
242    /// # use url::Url;
243    /// # async {
244    /// # let homeserver = Url::parse("http://example.com")?;
245    /// # let client = Client::new(homeserver).await?;
246    /// let backups = client.encryption().backups();
247    /// backups.disable_and_delete().await?;
248    ///
249    /// assert_eq!(backups.state(), BackupState::Unknown);
250    /// # anyhow::Ok(()) };
251    /// ```
252    pub async fn disable_and_delete(&self) -> Result<(), Error> {
253        let _guard = self.client.locks().backup_modify_lock.lock().await;
254
255        self.set_state(BackupState::Disabling);
256
257        // Create a future so we can catch errors and go back to the `Unknown` state.
258        let future = async {
259            let response = self.get_current_version().await?;
260
261            if let Some(response) = response {
262                self.delete_backup_from_server(response.version).await?;
263            }
264
265            let olm_machine = self.client.olm_machine().await;
266            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
267
268            olm_machine.backup_machine().disable_backup().await?;
269
270            Ok(())
271        };
272
273        let result = future.await;
274
275        self.set_state(BackupState::Unknown);
276
277        result
278    }
279
280    /// Returns a future to wait for room keys to be uploaded.
281    ///
282    /// Awaiting the future will wake up a task to upload room keys which have
283    /// not yet been uploaded to the homeserver. It will then wait for the task
284    /// to finish uploading.
285    ///
286    /// # Examples
287    ///
288    /// ```no_run
289    /// # use matrix_sdk::{Client, encryption::backups::UploadState};
290    /// # use url::Url;
291    /// # async {
292    /// # let homeserver = Url::parse("http://example.com")?;
293    /// # let client = Client::new(homeserver).await?;
294    /// use futures_util::StreamExt;
295    ///
296    /// let backups = client.encryption().backups();
297    /// let wait_for_steady_state = backups.wait_for_steady_state();
298    ///
299    /// let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
300    ///
301    /// tokio::spawn(async move {
302    ///     while let Some(update) = progress_stream.next().await {
303    ///         let Ok(update) = update else { break };
304    ///
305    ///         match update {
306    ///             UploadState::Uploading(counts) => {
307    ///                 println!(
308    ///                     "Uploaded {} out of {} room keys.",
309    ///                     counts.backed_up, counts.total
310    ///                 );
311    ///             }
312    ///             UploadState::Error => break,
313    ///             UploadState::Done => break,
314    ///             _ => (),
315    ///         }
316    ///     }
317    /// });
318    ///
319    /// wait_for_steady_state.await?;
320    ///
321    /// # anyhow::Ok(()) };
322    /// ```
323    pub fn wait_for_steady_state(&self) -> WaitForSteadyState<'_> {
324        WaitForSteadyState {
325            backups: self,
326            progress: self.client.inner.e2ee.backup_state.upload_progress.clone(),
327            timeout: None,
328        }
329    }
330
331    /// Get a stream of updates to the [`BackupState`].
332    ///
333    /// This method will send out the current state as the first update.
334    ///
335    /// # Examples
336    ///
337    /// ```no_run
338    /// # use matrix_sdk::{Client, encryption::backups::BackupState};
339    /// # use url::Url;
340    /// # async {
341    /// # let homeserver = Url::parse("http://example.com")?;
342    /// # let client = Client::new(homeserver).await?;
343    /// use futures_util::StreamExt;
344    ///
345    /// let backups = client.encryption().backups();
346    ///
347    /// let mut state_stream = backups.state_stream();
348    ///
349    /// while let Some(update) = state_stream.next().await {
350    ///     let Ok(update) = update else { break };
351    ///
352    ///     match update {
353    ///         BackupState::Enabled => {
354    ///             println!("Backups have been enabled");
355    ///         }
356    ///         _ => (),
357    ///     }
358    /// }
359    /// # anyhow::Ok(()) };
360    /// ```
361    pub fn state_stream(
362        &self,
363    ) -> impl Stream<Item = Result<BackupState, BroadcastStreamRecvError>> {
364        self.client.inner.e2ee.backup_state.global_state.subscribe()
365    }
366
367    /// Get the current [`BackupState`] for this [`Client`].
368    pub fn state(&self) -> BackupState {
369        self.client.inner.e2ee.backup_state.global_state.get()
370    }
371
372    /// Are backups enabled for the current [`Client`]?
373    ///
374    /// This method will check if we locally have an active backup key and
375    /// backup version and are ready to upload room keys to a backup.
376    pub async fn are_enabled(&self) -> bool {
377        let olm_machine = self.client.olm_machine().await;
378
379        if let Some(machine) = olm_machine.as_ref() {
380            machine.backup_machine().enabled().await
381        } else {
382            false
383        }
384    }
385
386    /// Does a backup exist on the server?
387    ///
388    /// This method will request info about the current backup from the
389    /// homeserver and if a backup exists return `true`, otherwise `false`.
390    pub async fn fetch_exists_on_server(&self) -> Result<bool, Error> {
391        let exists_on_server = self.get_current_version().await?.is_some();
392        self.client.inner.e2ee.backup_state.set_backup_exists_on_server(exists_on_server);
393        Ok(exists_on_server)
394    }
395
396    /// Does a backup exist on the server?
397    ///
398    /// This method is identical to [`Self::fetch_exists_on_server`] except that
399    /// we cache the latest answer in memory and only empty the cache if the
400    /// local device adds or deletes a backup itself.
401    ///
402    /// Do not use this method if you need an accurate answer about whether a
403    /// backup exists - instead use [`Self::fetch_exists_on_server`]. This
404    /// method is useful when performance is more important than guaranteed
405    /// accuracy, such as when classifying UTDs.
406    pub async fn exists_on_server(&self) -> Result<bool, Error> {
407        // If we have an answer cached, return it immediately
408        if let Some(cached_value) = self.client.inner.e2ee.backup_state.backup_exists_on_server() {
409            return Ok(cached_value);
410        }
411
412        // Otherwise, delegate to fetch_exists_on_server. (It will update the cached
413        // value for us.)
414        self.fetch_exists_on_server().await
415    }
416
417    /// Subscribe to a stream that notifies when a room key for the specified
418    /// room is downloaded from the key backup.
419    pub fn room_keys_for_room_stream(
420        &self,
421        room_id: &RoomId,
422    ) -> impl Stream<Item = Result<BTreeMap<String, BTreeSet<String>>, BroadcastStreamRecvError>>
423    {
424        let room_id = room_id.to_owned();
425
426        // TODO: This is a bit crap to say the least. The type is
427        // non-descriptive and doesn't even contain all the important data. It
428        // should be a stream of `RoomKeyInfo` like the OlmMachine has... But on
429        // the other hand we should just be able to use the corresponding
430        // OlmMachine stream and remove this. Currently we can't do this because
431        // the OlmMachine gets destroyed and recreated all the time to be able
432        // to support the notifications-related multiprocessing on iOS.
433        self.room_keys_stream().filter_map(move |import_result| {
434            let room_id = room_id.to_owned();
435
436            async move {
437                match import_result {
438                    Ok(mut import_result) => import_result.keys.remove(&room_id).map(Ok),
439                    Err(e) => Some(Err(e)),
440                }
441            }
442        })
443    }
444
445    /// Download all room keys for a certain room from the server-side key
446    /// backup.
447    pub async fn download_room_keys_for_room(&self, room_id: &RoomId) -> Result<(), Error> {
448        let olm_machine = self.client.olm_machine().await;
449        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
450
451        let backup_keys = olm_machine.store().load_backup_keys().await?;
452
453        if let Some(decryption_key) = backup_keys.decryption_key {
454            if let Some(version) = backup_keys.backup_version {
455                let request =
456                    get_backup_keys_for_room::v3::Request::new(version.clone(), room_id.to_owned());
457                let response = self.client.send(request).await?;
458
459                // Transform response to standard format (map of room ID -> room key).
460                let response = get_backup_keys::v3::Response::new(BTreeMap::from([(
461                    room_id.to_owned(),
462                    RoomKeyBackup::new(response.sessions),
463                )]));
464
465                self.handle_downloaded_room_keys(response, decryption_key, &version, olm_machine)
466                    .await?;
467            }
468        }
469
470        Ok(())
471    }
472
473    /// Download a single room key from the server-side key backup.
474    ///
475    /// Returns `true` if we managed to download a room key, `false` or an error
476    /// if we failed to download it. `false` indicates that there was no
477    /// error, we just don't have backups enabled so we can't download a
478    /// room key.
479    pub async fn download_room_key(
480        &self,
481        room_id: &RoomId,
482        session_id: &str,
483    ) -> Result<bool, Error> {
484        let olm_machine = self.client.olm_machine().await;
485        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
486
487        let backup_keys = olm_machine.store().load_backup_keys().await?;
488
489        if let Some(decryption_key) = backup_keys.decryption_key {
490            if let Some(version) = backup_keys.backup_version {
491                let request = get_backup_keys_for_session::v3::Request::new(
492                    version.clone(),
493                    room_id.to_owned(),
494                    session_id.to_owned(),
495                );
496                let response = self.client.send(request).await?;
497
498                // Transform response to standard format (map of room ID -> room key).
499                let response = get_backup_keys::v3::Response::new(BTreeMap::from([(
500                    room_id.to_owned(),
501                    RoomKeyBackup::new(BTreeMap::from([(
502                        session_id.to_owned(),
503                        response.key_data,
504                    )])),
505                )]));
506
507                self.handle_downloaded_room_keys(response, decryption_key, &version, olm_machine)
508                    .await?;
509
510                Ok(true)
511            } else {
512                Ok(false)
513            }
514        } else {
515            Ok(false)
516        }
517    }
518
519    /// Set the state of the backup.
520    fn set_state(&self, new_state: BackupState) {
521        let old_state = self.client.inner.e2ee.backup_state.global_state.set(new_state);
522
523        if old_state != new_state {
524            info!("Backup state changed from {old_state:?} to {new_state:?}");
525        }
526    }
527
528    /// Set the backup state to the `Enabled` variant and insert the backup key
529    /// and version into the [`OlmMachine`].
530    async fn enable(
531        &self,
532        olm_machine: &OlmMachine,
533        backup_key: MegolmV1BackupKey,
534        version: String,
535    ) -> Result<(), Error> {
536        backup_key.set_version(version);
537        olm_machine.backup_machine().enable_backup_v1(backup_key).await?;
538
539        self.set_state(BackupState::Enabled);
540
541        Ok(())
542    }
543
544    /// Decrypt and forward a response containing backed up room keys to the
545    /// [`OlmMachine`].
546    async fn handle_downloaded_room_keys(
547        &self,
548        backed_up_keys: get_backup_keys::v3::Response,
549        backup_decryption_key: BackupDecryptionKey,
550        backup_version: &str,
551        olm_machine: &OlmMachine,
552    ) -> Result<(), Error> {
553        let mut decrypted_room_keys: Vec<_> = Vec::new();
554
555        for (room_id, room_keys) in backed_up_keys.rooms {
556            for (session_id, room_key) in room_keys.sessions {
557                let room_key = match room_key.deserialize() {
558                    Ok(k) => k,
559                    Err(e) => {
560                        warn!(
561                            "Couldn't deserialize a room key we downloaded from backups, session \
562                             ID: {session_id}, error: {e:?}"
563                        );
564                        continue;
565                    }
566                };
567
568                let room_key =
569                    match backup_decryption_key.decrypt_session_data(room_key.session_data) {
570                        Ok(k) => k,
571                        Err(e) => {
572                            warn!(
573                                "Couldn't decrypt a room key we downloaded from backups, session \
574                                 ID: {session_id}, error: {e:?}"
575                            );
576                            continue;
577                        }
578                    };
579
580                decrypted_room_keys.push(ExportedRoomKey::from_backed_up_room_key(
581                    room_id.to_owned(),
582                    session_id,
583                    room_key,
584                ));
585            }
586        }
587
588        let result = olm_machine
589            .store()
590            .import_room_keys(decrypted_room_keys, Some(backup_version), |_, _| {})
591            .await?;
592
593        // Since we can't use the usual room keys stream from the `OlmMachine`
594        // we're going to send things out in our own custom broadcaster.
595        let _ = self.client.inner.e2ee.backup_state.room_keys_broadcaster.send(result);
596
597        Ok(())
598    }
599
600    /// Download all room keys from the backup on the homeserver.
601    async fn download_all_room_keys(
602        &self,
603        decryption_key: BackupDecryptionKey,
604        version: String,
605    ) -> Result<(), Error> {
606        let request = get_backup_keys::v3::Request::new(version.clone());
607        let response = self.client.send(request).await?;
608
609        let olm_machine = self.client.olm_machine().await;
610        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
611
612        self.handle_downloaded_room_keys(response, decryption_key, &version, olm_machine).await?;
613
614        Ok(())
615    }
616
617    fn room_keys_stream(
618        &self,
619    ) -> impl Stream<Item = Result<RoomKeyImportResult, BroadcastStreamRecvError>> {
620        BroadcastStream::new(self.client.inner.e2ee.backup_state.room_keys_broadcaster.subscribe())
621    }
622
623    /// Get info about the currently active backup from the server.
624    async fn get_current_version(
625        &self,
626    ) -> Result<Option<get_latest_backup_info::v3::Response>, Error> {
627        let request = get_latest_backup_info::v3::Request::new();
628
629        match self.client.send(request).await {
630            Ok(r) => Ok(Some(r)),
631            Err(e) => {
632                if let Some(kind) = e.client_api_error_kind() {
633                    if kind == &ErrorKind::NotFound {
634                        Ok(None)
635                    } else {
636                        Err(e.into())
637                    }
638                } else {
639                    Err(e.into())
640                }
641            }
642        }
643    }
644
645    async fn delete_backup_from_server(&self, version: String) -> Result<(), Error> {
646        let request = ruma::api::client::backup::delete_backup_version::v3::Request::new(version);
647
648        let ret = match self.client.send(request).await {
649            Ok(_) => Ok(()),
650            Err(e) => {
651                if let Some(kind) = e.client_api_error_kind() {
652                    if kind == &ErrorKind::NotFound {
653                        Ok(())
654                    } else {
655                        Err(e.into())
656                    }
657                } else {
658                    Err(e.into())
659                }
660            }
661        };
662
663        // If the request succeeded, the backup is gone. If it failed, we are not really
664        // sure what the backup state is. Either way, clear the cache so we check next
665        // time we need to know.
666        self.client.inner.e2ee.backup_state.clear_backup_exists_on_server();
667
668        ret
669    }
670
671    #[instrument(skip(self, olm_machine, request))]
672    async fn send_backup_request(
673        &self,
674        olm_machine: &OlmMachine,
675        request_id: &TransactionId,
676        request: KeysBackupRequest,
677    ) -> Result<(), Error> {
678        trace!("Uploading some room keys");
679
680        let add_backup_keys = add_backup_keys::v3::Request::new(request.version, request.rooms);
681
682        match self.client.send(add_backup_keys).await {
683            Ok(response) => {
684                olm_machine.mark_request_as_sent(request_id, &response).await?;
685
686                let new_counts = olm_machine.backup_machine().room_key_counts().await?;
687
688                self.client
689                    .inner
690                    .e2ee
691                    .backup_state
692                    .upload_progress
693                    .set(UploadState::Uploading(new_counts));
694
695                let delay =
696                    self.client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned();
697                crate::sleep::sleep(delay).await;
698
699                Ok(())
700            }
701            Err(error) => {
702                if let Some(kind) = error.client_api_error_kind() {
703                    match kind {
704                        ErrorKind::NotFound => {
705                            warn!(
706                                "No backup found on the server, the backup likely got deleted, \
707                                 disabling backups."
708                            );
709
710                            self.handle_deleted_backup_version(olm_machine).await?;
711                        }
712                        ErrorKind::WrongRoomKeysVersion { current_version } => {
713                            warn!(
714                                new_version = current_version,
715                                "A new backup version was found on the server, disabling backups."
716                            );
717
718                            // TODO: If we're verified and there are other devices besides us,
719                            // request the new backup key over `m.secret.send`.
720
721                            self.handle_deleted_backup_version(olm_machine).await?;
722                        }
723
724                        _ => (),
725                    }
726                }
727
728                Err(error.into())
729            }
730        }
731    }
732
733    /// Poll the [`OlmMachine`] for room keys which need to be backed up and
734    /// send out the request to the homeserver.
735    ///
736    /// This should only be called by the [`BackupUploadingTask`].
737    ///
738    /// [`BackupUploadingTask`]: crate::client::tasks::BackupUploadingTask
739    pub(crate) async fn backup_room_keys(&self) -> Result<(), Error> {
740        let _guard = self.client.locks().backup_upload_lock.lock().await;
741
742        let olm_machine = self.client.olm_machine().await;
743        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
744
745        while let Some((request_id, request)) = olm_machine.backup_machine().backup().await? {
746            self.send_backup_request(olm_machine, &request_id, request).await?;
747        }
748
749        self.client.inner.e2ee.backup_state.upload_progress.set(UploadState::Done);
750
751        Ok(())
752    }
753
754    /// Set up a `m.secret.send` listener and re-enable backups if we have a
755    /// backup recovery key stored.
756    pub(crate) async fn setup_and_resume(&self) -> Result<(), Error> {
757        info!("Setting up secret listeners and trying to resume backups");
758
759        self.client.add_event_handler(Self::secret_send_event_handler);
760
761        if self.client.inner.e2ee.encryption_settings.backup_download_strategy
762            == BackupDownloadStrategy::AfterDecryptionFailure
763        {
764            self.client.add_event_handler(Self::utd_event_handler);
765        }
766
767        self.maybe_resume_backups().await?;
768
769        Ok(())
770    }
771
772    /// Try to enable backups with the given backup recovery key.
773    ///
774    /// This should be called if we receive a backup recovery, either:
775    ///
776    /// * As an `m.secret.send` to-device message from a trusted device.
777    /// * From 4S (i.e. from the `m.megolm_backup.v1` event global account
778    ///   data).
779    ///
780    /// In both cases the method will compare the currently active backup
781    /// version to the backup recovery key's version and, if there is a match,
782    /// activate backups on this device and start uploading room keys to the
783    /// backup.
784    ///
785    /// Returns true if backups were just enabled or were already enabled,
786    /// otherwise false.
787    #[instrument(skip_all)]
788    pub(crate) async fn maybe_enable_backups(
789        &self,
790        maybe_recovery_key: &str,
791    ) -> Result<bool, Error> {
792        let _guard = self.client.locks().backup_modify_lock.lock().await;
793
794        // Create a future here which allows us to catch any failure that might happen
795        // so we can later on fall back to the correct `BackupState`.
796        let future = async {
797            self.set_state(BackupState::Enabling);
798
799            let olm_machine = self.client.olm_machine().await;
800            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
801            let backup_machine = olm_machine.backup_machine();
802
803            let decryption_key =
804                BackupDecryptionKey::from_base64(maybe_recovery_key).map_err(|e| {
805                    <serde_json::Error as serde::de::Error>::custom(format!(
806                        "Couldn't deserialize the backup recovery key: {e:?}"
807                    ))
808                })?;
809
810            // Let's try to see if there's a backup on the homeserver.
811            let current_version = self.get_current_version().await?;
812
813            let Some(current_version) = current_version else {
814                warn!("Tried to enable backups, but no backup version was found on the server.");
815                return Ok(false);
816            };
817
818            Span::current().record("backup_version", &current_version.version);
819
820            let backup_info: RoomKeyBackupInfo = current_version.algorithm.deserialize_as()?;
821            let stored_keys = backup_machine.get_backup_keys().await?;
822
823            if stored_keys.backup_version.as_ref() == Some(&current_version.version)
824                && self.are_enabled().await
825            {
826                // If we already have a backup enabled which is using the currently active
827                // backup version, do nothing but tell the caller using the return value that
828                // backups are enabled.
829                Ok(true)
830            } else if decryption_key.backup_key_matches(&backup_info) {
831                info!(
832                    "We have found the correct backup recovery key. Storing the backup recovery \
833                     key and enabling backups."
834                );
835
836                // We're enabling a new backup, reset the `backed_up` flags on the room keys and
837                // remove any key/version we might have.
838                backup_machine.disable_backup().await?;
839
840                let backup_key = decryption_key.megolm_v1_public_key();
841                backup_key.set_version(current_version.version.to_owned());
842
843                // Persist the new keys and enable the backup.
844                backup_machine
845                    .save_decryption_key(
846                        Some(decryption_key.to_owned()),
847                        Some(current_version.version.to_owned()),
848                    )
849                    .await?;
850                backup_machine.enable_backup_v1(backup_key).await?;
851
852                // If the user has set up the client to download any room keys, do so now. This
853                // is not really useful in a real scenario since the API to
854                // download room keys is not paginated.
855                //
856                // You need to download all room keys at once, parse a potentially huge JSON
857                // response and decrypt all the room keys found in the backup.
858                //
859                // This doesn't work for any sizeable account.
860                if self.client.inner.e2ee.encryption_settings.backup_download_strategy
861                    == BackupDownloadStrategy::OneShot
862                {
863                    self.set_state(BackupState::Downloading);
864
865                    if let Err(e) =
866                        self.download_all_room_keys(decryption_key, current_version.version).await
867                    {
868                        warn!("Couldn't automatically download all room keys from backup: {e:?}");
869                    }
870                }
871
872                // Trigger the upload of any room keys we might need to upload.
873                self.maybe_trigger_backup();
874
875                Ok(true)
876            } else {
877                let derived_key = decryption_key.megolm_v1_public_key();
878                let downloaded_key = current_version.algorithm;
879
880                warn!(
881                    ?derived_key,
882                    ?downloaded_key,
883                    "Found an active backup but the recovery key we received isn't the one used for \
884                     this backup version"
885                );
886
887                Ok(false)
888            }
889        };
890
891        match future.await {
892            Ok(enabled) => {
893                if enabled {
894                    self.set_state(BackupState::Enabled);
895                } else {
896                    self.set_state(BackupState::Unknown);
897                }
898
899                Ok(enabled)
900            }
901            Err(e) => {
902                self.set_state(BackupState::Unknown);
903
904                Err(e)
905            }
906        }
907    }
908
909    /// Try to resume backups from a backup recovery key we have found in the
910    /// crypto store.
911    ///
912    /// Returns true if backups have been resumed, false otherwise.
913    async fn resume_backup_from_stored_backup_key(
914        &self,
915        olm_machine: &OlmMachine,
916    ) -> Result<bool, Error> {
917        let backup_keys = olm_machine.store().load_backup_keys().await?;
918
919        if let Some(decryption_key) = backup_keys.decryption_key {
920            if let Some(version) = backup_keys.backup_version {
921                let backup_key = decryption_key.megolm_v1_public_key();
922
923                self.enable(olm_machine, backup_key, version).await?;
924
925                Ok(true)
926            } else {
927                Ok(false)
928            }
929        } else {
930            Ok(false)
931        }
932    }
933
934    /// Try to resume backups by iterating through the `m.secret.send` to-device
935    /// messages the [`OlmMachine`] has received and stored in the secret inbox.
936    async fn maybe_resume_from_secret_inbox(&self, olm_machine: &OlmMachine) -> Result<(), Error> {
937        let secrets = olm_machine.store().get_secrets_from_inbox(&SecretName::RecoveryKey).await?;
938
939        for secret in secrets {
940            if self.maybe_enable_backups(&secret.event.content.secret).await? {
941                break;
942            }
943        }
944
945        olm_machine.store().delete_secrets_from_inbox(&SecretName::RecoveryKey).await?;
946
947        Ok(())
948    }
949
950    /// Check and re-enable a backup if we have a backup recovery key locally.
951    async fn maybe_resume_backups(&self) -> Result<(), Error> {
952        let olm_machine = self.client.olm_machine().await;
953        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
954
955        // Let us first check if we have a stored backup recovery key and a backup
956        // version.
957        if !self.resume_backup_from_stored_backup_key(olm_machine).await? {
958            // We didn't manage to enable backups from a stored backup recovery key, let us
959            // check our secret inbox. Perhaps we can find a valid key there.
960            self.maybe_resume_from_secret_inbox(olm_machine).await?;
961        }
962
963        Ok(())
964    }
965
966    /// Listen for `m.secret.send` to-device messages and check the secret inbox
967    /// if we do receive one.
968    #[instrument(skip_all)]
969    pub(crate) async fn secret_send_event_handler(_: ToDeviceSecretSendEvent, client: Client) {
970        let olm_machine = client.olm_machine().await;
971
972        // TODO: Because of our crude multi-process support, which reloads the whole
973        // [`OlmMachine`] the `secrets_stream` might stop giving you updates. Once
974        // that's fixed, stop listening to individual secret send events and
975        // listen to the secrets stream.
976        if let Some(olm_machine) = olm_machine.as_ref() {
977            if let Err(e) =
978                client.encryption().backups().maybe_resume_from_secret_inbox(olm_machine).await
979            {
980                error!("Could not handle `m.secret.send` event: {e:?}");
981            }
982        } else {
983            error!("Tried to handle a `m.secret.send` event but no OlmMachine was initialized");
984        }
985    }
986
987    /// Handle UTD events by triggering download from key backup.
988    ///
989    /// This function is registered as an event handler; it exists to deal
990    /// with cases where [`Room::decrypt_event`] is not called and instead the
991    /// event should be decrypted by the time this crate sees the event, such as
992    /// for events received via `/sync` (as opposed to via `/messages`,
993    /// `/context`, etc.)
994    #[allow(clippy::unused_async)] // Because it's used as an event handler, which must be async.
995    pub(crate) async fn utd_event_handler(
996        event: Raw<OriginalSyncRoomEncryptedEvent>,
997        room: Room,
998        client: Client,
999    ) {
1000        client.encryption().backups().maybe_download_room_key(room.room_id().to_owned(), event);
1001    }
1002
1003    /// Send a notification to the task responsible for key backup downloads
1004    /// that it should attempt to download the keys for the given event.
1005    pub(crate) fn maybe_download_room_key(
1006        &self,
1007        room_id: OwnedRoomId,
1008        event: Raw<OriginalSyncRoomEncryptedEvent>,
1009    ) {
1010        let tasks = self.client.inner.e2ee.tasks.lock();
1011        if let Some(task) = tasks.download_room_keys.as_ref() {
1012            task.trigger_download_for_utd_event(room_id, event);
1013        }
1014    }
1015
1016    /// Send a notification to the task which is responsible for uploading room
1017    /// keys to the backup that it might have new room keys to back up.
1018    pub(crate) fn maybe_trigger_backup(&self) {
1019        let tasks = self.client.inner.e2ee.tasks.lock();
1020
1021        if let Some(tasks) = tasks.upload_room_keys.as_ref() {
1022            tasks.trigger_upload();
1023        }
1024    }
1025
1026    /// Disable our backups locally if we notice that the backup has been
1027    /// removed on the homeserver.
1028    async fn handle_deleted_backup_version(&self, olm_machine: &OlmMachine) -> Result<(), Error> {
1029        olm_machine.backup_machine().disable_backup().await?;
1030        self.set_state(BackupState::Unknown);
1031
1032        Ok(())
1033    }
1034}
1035
1036#[cfg(all(test, not(target_family = "wasm")))]
1037mod test {
1038    use std::time::Duration;
1039
1040    use matrix_sdk_test::async_test;
1041    use serde_json::json;
1042    use wiremock::{
1043        matchers::{header, method, path},
1044        Mock, MockServer, ResponseTemplate,
1045    };
1046
1047    use super::*;
1048    use crate::test_utils::{logged_in_client, mocks::MatrixMockServer};
1049
1050    fn room_key() -> ExportedRoomKey {
1051        let json = json!({
1052            "algorithm": "m.megolm.v1.aes-sha2",
1053            "room_id": "!DovneieKSTkdHKpIXy:morpheus.localhost",
1054            "sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
1055            "session_id": "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU",
1056            "session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
1057                            lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
1058                            DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
1059                            fHAblQV",
1060            "sender_claimed_keys":{},
1061            "forwarding_curve25519_key_chain":[]
1062        });
1063
1064        serde_json::from_value(json)
1065            .expect("We should be able to deserialize our exported room key")
1066    }
1067
1068    async fn backup_disabling_test_body(
1069        client: &Client,
1070        server: &MockServer,
1071        put_response: ResponseTemplate,
1072    ) {
1073        let _post_scope = Mock::given(method("POST"))
1074            .and(path("_matrix/client/unstable/room_keys/version"))
1075            .and(header("authorization", "Bearer 1234"))
1076            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1077              "version": "1"
1078            })))
1079            .expect(1)
1080            .named("POST for the backup creation")
1081            .mount_as_scoped(server)
1082            .await;
1083
1084        let _put_scope = Mock::given(method("PUT"))
1085            .and(path("_matrix/client/unstable/room_keys/keys"))
1086            .and(header("authorization", "Bearer 1234"))
1087            .respond_with(put_response)
1088            .expect(1)
1089            .named("POST for the backup creation")
1090            .mount_as_scoped(server)
1091            .await;
1092
1093        client
1094            .encryption()
1095            .backups()
1096            .create()
1097            .await
1098            .expect("We should be able to create a new backup");
1099
1100        assert_eq!(client.encryption().backups().state(), BackupState::Enabled);
1101
1102        client
1103            .encryption()
1104            .backups()
1105            .backup_room_keys()
1106            .await
1107            .expect_err("Backups should be disabled");
1108
1109        assert_eq!(client.encryption().backups().state(), BackupState::Unknown);
1110    }
1111
1112    #[async_test]
1113    async fn test_backup_disabling_after_remote_deletion() {
1114        let server = MockServer::start().await;
1115        let client = logged_in_client(Some(server.uri())).await;
1116
1117        {
1118            let machine = client.olm_machine().await;
1119            machine
1120                .as_ref()
1121                .unwrap()
1122                .store()
1123                .import_exported_room_keys(vec![room_key()], |_, _| {})
1124                .await
1125                .expect("We should be able to import a room key");
1126        }
1127
1128        backup_disabling_test_body(
1129            &client,
1130            &server,
1131            ResponseTemplate::new(404).set_body_json(json!({
1132                "errcode": "M_NOT_FOUND",
1133                "error": "Unknown backup version"
1134            })),
1135        )
1136        .await;
1137
1138        backup_disabling_test_body(
1139            &client,
1140            &server,
1141            ResponseTemplate::new(403).set_body_json(json!({
1142                "current_version": "42",
1143                "errcode": "M_WRONG_ROOM_KEYS_VERSION",
1144                "error": "Wrong backup version."
1145            })),
1146        )
1147        .await;
1148
1149        server.verify().await;
1150    }
1151
1152    #[async_test]
1153    async fn test_when_a_backup_exists_then_fetch_exists_on_server_returns_true() {
1154        let server = MatrixMockServer::new().await;
1155        let client = server.client_builder().build().await;
1156
1157        server.mock_room_keys_version().exists().expect(1).mount().await;
1158
1159        let exists = client
1160            .encryption()
1161            .backups()
1162            .fetch_exists_on_server()
1163            .await
1164            .expect("We should be able to check if backups exist on the server");
1165
1166        assert!(exists, "We should deduce that a backup exists on the server");
1167    }
1168
1169    #[async_test]
1170    async fn test_repeated_calls_to_fetch_exists_on_server_makes_repeated_requests() {
1171        let server = MatrixMockServer::new().await;
1172        let client = server.client_builder().build().await;
1173
1174        // Expect 2 requests to the server
1175        server.mock_room_keys_version().exists().expect(2).mount().await;
1176
1177        let backups = client.encryption().backups();
1178
1179        // Call fetch_exists_on_server twice
1180        backups.fetch_exists_on_server().await.unwrap();
1181        let exists = backups.fetch_exists_on_server().await.unwrap();
1182
1183        assert!(exists, "We should deduce that a backup exists on the server");
1184    }
1185
1186    #[async_test]
1187    async fn test_when_no_backup_exists_then_fetch_exists_on_server_returns_false() {
1188        let server = MatrixMockServer::new().await;
1189        let client = server.client_builder().build().await;
1190
1191        server.mock_room_keys_version().none().expect(1).mount().await;
1192
1193        let exists = client
1194            .encryption()
1195            .backups()
1196            .fetch_exists_on_server()
1197            .await
1198            .expect("We should be able to check if backups exist on the server");
1199
1200        assert!(!exists, "We should deduce that no backup exists on the server");
1201    }
1202
1203    #[async_test]
1204    async fn test_when_server_returns_an_error_then_fetch_exists_on_server_returns_an_error() {
1205        let server = MatrixMockServer::new().await;
1206        let client = server.client_builder().build().await;
1207
1208        {
1209            let _scope =
1210                server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await;
1211
1212            client.encryption().backups().fetch_exists_on_server().await.expect_err(
1213                "If the /version endpoint returns a non 404 error we should throw an error",
1214            );
1215        }
1216
1217        {
1218            let _scope =
1219                server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await;
1220
1221            client.encryption().backups().fetch_exists_on_server().await.expect_err(
1222                "If the /version endpoint returns a non-Matrix 404 error we should throw an error",
1223            );
1224        }
1225    }
1226
1227    #[async_test]
1228    async fn test_when_a_backup_exists_then_exists_on_server_returns_true() {
1229        let server = MatrixMockServer::new().await;
1230        let client = server.client_builder().build().await;
1231
1232        server.mock_room_keys_version().exists().expect(1).mount().await;
1233
1234        let exists = client
1235            .encryption()
1236            .backups()
1237            .exists_on_server()
1238            .await
1239            .expect("We should be able to check if backups exist on the server");
1240
1241        assert!(exists, "We should deduce that a backup exists on the server");
1242    }
1243
1244    #[async_test]
1245    async fn test_when_no_backup_exists_then_exists_on_server_returns_false() {
1246        let server = MatrixMockServer::new().await;
1247        let client = server.client_builder().build().await;
1248
1249        server.mock_room_keys_version().none().expect(1).mount().await;
1250
1251        let exists = client
1252            .encryption()
1253            .backups()
1254            .exists_on_server()
1255            .await
1256            .expect("We should be able to check if backups exist on the server");
1257
1258        assert!(!exists, "We should deduce that no backup exists on the server");
1259    }
1260
1261    #[async_test]
1262    async fn test_when_server_returns_an_error_then_exists_on_server_returns_an_error() {
1263        let server = MatrixMockServer::new().await;
1264        let client = server.client_builder().build().await;
1265
1266        {
1267            let _scope =
1268                server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await;
1269
1270            client.encryption().backups().exists_on_server().await.expect_err(
1271                "If the /version endpoint returns a non 404 error we should throw an error",
1272            );
1273        }
1274
1275        {
1276            let _scope =
1277                server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await;
1278
1279            client.encryption().backups().exists_on_server().await.expect_err(
1280                "If the /version endpoint returns a non-Matrix 404 error we should throw an error",
1281            );
1282        }
1283    }
1284
1285    #[async_test]
1286    async fn test_repeated_calls_to_exists_on_server_do_not_make_additional_requests() {
1287        let server = MatrixMockServer::new().await;
1288        let client = server.client_builder().build().await;
1289
1290        // Create a mock stating that the request should only be made once
1291        server.mock_room_keys_version().exists().expect(1).mount().await;
1292
1293        let backups = client.encryption().backups();
1294
1295        // Call exists_on_server several times
1296        backups.exists_on_server().await.unwrap();
1297        backups.exists_on_server().await.unwrap();
1298        backups.exists_on_server().await.unwrap();
1299
1300        let exists = backups
1301            .exists_on_server()
1302            .await
1303            .expect("We should be able to check if backups exist on the server");
1304
1305        assert!(exists, "We should deduce that a backup exists on the server");
1306
1307        // We check expectations here, confirming that only one call was made
1308    }
1309
1310    #[async_test]
1311    async fn test_adding_a_backup_invalidates_exists_on_server_cache() {
1312        let server = MatrixMockServer::new().await;
1313        let client = server.client_builder().build().await;
1314        let backups = client.encryption().backups();
1315
1316        {
1317            let _scope = server.mock_room_keys_version().none().expect(1).mount_as_scoped().await;
1318
1319            // Call exists_on_server to fill the cache
1320            let exists = backups.exists_on_server().await.unwrap();
1321            assert!(!exists, "No backup exists at this point");
1322        }
1323
1324        // Create a new backup. Should invalidate the cache
1325        server.mock_add_room_keys_version().ok().expect(1).mount().await;
1326        backups.create().await.expect("Failed to create a backup");
1327
1328        server.mock_room_keys_version().exists().expect(1).mount().await;
1329        let exists = backups
1330            .exists_on_server()
1331            .await
1332            .expect("We should be able to check if backups exist on the server");
1333
1334        assert!(exists, "But now a backup does exist");
1335    }
1336
1337    #[async_test]
1338    async fn test_removing_a_backup_invalidates_exists_on_server_cache() {
1339        let server = MatrixMockServer::new().await;
1340        let client = server.client_builder().build().await;
1341        let backups = client.encryption().backups();
1342
1343        {
1344            let _scope = server.mock_room_keys_version().exists().expect(1).mount_as_scoped().await;
1345
1346            // Call exists_on_server to fill the cache
1347            let exists = backups.exists_on_server().await.unwrap();
1348            assert!(exists, "A backup exists at this point");
1349        }
1350
1351        // Delete the backup. Should invalidate the cache
1352        server.mock_delete_room_keys_version().ok().expect(1).mount().await;
1353        backups.delete_backup_from_server("1".to_owned()).await.expect("Failed to delete a backup");
1354
1355        server.mock_room_keys_version().none().expect(1).mount().await;
1356        let exists = backups
1357            .exists_on_server()
1358            .await
1359            .expect("We should be able to check if backups exist on the server");
1360
1361        assert!(!exists, "But now there is no backup");
1362    }
1363
1364    #[async_test]
1365    async fn test_waiting_for_steady_state_resets_the_delay() {
1366        let server = MatrixMockServer::new().await;
1367        let client = server.client_builder().build().await;
1368
1369        server.mock_add_room_keys_version().ok().expect(1).mount().await;
1370
1371        client
1372            .encryption()
1373            .backups()
1374            .create()
1375            .await
1376            .expect("We should be able to create a new backup");
1377
1378        let backups = client.encryption().backups();
1379
1380        let old_duration =
1381            { client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned() };
1382
1383        let wait_for_steady_state =
1384            backups.wait_for_steady_state().with_delay(Duration::from_nanos(100));
1385
1386        let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
1387
1388        let task = matrix_sdk_common::executor::spawn({
1389            let client = client.to_owned();
1390            async move {
1391                while let Some(state) = progress_stream.next().await {
1392                    let Ok(state) = state else {
1393                        panic!("Error while waiting for the upload state")
1394                    };
1395
1396                    match state {
1397                        UploadState::Idle => (),
1398                        UploadState::Done => {
1399                            let current_delay = {
1400                                client
1401                                    .inner
1402                                    .e2ee
1403                                    .backup_state
1404                                    .upload_delay
1405                                    .read()
1406                                    .unwrap()
1407                                    .to_owned()
1408                            };
1409
1410                            assert_ne!(current_delay, old_duration);
1411                            break;
1412                        }
1413                        _ => panic!("We should not have entered any other state"),
1414                    }
1415                }
1416            }
1417        });
1418
1419        wait_for_steady_state.await.expect("We should be able to wait for the steady state");
1420        task.await.unwrap();
1421
1422        let current_duration =
1423            { client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned() };
1424
1425        assert_eq!(old_duration, current_duration);
1426    }
1427}