matrix_sdk_crypto/backups/
mod.rs

1// Copyright 2021, 2022 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//! Server-side backup support for room keys
16//!
17//! This module largely implements support for server-side backups using the
18//! `m.megolm_backup.v1.curve25519-aes-sha2` backup algorithm.
19//!
20//! Due to various flaws in this backup algorithm it is **not** recommended to
21//! use this module or any of its functionality. The module is only provided for
22//! backwards compatibility.
23//!
24//! [spec]: https://spec.matrix.org/unstable/client-server-api/#server-side-key-backups
25
26use std::{
27    collections::{BTreeMap, BTreeSet},
28    sync::Arc,
29};
30
31use ruma::{
32    api::client::backup::RoomKeyBackup, serde::Raw, DeviceId, DeviceKeyAlgorithm, OwnedDeviceId,
33    OwnedRoomId, OwnedTransactionId, RoomId, TransactionId,
34};
35use tokio::sync::RwLock;
36use tracing::{debug, info, instrument, trace, warn};
37
38use crate::{
39    olm::{BackedUpRoomKey, ExportedRoomKey, InboundGroupSession, SignedJsonObject},
40    store::{
41        types::{BackupDecryptionKey, BackupKeys, Changes, RoomKeyCounts},
42        Store,
43    },
44    types::{requests::KeysBackupRequest, MegolmV1AuthData, RoomKeyBackupInfo, Signatures},
45    CryptoStoreError, Device, RoomKeyImportResult, SignatureError,
46};
47
48mod keys;
49
50pub use keys::{DecodeError, DecryptionError, MegolmV1BackupKey};
51
52/// A state machine that handles backing up room keys.
53///
54/// The state machine can be activated using the
55/// [`BackupMachine::enable_backup_v1`] method. After the state machine has been
56/// enabled a request that will upload encrypted room keys can be generated
57/// using the [`BackupMachine::backup`] method.
58#[derive(Debug, Clone)]
59pub struct BackupMachine {
60    store: Store,
61    backup_key: Arc<RwLock<Option<MegolmV1BackupKey>>>,
62    pending_backup: Arc<RwLock<Option<PendingBackup>>>,
63}
64
65type SenderKey = String;
66type SessionId = String;
67
68#[derive(Debug, Clone)]
69struct PendingBackup {
70    request_id: OwnedTransactionId,
71    request: KeysBackupRequest,
72    sessions: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
73}
74
75/// The result of a signature verification of a signed JSON object.
76#[derive(Clone, Debug, Default, PartialEq, Eq)]
77pub struct SignatureVerification {
78    /// The result of the signature verification using the public key of our own
79    /// device.
80    pub device_signature: SignatureState,
81    /// The result of the signature verification using the public key of our own
82    /// user identity.
83    pub user_identity_signature: SignatureState,
84    /// The result of the signature verification using public keys of other
85    /// devices we own.
86    pub other_signatures: BTreeMap<OwnedDeviceId, SignatureState>,
87}
88
89impl SignatureVerification {
90    /// Is the result considered to be trusted?
91    ///
92    /// This tells us if the result has a valid signature from any of the
93    /// following:
94    ///
95    /// * Our own device
96    /// * Our own user identity, provided the identity is trusted as well
97    /// * Any of our own devices, provided the device is trusted as well
98    pub fn trusted(&self) -> bool {
99        self.device_signature.trusted()
100            || self.user_identity_signature.trusted()
101            || self.other_signatures.values().any(|s| s.trusted())
102    }
103}
104
105/// The result of a signature check.
106#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
107#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
108pub enum SignatureState {
109    /// The signature is missing.
110    #[default]
111    Missing,
112    /// The signature is invalid.
113    Invalid,
114    /// The signature is valid but the device or user identity that created the
115    /// signature is not trusted.
116    ValidButNotTrusted,
117    /// The signature is valid and the device or user identity that created the
118    /// signature is trusted.
119    ValidAndTrusted,
120}
121
122impl SignatureState {
123    /// Is the state considered to be trusted?
124    pub fn trusted(self) -> bool {
125        self == SignatureState::ValidAndTrusted
126    }
127
128    /// Did we find a valid signature?
129    pub fn signed(self) -> bool {
130        self == SignatureState::ValidButNotTrusted && self == SignatureState::ValidAndTrusted
131    }
132}
133
134impl BackupMachine {
135    const BACKUP_BATCH_SIZE: usize = 100;
136
137    pub(crate) fn new(store: Store, backup_key: Option<MegolmV1BackupKey>) -> Self {
138        Self {
139            store,
140            backup_key: RwLock::new(backup_key).into(),
141            pending_backup: RwLock::new(None).into(),
142        }
143    }
144
145    /// Are we able to back up room keys to the server?
146    pub async fn enabled(&self) -> bool {
147        self.backup_key.read().await.as_ref().is_some_and(|b| b.backup_version().is_some())
148    }
149
150    /// Check if our own device has signed the given signed JSON payload.
151    fn check_own_device_signature(
152        &self,
153        signatures: &Signatures,
154        auth_data: &str,
155    ) -> SignatureState {
156        match self.store.static_account().has_signed_raw(signatures, auth_data) {
157            Ok(_) => SignatureState::ValidAndTrusted,
158            Err(e) => match e {
159                SignatureError::NoSignatureFound => SignatureState::Missing,
160                _ => SignatureState::Invalid,
161            },
162        }
163    }
164
165    /// Check if our own cross-signing user identity has signed the given signed
166    /// JSON payload.
167    async fn check_own_identity_signature(
168        &self,
169        signatures: &Signatures,
170        auth_data: &str,
171    ) -> Result<SignatureState, CryptoStoreError> {
172        let user_id = &self.store.static_account().user_id;
173        let identity = self.store.get_identity(user_id).await?;
174
175        let ret = if let Some(identity) = identity.and_then(|i| i.own()) {
176            match identity.master_key().has_signed_raw(signatures, auth_data) {
177                Ok(_) => {
178                    if identity.is_verified() {
179                        SignatureState::ValidAndTrusted
180                    } else {
181                        SignatureState::ValidButNotTrusted
182                    }
183                }
184                Err(e) => match e {
185                    SignatureError::NoSignatureFound => SignatureState::Missing,
186                    _ => SignatureState::Invalid,
187                },
188            }
189        } else {
190            SignatureState::Missing
191        };
192
193        Ok(ret)
194    }
195
196    /// Check if the signed JSON payload `auth_data` has been signed by the
197    /// `device`.
198    fn backup_signed_by_device(
199        &self,
200        device: Device,
201        signatures: &Signatures,
202        auth_data: &str,
203    ) -> SignatureState {
204        if device.has_signed_raw(signatures, auth_data).is_ok() {
205            if device.is_verified() {
206                SignatureState::ValidAndTrusted
207            } else {
208                SignatureState::ValidButNotTrusted
209            }
210        } else {
211            SignatureState::Invalid
212        }
213    }
214
215    /// Check if the signed JSON payload `auth_data` has been signed by any of
216    /// our devices.
217    async fn test_device_signatures(
218        &self,
219        signatures: &Signatures,
220        auth_data: &str,
221        compute_all_signatures: bool,
222    ) -> Result<BTreeMap<OwnedDeviceId, SignatureState>, CryptoStoreError> {
223        let mut result = BTreeMap::new();
224
225        if let Some(user_signatures) = signatures.get(&self.store.static_account().user_id) {
226            for device_key_id in user_signatures.keys() {
227                if device_key_id.algorithm() == DeviceKeyAlgorithm::Ed25519 {
228                    // No need to check our own device here, we're doing that using
229                    // the check_own_device_signature().
230                    if device_key_id.key_name() == self.store.static_account().device_id {
231                        continue;
232                    }
233
234                    let state = self
235                        .test_ed25519_device_signature(
236                            device_key_id.key_name(),
237                            signatures,
238                            auth_data,
239                        )
240                        .await?;
241
242                    result.insert(device_key_id.key_name().to_owned(), state);
243
244                    // Abort the loop if we found a trusted and valid signature,
245                    // unless we should check all of them.
246                    if state.trusted() && !compute_all_signatures {
247                        break;
248                    }
249                }
250            }
251        }
252
253        Ok(result)
254    }
255
256    async fn test_ed25519_device_signature(
257        &self,
258        device_id: &DeviceId,
259        signatures: &Signatures,
260        auth_data: &str,
261    ) -> Result<SignatureState, CryptoStoreError> {
262        // We might iterate over some non-device signatures as well, but in this
263        // case there's no corresponding device and we get `Ok(None)` here, so
264        // things still work out.
265        let device = self.store.get_device(self.store.user_id(), device_id).await?;
266        trace!(?device_id, "Checking backup auth data for device");
267
268        if let Some(device) = device {
269            Ok(self.backup_signed_by_device(device, signatures, auth_data))
270        } else {
271            trace!(?device_id, "Device not found, can't check signature");
272            Ok(SignatureState::Missing)
273        }
274    }
275
276    async fn verify_auth_data_v1(
277        &self,
278        auth_data: MegolmV1AuthData,
279        compute_all_signatures: bool,
280    ) -> Result<SignatureVerification, CryptoStoreError> {
281        let serialized_auth_data = match auth_data.to_canonical_json() {
282            Ok(s) => s,
283            Err(e) => {
284                warn!(error =? e, "Error while verifying backup, can't canonicalize auth data");
285                return Ok(Default::default());
286            }
287        };
288
289        // Check if there's a signature from our own device.
290        let device_signature =
291            self.check_own_device_signature(&auth_data.signatures, &serialized_auth_data);
292        // Check if there's a signature from our own user identity.
293        let user_identity_signature =
294            self.check_own_identity_signature(&auth_data.signatures, &serialized_auth_data).await?;
295
296        // Collect all the other signatures if there isn't already a valid one,
297        // or if we're told to collect all of them anyways.
298        let other_signatures = if !(device_signature.trusted() || user_identity_signature.trusted())
299            || compute_all_signatures
300        {
301            self.test_device_signatures(
302                &auth_data.signatures,
303                &serialized_auth_data,
304                compute_all_signatures,
305            )
306            .await?
307        } else {
308            Default::default()
309        };
310
311        Ok(SignatureVerification { device_signature, user_identity_signature, other_signatures })
312    }
313
314    /// Verify some backup info that we downloaded from the server.
315    ///
316    /// # Arguments
317    ///
318    /// * `backup_info`: The backup info that should be verified. Should be
319    ///   fetched from the server using the [`/room_keys/version`] endpoint.
320    ///
321    /// * `compute_all_signatures`: *Useful for debugging only*. If this
322    ///   parameter is `true`, the internal machinery will compute the trust
323    ///   state for all signatures before returning, instead of short-circuiting
324    ///   on the first trusted signature. Has no impact on whether the backup
325    ///   will be considered verified.
326    ///
327    /// [`/room_keys/version`]: https://spec.matrix.org/unstable/client-server-api/#get_matrixclientv3room_keysversion
328    pub async fn verify_backup(
329        &self,
330        backup_info: RoomKeyBackupInfo,
331        compute_all_signatures: bool,
332    ) -> Result<SignatureVerification, CryptoStoreError> {
333        trace!(?backup_info, "Verifying backup auth data");
334
335        if let RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(data) = backup_info {
336            self.verify_auth_data_v1(data, compute_all_signatures).await
337        } else {
338            Ok(Default::default())
339        }
340    }
341
342    /// Sign a [`RoomKeyBackupInfo`] using the device's identity key and, if
343    /// available, the cross-signing master key.
344    ///
345    /// # Arguments
346    ///
347    /// * `backup_info`: The backup version that should be verified. Should be
348    ///   created from the [`BackupDecryptionKey`] using the
349    ///   [`BackupDecryptionKey::to_backup_info()`] method.
350    pub async fn sign_backup(
351        &self,
352        backup_info: &mut RoomKeyBackupInfo,
353    ) -> Result<(), SignatureError> {
354        if let RoomKeyBackupInfo::MegolmBackupV1Curve25519AesSha2(data) = backup_info {
355            let canonical_json = data.to_canonical_json()?;
356
357            let private_identity = self.store.private_identity();
358            let identity = private_identity.lock().await;
359
360            if let Some(key_id) = identity.master_key_id().await {
361                if let Ok(signature) = identity.sign(&canonical_json).await {
362                    data.signatures.add_signature(
363                        self.store.user_id().to_owned(),
364                        key_id,
365                        signature,
366                    );
367                }
368            }
369
370            let cache = self.store.cache().await?;
371            let account = cache.account().await?;
372            let key_id = account.signing_key_id();
373            let signature = account.sign(&canonical_json);
374            data.signatures.add_signature(self.store.user_id().to_owned(), key_id, signature);
375
376            Ok(())
377        } else {
378            Err(SignatureError::UnsupportedAlgorithm)
379        }
380    }
381
382    /// Activate the given backup key to be used to encrypt and backup room
383    /// keys.
384    ///
385    /// This will use the [`m.megolm_backup.v1.curve25519-aes-sha2`] algorithm
386    /// to encrypt the room keys.
387    ///
388    /// [`m.megolm_backup.v1.curve25519-aes-sha2`]:
389    /// https://spec.matrix.org/unstable/client-server-api/#backup-algorithm-mmegolm_backupv1curve25519-aes-sha2
390    pub async fn enable_backup_v1(&self, key: MegolmV1BackupKey) -> Result<(), CryptoStoreError> {
391        if key.backup_version().is_some() {
392            *self.backup_key.write().await = Some(key.clone());
393            info!(backup_key = ?key, "Activated a backup");
394        } else {
395            warn!(backup_key = ?key, "Tried to activate a backup without having the backup key uploaded");
396        }
397
398        Ok(())
399    }
400
401    /// Get the number of backed up room keys and the total number of room keys.
402    pub async fn room_key_counts(&self) -> Result<RoomKeyCounts, CryptoStoreError> {
403        let backup_version = self.backup_key.read().await.as_ref().and_then(|k| k.backup_version());
404        self.store.inbound_group_session_counts(backup_version.as_deref()).await
405    }
406
407    /// Disable and reset our backup state.
408    ///
409    /// This will remove any pending backup request, remove the backup key and
410    /// reset the backup state of each room key we have.
411    #[instrument(skip(self))]
412    pub async fn disable_backup(&self) -> Result<(), CryptoStoreError> {
413        debug!("Disabling key backup and resetting backup state for room keys");
414
415        self.backup_key.write().await.take();
416        self.pending_backup.write().await.take();
417
418        self.store.reset_backup_state().await?;
419
420        debug!("Done disabling backup");
421
422        Ok(())
423    }
424
425    /// Provide the `backup_version` of the current `backup_key`, or None if
426    /// there is no current key, or the key is not used with any backup
427    /// version.
428    pub async fn backup_version(&self) -> Option<String> {
429        self.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
430    }
431
432    /// Store the backup decryption key in the crypto store.
433    ///
434    /// This is useful if the client wants to support gossiping of the backup
435    /// key.
436    pub async fn save_decryption_key(
437        &self,
438        backup_decryption_key: Option<BackupDecryptionKey>,
439        version: Option<String>,
440    ) -> Result<(), CryptoStoreError> {
441        let changes =
442            Changes { backup_decryption_key, backup_version: version, ..Default::default() };
443        self.store.save_changes(changes).await
444    }
445
446    /// Get the backup keys we have saved in our crypto store.
447    pub async fn get_backup_keys(&self) -> Result<BackupKeys, CryptoStoreError> {
448        self.store.load_backup_keys().await
449    }
450
451    /// Encrypt a batch of room keys and return a request that needs to be sent
452    /// out to backup the room keys.
453    pub async fn backup(
454        &self,
455    ) -> Result<Option<(OwnedTransactionId, KeysBackupRequest)>, CryptoStoreError> {
456        let mut request = self.pending_backup.write().await;
457
458        if let Some(request) = &*request {
459            trace!("Backing up, returning an existing request");
460
461            Ok(Some((request.request_id.clone(), request.request.clone())))
462        } else {
463            trace!("Backing up, creating a new request");
464
465            let new_request = self.backup_helper().await?;
466            *request = new_request.clone();
467
468            Ok(new_request.map(|r| (r.request_id, r.request)))
469        }
470    }
471
472    pub(crate) async fn mark_request_as_sent(
473        &self,
474        request_id: &TransactionId,
475    ) -> Result<(), CryptoStoreError> {
476        let mut request = self.pending_backup.write().await;
477        if let Some(r) = &*request {
478            if r.request_id == request_id {
479                let room_and_session_ids: Vec<(&RoomId, &str)> = r
480                    .sessions
481                    .iter()
482                    .flat_map(|(room_id, sender_key_to_session_ids)| {
483                        std::iter::repeat(room_id).zip(sender_key_to_session_ids.values().flatten())
484                    })
485                    .map(|(room_id, session_id)| (room_id.as_ref(), session_id.as_str()))
486                    .collect();
487
488                trace!(request_id = ?r.request_id, keys = ?r.sessions, "Marking room keys as backed up");
489
490                self.store
491                    .mark_inbound_group_sessions_as_backed_up(
492                        &r.request.version,
493                        &room_and_session_ids,
494                    )
495                    .await?;
496
497                trace!(
498                    request_id = ?r.request_id,
499                    keys = ?r.sessions,
500                    "Marked room keys as backed up"
501                );
502
503                *request = None;
504            } else {
505                warn!(
506                    expected = ?r.request_id,
507                    got = ?request_id,
508                    "Tried to mark a pending backup as sent but the request id didn't match"
509                );
510            }
511        } else {
512            warn!(
513                ?request_id,
514                "Tried to mark a pending backup as sent but there isn't a backup pending"
515            );
516        }
517
518        Ok(())
519    }
520
521    async fn backup_helper(&self) -> Result<Option<PendingBackup>, CryptoStoreError> {
522        let Some(backup_key) = &*self.backup_key.read().await else {
523            warn!("Trying to backup room keys but no backup key was found");
524            return Ok(None);
525        };
526
527        let Some(version) = backup_key.backup_version() else {
528            warn!("Trying to backup room keys but the backup key wasn't uploaded");
529            return Ok(None);
530        };
531
532        let sessions =
533            self.store.inbound_group_sessions_for_backup(&version, Self::BACKUP_BATCH_SIZE).await?;
534
535        if sessions.is_empty() {
536            trace!(?backup_key, "No room keys need to be backed up");
537            return Ok(None);
538        }
539
540        let key_count = sessions.len();
541        let (backup, session_record) = Self::backup_keys(sessions, backup_key).await;
542
543        info!(
544            key_count = key_count,
545            keys = ?session_record,
546            ?backup_key,
547            "Successfully created a room keys backup request"
548        );
549
550        let request = PendingBackup {
551            request_id: TransactionId::new(),
552            request: KeysBackupRequest { version, rooms: backup },
553            sessions: session_record,
554        };
555
556        Ok(Some(request))
557    }
558
559    /// Backup all the non-backed up room keys we know about
560    async fn backup_keys(
561        sessions: Vec<InboundGroupSession>,
562        backup_key: &MegolmV1BackupKey,
563    ) -> (
564        BTreeMap<OwnedRoomId, RoomKeyBackup>,
565        BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>>,
566    ) {
567        let mut backup: BTreeMap<OwnedRoomId, RoomKeyBackup> = BTreeMap::new();
568        let mut session_record: BTreeMap<OwnedRoomId, BTreeMap<SenderKey, BTreeSet<SessionId>>> =
569            BTreeMap::new();
570
571        for session in sessions {
572            let room_id = session.room_id().to_owned();
573            let session_id = session.session_id().to_owned();
574            let sender_key = session.sender_key().to_owned();
575            let session = backup_key.encrypt(session).await;
576
577            session_record
578                .entry(room_id.to_owned())
579                .or_default()
580                .entry(sender_key.to_base64())
581                .or_default()
582                .insert(session_id.clone());
583
584            let session = Raw::new(&session).expect("Can't serialize a backed up room key");
585
586            backup
587                .entry(room_id)
588                .or_insert_with(|| RoomKeyBackup::new(BTreeMap::new()))
589                .sessions
590                .insert(session_id, session);
591        }
592
593        (backup, session_record)
594    }
595
596    /// Import the given room keys into our store.
597    ///
598    /// # Arguments
599    ///
600    /// * `room_keys` - A list of previously exported keys that should be
601    ///   imported into our store. If we already have a better version of a key
602    ///   the key will *not* be imported.
603    ///
604    /// Returns a [`RoomKeyImportResult`] containing information about room keys
605    /// which were imported.
606    #[deprecated(note = "Use the OlmMachine::store::import_room_keys method instead")]
607    pub async fn import_backed_up_room_keys(
608        &self,
609        room_keys: BTreeMap<OwnedRoomId, BTreeMap<String, BackedUpRoomKey>>,
610        progress_listener: impl Fn(usize, usize),
611    ) -> Result<RoomKeyImportResult, CryptoStoreError> {
612        let mut decrypted_room_keys = vec![];
613
614        for (room_id, room_keys) in room_keys {
615            for (session_id, room_key) in room_keys {
616                let room_key = ExportedRoomKey::from_backed_up_room_key(
617                    room_id.to_owned(),
618                    session_id,
619                    room_key,
620                );
621
622                decrypted_room_keys.push(room_key);
623            }
624        }
625
626        // FIXME: This method is a bit flawed: we have no real idea which backup version
627        //   these keys came from. For example, we might have reset the backup
628        //   since the keys were downloaded. For now, let's assume they came from
629        //   the "current" backup version.
630        let backup_version = self.backup_version().await;
631
632        self.store
633            .import_room_keys(decrypted_room_keys, backup_version.as_deref(), progress_listener)
634            .await
635    }
636}
637
638#[cfg(test)]
639mod tests {
640    use std::collections::BTreeMap;
641
642    use assert_matches2::assert_let;
643    use matrix_sdk_test::async_test;
644    use ruma::{device_id, room_id, user_id, CanonicalJsonValue, DeviceId, RoomId, UserId};
645    use serde_json::json;
646
647    use super::BackupMachine;
648    use crate::{
649        olm::BackedUpRoomKey,
650        store::{
651            types::{BackupDecryptionKey, Changes},
652            CryptoStore, MemoryStore,
653        },
654        types::RoomKeyBackupInfo,
655        OlmError, OlmMachine,
656    };
657
658    fn room_key() -> BackedUpRoomKey {
659        let json = json!({
660            "algorithm": "m.megolm.v1.aes-sha2",
661            "sender_key": "DeHIg4gwhClxzFYcmNntPNF9YtsdZbmMy8+3kzCMXHA",
662            "session_key": "AQAAAABvWMNZjKFtebYIePKieQguozuoLgzeY6wKcyJjLJcJtQgy1dPqTBD12U+XrYLrRHn\
663                            lKmxoozlhFqJl456+9hlHCL+yq+6ScFuBHtJepnY1l2bdLb4T0JMDkNsNErkiLiLnD6yp3J\
664                            DSjIhkdHxmup/huygrmroq6/L5TaThEoqvW4DPIuO14btKudsS34FF82pwjKS4p6Mlch+0e\
665                            fHAblQV",
666            "sender_claimed_keys":{},
667            "forwarding_curve25519_key_chain":[]
668        });
669
670        serde_json::from_value(json)
671            .expect("We should be able to deserialize our backed up room key")
672    }
673
674    fn alice_id() -> &'static UserId {
675        user_id!("@alice:example.org")
676    }
677
678    fn alice_device_id() -> &'static DeviceId {
679        device_id!("JLAFKJWSCS")
680    }
681
682    fn room_id() -> &'static RoomId {
683        room_id!("!test:localhost")
684    }
685
686    fn room_id2() -> &'static RoomId {
687        room_id!("!test2:localhost")
688    }
689
690    async fn backup_flow(machine: OlmMachine) -> Result<(), OlmError> {
691        let backup_machine = machine.backup_machine();
692        let backup_version = current_backup_version(backup_machine).await;
693
694        let counts =
695            backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
696
697        assert_eq!(counts.total, 0, "Initially no keys exist");
698        assert_eq!(counts.backed_up, 0, "Initially no backed up keys exist");
699
700        machine.create_outbound_group_session_with_defaults_test_helper(room_id()).await?;
701        machine.create_outbound_group_session_with_defaults_test_helper(room_id2()).await?;
702
703        let counts =
704            backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
705        assert_eq!(counts.total, 2, "Two room keys need to exist in the store");
706        assert_eq!(counts.backed_up, 0, "No room keys have been backed up yet");
707
708        let decryption_key = BackupDecryptionKey::new().expect("Can't create new recovery key");
709        let backup_key = decryption_key.megolm_v1_public_key();
710        backup_key.set_version("1".to_owned());
711
712        backup_machine.enable_backup_v1(backup_key).await?;
713
714        let (request_id, _) =
715            backup_machine.backup().await?.expect("Created a backup request successfully");
716        assert_eq!(
717            Some(&request_id),
718            backup_machine.backup().await?.as_ref().map(|(request_id, _)| request_id),
719            "Calling backup again without uploading creates the same backup request"
720        );
721
722        backup_machine.mark_request_as_sent(&request_id).await?;
723        let backup_version = current_backup_version(backup_machine).await;
724
725        let counts =
726            backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
727        assert_eq!(counts.total, 2);
728        assert_eq!(counts.backed_up, 2, "All room keys have been backed up");
729
730        assert!(
731            backup_machine.backup().await?.is_none(),
732            "No room keys need to be backed up, no request needs to be created"
733        );
734
735        backup_machine.disable_backup().await?;
736        let backup_version = current_backup_version(backup_machine).await;
737
738        let counts =
739            backup_machine.store.inbound_group_session_counts(backup_version.as_deref()).await?;
740        assert_eq!(counts.total, 2);
741        assert_eq!(
742            counts.backed_up, 0,
743            "Disabling the backup resets the backup flag on the room keys"
744        );
745
746        Ok(())
747    }
748
749    async fn current_backup_version(backup_machine: &BackupMachine) -> Option<String> {
750        backup_machine.backup_key.read().await.as_ref().and_then(|k| k.backup_version())
751    }
752
753    #[async_test]
754    async fn test_memory_store_backups() -> Result<(), OlmError> {
755        let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
756
757        backup_flow(machine).await
758    }
759
760    #[async_test]
761    async fn test_verify_auth_data() -> Result<(), OlmError> {
762        let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
763        let backup_machine = machine.backup_machine();
764
765        let auth_data = json!({
766            "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM",
767        });
768
769        let backup_version = json!({
770            "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
771            "auth_data": auth_data,
772        });
773
774        let canonical_json: CanonicalJsonValue =
775            auth_data.clone().try_into().expect("Canonicalizing should always work");
776        let serialized = canonical_json.to_string();
777
778        let backup_version: RoomKeyBackupInfo = serde_json::from_value(backup_version).unwrap();
779
780        let state = backup_machine
781            .verify_backup(backup_version, false)
782            .await
783            .expect("Verifying should work");
784        assert!(!state.trusted());
785        assert!(!state.device_signature.trusted());
786        assert!(!state.user_identity_signature.trusted());
787        assert!(!state.other_signatures.values().any(|s| s.trusted()));
788
789        let signatures = machine.sign(&serialized).await?;
790
791        let backup_version = json!({
792            "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
793            "auth_data": {
794                "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM",
795                "signatures": signatures,
796            }
797        });
798        let backup_version: RoomKeyBackupInfo = serde_json::from_value(backup_version).unwrap();
799
800        let state = backup_machine
801            .verify_backup(backup_version, false)
802            .await
803            .expect("Verifying should work");
804
805        assert!(state.trusted());
806        assert!(state.device_signature.trusted());
807        assert!(!state.user_identity_signature.trusted());
808        assert!(!state.other_signatures.values().any(|s| s.trusted()));
809
810        machine
811            .bootstrap_cross_signing(true)
812            .await
813            .expect("Bootstrapping a new identity always works");
814
815        let signatures = machine.sign(&serialized).await?;
816
817        let backup_version = json!({
818            "algorithm": "m.megolm_backup.v1.curve25519-aes-sha2",
819            "auth_data": {
820                "public_key":"XjhWTCjW7l59pbfx9tlCBQolfnIQWARoKOzjTOPSlWM",
821                "signatures": signatures,
822            }
823        });
824        let backup_version: RoomKeyBackupInfo = serde_json::from_value(backup_version).unwrap();
825
826        let state = backup_machine
827            .verify_backup(backup_version, false)
828            .await
829            .expect("Verifying should work");
830
831        assert!(state.trusted());
832        assert!(state.device_signature.trusted());
833        assert!(state.user_identity_signature.trusted());
834        assert!(!state.other_signatures.values().any(|s| s.trusted()));
835
836        Ok(())
837    }
838
839    #[async_test]
840    async fn test_import_backed_up_room_keys() {
841        let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
842        let backup_machine = machine.backup_machine();
843
844        // We set up a backup key, so that we can test `backup_machine.backup()` later.
845        let decryption_key = BackupDecryptionKey::new().expect("Couldn't create new recovery key");
846        let backup_key = decryption_key.megolm_v1_public_key();
847        backup_key.set_version("1".to_owned());
848        backup_machine.enable_backup_v1(backup_key).await.expect("Couldn't enable backup");
849
850        let room_id = room_id!("!DovneieKSTkdHKpIXy:morpheus.localhost");
851        let session_id = "gM8i47Xhu0q52xLfgUXzanCMpLinoyVyH7R58cBuVBU";
852        let room_key = room_key();
853
854        let room_keys: BTreeMap<_, BTreeMap<_, _>> = BTreeMap::from([(
855            room_id.to_owned(),
856            BTreeMap::from([(session_id.to_owned(), room_key)]),
857        )]);
858
859        let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
860
861        assert!(session.is_none(), "Initially we should not have the session in the store");
862
863        #[allow(deprecated)]
864        backup_machine
865            .import_backed_up_room_keys(room_keys, |_, _| {})
866            .await
867            .expect("We should be able to import a room key");
868
869        // Now check that the session was correctly imported, and that it is marked as
870        // backed up
871        let session = machine.store().get_inbound_group_session(room_id, session_id).await.unwrap();
872        assert_let!(Some(session) = session);
873        assert!(
874            session.backed_up(),
875            "If a session was imported from a backup, it should be considered to be backed up"
876        );
877        assert!(session.has_been_imported());
878
879        // Also check that it is not returned by a backup request.
880        let backup_request =
881            backup_machine.backup().await.expect("We should be able to create a backup request");
882        assert!(
883            backup_request.is_none(),
884            "If a session was imported from backup, it should not be backed up again."
885        );
886    }
887
888    #[async_test]
889    async fn test_sign_backup_info() {
890        let machine = OlmMachine::new(alice_id(), alice_device_id()).await;
891        let backup_machine = machine.backup_machine();
892
893        let decryption_key = BackupDecryptionKey::new().unwrap();
894        let mut backup_info = decryption_key.to_backup_info();
895
896        let result = backup_machine.verify_backup(backup_info.to_owned(), false).await.unwrap();
897
898        assert!(!result.trusted());
899
900        backup_machine.sign_backup(&mut backup_info).await.unwrap();
901
902        let result = backup_machine.verify_backup(backup_info, false).await.unwrap();
903
904        assert!(result.trusted());
905    }
906
907    #[async_test]
908    async fn test_fix_backup_key_mismatch() {
909        let store = MemoryStore::new();
910
911        let backup_decryption_key = BackupDecryptionKey::new().unwrap();
912
913        store
914            .save_changes(Changes {
915                backup_decryption_key: Some(backup_decryption_key.clone()),
916                backup_version: Some("1".to_owned()),
917                ..Default::default()
918            })
919            .await
920            .unwrap();
921
922        // Create the machine using `with_store` and without a call to enable_backup_v1,
923        // like regenerate_olm would do
924        let alice =
925            OlmMachine::with_store(alice_id(), alice_device_id(), store, None).await.unwrap();
926
927        let binding = alice.backup_machine().backup_key.read().await;
928        let machine_backup_key = binding.as_ref().unwrap();
929
930        assert_eq!(
931            machine_backup_key.to_base64(),
932            backup_decryption_key.megolm_v1_public_key().to_base64(),
933            "The OlmMachine loaded the wrong backup key."
934        );
935    }
936}