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