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