Skip to main content

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