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::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!("No backup found on the server, the backup likely got deleted, disabling backups.");
706
707                            self.handle_deleted_backup_version(olm_machine).await?;
708                        }
709                        ErrorKind::WrongRoomKeysVersion { current_version } => {
710                            warn!(
711                                new_version = current_version,
712                                "A new backup version was found on the server, disabling backups."
713                            );
714
715                            // TODO: If we're verified and there are other devices besides us,
716                            // request the new backup key over `m.secret.send`.
717
718                            self.handle_deleted_backup_version(olm_machine).await?;
719                        }
720
721                        _ => (),
722                    }
723                }
724
725                Err(error.into())
726            }
727        }
728    }
729
730    /// Poll the [`OlmMachine`] for room keys which need to be backed up and
731    /// send out the request to the homeserver.
732    ///
733    /// This should only be called by the [`BackupUploadingTask`].
734    ///
735    /// [`BackupUploadingTask`]: crate::client::tasks::BackupUploadingTask
736    pub(crate) async fn backup_room_keys(&self) -> Result<(), Error> {
737        let _guard = self.client.locks().backup_upload_lock.lock().await;
738
739        let olm_machine = self.client.olm_machine().await;
740        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
741
742        while let Some((request_id, request)) = olm_machine.backup_machine().backup().await? {
743            self.send_backup_request(olm_machine, &request_id, request).await?;
744        }
745
746        self.client.inner.e2ee.backup_state.upload_progress.set(UploadState::Done);
747
748        Ok(())
749    }
750
751    /// Set up a `m.secret.send` listener and re-enable backups if we have a
752    /// backup recovery key stored.
753    pub(crate) async fn setup_and_resume(&self) -> Result<(), Error> {
754        info!("Setting up secret listeners and trying to resume backups");
755
756        self.client.add_event_handler(Self::secret_send_event_handler);
757
758        if self.client.inner.e2ee.encryption_settings.backup_download_strategy
759            == BackupDownloadStrategy::AfterDecryptionFailure
760        {
761            self.client.add_event_handler(Self::utd_event_handler);
762        }
763
764        self.maybe_resume_backups().await?;
765
766        Ok(())
767    }
768
769    /// Try to enable backups with the given backup recovery key.
770    ///
771    /// This should be called if we receive a backup recovery, either:
772    ///
773    /// * As an `m.secret.send` to-device message from a trusted device.
774    /// * From 4S (i.e. from the `m.megolm_backup.v1` event global account
775    ///   data).
776    ///
777    /// In both cases the method will compare the currently active backup
778    /// version to the backup recovery key's version and, if there is a match,
779    /// activate backups on this device and start uploading room keys to the
780    /// backup.
781    ///
782    /// Returns true if backups were just enabled or were already enabled,
783    /// otherwise false.
784    #[instrument(skip_all)]
785    pub(crate) async fn maybe_enable_backups(
786        &self,
787        maybe_recovery_key: &str,
788    ) -> Result<bool, Error> {
789        let _guard = self.client.locks().backup_modify_lock.lock().await;
790
791        // Create a future here which allows us to catch any failure that might happen
792        // so we can later on fall back to the correct `BackupState`.
793        let future = async {
794            self.set_state(BackupState::Enabling);
795
796            let olm_machine = self.client.olm_machine().await;
797            let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
798            let backup_machine = olm_machine.backup_machine();
799
800            let decryption_key =
801                BackupDecryptionKey::from_base64(maybe_recovery_key).map_err(|e| {
802                    <serde_json::Error as serde::de::Error>::custom(format!(
803                        "Couldn't deserialize the backup recovery key: {e:?}"
804                    ))
805                })?;
806
807            // Let's try to see if there's a backup on the homeserver.
808            let current_version = self.get_current_version().await?;
809
810            let Some(current_version) = current_version else {
811                warn!("Tried to enable backups, but no backup version was found on the server.");
812                return Ok(false);
813            };
814
815            Span::current().record("backup_version", &current_version.version);
816
817            let backup_info: RoomKeyBackupInfo = current_version.algorithm.deserialize_as()?;
818            let stored_keys = backup_machine.get_backup_keys().await?;
819
820            if stored_keys.backup_version.as_ref() == Some(&current_version.version)
821                && self.are_enabled().await
822            {
823                // If we already have a backup enabled which is using the currently active
824                // backup version, do nothing but tell the caller using the return value that
825                // backups are enabled.
826                Ok(true)
827            } else if decryption_key.backup_key_matches(&backup_info) {
828                info!(
829                    "We have found the correct backup recovery key. Storing the backup recovery \
830                     key and enabling backups."
831                );
832
833                // We're enabling a new backup, reset the `backed_up` flags on the room keys and
834                // remove any key/version we might have.
835                backup_machine.disable_backup().await?;
836
837                let backup_key = decryption_key.megolm_v1_public_key();
838                backup_key.set_version(current_version.version.to_owned());
839
840                // Persist the new keys and enable the backup.
841                backup_machine
842                    .save_decryption_key(
843                        Some(decryption_key.to_owned()),
844                        Some(current_version.version.to_owned()),
845                    )
846                    .await?;
847                backup_machine.enable_backup_v1(backup_key).await?;
848
849                // If the user has set up the client to download any room keys, do so now. This
850                // is not really useful in a real scenario since the API to
851                // download room keys is not paginated.
852                //
853                // You need to download all room keys at once, parse a potentially huge JSON
854                // response and decrypt all the room keys found in the backup.
855                //
856                // This doesn't work for any sizeable account.
857                if self.client.inner.e2ee.encryption_settings.backup_download_strategy
858                    == BackupDownloadStrategy::OneShot
859                {
860                    self.set_state(BackupState::Downloading);
861
862                    if let Err(e) =
863                        self.download_all_room_keys(decryption_key, current_version.version).await
864                    {
865                        warn!("Couldn't automatically download all room keys from backup: {e:?}");
866                    }
867                }
868
869                // Trigger the upload of any room keys we might need to upload.
870                self.maybe_trigger_backup();
871
872                Ok(true)
873            } else {
874                let derived_key = decryption_key.megolm_v1_public_key();
875                let downloaded_key = current_version.algorithm;
876
877                warn!(
878                    ?derived_key,
879                    ?downloaded_key,
880                    "Found an active backup but the recovery key we received isn't the one used for \
881                     this backup version"
882                );
883
884                Ok(false)
885            }
886        };
887
888        match future.await {
889            Ok(enabled) => {
890                if enabled {
891                    self.set_state(BackupState::Enabled);
892                } else {
893                    self.set_state(BackupState::Unknown);
894                }
895
896                Ok(enabled)
897            }
898            Err(e) => {
899                self.set_state(BackupState::Unknown);
900
901                Err(e)
902            }
903        }
904    }
905
906    /// Try to resume backups from a backup recovery key we have found in the
907    /// crypto store.
908    ///
909    /// Returns true if backups have been resumed, false otherwise.
910    async fn resume_backup_from_stored_backup_key(
911        &self,
912        olm_machine: &OlmMachine,
913    ) -> Result<bool, Error> {
914        let backup_keys = olm_machine.store().load_backup_keys().await?;
915
916        if let Some(decryption_key) = backup_keys.decryption_key {
917            if let Some(version) = backup_keys.backup_version {
918                let backup_key = decryption_key.megolm_v1_public_key();
919
920                self.enable(olm_machine, backup_key, version).await?;
921
922                Ok(true)
923            } else {
924                Ok(false)
925            }
926        } else {
927            Ok(false)
928        }
929    }
930
931    /// Try to resume backups by iterating through the `m.secret.send` to-device
932    /// messages the [`OlmMachine`] has received and stored in the secret inbox.
933    async fn maybe_resume_from_secret_inbox(&self, olm_machine: &OlmMachine) -> Result<(), Error> {
934        let secrets = olm_machine.store().get_secrets_from_inbox(&SecretName::RecoveryKey).await?;
935
936        for secret in secrets {
937            if self.maybe_enable_backups(&secret.event.content.secret).await? {
938                break;
939            }
940        }
941
942        olm_machine.store().delete_secrets_from_inbox(&SecretName::RecoveryKey).await?;
943
944        Ok(())
945    }
946
947    /// Check and re-enable a backup if we have a backup recovery key locally.
948    async fn maybe_resume_backups(&self) -> Result<(), Error> {
949        let olm_machine = self.client.olm_machine().await;
950        let olm_machine = olm_machine.as_ref().ok_or(Error::NoOlmMachine)?;
951
952        // Let us first check if we have a stored backup recovery key and a backup
953        // version.
954        if !self.resume_backup_from_stored_backup_key(olm_machine).await? {
955            // We didn't manage to enable backups from a stored backup recovery key, let us
956            // check our secret inbox. Perhaps we can find a valid key there.
957            self.maybe_resume_from_secret_inbox(olm_machine).await?;
958        }
959
960        Ok(())
961    }
962
963    /// Listen for `m.secret.send` to-device messages and check the secret inbox
964    /// if we do receive one.
965    #[instrument(skip_all)]
966    pub(crate) async fn secret_send_event_handler(_: ToDeviceSecretSendEvent, client: Client) {
967        let olm_machine = client.olm_machine().await;
968
969        // TODO: Because of our crude multi-process support, which reloads the whole
970        // [`OlmMachine`] the `secrets_stream` might stop giving you updates. Once
971        // that's fixed, stop listening to individual secret send events and
972        // listen to the secrets stream.
973        if let Some(olm_machine) = olm_machine.as_ref() {
974            if let Err(e) =
975                client.encryption().backups().maybe_resume_from_secret_inbox(olm_machine).await
976            {
977                error!("Could not handle `m.secret.send` event: {e:?}");
978            }
979        } else {
980            error!("Tried to handle a `m.secret.send` event but no OlmMachine was initialized");
981        }
982    }
983
984    /// Handle UTD events by triggering download from key backup.
985    ///
986    /// This function is registered as an event handler; it exists to deal
987    /// with cases where [`Room::decrypt_event`] is not called and instead the
988    /// event should be decrypted by the time this crate sees the event, such as
989    /// for events received via `/sync` (as opposed to via `/messages`,
990    /// `/context`, etc.)
991    #[allow(clippy::unused_async)] // Because it's used as an event handler, which must be async.
992    pub(crate) async fn utd_event_handler(
993        event: Raw<OriginalSyncRoomEncryptedEvent>,
994        room: Room,
995        client: Client,
996    ) {
997        client.encryption().backups().maybe_download_room_key(room.room_id().to_owned(), event);
998    }
999
1000    /// Send a notification to the task responsible for key backup downloads
1001    /// that it should attempt to download the keys for the given event.
1002    pub(crate) fn maybe_download_room_key(
1003        &self,
1004        room_id: OwnedRoomId,
1005        event: Raw<OriginalSyncRoomEncryptedEvent>,
1006    ) {
1007        let tasks = self.client.inner.e2ee.tasks.lock();
1008        if let Some(task) = tasks.download_room_keys.as_ref() {
1009            task.trigger_download_for_utd_event(room_id, event);
1010        }
1011    }
1012
1013    /// Send a notification to the task which is responsible for uploading room
1014    /// keys to the backup that it might have new room keys to back up.
1015    pub(crate) fn maybe_trigger_backup(&self) {
1016        let tasks = self.client.inner.e2ee.tasks.lock();
1017
1018        if let Some(tasks) = tasks.upload_room_keys.as_ref() {
1019            tasks.trigger_upload();
1020        }
1021    }
1022
1023    /// Disable our backups locally if we notice that the backup has been
1024    /// removed on the homeserver.
1025    async fn handle_deleted_backup_version(&self, olm_machine: &OlmMachine) -> Result<(), Error> {
1026        olm_machine.backup_machine().disable_backup().await?;
1027        self.set_state(BackupState::Unknown);
1028
1029        Ok(())
1030    }
1031}
1032
1033#[cfg(all(test, not(target_arch = "wasm32")))]
1034mod test {
1035    use std::time::Duration;
1036
1037    use matrix_sdk_test::async_test;
1038    use serde_json::json;
1039    use wiremock::{
1040        matchers::{header, method, path},
1041        Mock, MockServer, ResponseTemplate,
1042    };
1043
1044    use super::*;
1045    use crate::test_utils::{logged_in_client, mocks::MatrixMockServer};
1046
1047    fn room_key() -> ExportedRoomKey {
1048        let json = json!({
1049            "algorithm": "m.megolm.v1.aes-sha2",
1050            "room_id": "!DovneieKSTkdHKpIXy:morpheus.localhost",
1051            "sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
1052            "session_id": "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU",
1053            "session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
1054                            lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
1055                            DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
1056                            fHAblQV",
1057            "sender_claimed_keys":{},
1058            "forwarding_curve25519_key_chain":[]
1059        });
1060
1061        serde_json::from_value(json)
1062            .expect("We should be able to deserialize our exported room key")
1063    }
1064
1065    async fn backup_disabling_test_body(
1066        client: &Client,
1067        server: &MockServer,
1068        put_response: ResponseTemplate,
1069    ) {
1070        let _post_scope = Mock::given(method("POST"))
1071            .and(path("_matrix/client/unstable/room_keys/version"))
1072            .and(header("authorization", "Bearer 1234"))
1073            .respond_with(ResponseTemplate::new(200).set_body_json(json!({
1074              "version": "1"
1075            })))
1076            .expect(1)
1077            .named("POST for the backup creation")
1078            .mount_as_scoped(server)
1079            .await;
1080
1081        let _put_scope = Mock::given(method("PUT"))
1082            .and(path("_matrix/client/unstable/room_keys/keys"))
1083            .and(header("authorization", "Bearer 1234"))
1084            .respond_with(put_response)
1085            .expect(1)
1086            .named("POST for the backup creation")
1087            .mount_as_scoped(server)
1088            .await;
1089
1090        client
1091            .encryption()
1092            .backups()
1093            .create()
1094            .await
1095            .expect("We should be able to create a new backup");
1096
1097        assert_eq!(client.encryption().backups().state(), BackupState::Enabled);
1098
1099        client
1100            .encryption()
1101            .backups()
1102            .backup_room_keys()
1103            .await
1104            .expect_err("Backups should be disabled");
1105
1106        assert_eq!(client.encryption().backups().state(), BackupState::Unknown);
1107    }
1108
1109    #[async_test]
1110    async fn test_backup_disabling_after_remote_deletion() {
1111        let server = MockServer::start().await;
1112        let client = logged_in_client(Some(server.uri())).await;
1113
1114        {
1115            let machine = client.olm_machine().await;
1116            machine
1117                .as_ref()
1118                .unwrap()
1119                .store()
1120                .import_exported_room_keys(vec![room_key()], |_, _| {})
1121                .await
1122                .expect("We should be able to import a room key");
1123        }
1124
1125        backup_disabling_test_body(
1126            &client,
1127            &server,
1128            ResponseTemplate::new(404).set_body_json(json!({
1129                "errcode": "M_NOT_FOUND",
1130                "error": "Unknown backup version"
1131            })),
1132        )
1133        .await;
1134
1135        backup_disabling_test_body(
1136            &client,
1137            &server,
1138            ResponseTemplate::new(403).set_body_json(json!({
1139                "current_version": "42",
1140                "errcode": "M_WRONG_ROOM_KEYS_VERSION",
1141                "error": "Wrong backup version."
1142            })),
1143        )
1144        .await;
1145
1146        server.verify().await;
1147    }
1148
1149    #[async_test]
1150    async fn test_when_a_backup_exists_then_fetch_exists_on_server_returns_true() {
1151        let server = MatrixMockServer::new().await;
1152        let client = server.client_builder().build().await;
1153
1154        server.mock_room_keys_version().exists().expect(1).mount().await;
1155
1156        let exists = client
1157            .encryption()
1158            .backups()
1159            .fetch_exists_on_server()
1160            .await
1161            .expect("We should be able to check if backups exist on the server");
1162
1163        assert!(exists, "We should deduce that a backup exists on the server");
1164    }
1165
1166    #[async_test]
1167    async fn test_repeated_calls_to_fetch_exists_on_server_makes_repeated_requests() {
1168        let server = MatrixMockServer::new().await;
1169        let client = server.client_builder().build().await;
1170
1171        // Expect 2 requests to the server
1172        server.mock_room_keys_version().exists().expect(2).mount().await;
1173
1174        let backups = client.encryption().backups();
1175
1176        // Call fetch_exists_on_server twice
1177        backups.fetch_exists_on_server().await.unwrap();
1178        let exists = backups.fetch_exists_on_server().await.unwrap();
1179
1180        assert!(exists, "We should deduce that a backup exists on the server");
1181    }
1182
1183    #[async_test]
1184    async fn test_when_no_backup_exists_then_fetch_exists_on_server_returns_false() {
1185        let server = MatrixMockServer::new().await;
1186        let client = server.client_builder().build().await;
1187
1188        server.mock_room_keys_version().none().expect(1).mount().await;
1189
1190        let exists = client
1191            .encryption()
1192            .backups()
1193            .fetch_exists_on_server()
1194            .await
1195            .expect("We should be able to check if backups exist on the server");
1196
1197        assert!(!exists, "We should deduce that no backup exists on the server");
1198    }
1199
1200    #[async_test]
1201    async fn test_when_server_returns_an_error_then_fetch_exists_on_server_returns_an_error() {
1202        let server = MatrixMockServer::new().await;
1203        let client = server.client_builder().build().await;
1204
1205        {
1206            let _scope =
1207                server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await;
1208
1209            client.encryption().backups().fetch_exists_on_server().await.expect_err(
1210                "If the /version endpoint returns a non 404 error we should throw an error",
1211            );
1212        }
1213
1214        {
1215            let _scope =
1216                server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await;
1217
1218            client.encryption().backups().fetch_exists_on_server().await.expect_err(
1219                "If the /version endpoint returns a non-Matrix 404 error we should throw an error",
1220            );
1221        }
1222    }
1223
1224    #[async_test]
1225    async fn test_when_a_backup_exists_then_exists_on_server_returns_true() {
1226        let server = MatrixMockServer::new().await;
1227        let client = server.client_builder().build().await;
1228
1229        server.mock_room_keys_version().exists().expect(1).mount().await;
1230
1231        let exists = client
1232            .encryption()
1233            .backups()
1234            .exists_on_server()
1235            .await
1236            .expect("We should be able to check if backups exist on the server");
1237
1238        assert!(exists, "We should deduce that a backup exists on the server");
1239    }
1240
1241    #[async_test]
1242    async fn test_when_no_backup_exists_then_exists_on_server_returns_false() {
1243        let server = MatrixMockServer::new().await;
1244        let client = server.client_builder().build().await;
1245
1246        server.mock_room_keys_version().none().expect(1).mount().await;
1247
1248        let exists = client
1249            .encryption()
1250            .backups()
1251            .exists_on_server()
1252            .await
1253            .expect("We should be able to check if backups exist on the server");
1254
1255        assert!(!exists, "We should deduce that no backup exists on the server");
1256    }
1257
1258    #[async_test]
1259    async fn test_when_server_returns_an_error_then_exists_on_server_returns_an_error() {
1260        let server = MatrixMockServer::new().await;
1261        let client = server.client_builder().build().await;
1262
1263        {
1264            let _scope =
1265                server.mock_room_keys_version().error429().expect(1).mount_as_scoped().await;
1266
1267            client.encryption().backups().exists_on_server().await.expect_err(
1268                "If the /version endpoint returns a non 404 error we should throw an error",
1269            );
1270        }
1271
1272        {
1273            let _scope =
1274                server.mock_room_keys_version().error404().expect(1).mount_as_scoped().await;
1275
1276            client.encryption().backups().exists_on_server().await.expect_err(
1277                "If the /version endpoint returns a non-Matrix 404 error we should throw an error",
1278            );
1279        }
1280    }
1281
1282    #[async_test]
1283    async fn test_repeated_calls_to_exists_on_server_do_not_make_additional_requests() {
1284        let server = MatrixMockServer::new().await;
1285        let client = server.client_builder().build().await;
1286
1287        // Create a mock stating that the request should only be made once
1288        server.mock_room_keys_version().exists().expect(1).mount().await;
1289
1290        let backups = client.encryption().backups();
1291
1292        // Call exists_on_server several times
1293        backups.exists_on_server().await.unwrap();
1294        backups.exists_on_server().await.unwrap();
1295        backups.exists_on_server().await.unwrap();
1296
1297        let exists = backups
1298            .exists_on_server()
1299            .await
1300            .expect("We should be able to check if backups exist on the server");
1301
1302        assert!(exists, "We should deduce that a backup exists on the server");
1303
1304        // We check expectations here, confirming that only one call was made
1305    }
1306
1307    #[async_test]
1308    async fn test_adding_a_backup_invalidates_exists_on_server_cache() {
1309        let server = MatrixMockServer::new().await;
1310        let client = server.client_builder().build().await;
1311        let backups = client.encryption().backups();
1312
1313        {
1314            let _scope = server.mock_room_keys_version().none().expect(1).mount_as_scoped().await;
1315
1316            // Call exists_on_server to fill the cache
1317            let exists = backups.exists_on_server().await.unwrap();
1318            assert!(!exists, "No backup exists at this point");
1319        }
1320
1321        // Create a new backup. Should invalidate the cache
1322        server.mock_add_room_keys_version().ok().expect(1).mount().await;
1323        backups.create().await.expect("Failed to create a backup");
1324
1325        server.mock_room_keys_version().exists().expect(1).mount().await;
1326        let exists = backups
1327            .exists_on_server()
1328            .await
1329            .expect("We should be able to check if backups exist on the server");
1330
1331        assert!(exists, "But now a backup does exist");
1332    }
1333
1334    #[async_test]
1335    async fn test_removing_a_backup_invalidates_exists_on_server_cache() {
1336        let server = MatrixMockServer::new().await;
1337        let client = server.client_builder().build().await;
1338        let backups = client.encryption().backups();
1339
1340        {
1341            let _scope = server.mock_room_keys_version().exists().expect(1).mount_as_scoped().await;
1342
1343            // Call exists_on_server to fill the cache
1344            let exists = backups.exists_on_server().await.unwrap();
1345            assert!(exists, "A backup exists at this point");
1346        }
1347
1348        // Delete the backup. Should invalidate the cache
1349        server.mock_delete_room_keys_version().ok().expect(1).mount().await;
1350        backups.delete_backup_from_server("1".to_owned()).await.expect("Failed to delete a backup");
1351
1352        server.mock_room_keys_version().none().expect(1).mount().await;
1353        let exists = backups
1354            .exists_on_server()
1355            .await
1356            .expect("We should be able to check if backups exist on the server");
1357
1358        assert!(!exists, "But now there is no backup");
1359    }
1360
1361    #[async_test]
1362    async fn test_waiting_for_steady_state_resets_the_delay() {
1363        let server = MatrixMockServer::new().await;
1364        let client = server.client_builder().build().await;
1365
1366        server.mock_add_room_keys_version().ok().expect(1).mount().await;
1367
1368        client
1369            .encryption()
1370            .backups()
1371            .create()
1372            .await
1373            .expect("We should be able to create a new backup");
1374
1375        let backups = client.encryption().backups();
1376
1377        let old_duration =
1378            { client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned() };
1379
1380        let wait_for_steady_state =
1381            backups.wait_for_steady_state().with_delay(Duration::from_nanos(100));
1382
1383        let mut progress_stream = wait_for_steady_state.subscribe_to_progress();
1384
1385        let task = matrix_sdk_common::executor::spawn({
1386            let client = client.to_owned();
1387            async move {
1388                while let Some(state) = progress_stream.next().await {
1389                    let Ok(state) = state else {
1390                        panic!("Error while waiting for the upload state")
1391                    };
1392
1393                    match state {
1394                        UploadState::Idle => (),
1395                        UploadState::Done => {
1396                            let current_delay = {
1397                                client
1398                                    .inner
1399                                    .e2ee
1400                                    .backup_state
1401                                    .upload_delay
1402                                    .read()
1403                                    .unwrap()
1404                                    .to_owned()
1405                            };
1406
1407                            assert_ne!(current_delay, old_duration);
1408                            break;
1409                        }
1410                        _ => panic!("We should not have entered any other state"),
1411                    }
1412                }
1413            }
1414        });
1415
1416        wait_for_steady_state.await.expect("We should be able to wait for the steady state");
1417        task.await.unwrap();
1418
1419        let current_duration =
1420            { client.inner.e2ee.backup_state.upload_delay.read().unwrap().to_owned() };
1421
1422        assert_eq!(old_duration, current_duration);
1423    }
1424}