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