matrix_sdk_indexeddb/crypto_store/
mod.rs

1// Copyright 2020 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
15use std::{
16    collections::{BTreeMap, HashMap},
17    sync::{Arc, RwLock},
18};
19
20use async_trait::async_trait;
21use gloo_utils::format::JsValueSerdeExt;
22use hkdf::Hkdf;
23use indexed_db_futures::{
24    KeyRange,
25    cursor::Cursor,
26    database::Database,
27    internals::SystemRepr,
28    object_store::ObjectStore,
29    prelude::*,
30    transaction::{Transaction, TransactionMode},
31};
32use js_sys::Array;
33use matrix_sdk_base::cross_process_lock::{
34    CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION,
35};
36use matrix_sdk_crypto::{
37    Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData,
38    olm::{
39        Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
40        PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session,
41        StaticAccountData,
42    },
43    store::{
44        CryptoStore, CryptoStoreError,
45        types::{
46            BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts,
47            RoomKeyWithheldEntry, RoomSettings, StoredRoomKeyBundleData,
48        },
49    },
50    vodozemac::base64_encode,
51};
52use matrix_sdk_store_encryption::StoreCipher;
53use ruma::{
54    DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, RoomId, TransactionId, UserId,
55    events::secret::request::SecretName,
56};
57use serde::{Deserialize, Serialize};
58use sha2::Sha256;
59use tokio::sync::Mutex;
60use tracing::{debug, warn};
61use wasm_bindgen::JsValue;
62
63use crate::{
64    crypto_store::migrations::open_and_upgrade_db,
65    error::GenericError,
66    serializer::{MaybeEncrypted, SafeEncodeSerializer, SafeEncodeSerializerError},
67};
68
69mod migrations;
70
71mod keys {
72    // stores
73    pub const CORE: &str = "core";
74
75    pub const SESSION: &str = "session";
76
77    pub const INBOUND_GROUP_SESSIONS_V3: &str = "inbound_group_sessions3";
78    pub const INBOUND_GROUP_SESSIONS_BACKUP_INDEX: &str = "backup";
79    pub const INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX: &str = "backed_up_to";
80    pub const INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX: &str =
81        "inbound_group_session_sender_key_sender_data_type_idx";
82
83    pub const OUTBOUND_GROUP_SESSIONS: &str = "outbound_group_sessions";
84
85    pub const TRACKED_USERS: &str = "tracked_users";
86    pub const OLM_HASHES: &str = "olm_hashes";
87
88    pub const DEVICES: &str = "devices";
89    pub const IDENTITIES: &str = "identities";
90
91    pub const GOSSIP_REQUESTS: &str = "gossip_requests";
92    pub const GOSSIP_REQUESTS_UNSENT_INDEX: &str = "unsent";
93    pub const GOSSIP_REQUESTS_BY_INFO_INDEX: &str = "by_info";
94
95    pub const ROOM_SETTINGS: &str = "room_settings";
96
97    pub const SECRETS_INBOX: &str = "secrets_inbox";
98
99    pub const WITHHELD_SESSIONS: &str = "withheld_sessions";
100
101    pub const RECEIVED_ROOM_KEY_BUNDLES: &str = "received_room_key_bundles";
102
103    pub const LEASE_LOCKS: &str = "lease_locks";
104
105    pub const ROOM_KEY_BACKUPS_FULLY_DOWNLOADED: &str = "room_key_backups_fully_downloaded";
106
107    // keys
108    pub const STORE_CIPHER: &str = "store_cipher";
109    pub const ACCOUNT: &str = "account";
110    pub const NEXT_BATCH_TOKEN: &str = "next_batch_token";
111    pub const PRIVATE_IDENTITY: &str = "private_identity";
112
113    // backup v1
114    pub const BACKUP_KEYS: &str = "backup_keys";
115
116    /// Indexeddb key for the key backup version that [`RECOVERY_KEY_V1`]
117    /// corresponds to.
118    pub const BACKUP_VERSION_V1: &str = "backup_version_v1";
119
120    /// Indexeddb key for the backup decryption key.
121    ///
122    /// Known, for historical reasons, as the recovery key. Not to be confused
123    /// with the client-side recovery key, which is actually an AES key for use
124    /// with SSSS.
125    pub const RECOVERY_KEY_V1: &str = "recovery_key_v1";
126
127    /// Indexeddb key for the dehydrated device pickle key.
128    pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key";
129}
130
131/// An implementation of [CryptoStore] that uses [IndexedDB] for persistent
132/// storage.
133///
134/// [IndexedDB]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
135pub struct IndexeddbCryptoStore {
136    static_account: RwLock<Option<StaticAccountData>>,
137    name: String,
138    pub(crate) inner: Database,
139
140    serializer: SafeEncodeSerializer,
141    save_changes_lock: Arc<Mutex<()>>,
142}
143
144#[cfg(not(tarpaulin_include))]
145impl std::fmt::Debug for IndexeddbCryptoStore {
146    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
147        f.debug_struct("IndexeddbCryptoStore").field("name", &self.name).finish()
148    }
149}
150
151#[derive(Debug, thiserror::Error)]
152pub enum IndexeddbCryptoStoreError {
153    #[error(transparent)]
154    Serialization(#[from] serde_json::Error),
155    #[error("DomException {name} ({code}): {message}")]
156    DomException {
157        /// DomException code
158        code: u16,
159        /// Specific name of the DomException
160        name: String,
161        /// Message given to the DomException
162        message: String,
163    },
164    #[error(transparent)]
165    CryptoStoreError(#[from] CryptoStoreError),
166    #[error(
167        "The schema version of the crypto store is too new. \
168         Existing version: {current_version}; max supported version: {max_supported_version}"
169    )]
170    SchemaTooNewError { max_supported_version: u32, current_version: u32 },
171}
172
173impl From<SafeEncodeSerializerError> for IndexeddbCryptoStoreError {
174    fn from(value: SafeEncodeSerializerError) -> Self {
175        match value {
176            SafeEncodeSerializerError::Serialization(error) => Self::Serialization(error),
177            SafeEncodeSerializerError::DomException { code, name, message } => {
178                Self::DomException { code, name, message }
179            }
180            SafeEncodeSerializerError::CryptoStoreError(crypto_store_error) => {
181                Self::CryptoStoreError(crypto_store_error)
182            }
183        }
184    }
185}
186
187impl From<web_sys::DomException> for IndexeddbCryptoStoreError {
188    fn from(frm: web_sys::DomException) -> IndexeddbCryptoStoreError {
189        IndexeddbCryptoStoreError::DomException {
190            name: frm.name(),
191            message: frm.message(),
192            code: frm.code(),
193        }
194    }
195}
196
197impl From<serde_wasm_bindgen::Error> for IndexeddbCryptoStoreError {
198    fn from(e: serde_wasm_bindgen::Error) -> Self {
199        IndexeddbCryptoStoreError::Serialization(serde::de::Error::custom(e.to_string()))
200    }
201}
202
203impl From<IndexeddbCryptoStoreError> for CryptoStoreError {
204    fn from(frm: IndexeddbCryptoStoreError) -> CryptoStoreError {
205        match frm {
206            IndexeddbCryptoStoreError::Serialization(e) => CryptoStoreError::Serialization(e),
207            IndexeddbCryptoStoreError::CryptoStoreError(e) => e,
208            _ => CryptoStoreError::backend(frm),
209        }
210    }
211}
212
213impl From<indexed_db_futures::error::DomException> for IndexeddbCryptoStoreError {
214    fn from(value: indexed_db_futures::error::DomException) -> Self {
215        web_sys::DomException::from(value).into()
216    }
217}
218
219impl From<indexed_db_futures::error::SerialisationError> for IndexeddbCryptoStoreError {
220    fn from(value: indexed_db_futures::error::SerialisationError) -> Self {
221        Self::Serialization(serde::de::Error::custom(value.to_string()))
222    }
223}
224
225impl From<indexed_db_futures::error::UnexpectedDataError> for IndexeddbCryptoStoreError {
226    fn from(value: indexed_db_futures::error::UnexpectedDataError) -> Self {
227        Self::CryptoStoreError(CryptoStoreError::backend(value))
228    }
229}
230
231impl From<GenericError> for IndexeddbCryptoStoreError {
232    fn from(value: GenericError) -> Self {
233        Self::CryptoStoreError(value.into())
234    }
235}
236
237impl From<indexed_db_futures::error::JSError> for IndexeddbCryptoStoreError {
238    fn from(value: indexed_db_futures::error::JSError) -> Self {
239        GenericError::from(value.to_string()).into()
240    }
241}
242
243impl From<indexed_db_futures::error::Error> for IndexeddbCryptoStoreError {
244    fn from(value: indexed_db_futures::error::Error) -> Self {
245        use indexed_db_futures::error::Error;
246        match value {
247            Error::DomException(e) => e.into(),
248            Error::Serialisation(e) => e.into(),
249            Error::MissingData(e) => e.into(),
250            Error::Unknown(e) => e.into(),
251        }
252    }
253}
254
255impl From<indexed_db_futures::error::OpenDbError> for IndexeddbCryptoStoreError {
256    fn from(value: indexed_db_futures::error::OpenDbError) -> Self {
257        use indexed_db_futures::error::OpenDbError;
258        match value {
259            OpenDbError::Base(error) => error.into(),
260            _ => GenericError::from(value.to_string()).into(),
261        }
262    }
263}
264
265type Result<A, E = IndexeddbCryptoStoreError> = std::result::Result<A, E>;
266
267/// Defines an operation to perform on the database.
268enum PendingOperation {
269    Put { key: JsValue, value: JsValue },
270    Delete(JsValue),
271}
272
273/// A struct that represents all the operations that need to be done to the
274/// database when calls to the store `save_changes` are made.
275/// The idea is to do all the serialization and encryption before the
276/// transaction, and then just do the actual Indexeddb operations in the
277/// transaction.
278struct PendingIndexeddbChanges {
279    /// A map of the object store names to the operations to perform on that
280    /// store.
281    store_to_key_values: BTreeMap<&'static str, Vec<PendingOperation>>,
282}
283
284/// Represents the changes on a single object store.
285struct PendingStoreChanges<'a> {
286    operations: &'a mut Vec<PendingOperation>,
287}
288
289impl PendingStoreChanges<'_> {
290    fn put(&mut self, key: JsValue, value: JsValue) {
291        self.operations.push(PendingOperation::Put { key, value });
292    }
293
294    fn delete(&mut self, key: JsValue) {
295        self.operations.push(PendingOperation::Delete(key));
296    }
297}
298
299impl PendingIndexeddbChanges {
300    fn get(&mut self, store: &'static str) -> PendingStoreChanges<'_> {
301        PendingStoreChanges { operations: self.store_to_key_values.entry(store).or_default() }
302    }
303}
304
305impl PendingIndexeddbChanges {
306    fn new() -> Self {
307        Self { store_to_key_values: BTreeMap::new() }
308    }
309
310    /// Returns the list of stores that have pending operations.
311    /// Should be used as the list of store names when starting the indexeddb
312    /// transaction (`transaction_on_multi_with_mode`).
313    fn touched_stores(&self) -> Vec<&str> {
314        self.store_to_key_values
315            .iter()
316            .filter_map(
317                |(store, pending_operations)| {
318                    if !pending_operations.is_empty() { Some(*store) } else { None }
319                },
320            )
321            .collect()
322    }
323
324    /// Applies all the pending operations to the store.
325    fn apply(self, tx: &Transaction<'_>) -> Result<()> {
326        for (store, operations) in self.store_to_key_values {
327            if operations.is_empty() {
328                continue;
329            }
330            let object_store = tx.object_store(store)?;
331            for op in operations {
332                match op {
333                    PendingOperation::Put { key, value } => {
334                        object_store.put(&value).with_key(key).build()?;
335                    }
336                    PendingOperation::Delete(key) => {
337                        object_store.delete(&key).build()?;
338                    }
339                }
340            }
341        }
342        Ok(())
343    }
344}
345
346impl IndexeddbCryptoStore {
347    pub(crate) async fn open_with_store_cipher(
348        prefix: &str,
349        store_cipher: Option<Arc<StoreCipher>>,
350    ) -> Result<Self> {
351        let name = format!("{prefix:0}::matrix-sdk-crypto");
352
353        let serializer = SafeEncodeSerializer::new(store_cipher);
354        debug!("IndexedDbCryptoStore: opening main store {name}");
355        let db = open_and_upgrade_db(&name, &serializer).await?;
356
357        Ok(Self {
358            name,
359            inner: db,
360            serializer,
361            static_account: RwLock::new(None),
362            save_changes_lock: Default::default(),
363        })
364    }
365
366    /// Open a new `IndexeddbCryptoStore` with default name and no passphrase
367    pub async fn open() -> Result<Self> {
368        IndexeddbCryptoStore::open_with_store_cipher("crypto", None).await
369    }
370
371    /// Open an `IndexeddbCryptoStore` with given name and passphrase.
372    ///
373    /// If the store previously existed, the encryption cipher is initialised
374    /// using the given passphrase and the details from the meta store. If the
375    /// store did not previously exist, a new encryption cipher is derived
376    /// from the passphrase, and the details are stored to the metastore.
377    ///
378    /// The store is then opened, or a new one created, using the encryption
379    /// cipher.
380    ///
381    /// # Arguments
382    ///
383    /// * `prefix` - Common prefix for the names of the two IndexedDB stores.
384    /// * `passphrase` - Passphrase which is used to derive a key to encrypt the
385    ///   key which is used to encrypt the store. Must be the same each time the
386    ///   store is opened.
387    pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result<Self> {
388        let db = open_meta_db(prefix).await?;
389        let store_cipher = load_store_cipher(&db).await?;
390
391        let store_cipher = match store_cipher {
392            Some(cipher) => {
393                debug!("IndexedDbCryptoStore: decrypting store cipher");
394                StoreCipher::import(passphrase, &cipher)
395                    .map_err(|_| CryptoStoreError::UnpicklingError)?
396            }
397            None => {
398                debug!("IndexedDbCryptoStore: encrypting new store cipher");
399                let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
400                #[cfg(not(test))]
401                let export = cipher.export(passphrase);
402                #[cfg(test)]
403                let export = cipher._insecure_export_fast_for_testing(passphrase);
404
405                let export = export.map_err(CryptoStoreError::backend)?;
406
407                save_store_cipher(&db, &export).await?;
408                cipher
409            }
410        };
411
412        // Must release the database access manually as it's not done when
413        // dropping it.
414        db.close();
415
416        IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
417    }
418
419    /// Open an `IndexeddbCryptoStore` with given name and key.
420    ///
421    /// If the store previously existed, the encryption cipher is initialised
422    /// using the given key and the details from the meta store. If the store
423    /// did not previously exist, a new encryption cipher is derived from
424    /// the passphrase, and the details are stored to the metastore.
425    ///
426    /// The store is then opened, or a new one created, using the encryption
427    /// cipher.
428    ///
429    /// # Arguments
430    ///
431    /// * `prefix` - Common prefix for the names of the two IndexedDB stores.
432    /// * `key` - Key with which to encrypt the key which is used to encrypt the
433    ///   store. Must be the same each time the store is opened.
434    pub async fn open_with_key(prefix: &str, key: &[u8; 32]) -> Result<Self> {
435        // The application might also use the provided key for something else, so to
436        // avoid key reuse, we pass the provided key through an HKDF
437        let mut chacha_key = zeroize::Zeroizing::new([0u8; 32]);
438        const HKDF_INFO: &[u8] = b"CRYPTOSTORE_CIPHER";
439        let hkdf = Hkdf::<Sha256>::new(None, key);
440        hkdf.expand(HKDF_INFO, &mut *chacha_key)
441            .expect("We should be able to generate a 32-byte key");
442
443        let db = open_meta_db(prefix).await?;
444        let store_cipher = load_store_cipher(&db).await?;
445
446        let store_cipher = match store_cipher {
447            Some(cipher) => {
448                debug!("IndexedDbCryptoStore: decrypting store cipher");
449                import_store_cipher_with_key(&chacha_key, key, &cipher, &db).await?
450            }
451            None => {
452                debug!("IndexedDbCryptoStore: encrypting new store cipher");
453                let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
454                let export =
455                    cipher.export_with_key(&chacha_key).map_err(CryptoStoreError::backend)?;
456                save_store_cipher(&db, &export).await?;
457                cipher
458            }
459        };
460
461        // Must release the database access manually as it's not done when
462        // dropping it.
463        db.close();
464
465        IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
466    }
467
468    /// Open a new `IndexeddbCryptoStore` with given name and no passphrase
469    pub async fn open_with_name(name: &str) -> Result<Self> {
470        IndexeddbCryptoStore::open_with_store_cipher(name, None).await
471    }
472
473    /// Delete the IndexedDB databases for the given name.
474    #[cfg(test)]
475    pub fn delete_stores(prefix: &str) -> Result<()> {
476        Database::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto-meta"))?;
477        Database::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto"))?;
478        Ok(())
479    }
480
481    fn get_static_account(&self) -> Option<StaticAccountData> {
482        self.static_account.read().unwrap().clone()
483    }
484
485    /// Transform an [`InboundGroupSession`] into a `JsValue` holding a
486    /// [`InboundGroupSessionIndexedDbObject`], ready for storing.
487    async fn serialize_inbound_group_session(
488        &self,
489        session: &InboundGroupSession,
490    ) -> Result<JsValue> {
491        let obj =
492            InboundGroupSessionIndexedDbObject::from_session(session, &self.serializer).await?;
493        Ok(serde_wasm_bindgen::to_value(&obj)?)
494    }
495
496    /// Transform a JsValue holding a [`InboundGroupSessionIndexedDbObject`]
497    /// back into a [`InboundGroupSession`].
498    fn deserialize_inbound_group_session(
499        &self,
500        stored_value: JsValue,
501    ) -> Result<InboundGroupSession> {
502        let idb_object: InboundGroupSessionIndexedDbObject =
503            serde_wasm_bindgen::from_value(stored_value)?;
504        let pickled_session: PickledInboundGroupSession =
505            self.serializer.maybe_decrypt_value(idb_object.pickled_session)?;
506        let session = InboundGroupSession::from_pickle(pickled_session)
507            .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
508
509        // Although a "backed up" flag is stored inside `idb_object.pickled_session`, it
510        // is not maintained when backups are reset. Overwrite the flag with the
511        // needs_backup value from the IDB object.
512        if idb_object.needs_backup {
513            session.reset_backup_state();
514        } else {
515            session.mark_as_backed_up();
516        }
517
518        Ok(session)
519    }
520
521    /// Transform a [`GossipRequest`] into a `JsValue` holding a
522    /// [`GossipRequestIndexedDbObject`], ready for storing.
523    fn serialize_gossip_request(&self, gossip_request: &GossipRequest) -> Result<JsValue> {
524        let obj = GossipRequestIndexedDbObject {
525            // hash the info as a key so that it can be used in index lookups.
526            info: self
527                .serializer
528                .encode_key_as_string(keys::GOSSIP_REQUESTS, gossip_request.info.as_key()),
529
530            // serialize and encrypt the data about the request
531            request: self.serializer.serialize_value_as_bytes(gossip_request)?,
532
533            unsent: !gossip_request.sent_out,
534        };
535
536        Ok(serde_wasm_bindgen::to_value(&obj)?)
537    }
538
539    /// Transform a JsValue holding a [`GossipRequestIndexedDbObject`] back into
540    /// a [`GossipRequest`].
541    fn deserialize_gossip_request(&self, stored_request: JsValue) -> Result<GossipRequest> {
542        let idb_object: GossipRequestIndexedDbObject =
543            serde_wasm_bindgen::from_value(stored_request)?;
544        Ok(self.serializer.deserialize_value_from_bytes(&idb_object.request)?)
545    }
546
547    /// Process all the changes and do all encryption/serialization before the
548    /// actual transaction.
549    ///
550    /// Returns a tuple where the first item is a `PendingIndexeddbChanges`
551    /// struct, and the second item is a boolean indicating whether the session
552    /// cache should be cleared.
553    async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
554        let mut indexeddb_changes = PendingIndexeddbChanges::new();
555
556        let private_identity_pickle =
557            if let Some(i) = &changes.private_identity { Some(i.pickle().await) } else { None };
558
559        let decryption_key_pickle = &changes.backup_decryption_key;
560        let backup_version = &changes.backup_version;
561        let dehydration_pickle_key = &changes.dehydrated_device_pickle_key;
562
563        let mut core = indexeddb_changes.get(keys::CORE);
564        if let Some(next_batch) = &changes.next_batch_token {
565            core.put(
566                JsValue::from_str(keys::NEXT_BATCH_TOKEN),
567                self.serializer.serialize_value(next_batch)?,
568            );
569        }
570
571        if let Some(i) = &private_identity_pickle {
572            core.put(
573                JsValue::from_str(keys::PRIVATE_IDENTITY),
574                self.serializer.serialize_value(i)?,
575            );
576        }
577
578        if let Some(i) = &dehydration_pickle_key {
579            core.put(
580                JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY),
581                self.serializer.serialize_value(i)?,
582            );
583        }
584
585        if let Some(a) = &decryption_key_pickle {
586            indexeddb_changes.get(keys::BACKUP_KEYS).put(
587                JsValue::from_str(keys::RECOVERY_KEY_V1),
588                self.serializer.serialize_value(&a)?,
589            );
590        }
591
592        if let Some(a) = &backup_version {
593            indexeddb_changes.get(keys::BACKUP_KEYS).put(
594                JsValue::from_str(keys::BACKUP_VERSION_V1),
595                self.serializer.serialize_value(&a)?,
596            );
597        }
598
599        if !changes.sessions.is_empty() {
600            let mut sessions = indexeddb_changes.get(keys::SESSION);
601
602            for session in &changes.sessions {
603                let sender_key = session.sender_key().to_base64();
604                let session_id = session.session_id();
605
606                let pickle = session.pickle().await;
607                let key = self.serializer.encode_key(keys::SESSION, (&sender_key, session_id));
608
609                sessions.put(key, self.serializer.serialize_value(&pickle)?);
610            }
611        }
612
613        if !changes.inbound_group_sessions.is_empty() {
614            let mut sessions = indexeddb_changes.get(keys::INBOUND_GROUP_SESSIONS_V3);
615
616            for session in &changes.inbound_group_sessions {
617                let room_id = session.room_id();
618                let session_id = session.session_id();
619                let key = self
620                    .serializer
621                    .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
622                let value = self.serialize_inbound_group_session(session).await?;
623                sessions.put(key, value);
624            }
625        }
626
627        if !changes.outbound_group_sessions.is_empty() {
628            let mut sessions = indexeddb_changes.get(keys::OUTBOUND_GROUP_SESSIONS);
629
630            for session in &changes.outbound_group_sessions {
631                let room_id = session.room_id();
632                let pickle = session.pickle().await;
633                sessions.put(
634                    self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
635                    self.serializer.serialize_value(&pickle)?,
636                );
637            }
638        }
639
640        let device_changes = &changes.devices;
641        let identity_changes = &changes.identities;
642        let olm_hashes = &changes.message_hashes;
643        let key_requests = &changes.key_requests;
644        let withheld_session_info = &changes.withheld_session_info;
645        let room_settings_changes = &changes.room_settings;
646
647        let mut device_store = indexeddb_changes.get(keys::DEVICES);
648
649        for device in device_changes.new.iter().chain(&device_changes.changed) {
650            let key =
651                self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
652            let device = self.serializer.serialize_value(&device)?;
653
654            device_store.put(key, device);
655        }
656
657        for device in &device_changes.deleted {
658            let key =
659                self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
660            device_store.delete(key);
661        }
662
663        if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
664            let mut identities = indexeddb_changes.get(keys::IDENTITIES);
665            for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
666                identities.put(
667                    self.serializer.encode_key(keys::IDENTITIES, identity.user_id()),
668                    self.serializer.serialize_value(&identity)?,
669                );
670            }
671        }
672
673        if !olm_hashes.is_empty() {
674            let mut hashes = indexeddb_changes.get(keys::OLM_HASHES);
675            for hash in olm_hashes {
676                hashes.put(
677                    self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
678                    JsValue::TRUE,
679                );
680            }
681        }
682
683        if !key_requests.is_empty() {
684            let mut gossip_requests = indexeddb_changes.get(keys::GOSSIP_REQUESTS);
685
686            for gossip_request in key_requests {
687                let key_request_id = self
688                    .serializer
689                    .encode_key(keys::GOSSIP_REQUESTS, gossip_request.request_id.as_str());
690                let key_request_value = self.serialize_gossip_request(gossip_request)?;
691                gossip_requests.put(key_request_id, key_request_value);
692            }
693        }
694
695        if !withheld_session_info.is_empty() {
696            let mut withhelds = indexeddb_changes.get(keys::WITHHELD_SESSIONS);
697
698            for (room_id, data) in withheld_session_info {
699                for (session_id, event) in data {
700                    let key =
701                        self.serializer.encode_key(keys::WITHHELD_SESSIONS, (&room_id, session_id));
702                    withhelds.put(key, self.serializer.serialize_value(&event)?);
703                }
704            }
705        }
706
707        if !room_settings_changes.is_empty() {
708            let mut settings_store = indexeddb_changes.get(keys::ROOM_SETTINGS);
709
710            for (room_id, settings) in room_settings_changes {
711                let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
712                let value = self.serializer.serialize_value(&settings)?;
713                settings_store.put(key, value);
714            }
715        }
716
717        if !changes.secrets.is_empty() {
718            let mut secret_store = indexeddb_changes.get(keys::SECRETS_INBOX);
719
720            for secret in &changes.secrets {
721                let key = self.serializer.encode_key(
722                    keys::SECRETS_INBOX,
723                    (secret.secret_name.as_str(), secret.event.content.request_id.as_str()),
724                );
725                let value = self.serializer.serialize_value(&secret)?;
726
727                secret_store.put(key, value);
728            }
729        }
730
731        if !changes.received_room_key_bundles.is_empty() {
732            let mut bundle_store = indexeddb_changes.get(keys::RECEIVED_ROOM_KEY_BUNDLES);
733            for bundle in &changes.received_room_key_bundles {
734                let key = self.serializer.encode_key(
735                    keys::RECEIVED_ROOM_KEY_BUNDLES,
736                    (&bundle.bundle_data.room_id, &bundle.sender_user),
737                );
738                let value = self.serializer.serialize_value(&bundle)?;
739                bundle_store.put(key, value);
740            }
741        }
742
743        if !changes.room_key_backups_fully_downloaded.is_empty() {
744            let mut room_store = indexeddb_changes.get(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED);
745            for room_id in &changes.room_key_backups_fully_downloaded {
746                room_store.put(
747                    self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id),
748                    JsValue::TRUE,
749                );
750            }
751        }
752
753        Ok(indexeddb_changes)
754    }
755}
756
757// Small hack to have the following macro invocation act as the appropriate
758// trait impl block on wasm, but still be compiled on non-wasm as a regular
759// impl block otherwise.
760//
761// The trait impl doesn't compile on non-wasm due to unfulfilled trait bounds,
762// this hack allows us to still have most of rust-analyzer's IDE functionality
763// within the impl block without having to set it up to check things against
764// the wasm target (which would disable many other parts of the codebase).
765#[cfg(target_family = "wasm")]
766macro_rules! impl_crypto_store {
767    ( $($body:tt)* ) => {
768        #[async_trait(?Send)]
769        impl CryptoStore for IndexeddbCryptoStore {
770            type Error = IndexeddbCryptoStoreError;
771
772            $($body)*
773        }
774    };
775}
776
777#[cfg(not(target_family = "wasm"))]
778macro_rules! impl_crypto_store {
779    ( $($body:tt)* ) => {
780        impl IndexeddbCryptoStore {
781            $($body)*
782        }
783    };
784}
785
786impl_crypto_store! {
787    async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> {
788        // Serialize calls to `save_pending_changes`; there are multiple await points
789        // below, and we're pickling data as we go, so we don't want to
790        // invalidate data we've previously read and overwrite it in the store.
791        // TODO: #2000 should make this lock go away, or change its shape.
792        let _guard = self.save_changes_lock.lock().await;
793
794        let stores: Vec<&str> = [(changes.account.is_some(), keys::CORE)]
795            .iter()
796            .filter_map(|(id, key)| if *id { Some(*key) } else { None })
797            .collect();
798
799        if stores.is_empty() {
800            // nothing to do, quit early
801            return Ok(());
802        }
803
804        let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
805
806        let account_pickle = if let Some(account) = changes.account {
807            *self.static_account.write().unwrap() = Some(account.static_data().clone());
808            Some(account.pickle())
809        } else {
810            None
811        };
812
813        if let Some(a) = &account_pickle {
814            tx.object_store(keys::CORE)?
815                .put(&self.serializer.serialize_value(&a)?)
816                .with_key(JsValue::from_str(keys::ACCOUNT))
817                .build()?;
818        }
819
820        tx.commit().await?;
821
822        Ok(())
823    }
824
825    async fn save_changes(&self, changes: Changes) -> Result<()> {
826        // Serialize calls to `save_changes`; there are multiple await points below, and
827        // we're pickling data as we go, so we don't want to invalidate data
828        // we've previously read and overwrite it in the store.
829        // TODO: #2000 should make this lock go away, or change its shape.
830        let _guard = self.save_changes_lock.lock().await;
831
832        let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
833
834        let stores = indexeddb_changes.touched_stores();
835
836        if stores.is_empty() {
837            // nothing to do, quit early
838            return Ok(());
839        }
840
841        let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
842
843        indexeddb_changes.apply(&tx)?;
844
845        tx.commit().await?;
846
847        Ok(())
848    }
849
850    async fn save_inbound_group_sessions(
851        &self,
852        sessions: Vec<InboundGroupSession>,
853        backed_up_to_version: Option<&str>,
854    ) -> Result<()> {
855        // Sanity-check that the data in the sessions corresponds to backed_up_version
856        sessions.iter().for_each(|s| {
857            let backed_up = s.backed_up();
858            if backed_up != backed_up_to_version.is_some() {
859                warn!(
860                    backed_up,
861                    backed_up_to_version,
862                    "Session backed-up flag does not correspond to backup version setting",
863                );
864            }
865        });
866
867        // Currently, this store doesn't save the backup version separately, so this
868        // just delegates to save_changes.
869        self.save_changes(Changes { inbound_group_sessions: sessions, ..Changes::default() }).await
870    }
871
872    async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
873        let tx = self
874            .inner
875            .transaction(keys::TRACKED_USERS)
876            .with_mode(TransactionMode::Readonly)
877            .build()?;
878        let os = tx.object_store(keys::TRACKED_USERS)?;
879        let user_ids = os.get_all_keys::<JsValue>().await?;
880
881        let mut users = Vec::new();
882
883        for result in user_ids {
884            let user_id = result?;
885            let dirty: bool = !matches!(
886                os.get(&user_id).await?.map(|v: JsValue| v.into_serde()),
887                Some(Ok(false))
888            );
889            let Some(Ok(user_id)) = user_id.as_string().map(UserId::parse) else { continue };
890
891            users.push(TrackedUser { user_id, dirty });
892        }
893
894        Ok(users)
895    }
896
897    async fn get_outbound_group_session(
898        &self,
899        room_id: &RoomId,
900    ) -> Result<Option<OutboundGroupSession>> {
901        let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
902        if let Some(value) = self
903            .inner
904            .transaction(keys::OUTBOUND_GROUP_SESSIONS)
905            .with_mode(TransactionMode::Readonly)
906            .build()?
907            .object_store(keys::OUTBOUND_GROUP_SESSIONS)?
908            .get(&self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))
909            .await?
910        {
911            Ok(Some(
912                OutboundGroupSession::from_pickle(
913                    account_info.device_id,
914                    account_info.identity_keys,
915                    self.serializer.deserialize_value(value)?,
916                )
917                .map_err(CryptoStoreError::from)?,
918            ))
919        } else {
920            Ok(None)
921        }
922    }
923
924    async fn get_outgoing_secret_requests(
925        &self,
926        request_id: &TransactionId,
927    ) -> Result<Option<GossipRequest>> {
928        let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id.as_str());
929        self.inner
930            .transaction(keys::GOSSIP_REQUESTS)
931            .with_mode(TransactionMode::Readonly)
932            .build()?
933            .object_store(keys::GOSSIP_REQUESTS)?
934            .get(jskey)
935            .await?
936            .map(|val| self.deserialize_gossip_request(val))
937            .transpose()
938    }
939
940    async fn load_account(&self) -> Result<Option<Account>> {
941        if let Some(pickle) = self
942            .inner
943            .transaction(keys::CORE)
944            .with_mode(TransactionMode::Readonly)
945            .build()?
946            .object_store(keys::CORE)?
947            .get(&JsValue::from_str(keys::ACCOUNT))
948            .await?
949        {
950            let pickle = self.serializer.deserialize_value(pickle)?;
951
952            let account = Account::from_pickle(pickle).map_err(CryptoStoreError::from)?;
953
954            *self.static_account.write().unwrap() = Some(account.static_data().clone());
955
956            Ok(Some(account))
957        } else {
958            Ok(None)
959        }
960    }
961
962    async fn next_batch_token(&self) -> Result<Option<String>> {
963        if let Some(serialized) = self
964            .inner
965            .transaction(keys::CORE)
966            .with_mode(TransactionMode::Readonly)
967            .build()?
968            .object_store(keys::CORE)?
969            .get(&JsValue::from_str(keys::NEXT_BATCH_TOKEN))
970            .await?
971        {
972            let token = self.serializer.deserialize_value(serialized)?;
973            Ok(Some(token))
974        } else {
975            Ok(None)
976        }
977    }
978
979    async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
980        if let Some(pickle) = self
981            .inner
982            .transaction(keys::CORE)
983            .with_mode(TransactionMode::Readonly)
984            .build()?
985            .object_store(keys::CORE)?
986            .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))
987            .await?
988        {
989            let pickle = self.serializer.deserialize_value(pickle)?;
990
991            Ok(Some(
992                PrivateCrossSigningIdentity::from_pickle(pickle)
993                    .map_err(|_| CryptoStoreError::UnpicklingError)?,
994            ))
995        } else {
996            Ok(None)
997        }
998    }
999
1000    async fn get_sessions(&self, sender_key: &str) -> Result<Option<Vec<Session>>> {
1001        let device_keys = self.get_own_device().await?.as_device_keys().clone();
1002
1003        let range = self.serializer.encode_to_range(keys::SESSION, sender_key);
1004        let sessions: Vec<Session> = self
1005            .inner
1006            .transaction(keys::SESSION)
1007            .with_mode(TransactionMode::Readonly)
1008            .build()?
1009            .object_store(keys::SESSION)?
1010            .get_all()
1011            .with_query(&range)
1012            .await?
1013            .filter_map(Result::ok)
1014            .filter_map(|f| {
1015                self.serializer.deserialize_value(f).ok().map(|p| {
1016                    Session::from_pickle(device_keys.clone(), p).map_err(|_| {
1017                        IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset)
1018                    })
1019                })
1020            })
1021            .collect::<Result<Vec<Session>>>()?;
1022
1023        if sessions.is_empty() {
1024            Ok(None)
1025        } else {
1026            Ok(Some(sessions))
1027        }
1028    }
1029
1030    async fn get_inbound_group_session(
1031        &self,
1032        room_id: &RoomId,
1033        session_id: &str,
1034    ) -> Result<Option<InboundGroupSession>> {
1035        let key =
1036            self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1037        if let Some(value) = self
1038            .inner
1039            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1040            .with_mode(TransactionMode::Readonly)
1041            .build()?
1042            .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1043            .get(&key)
1044            .await?
1045        {
1046            Ok(Some(self.deserialize_inbound_group_session(value)?))
1047        } else {
1048            Ok(None)
1049        }
1050    }
1051
1052    async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
1053        const INBOUND_GROUP_SESSIONS_BATCH_SIZE: usize = 1000;
1054
1055        let transaction = self
1056            .inner
1057            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1058            .with_mode(TransactionMode::Readonly)
1059            .build()?;
1060
1061        let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1062
1063        fetch_from_object_store_batched(
1064            object_store,
1065            |value| self.deserialize_inbound_group_session(value),
1066            INBOUND_GROUP_SESSIONS_BATCH_SIZE,
1067        )
1068        .await
1069    }
1070
1071    async fn get_inbound_group_sessions_by_room_id(
1072        &self,
1073        room_id: &RoomId,
1074    ) -> Result<Vec<InboundGroupSession>> {
1075        let range = self.serializer.encode_to_range(keys::INBOUND_GROUP_SESSIONS_V3, room_id);
1076        Ok(self
1077            .inner
1078            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1079            .with_mode(TransactionMode::Readonly)
1080            .build()?
1081            .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1082            .get_all()
1083            .with_query(&range)
1084            .await?
1085            .filter_map(Result::ok)
1086            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1087                Ok(session) => Some(session),
1088                Err(e) => {
1089                    warn!("Failed to deserialize inbound group session: {e}");
1090                    None
1091                }
1092            })
1093            .collect::<Vec<InboundGroupSession>>())
1094    }
1095
1096    async fn get_inbound_group_sessions_for_device_batch(
1097        &self,
1098        sender_key: Curve25519PublicKey,
1099        sender_data_type: SenderDataType,
1100        after_session_id: Option<String>,
1101        limit: usize,
1102    ) -> Result<Vec<InboundGroupSession>> {
1103        let sender_key =
1104            self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
1105
1106        // The empty string is before all keys in Indexed DB - first batch starts there.
1107        let after_session_id = after_session_id
1108            .map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s))
1109            .unwrap_or("".into());
1110
1111        let lower_bound: Array =
1112            [sender_key.clone(), (sender_data_type as u8).into(), after_session_id]
1113                .iter()
1114                .collect();
1115        let upper_bound: Array =
1116            [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
1117        let key = KeyRange::Bound(lower_bound, true, upper_bound, true);
1118
1119        let tx = self
1120            .inner
1121            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1122            .with_mode(TransactionMode::Readonly)
1123            .build()?;
1124
1125        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1126        let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1127        let serialized_sessions =
1128            idx.get_all().with_query::<Array, _>(key).with_limit(limit as u32).await?;
1129
1130        // Deserialize and decrypt after the transaction is complete.
1131        let result = serialized_sessions
1132            .filter_map(Result::ok)
1133            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1134                Ok(session) => Some(session),
1135                Err(e) => {
1136                    warn!("Failed to deserialize inbound group session: {e}");
1137                    None
1138                }
1139            })
1140            .collect::<Vec<InboundGroupSession>>();
1141
1142        Ok(result)
1143    }
1144
1145    async fn inbound_group_session_counts(
1146        &self,
1147        _backup_version: Option<&str>,
1148    ) -> Result<RoomKeyCounts> {
1149        let tx = self
1150            .inner
1151            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1152            .with_mode(TransactionMode::Readonly)
1153            .build()?;
1154        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1155        let all = store.count().await? as usize;
1156        let not_backed_up =
1157            store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count().await? as usize;
1158        tx.commit().await?;
1159        Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1160    }
1161
1162    async fn inbound_group_sessions_for_backup(
1163        &self,
1164        _backup_version: &str,
1165        limit: usize,
1166    ) -> Result<Vec<InboundGroupSession>> {
1167        let tx = self
1168            .inner
1169            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1170            .with_mode(TransactionMode::Readonly)
1171            .build()?;
1172
1173        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1174        let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1175
1176        // XXX ideally we would use `get_all_with_key_and_limit`, but that doesn't
1177        // appear to be   exposed (https://github.com/Alorel/rust-indexed-db/issues/31). Instead we replicate
1178        //   the behaviour with a cursor.
1179        let Some(mut cursor) = idx.open_cursor().await? else {
1180            return Ok(vec![]);
1181        };
1182
1183        let mut serialized_sessions = Vec::with_capacity(limit);
1184        for _ in 0..limit {
1185            let Some(value) = cursor.next_record().await? else {
1186                break;
1187            };
1188            serialized_sessions.push(value)
1189        }
1190
1191        tx.commit().await?;
1192
1193        // Deserialize and decrypt after the transaction is complete.
1194        let result = serialized_sessions
1195            .into_iter()
1196            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1197                Ok(session) => Some(session),
1198                Err(e) => {
1199                    warn!("Failed to deserialize inbound group session: {e}");
1200                    None
1201                }
1202            })
1203            .collect::<Vec<InboundGroupSession>>();
1204
1205        Ok(result)
1206    }
1207
1208    async fn mark_inbound_group_sessions_as_backed_up(
1209        &self,
1210        _backup_version: &str,
1211        room_and_session_ids: &[(&RoomId, &str)],
1212    ) -> Result<()> {
1213        let tx = self
1214            .inner
1215            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1216            .with_mode(TransactionMode::Readwrite)
1217            .build()?;
1218
1219        let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1220
1221        for (room_id, session_id) in room_and_session_ids {
1222            let key =
1223                self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1224            if let Some(idb_object_js) = object_store.get(&key).await? {
1225                let mut idb_object: InboundGroupSessionIndexedDbObject =
1226                    serde_wasm_bindgen::from_value(idb_object_js)?;
1227                idb_object.needs_backup = false;
1228                object_store
1229                    .put(&serde_wasm_bindgen::to_value(&idb_object)?)
1230                    .with_key(key)
1231                    .build()?;
1232            } else {
1233                warn!(?key, "Could not find inbound group session to mark it as backed up.");
1234            }
1235        }
1236
1237        Ok(tx.commit().await?)
1238    }
1239
1240    async fn reset_backup_state(&self) -> Result<()> {
1241        let tx = self
1242            .inner
1243            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1244            .with_mode(TransactionMode::Readwrite)
1245            .build()?;
1246
1247        if let Some(mut cursor) =
1248            tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor().await?
1249        {
1250            while let Some(value) = cursor.next_record().await? {
1251                let mut idb_object: InboundGroupSessionIndexedDbObject =
1252                    serde_wasm_bindgen::from_value(value)?;
1253                if !idb_object.needs_backup {
1254                    idb_object.needs_backup = true;
1255                    // We don't bother to update the encrypted `InboundGroupSession` object stored
1256                    // inside `idb_object.data`, since that would require decryption and encryption.
1257                    // Instead, it will be patched up by `deserialize_inbound_group_session`.
1258                    let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1259                    cursor.update(&idb_object).await?;
1260                }
1261            }
1262        }
1263
1264        Ok(tx.commit().await?)
1265    }
1266
1267    async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1268        let tx = self
1269            .inner
1270            .transaction(keys::TRACKED_USERS)
1271            .with_mode(TransactionMode::Readwrite)
1272            .build()?;
1273        let os = tx.object_store(keys::TRACKED_USERS)?;
1274
1275        for (user, dirty) in users {
1276            os.put(&JsValue::from(*dirty)).with_key(JsValue::from_str(user.as_str())).build()?;
1277        }
1278
1279        tx.commit().await?;
1280        Ok(())
1281    }
1282
1283    async fn get_device(
1284        &self,
1285        user_id: &UserId,
1286        device_id: &DeviceId,
1287    ) -> Result<Option<DeviceData>> {
1288        let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1289        self.inner
1290            .transaction(keys::DEVICES)
1291            .with_mode(TransactionMode::Readonly)
1292            .build()?
1293            .object_store(keys::DEVICES)?
1294            .get(&key)
1295            .await?
1296            .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1297            .transpose()
1298    }
1299
1300    async fn get_user_devices(
1301        &self,
1302        user_id: &UserId,
1303    ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1304        let range = self.serializer.encode_to_range(keys::DEVICES, user_id);
1305        Ok(self
1306            .inner
1307            .transaction(keys::DEVICES)
1308            .with_mode(TransactionMode::Readonly)
1309            .build()?
1310            .object_store(keys::DEVICES)?
1311            .get_all()
1312            .with_query(&range)
1313            .await?
1314            .filter_map(Result::ok)
1315            .filter_map(|d| {
1316                let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1317                Some((d.device_id().to_owned(), d))
1318            })
1319            .collect::<HashMap<_, _>>())
1320    }
1321
1322    async fn get_own_device(&self) -> Result<DeviceData> {
1323        let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1324        Ok(self.get_device(&account_info.user_id, &account_info.device_id).await?.unwrap())
1325    }
1326
1327    async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1328        self.inner
1329            .transaction(keys::IDENTITIES)
1330            .with_mode(TransactionMode::Readonly)
1331            .build()?
1332            .object_store(keys::IDENTITIES)?
1333            .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))
1334            .await?
1335            .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1336            .transpose()
1337    }
1338
1339    async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1340        Ok(self
1341            .inner
1342            .transaction(keys::OLM_HASHES)
1343            .with_mode(TransactionMode::Readonly)
1344            .build()?
1345            .object_store(keys::OLM_HASHES)?
1346            .get::<JsValue, _, _>(
1347                &self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
1348            )
1349            .await?
1350            .is_some())
1351    }
1352
1353    async fn get_secrets_from_inbox(
1354        &self,
1355        secret_name: &SecretName,
1356    ) -> Result<Vec<GossippedSecret>> {
1357        let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str());
1358
1359        self.inner
1360            .transaction(keys::SECRETS_INBOX)
1361            .with_mode(TransactionMode::Readonly)
1362            .build()?
1363            .object_store(keys::SECRETS_INBOX)?
1364            .get_all()
1365            .with_query(&range)
1366            .await?
1367            .map(|result| {
1368                let d = result?;
1369                let secret = self.serializer.deserialize_value(d)?;
1370                Ok(secret)
1371            })
1372            .collect()
1373    }
1374
1375    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1376    async fn delete_secrets_from_inbox(&self, secret_name: &SecretName) -> Result<()> {
1377        let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str());
1378
1379        let transaction = self
1380            .inner
1381            .transaction(keys::SECRETS_INBOX)
1382            .with_mode(TransactionMode::Readwrite)
1383            .build()?;
1384        transaction.object_store(keys::SECRETS_INBOX)?.delete(&range).build()?;
1385        transaction.commit().await?;
1386
1387        Ok(())
1388    }
1389
1390    async fn get_secret_request_by_info(
1391        &self,
1392        key_info: &SecretInfo,
1393    ) -> Result<Option<GossipRequest>> {
1394        let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1395
1396        let val = self
1397            .inner
1398            .transaction(keys::GOSSIP_REQUESTS)
1399            .with_mode(TransactionMode::Readonly)
1400            .build()?
1401            .object_store(keys::GOSSIP_REQUESTS)?
1402            .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1403            .get(key)
1404            .await?;
1405
1406        if let Some(val) = val {
1407            let deser = self.deserialize_gossip_request(val)?;
1408            Ok(Some(deser))
1409        } else {
1410            Ok(None)
1411        }
1412    }
1413
1414    async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1415        let results = self
1416            .inner
1417            .transaction(keys::GOSSIP_REQUESTS)
1418            .with_mode(TransactionMode::Readonly)
1419            .build()?
1420            .object_store(keys::GOSSIP_REQUESTS)?
1421            .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1422            .get_all()
1423            .await?
1424            .filter_map(Result::ok)
1425            .filter_map(|val| self.deserialize_gossip_request(val).ok())
1426            .collect();
1427
1428        Ok(results)
1429    }
1430
1431    async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1432        let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1433        let tx = self
1434            .inner
1435            .transaction(keys::GOSSIP_REQUESTS)
1436            .with_mode(TransactionMode::Readwrite)
1437            .build()?;
1438        tx.object_store(keys::GOSSIP_REQUESTS)?.delete(jskey).build()?;
1439        tx.commit().await.map_err(|e| e.into())
1440    }
1441
1442    async fn load_backup_keys(&self) -> Result<BackupKeys> {
1443        let key = {
1444            let tx = self
1445                .inner
1446                .transaction(keys::BACKUP_KEYS)
1447                .with_mode(TransactionMode::Readonly)
1448                .build()?;
1449            let store = tx.object_store(keys::BACKUP_KEYS)?;
1450
1451            let backup_version = store
1452                .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))
1453                .await?
1454                .map(|i| self.serializer.deserialize_value(i))
1455                .transpose()?;
1456
1457            let decryption_key = store
1458                .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))
1459                .await?
1460                .map(|i| self.serializer.deserialize_value(i))
1461                .transpose()?;
1462
1463            BackupKeys { backup_version, decryption_key }
1464        };
1465
1466        Ok(key)
1467    }
1468
1469    async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1470        if let Some(pickle) = self
1471            .inner
1472            .transaction(keys::CORE)
1473            .with_mode(TransactionMode::Readonly)
1474            .build()?
1475            .object_store(keys::CORE)?
1476            .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))
1477            .await?
1478        {
1479            let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1480
1481            Ok(Some(pickle))
1482        } else {
1483            Ok(None)
1484        }
1485    }
1486
1487    async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1488        self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1489        Ok(())
1490    }
1491
1492    async fn get_withheld_info(
1493        &self,
1494        room_id: &RoomId,
1495        session_id: &str,
1496    ) -> Result<Option<RoomKeyWithheldEntry>> {
1497        let key = self.serializer.encode_key(keys::WITHHELD_SESSIONS, (room_id, session_id));
1498        if let Some(pickle) = self
1499            .inner
1500            .transaction(keys::WITHHELD_SESSIONS)
1501            .with_mode(TransactionMode::Readonly)
1502            .build()?
1503            .object_store(keys::WITHHELD_SESSIONS)?
1504            .get(&key)
1505            .await?
1506        {
1507            let info = self.serializer.deserialize_value(pickle)?;
1508            Ok(Some(info))
1509        } else {
1510            Ok(None)
1511        }
1512    }
1513
1514    async fn get_withheld_sessions_by_room_id(
1515        &self,
1516        room_id: &RoomId,
1517    ) -> Result<Vec<RoomKeyWithheldEntry>> {
1518        let range = self.serializer.encode_to_range(keys::WITHHELD_SESSIONS, room_id);
1519
1520        self
1521            .inner
1522            .transaction(keys::WITHHELD_SESSIONS)
1523            .with_mode(TransactionMode::Readonly)
1524            .build()?
1525            .object_store(keys::WITHHELD_SESSIONS)?
1526            .get_all()
1527            .with_query(&range)
1528            .await?
1529            .map(|val| self.serializer.deserialize_value(val?).map_err(Into::into))
1530            .collect()
1531    }
1532
1533    async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1534        let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1535        self.inner
1536            .transaction(keys::ROOM_SETTINGS)
1537            .with_mode(TransactionMode::Readonly)
1538            .build()?
1539            .object_store(keys::ROOM_SETTINGS)?
1540            .get(&key)
1541            .await?
1542            .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1543            .transpose()
1544    }
1545
1546    async fn get_received_room_key_bundle_data(
1547        &self,
1548        room_id: &RoomId,
1549        user_id: &UserId,
1550    ) -> Result<Option<StoredRoomKeyBundleData>> {
1551        let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1552        let result = self
1553            .inner
1554            .transaction(keys::RECEIVED_ROOM_KEY_BUNDLES)
1555            .with_mode(TransactionMode::Readonly)
1556            .build()?
1557            .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1558            .get(&key)
1559            .await?
1560            .map(|v| self.serializer.deserialize_value(v))
1561            .transpose()?;
1562
1563        Ok(result)
1564    }
1565
1566    async fn has_downloaded_all_room_keys(&self, room_id: &RoomId) -> Result<bool> {
1567        let key = self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id);
1568        let result = self
1569            .inner
1570            .transaction(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)
1571            .with_mode(TransactionMode::Readonly)
1572            .build()?
1573            .object_store(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)?
1574            .get::<JsValue, _, _>(&key)
1575            .await?
1576            .is_some();
1577
1578        Ok(result)
1579    }
1580
1581    async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1582        self.inner
1583            .transaction(keys::CORE)
1584            .with_mode(TransactionMode::Readonly)
1585            .build()?
1586            .object_store(keys::CORE)?
1587            .get(&JsValue::from_str(key))
1588            .await?
1589            .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1590            .transpose()
1591    }
1592
1593    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1594    async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1595        let transaction =
1596            self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1597        transaction
1598            .object_store(keys::CORE)?
1599            .put(&self.serializer.serialize_value(&value)?)
1600            .with_key(JsValue::from_str(key))
1601            .build()?;
1602        transaction.commit().await?;
1603        Ok(())
1604    }
1605
1606    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1607    async fn remove_custom_value(&self, key: &str) -> Result<()> {
1608        let transaction =
1609            self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1610        transaction.object_store(keys::CORE)?.delete(&JsValue::from_str(key)).build()?;
1611        transaction.commit().await?;
1612        Ok(())
1613    }
1614
1615    async fn try_take_leased_lock(
1616        &self,
1617        lease_duration_ms: u32,
1618        key: &str,
1619        holder: &str,
1620    ) -> Result<Option<CrossProcessLockGeneration>> {
1621        // As of 2023-06-23, the code below hasn't been tested yet.
1622        let key = JsValue::from_str(key);
1623        let txn = self
1624            .inner
1625            .transaction(keys::LEASE_LOCKS)
1626            .with_mode(TransactionMode::Readwrite)
1627            .build()?;
1628        let object_store = txn.object_store(keys::LEASE_LOCKS)?;
1629
1630        #[derive(Deserialize, Serialize)]
1631        struct Lease {
1632            holder: String,
1633            expiration: u64,
1634            generation: CrossProcessLockGeneration,
1635        }
1636
1637        let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1638        let expiration = now + lease_duration_ms as u64;
1639
1640        let lease = match object_store.get(&key).await? {
1641            Some(entry) => {
1642                let mut lease: Lease = self.serializer.deserialize_value(entry)?;
1643
1644                if lease.holder == holder {
1645                    // We had the lease before, extend it.
1646                    lease.expiration = expiration;
1647
1648                    Some(lease)
1649                } else {
1650                    // We didn't have it.
1651                    if lease.expiration < now {
1652                        // Steal it!
1653                        lease.holder = holder.to_owned();
1654                        lease.expiration = expiration;
1655                        lease.generation += 1;
1656
1657                        Some(lease)
1658                    } else {
1659                        // We tried our best.
1660                        None
1661                    }
1662                }
1663            }
1664            None => {
1665                let lease = Lease {
1666                    holder: holder.to_owned(),
1667                    expiration,
1668                    generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1669                };
1670
1671                Some(lease)
1672            }
1673        };
1674
1675        Ok(if let Some(lease) = lease {
1676            object_store.put(&self.serializer.serialize_value(&lease)?).with_key(key).build()?;
1677
1678            Some(lease.generation)
1679        } else {
1680            None
1681        })
1682    }
1683
1684    #[allow(clippy::unused_async)]
1685    async fn get_size(&self) -> Result<Option<usize>> {
1686        Ok(None)
1687    }
1688}
1689
1690impl Drop for IndexeddbCryptoStore {
1691    fn drop(&mut self) {
1692        // Must release the database access manually as it's not done when
1693        // dropping it.
1694        self.inner.as_sys().close();
1695    }
1696}
1697
1698/// Open the meta store.
1699///
1700/// The meta store contains details about the encryption of the main store.
1701async fn open_meta_db(prefix: &str) -> Result<Database, IndexeddbCryptoStoreError> {
1702    let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1703
1704    debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1705    Database::open(&name)
1706        .with_version(1u32)
1707        .with_on_upgrade_needed(|evt, tx| {
1708            let old_version = evt.old_version() as u32;
1709            if old_version < 1 {
1710                // migrating to version 1
1711                tx.db().create_object_store("matrix-sdk-crypto").build()?;
1712            }
1713            Ok(())
1714        })
1715        .await
1716        .map_err(Into::into)
1717}
1718
1719/// Load the serialised store cipher from the meta store.
1720///
1721/// # Arguments:
1722///
1723/// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1724///
1725/// # Returns:
1726///
1727/// The serialised `StoreCipher` object.
1728async fn load_store_cipher(
1729    meta_db: &Database,
1730) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1731    let tx: Transaction<'_> =
1732        meta_db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readonly).build()?;
1733    let ob = tx.object_store("matrix-sdk-crypto")?;
1734
1735    let store_cipher: Option<Vec<u8>> = ob
1736        .get(&JsValue::from_str(keys::STORE_CIPHER))
1737        .await?
1738        .map(|k: JsValue| k.into_serde())
1739        .transpose()?;
1740    Ok(store_cipher)
1741}
1742
1743/// Save the serialised store cipher to the meta store.
1744///
1745/// # Arguments:
1746///
1747/// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1748/// * `store_cipher`: The serialised `StoreCipher` object.
1749async fn save_store_cipher(
1750    db: &Database,
1751    export: &Vec<u8>,
1752) -> Result<(), IndexeddbCryptoStoreError> {
1753    let tx: Transaction<'_> =
1754        db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readwrite).build()?;
1755    let ob = tx.object_store("matrix-sdk-crypto")?;
1756
1757    ob.put(&JsValue::from_serde(&export)?)
1758        .with_key(JsValue::from_str(keys::STORE_CIPHER))
1759        .build()?;
1760    tx.commit().await?;
1761    Ok(())
1762}
1763
1764/// Given a serialised store cipher, try importing with the given key.
1765///
1766/// This is a helper for [`IndexeddbCryptoStore::open_with_key`].
1767///
1768/// # Arguments
1769///
1770/// * `chacha_key`: The key to use with [`StoreCipher::import_with_key`].
1771///   Derived from `original_key` via an HKDF.
1772/// * `original_key`: The key provided by the application. Used to provide a
1773///   migration path from an older key derivation system.
1774/// * `serialised_cipher`: The serialized `EncryptedStoreCipher`, retrieved from
1775///   the database.
1776/// * `db`: Connection to the database.
1777async fn import_store_cipher_with_key(
1778    chacha_key: &[u8; 32],
1779    original_key: &[u8],
1780    serialised_cipher: &[u8],
1781    db: &Database,
1782) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1783    let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1784        Ok(cipher) => cipher,
1785        Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1786            // Old versions of the matrix-js-sdk used to base64-encode their encryption
1787            // key, and pass it into [`IndexeddbCryptoStore::open_with_passphrase`]. For
1788            // backwards compatibility, we fall back to that if we discover we have a cipher
1789            // encrypted with a KDF when we expected it to be encrypted directly with a key.
1790            let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1791                .map_err(|_| CryptoStoreError::UnpicklingError)?;
1792
1793            // Loading the cipher with the passphrase was successful. Let's update the
1794            // stored version of the cipher so that it is encrypted with a key,
1795            // to save doing this again.
1796            debug!(
1797                "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1798            );
1799
1800            let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1801            save_store_cipher(db, &export).await?;
1802            cipher
1803        }
1804        Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1805    };
1806    Ok(cipher)
1807}
1808
1809/// Fetch items from an object store in batches, transform each item using
1810/// the supplied function, and stuff the transformed items into a single
1811/// vector to return.
1812async fn fetch_from_object_store_batched<R, F>(
1813    object_store: ObjectStore<'_>,
1814    f: F,
1815    batch_size: usize,
1816) -> Result<Vec<R>>
1817where
1818    F: Fn(JsValue) -> Result<R>,
1819{
1820    let mut result = Vec::new();
1821    let mut batch_n = 0;
1822
1823    // The empty string is before all keys in Indexed DB - first batch starts there.
1824    let mut latest_key: JsValue = "".into();
1825
1826    loop {
1827        debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1828
1829        // See https://github.com/Alorel/rust-indexed-db/issues/31 - we
1830        // would like to use `get_all_with_key_and_limit` if it ever exists
1831        // but for now we use a cursor and manually limit batch size.
1832
1833        // Get hold of a cursor for this batch. (This should not panic in expect()
1834        // because we always use "", or the result of cursor.key(), both of
1835        // which are valid keys.)
1836        let after_latest_key = KeyRange::LowerBound(&latest_key, true);
1837        let cursor = object_store.open_cursor().with_query(&after_latest_key).await?;
1838
1839        // Fetch batch_size records into result
1840        let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1841        if let Some(next_key) = next_key {
1842            latest_key = next_key;
1843        } else {
1844            break;
1845        }
1846
1847        batch_n += 1;
1848    }
1849
1850    Ok(result)
1851}
1852
1853/// Fetch batch_size records from the supplied cursor,
1854/// and return the last key we processed, or None if
1855/// we reached the end of the cursor.
1856async fn fetch_batch<R, F, Q>(
1857    cursor: Option<Cursor<'_, Q>>,
1858    batch_size: usize,
1859    f: &F,
1860    result: &mut Vec<R>,
1861) -> Result<Option<JsValue>>
1862where
1863    F: Fn(JsValue) -> Result<R>,
1864    Q: QuerySource,
1865{
1866    let Some(mut cursor) = cursor else {
1867        // Cursor was None - there are no more records
1868        return Ok(None);
1869    };
1870
1871    let mut latest_key = None;
1872
1873    for _ in 0..batch_size {
1874        let Some(value) = cursor.next_record().await? else {
1875            return Ok(None);
1876        };
1877
1878        // Process the record
1879        let processed = f(value);
1880        if let Ok(processed) = processed {
1881            result.push(processed);
1882        }
1883        // else processing failed: don't return this record at all
1884
1885        // Remember that we have processed this record, so if we hit
1886        // the end of the batch, the next batch can start after this one
1887        if let Some(key) = cursor.key()? {
1888            latest_key = Some(key);
1889        }
1890    }
1891
1892    // We finished the batch but there are more records -
1893    // return the key of the last one we processed
1894    Ok(latest_key)
1895}
1896
1897/// The objects we store in the gossip_requests indexeddb object store
1898#[derive(Debug, Serialize, Deserialize)]
1899struct GossipRequestIndexedDbObject {
1900    /// Encrypted hash of the [`SecretInfo`] structure.
1901    info: String,
1902
1903    /// Encrypted serialised representation of the [`GossipRequest`] as a whole.
1904    request: Vec<u8>,
1905
1906    /// Whether the request has yet to be sent out.
1907    ///
1908    /// Since we only need to be able to find requests where this is `true`, we
1909    /// skip serialization in cases where it is `false`. That has the effect
1910    /// of omitting it from the indexeddb index.
1911    ///
1912    /// We also use a custom serializer because bools can't be used as keys in
1913    /// indexeddb.
1914    #[serde(
1915        default,
1916        skip_serializing_if = "std::ops::Not::not",
1917        with = "crate::serializer::foreign::bool"
1918    )]
1919    unsent: bool,
1920}
1921
1922/// The objects we store in the inbound_group_sessions3 indexeddb object store
1923#[derive(Serialize, Deserialize)]
1924struct InboundGroupSessionIndexedDbObject {
1925    /// Possibly encrypted
1926    /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
1927    pickled_session: MaybeEncrypted,
1928
1929    /// The (hashed) session ID of this session. This is somewhat redundant, but
1930    /// we have to pull it out to its own object so that we can do batched
1931    /// queries such as
1932    /// [`IndexeddbStore::get_inbound_group_sessions_for_device_batch`].
1933    ///
1934    /// Added in database schema v12, and lazily populated, so it is only
1935    /// present for sessions received or modified since DB schema v12.
1936    #[serde(default, skip_serializing_if = "Option::is_none")]
1937    session_id: Option<String>,
1938
1939    /// Whether the session data has yet to be backed up.
1940    ///
1941    /// Since we only need to be able to find entries where this is `true`, we
1942    /// skip serialization in cases where it is `false`. That has the effect
1943    /// of omitting it from the indexeddb index.
1944    ///
1945    /// We also use a custom serializer because bools can't be used as keys in
1946    /// indexeddb.
1947    #[serde(
1948        default,
1949        skip_serializing_if = "std::ops::Not::not",
1950        with = "crate::serializer::foreign::bool"
1951    )]
1952    needs_backup: bool,
1953
1954    /// Unused: for future compatibility. In future, will contain the order
1955    /// number (not the ID!) of the backup for which this key has been
1956    /// backed up. This will replace `needs_backup`, fixing the performance
1957    /// problem identified in
1958    /// https://github.com/element-hq/element-web/issues/26892
1959    /// because we won't need to update all records when we spot a new backup
1960    /// version.
1961    /// In this version of the code, this is always set to -1, meaning:
1962    /// "refer to the `needs_backup` property". See:
1963    /// https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
1964    backed_up_to: i32,
1965
1966    /// The (hashed) curve25519 key of the device that sent us this room key,
1967    /// base64-encoded.
1968    ///
1969    /// Added in database schema v12, and lazily populated, so it is only
1970    /// present for sessions received or modified since DB schema v12.
1971    #[serde(default, skip_serializing_if = "Option::is_none")]
1972    sender_key: Option<String>,
1973
1974    /// The type of the [`SenderData`] within this session, converted to a u8
1975    /// from [`SenderDataType`].
1976    ///
1977    /// Added in database schema v12, and lazily populated, so it is only
1978    /// present for sessions received or modified since DB schema v12.
1979    #[serde(default, skip_serializing_if = "Option::is_none")]
1980    sender_data_type: Option<u8>,
1981}
1982
1983impl InboundGroupSessionIndexedDbObject {
1984    /// Build an [`InboundGroupSessionIndexedDbObject`] wrapping the given
1985    /// session.
1986    pub async fn from_session(
1987        session: &InboundGroupSession,
1988        serializer: &SafeEncodeSerializer,
1989    ) -> Result<Self, CryptoStoreError> {
1990        let session_id =
1991            serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
1992
1993        let sender_key = serializer.encode_key_as_string(
1994            keys::INBOUND_GROUP_SESSIONS_V3,
1995            session.sender_key().to_base64(),
1996        );
1997
1998        Ok(InboundGroupSessionIndexedDbObject {
1999            pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
2000            session_id: Some(session_id),
2001            needs_backup: !session.backed_up(),
2002            backed_up_to: -1,
2003            sender_key: Some(sender_key),
2004            sender_data_type: Some(session.sender_data_type() as u8),
2005        })
2006    }
2007}
2008
2009#[cfg(test)]
2010mod unit_tests {
2011    use matrix_sdk_crypto::{
2012        olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
2013        types::EventEncryptionAlgorithm,
2014        vodozemac::Ed25519Keypair,
2015    };
2016    use matrix_sdk_store_encryption::EncryptedValueBase64;
2017    use matrix_sdk_test::async_test;
2018    use ruma::{device_id, room_id, user_id};
2019
2020    use super::InboundGroupSessionIndexedDbObject;
2021    use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
2022
2023    #[test]
2024    fn needs_backup_is_serialized_as_a_u8_in_json() {
2025        let session_needs_backup = backup_test_session(true);
2026
2027        // Testing the exact JSON here is theoretically flaky in the face of
2028        // serialization changes in serde_json but it seems unlikely, and it's
2029        // simple enough to fix if we need to.
2030        assert!(
2031            serde_json::to_string(&session_needs_backup).unwrap().contains(r#""needs_backup":1"#),
2032        );
2033    }
2034
2035    #[test]
2036    fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
2037        let session_backed_up = backup_test_session(false);
2038
2039        assert!(
2040            !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
2041            "The needs_backup field should be missing!"
2042        );
2043    }
2044
2045    pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
2046        InboundGroupSessionIndexedDbObject {
2047            pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
2048            session_id: None,
2049            needs_backup,
2050            backed_up_to: -1,
2051            sender_key: None,
2052            sender_data_type: None,
2053        }
2054    }
2055
2056    #[async_test]
2057    async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
2058        let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2059
2060        let sender_data = SenderData::sender_verified(
2061            user_id!("@test:user"),
2062            device_id!("ABC"),
2063            Ed25519Keypair::new().public_key(),
2064        );
2065
2066        let db_object = sender_data_test_session(sender_key, sender_data).await;
2067        let serialized = serde_json::to_string(&db_object).unwrap();
2068
2069        assert!(
2070            serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
2071        );
2072        assert!(serialized.contains(r#""sender_data_type":5"#));
2073    }
2074
2075    pub async fn sender_data_test_session(
2076        sender_key: Curve25519PublicKey,
2077        sender_data: SenderData,
2078    ) -> InboundGroupSessionIndexedDbObject {
2079        let session = InboundGroupSession::new(
2080            sender_key,
2081            Ed25519Keypair::new().public_key(),
2082            room_id!("!test:localhost"),
2083            // Arbitrary session data
2084            &SessionKey::from_base64(
2085                "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
2086                 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
2087                 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
2088                 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
2089                 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
2090            )
2091            .unwrap(),
2092            sender_data,
2093            None,
2094            EventEncryptionAlgorithm::MegolmV1AesSha2,
2095            None,
2096            false,
2097        )
2098        .unwrap();
2099
2100        InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
2101            .await
2102            .unwrap()
2103    }
2104}
2105
2106#[cfg(all(test, target_family = "wasm"))]
2107mod wasm_unit_tests {
2108    use std::collections::BTreeMap;
2109
2110    use matrix_sdk_crypto::{
2111        olm::{Curve25519PublicKey, SenderData},
2112        types::{DeviceKeys, Signatures},
2113    };
2114    use matrix_sdk_test::async_test;
2115    use ruma::{device_id, user_id};
2116    use wasm_bindgen::JsValue;
2117
2118    use crate::crypto_store::unit_tests::sender_data_test_session;
2119
2120    fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
2121        assert_eq!(
2122            js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
2123            JsValue::from_f64(expected.into())
2124        );
2125    }
2126
2127    #[async_test]
2128    fn test_needs_backup_is_serialized_as_a_u8_in_js() {
2129        let session_needs_backup = super::unit_tests::backup_test_session(true);
2130
2131        let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
2132
2133        assert!(js_value.is_object());
2134        assert_field_equals(&js_value, "needs_backup", 1);
2135    }
2136
2137    #[async_test]
2138    fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
2139        let session_backed_up = super::unit_tests::backup_test_session(false);
2140
2141        let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
2142
2143        assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
2144    }
2145
2146    #[async_test]
2147    async fn test_sender_key_and_device_type_are_serialized_in_js() {
2148        let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2149
2150        let sender_data = SenderData::device_info(DeviceKeys::new(
2151            user_id!("@test:user").to_owned(),
2152            device_id!("ABC").to_owned(),
2153            vec![],
2154            BTreeMap::new(),
2155            Signatures::new(),
2156        ));
2157        let db_object = sender_data_test_session(sender_key, sender_data).await;
2158
2159        let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
2160
2161        assert!(js_value.is_object());
2162        assert_field_equals(&js_value, "sender_data_type", 2);
2163        assert_eq!(
2164            js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
2165            JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
2166        );
2167    }
2168}
2169
2170#[cfg(all(test, target_family = "wasm"))]
2171mod tests {
2172    use matrix_sdk_crypto::cryptostore_integration_tests;
2173
2174    use super::IndexeddbCryptoStore;
2175
2176    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2177
2178    async fn get_store(
2179        name: &str,
2180        passphrase: Option<&str>,
2181        clear_data: bool,
2182    ) -> IndexeddbCryptoStore {
2183        if clear_data {
2184            IndexeddbCryptoStore::delete_stores(name).unwrap();
2185        }
2186        match passphrase {
2187            Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2188                .await
2189                .expect("Can't create a passphrase protected store"),
2190            None => IndexeddbCryptoStore::open_with_name(name)
2191                .await
2192                .expect("Can't create store without passphrase"),
2193        }
2194    }
2195
2196    cryptostore_integration_tests!();
2197}
2198
2199#[cfg(all(test, target_family = "wasm"))]
2200mod encrypted_tests {
2201    use matrix_sdk_crypto::{
2202        cryptostore_integration_tests,
2203        olm::Account,
2204        store::{CryptoStore, types::PendingChanges},
2205        vodozemac::base64_encode,
2206    };
2207    use matrix_sdk_test::async_test;
2208    use ruma::{device_id, user_id};
2209
2210    use super::IndexeddbCryptoStore;
2211
2212    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2213
2214    async fn get_store(
2215        name: &str,
2216        passphrase: Option<&str>,
2217        clear_data: bool,
2218    ) -> IndexeddbCryptoStore {
2219        if clear_data {
2220            IndexeddbCryptoStore::delete_stores(name).unwrap();
2221        }
2222
2223        let pass = passphrase.unwrap_or(name);
2224        IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2225            .await
2226            .expect("Can't create a passphrase protected store")
2227    }
2228    cryptostore_integration_tests!();
2229
2230    /// Test that we can migrate a store created with a passphrase, to being
2231    /// encrypted with a key instead.
2232    #[async_test]
2233    async fn test_migrate_passphrase_to_key() {
2234        let store_name = "test_migrate_passphrase_to_key";
2235        let passdata: [u8; 32] = rand::random();
2236        let b64_passdata = base64_encode(passdata);
2237
2238        // Initialise the store with some account data
2239        IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2240        let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2241            .await
2242            .expect("Can't create a passphrase-protected store");
2243
2244        store
2245            .save_pending_changes(PendingChanges {
2246                account: Some(Account::with_device_id(
2247                    user_id!("@alice:example.org"),
2248                    device_id!("ALICEDEVICE"),
2249                )),
2250            })
2251            .await
2252            .expect("Can't save account");
2253
2254        // Now reopen the store, passing the key directly rather than as a b64 string.
2255        let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2256            .await
2257            .expect("Can't create a key-protected store");
2258        let loaded_account =
2259            store.load_account().await.expect("Can't load account").expect("Account was not saved");
2260        assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2261    }
2262}