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