Skip to main content

matrix_sdk_indexeddb/crypto_store/
mod.rs

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