Skip to main content

matrix_sdk_indexeddb/crypto_store/
mod.rs

1// Copyright 2020 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use std::{
16    collections::{BTreeMap, HashMap},
17    sync::{Arc, RwLock},
18};
19
20use async_trait::async_trait;
21use gloo_utils::format::JsValueSerdeExt;
22use hkdf::Hkdf;
23use indexed_db_futures::{
24    KeyRange,
25    cursor::Cursor,
26    database::Database,
27    internals::SystemRepr,
28    object_store::ObjectStore,
29    prelude::*,
30    transaction::{Transaction, TransactionMode},
31};
32use js_sys::Array;
33use matrix_sdk_base::cross_process_lock::{
34    CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION,
35};
36use matrix_sdk_crypto::{
37    Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData,
38    olm::{
39        Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
40        PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session,
41        StaticAccountData,
42    },
43    store::{
44        CryptoStore, CryptoStoreError,
45        types::{
46            BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts,
47            RoomKeyWithheldEntry, 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: &str = "secrets_inbox";
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);
721
722            for secret in &changes.secrets {
723                let key = self.serializer.encode_key(
724                    keys::SECRETS_INBOX,
725                    (secret.secret_name.as_str(), secret.event.content.request_id.as_str()),
726                );
727                let value = self.serializer.serialize_value(&secret)?;
728
729                secret_store.put(key, value);
730            }
731        }
732
733        if !changes.received_room_key_bundles.is_empty() {
734            let mut bundle_store = indexeddb_changes.get(keys::RECEIVED_ROOM_KEY_BUNDLES);
735            for bundle in &changes.received_room_key_bundles {
736                let key = self.serializer.encode_key(
737                    keys::RECEIVED_ROOM_KEY_BUNDLES,
738                    (&bundle.bundle_data.room_id, &bundle.sender_user),
739                );
740                let value = self.serializer.serialize_value(&bundle)?;
741                bundle_store.put(key, value);
742            }
743        }
744
745        if !changes.room_key_backups_fully_downloaded.is_empty() {
746            let mut room_store = indexeddb_changes.get(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED);
747            for room_id in &changes.room_key_backups_fully_downloaded {
748                room_store.put(
749                    self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id),
750                    JsValue::TRUE,
751                );
752            }
753        }
754
755        if !changes.rooms_pending_key_bundle.is_empty() {
756            let mut room_store = indexeddb_changes.get(keys::ROOMS_PENDING_KEY_BUNDLE);
757            for (room_id, details) in &changes.rooms_pending_key_bundle {
758                let key = self.serializer.encode_key(keys::ROOMS_PENDING_KEY_BUNDLE, room_id);
759                if let Some(details) = details {
760                    let value = self.serializer.serialize_value(details)?;
761                    room_store.put(key, value);
762                } else {
763                    room_store.delete(key);
764                }
765            }
766        }
767
768        Ok(indexeddb_changes)
769    }
770}
771
772// Small hack to have the following macro invocation act as the appropriate
773// trait impl block on wasm, but still be compiled on non-wasm as a regular
774// impl block otherwise.
775//
776// The trait impl doesn't compile on non-wasm due to unfulfilled trait bounds,
777// this hack allows us to still have most of rust-analyzer's IDE functionality
778// within the impl block without having to set it up to check things against
779// the wasm target (which would disable many other parts of the codebase).
780#[cfg(target_family = "wasm")]
781macro_rules! impl_crypto_store {
782    ( $($body:tt)* ) => {
783        #[async_trait(?Send)]
784        impl CryptoStore for IndexeddbCryptoStore {
785            type Error = IndexeddbCryptoStoreError;
786
787            $($body)*
788        }
789    };
790}
791
792#[cfg(not(target_family = "wasm"))]
793macro_rules! impl_crypto_store {
794    ( $($body:tt)* ) => {
795        impl IndexeddbCryptoStore {
796            $($body)*
797        }
798    };
799}
800
801impl_crypto_store! {
802    async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> {
803        // Serialize calls to `save_pending_changes`; there are multiple await points
804        // below, and we're pickling data as we go, so we don't want to
805        // invalidate data we've previously read and overwrite it in the store.
806        // TODO: #2000 should make this lock go away, or change its shape.
807        let _guard = self.save_changes_lock.lock().await;
808
809        let stores: Vec<&str> = [(changes.account.is_some(), keys::CORE)]
810            .iter()
811            .filter_map(|(id, key)| if *id { Some(*key) } else { None })
812            .collect();
813
814        if stores.is_empty() {
815            // nothing to do, quit early
816            return Ok(());
817        }
818
819        let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
820
821        let account_pickle = if let Some(account) = changes.account {
822            *self.static_account.write().unwrap() = Some(account.static_data().clone());
823            Some(account.pickle())
824        } else {
825            None
826        };
827
828        if let Some(a) = &account_pickle {
829            tx.object_store(keys::CORE)?
830                .put(&self.serializer.serialize_value(&a)?)
831                .with_key(JsValue::from_str(keys::ACCOUNT))
832                .build()?;
833        }
834
835        tx.commit().await?;
836
837        Ok(())
838    }
839
840    async fn save_changes(&self, changes: Changes) -> Result<()> {
841        // Serialize calls to `save_changes`; there are multiple await points below, and
842        // we're pickling data as we go, so we don't want to invalidate data
843        // we've previously read and overwrite it in the store.
844        // TODO: #2000 should make this lock go away, or change its shape.
845        let _guard = self.save_changes_lock.lock().await;
846
847        let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
848
849        let stores = indexeddb_changes.touched_stores();
850
851        if stores.is_empty() {
852            // nothing to do, quit early
853            return Ok(());
854        }
855
856        let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
857
858        indexeddb_changes.apply(&tx)?;
859
860        tx.commit().await?;
861
862        Ok(())
863    }
864
865    async fn save_inbound_group_sessions(
866        &self,
867        sessions: Vec<InboundGroupSession>,
868        backed_up_to_version: Option<&str>,
869    ) -> Result<()> {
870        // Sanity-check that the data in the sessions corresponds to backed_up_version
871        sessions.iter().for_each(|s| {
872            let backed_up = s.backed_up();
873            if backed_up != backed_up_to_version.is_some() {
874                warn!(
875                    backed_up,
876                    backed_up_to_version,
877                    "Session backed-up flag does not correspond to backup version setting",
878                );
879            }
880        });
881
882        // Currently, this store doesn't save the backup version separately, so this
883        // just delegates to save_changes.
884        self.save_changes(Changes { inbound_group_sessions: sessions, ..Changes::default() }).await
885    }
886
887    async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
888        let tx = self
889            .inner
890            .transaction(keys::TRACKED_USERS)
891            .with_mode(TransactionMode::Readonly)
892            .build()?;
893        let os = tx.object_store(keys::TRACKED_USERS)?;
894        let user_ids = os.get_all_keys::<JsValue>().await?;
895
896        let mut users = Vec::new();
897
898        for result in user_ids {
899            let user_id = result?;
900            let dirty: bool = !matches!(
901                os.get(&user_id).await?.map(|v: JsValue| v.into_serde()),
902                Some(Ok(false))
903            );
904            let Some(Ok(user_id)) = user_id.as_string().map(UserId::parse) else { continue };
905
906            users.push(TrackedUser { user_id, dirty });
907        }
908
909        Ok(users)
910    }
911
912    async fn get_outbound_group_session(
913        &self,
914        room_id: &RoomId,
915    ) -> Result<Option<OutboundGroupSession>> {
916        let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
917        if let Some(value) = self
918            .inner
919            .transaction(keys::OUTBOUND_GROUP_SESSIONS)
920            .with_mode(TransactionMode::Readonly)
921            .build()?
922            .object_store(keys::OUTBOUND_GROUP_SESSIONS)?
923            .get(&self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))
924            .await?
925        {
926            Ok(Some(
927                OutboundGroupSession::from_pickle(
928                    account_info.device_id,
929                    account_info.identity_keys,
930                    self.serializer.deserialize_value(value)?,
931                )
932                .map_err(CryptoStoreError::from)?,
933            ))
934        } else {
935            Ok(None)
936        }
937    }
938
939    async fn get_outgoing_secret_requests(
940        &self,
941        request_id: &TransactionId,
942    ) -> Result<Option<GossipRequest>> {
943        let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id.as_str());
944        self.inner
945            .transaction(keys::GOSSIP_REQUESTS)
946            .with_mode(TransactionMode::Readonly)
947            .build()?
948            .object_store(keys::GOSSIP_REQUESTS)?
949            .get(jskey)
950            .await?
951            .map(|val| self.deserialize_gossip_request(val))
952            .transpose()
953    }
954
955    async fn load_account(&self) -> Result<Option<Account>> {
956        if let Some(pickle) = self
957            .inner
958            .transaction(keys::CORE)
959            .with_mode(TransactionMode::Readonly)
960            .build()?
961            .object_store(keys::CORE)?
962            .get(&JsValue::from_str(keys::ACCOUNT))
963            .await?
964        {
965            let pickle = self.serializer.deserialize_value(pickle)?;
966
967            let account = Account::from_pickle(pickle).map_err(CryptoStoreError::from)?;
968
969            *self.static_account.write().unwrap() = Some(account.static_data().clone());
970
971            Ok(Some(account))
972        } else {
973            Ok(None)
974        }
975    }
976
977    async fn next_batch_token(&self) -> Result<Option<String>> {
978        if let Some(serialized) = self
979            .inner
980            .transaction(keys::CORE)
981            .with_mode(TransactionMode::Readonly)
982            .build()?
983            .object_store(keys::CORE)?
984            .get(&JsValue::from_str(keys::NEXT_BATCH_TOKEN))
985            .await?
986        {
987            let token = self.serializer.deserialize_value(serialized)?;
988            Ok(Some(token))
989        } else {
990            Ok(None)
991        }
992    }
993
994    async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
995        if let Some(pickle) = self
996            .inner
997            .transaction(keys::CORE)
998            .with_mode(TransactionMode::Readonly)
999            .build()?
1000            .object_store(keys::CORE)?
1001            .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))
1002            .await?
1003        {
1004            let pickle = self.serializer.deserialize_value(pickle)?;
1005
1006            Ok(Some(
1007                PrivateCrossSigningIdentity::from_pickle(pickle)
1008                    .map_err(|_| CryptoStoreError::UnpicklingError)?,
1009            ))
1010        } else {
1011            Ok(None)
1012        }
1013    }
1014
1015    async fn get_sessions(&self, sender_key: &str) -> Result<Option<Vec<Session>>> {
1016        let device_keys = self.get_own_device().await?.as_device_keys().clone();
1017
1018        let range = self.serializer.encode_to_range(keys::SESSION, sender_key);
1019        let sessions: Vec<Session> = self
1020            .inner
1021            .transaction(keys::SESSION)
1022            .with_mode(TransactionMode::Readonly)
1023            .build()?
1024            .object_store(keys::SESSION)?
1025            .get_all()
1026            .with_query(&range)
1027            .await?
1028            .filter_map(Result::ok)
1029            .filter_map(|f| {
1030                self.serializer.deserialize_value(f).ok().map(|p| {
1031                    Session::from_pickle(device_keys.clone(), p).map_err(|_| {
1032                        IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset)
1033                    })
1034                })
1035            })
1036            .collect::<Result<Vec<Session>>>()?;
1037
1038        if sessions.is_empty() {
1039            Ok(None)
1040        } else {
1041            Ok(Some(sessions))
1042        }
1043    }
1044
1045    async fn get_inbound_group_session(
1046        &self,
1047        room_id: &RoomId,
1048        session_id: &str,
1049    ) -> Result<Option<InboundGroupSession>> {
1050        let key =
1051            self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1052        if let Some(value) = self
1053            .inner
1054            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1055            .with_mode(TransactionMode::Readonly)
1056            .build()?
1057            .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1058            .get(&key)
1059            .await?
1060        {
1061            Ok(Some(self.deserialize_inbound_group_session(value)?))
1062        } else {
1063            Ok(None)
1064        }
1065    }
1066
1067    async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
1068        const INBOUND_GROUP_SESSIONS_BATCH_SIZE: usize = 1000;
1069
1070        let transaction = self
1071            .inner
1072            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1073            .with_mode(TransactionMode::Readonly)
1074            .build()?;
1075
1076        let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1077
1078        fetch_from_object_store_batched(
1079            object_store,
1080            |value| self.deserialize_inbound_group_session(value),
1081            INBOUND_GROUP_SESSIONS_BATCH_SIZE,
1082        )
1083        .await
1084    }
1085
1086    async fn get_inbound_group_sessions_by_room_id(
1087        &self,
1088        room_id: &RoomId,
1089    ) -> Result<Vec<InboundGroupSession>> {
1090        let range = self.serializer.encode_to_range(keys::INBOUND_GROUP_SESSIONS_V3, room_id);
1091        Ok(self
1092            .inner
1093            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1094            .with_mode(TransactionMode::Readonly)
1095            .build()?
1096            .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1097            .get_all()
1098            .with_query(&range)
1099            .await?
1100            .filter_map(Result::ok)
1101            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1102                Ok(session) => Some(session),
1103                Err(e) => {
1104                    warn!("Failed to deserialize inbound group session: {e}");
1105                    None
1106                }
1107            })
1108            .collect::<Vec<InboundGroupSession>>())
1109    }
1110
1111    async fn get_inbound_group_sessions_for_device_batch(
1112        &self,
1113        sender_key: Curve25519PublicKey,
1114        sender_data_type: SenderDataType,
1115        after_session_id: Option<String>,
1116        limit: usize,
1117    ) -> Result<Vec<InboundGroupSession>> {
1118        let sender_key =
1119            self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
1120
1121        // The empty string is before all keys in Indexed DB - first batch starts there.
1122        let after_session_id = after_session_id
1123            .map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s))
1124            .unwrap_or("".into());
1125
1126        let lower_bound: Array =
1127            [sender_key.clone(), (sender_data_type as u8).into(), after_session_id]
1128                .iter()
1129                .collect();
1130        let upper_bound: Array =
1131            [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
1132        let key = KeyRange::Bound(lower_bound, true, upper_bound, true);
1133
1134        let tx = self
1135            .inner
1136            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1137            .with_mode(TransactionMode::Readonly)
1138            .build()?;
1139
1140        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1141        let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1142        let serialized_sessions =
1143            idx.get_all().with_query::<Array, _>(key).with_limit(limit as u32).await?;
1144
1145        // Deserialize and decrypt after the transaction is complete.
1146        let result = serialized_sessions
1147            .filter_map(Result::ok)
1148            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1149                Ok(session) => Some(session),
1150                Err(e) => {
1151                    warn!("Failed to deserialize inbound group session: {e}");
1152                    None
1153                }
1154            })
1155            .collect::<Vec<InboundGroupSession>>();
1156
1157        Ok(result)
1158    }
1159
1160    async fn inbound_group_session_counts(
1161        &self,
1162        _backup_version: Option<&str>,
1163    ) -> Result<RoomKeyCounts> {
1164        let tx = self
1165            .inner
1166            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1167            .with_mode(TransactionMode::Readonly)
1168            .build()?;
1169        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1170        let all = store.count().await? as usize;
1171        let not_backed_up =
1172            store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count().await? as usize;
1173        tx.commit().await?;
1174        Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1175    }
1176
1177    async fn inbound_group_sessions_for_backup(
1178        &self,
1179        _backup_version: &str,
1180        limit: usize,
1181    ) -> Result<Vec<InboundGroupSession>> {
1182        let tx = self
1183            .inner
1184            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1185            .with_mode(TransactionMode::Readonly)
1186            .build()?;
1187
1188        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1189        let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1190
1191        // XXX ideally we would use `get_all_with_key_and_limit`, but that doesn't
1192        // appear to be   exposed (https://github.com/Alorel/rust-indexed-db/issues/31). Instead we replicate
1193        //   the behaviour with a cursor.
1194        let Some(mut cursor) = idx.open_cursor().await? else {
1195            return Ok(vec![]);
1196        };
1197
1198        let mut serialized_sessions = Vec::with_capacity(limit);
1199        for _ in 0..limit {
1200            let Some(value) = cursor.next_record().await? else {
1201                break;
1202            };
1203            serialized_sessions.push(value)
1204        }
1205
1206        tx.commit().await?;
1207
1208        // Deserialize and decrypt after the transaction is complete.
1209        let result = serialized_sessions
1210            .into_iter()
1211            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1212                Ok(session) => Some(session),
1213                Err(e) => {
1214                    warn!("Failed to deserialize inbound group session: {e}");
1215                    None
1216                }
1217            })
1218            .collect::<Vec<InboundGroupSession>>();
1219
1220        Ok(result)
1221    }
1222
1223    async fn mark_inbound_group_sessions_as_backed_up(
1224        &self,
1225        _backup_version: &str,
1226        room_and_session_ids: &[(&RoomId, &str)],
1227    ) -> Result<()> {
1228        let tx = self
1229            .inner
1230            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1231            .with_mode(TransactionMode::Readwrite)
1232            .build()?;
1233
1234        let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1235
1236        for (room_id, session_id) in room_and_session_ids {
1237            let key =
1238                self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1239            if let Some(idb_object_js) = object_store.get(&key).await? {
1240                let mut idb_object: InboundGroupSessionIndexedDbObject =
1241                    serde_wasm_bindgen::from_value(idb_object_js)?;
1242                idb_object.needs_backup = false;
1243                object_store
1244                    .put(&serde_wasm_bindgen::to_value(&idb_object)?)
1245                    .with_key(key)
1246                    .build()?;
1247            } else {
1248                warn!(?key, "Could not find inbound group session to mark it as backed up.");
1249            }
1250        }
1251
1252        Ok(tx.commit().await?)
1253    }
1254
1255    async fn reset_backup_state(&self) -> Result<()> {
1256        let tx = self
1257            .inner
1258            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1259            .with_mode(TransactionMode::Readwrite)
1260            .build()?;
1261
1262        if let Some(mut cursor) =
1263            tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor().await?
1264        {
1265            while let Some(value) = cursor.next_record().await? {
1266                let mut idb_object: InboundGroupSessionIndexedDbObject =
1267                    serde_wasm_bindgen::from_value(value)?;
1268                if !idb_object.needs_backup {
1269                    idb_object.needs_backup = true;
1270                    // We don't bother to update the encrypted `InboundGroupSession` object stored
1271                    // inside `idb_object.data`, since that would require decryption and encryption.
1272                    // Instead, it will be patched up by `deserialize_inbound_group_session`.
1273                    let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1274                    cursor.update(&idb_object).await?;
1275                }
1276            }
1277        }
1278
1279        Ok(tx.commit().await?)
1280    }
1281
1282    async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1283        let tx = self
1284            .inner
1285            .transaction(keys::TRACKED_USERS)
1286            .with_mode(TransactionMode::Readwrite)
1287            .build()?;
1288        let os = tx.object_store(keys::TRACKED_USERS)?;
1289
1290        for (user, dirty) in users {
1291            os.put(&JsValue::from(*dirty)).with_key(JsValue::from_str(user.as_str())).build()?;
1292        }
1293
1294        tx.commit().await?;
1295        Ok(())
1296    }
1297
1298    async fn get_device(
1299        &self,
1300        user_id: &UserId,
1301        device_id: &DeviceId,
1302    ) -> Result<Option<DeviceData>> {
1303        let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1304        self.inner
1305            .transaction(keys::DEVICES)
1306            .with_mode(TransactionMode::Readonly)
1307            .build()?
1308            .object_store(keys::DEVICES)?
1309            .get(&key)
1310            .await?
1311            .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1312            .transpose()
1313    }
1314
1315    async fn get_user_devices(
1316        &self,
1317        user_id: &UserId,
1318    ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1319        let range = self.serializer.encode_to_range(keys::DEVICES, user_id);
1320        Ok(self
1321            .inner
1322            .transaction(keys::DEVICES)
1323            .with_mode(TransactionMode::Readonly)
1324            .build()?
1325            .object_store(keys::DEVICES)?
1326            .get_all()
1327            .with_query(&range)
1328            .await?
1329            .filter_map(Result::ok)
1330            .filter_map(|d| {
1331                let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1332                Some((d.device_id().to_owned(), d))
1333            })
1334            .collect::<HashMap<_, _>>())
1335    }
1336
1337    async fn get_own_device(&self) -> Result<DeviceData> {
1338        let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1339        Ok(self.get_device(&account_info.user_id, &account_info.device_id).await?.unwrap())
1340    }
1341
1342    async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1343        self.inner
1344            .transaction(keys::IDENTITIES)
1345            .with_mode(TransactionMode::Readonly)
1346            .build()?
1347            .object_store(keys::IDENTITIES)?
1348            .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))
1349            .await?
1350            .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1351            .transpose()
1352    }
1353
1354    async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1355        Ok(self
1356            .inner
1357            .transaction(keys::OLM_HASHES)
1358            .with_mode(TransactionMode::Readonly)
1359            .build()?
1360            .object_store(keys::OLM_HASHES)?
1361            .get::<JsValue, _, _>(
1362                &self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
1363            )
1364            .await?
1365            .is_some())
1366    }
1367
1368    async fn get_secrets_from_inbox(
1369        &self,
1370        secret_name: &SecretName,
1371    ) -> Result<Vec<GossippedSecret>> {
1372        let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str());
1373
1374        self.inner
1375            .transaction(keys::SECRETS_INBOX)
1376            .with_mode(TransactionMode::Readonly)
1377            .build()?
1378            .object_store(keys::SECRETS_INBOX)?
1379            .get_all()
1380            .with_query(&range)
1381            .await?
1382            .map(|result| {
1383                let d = result?;
1384                let secret = self.serializer.deserialize_value(d)?;
1385                Ok(secret)
1386            })
1387            .collect()
1388    }
1389
1390    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1391    async fn delete_secrets_from_inbox(&self, secret_name: &SecretName) -> Result<()> {
1392        let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str());
1393
1394        let transaction = self
1395            .inner
1396            .transaction(keys::SECRETS_INBOX)
1397            .with_mode(TransactionMode::Readwrite)
1398            .build()?;
1399        transaction.object_store(keys::SECRETS_INBOX)?.delete(&range).build()?;
1400        transaction.commit().await?;
1401
1402        Ok(())
1403    }
1404
1405    async fn get_secret_request_by_info(
1406        &self,
1407        key_info: &SecretInfo,
1408    ) -> Result<Option<GossipRequest>> {
1409        let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1410
1411        let val = self
1412            .inner
1413            .transaction(keys::GOSSIP_REQUESTS)
1414            .with_mode(TransactionMode::Readonly)
1415            .build()?
1416            .object_store(keys::GOSSIP_REQUESTS)?
1417            .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1418            .get(key)
1419            .await?;
1420
1421        if let Some(val) = val {
1422            let deser = self.deserialize_gossip_request(val)?;
1423            Ok(Some(deser))
1424        } else {
1425            Ok(None)
1426        }
1427    }
1428
1429    async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1430        let results = self
1431            .inner
1432            .transaction(keys::GOSSIP_REQUESTS)
1433            .with_mode(TransactionMode::Readonly)
1434            .build()?
1435            .object_store(keys::GOSSIP_REQUESTS)?
1436            .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1437            .get_all()
1438            .await?
1439            .filter_map(Result::ok)
1440            .filter_map(|val| self.deserialize_gossip_request(val).ok())
1441            .collect();
1442
1443        Ok(results)
1444    }
1445
1446    async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1447        let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1448        let tx = self
1449            .inner
1450            .transaction(keys::GOSSIP_REQUESTS)
1451            .with_mode(TransactionMode::Readwrite)
1452            .build()?;
1453        tx.object_store(keys::GOSSIP_REQUESTS)?.delete(jskey).build()?;
1454        tx.commit().await.map_err(|e| e.into())
1455    }
1456
1457    async fn load_backup_keys(&self) -> Result<BackupKeys> {
1458        let key = {
1459            let tx = self
1460                .inner
1461                .transaction(keys::BACKUP_KEYS)
1462                .with_mode(TransactionMode::Readonly)
1463                .build()?;
1464            let store = tx.object_store(keys::BACKUP_KEYS)?;
1465
1466            let backup_version = store
1467                .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))
1468                .await?
1469                .map(|i| self.serializer.deserialize_value(i))
1470                .transpose()?;
1471
1472            let decryption_key = store
1473                .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))
1474                .await?
1475                .map(|i| self.serializer.deserialize_value(i))
1476                .transpose()?;
1477
1478            BackupKeys { backup_version, decryption_key }
1479        };
1480
1481        Ok(key)
1482    }
1483
1484    async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1485        if let Some(pickle) = self
1486            .inner
1487            .transaction(keys::CORE)
1488            .with_mode(TransactionMode::Readonly)
1489            .build()?
1490            .object_store(keys::CORE)?
1491            .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))
1492            .await?
1493        {
1494            let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1495
1496            Ok(Some(pickle))
1497        } else {
1498            Ok(None)
1499        }
1500    }
1501
1502    async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1503        self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1504        Ok(())
1505    }
1506
1507    async fn get_withheld_info(
1508        &self,
1509        room_id: &RoomId,
1510        session_id: &str,
1511    ) -> Result<Option<RoomKeyWithheldEntry>> {
1512        let key = self.serializer.encode_key(keys::WITHHELD_SESSIONS, (room_id, session_id));
1513        if let Some(pickle) = self
1514            .inner
1515            .transaction(keys::WITHHELD_SESSIONS)
1516            .with_mode(TransactionMode::Readonly)
1517            .build()?
1518            .object_store(keys::WITHHELD_SESSIONS)?
1519            .get(&key)
1520            .await?
1521        {
1522            let info = self.serializer.deserialize_value(pickle)?;
1523            Ok(Some(info))
1524        } else {
1525            Ok(None)
1526        }
1527    }
1528
1529    async fn get_withheld_sessions_by_room_id(
1530        &self,
1531        room_id: &RoomId,
1532    ) -> Result<Vec<RoomKeyWithheldEntry>> {
1533        let range = self.serializer.encode_to_range(keys::WITHHELD_SESSIONS, room_id);
1534
1535        self
1536            .inner
1537            .transaction(keys::WITHHELD_SESSIONS)
1538            .with_mode(TransactionMode::Readonly)
1539            .build()?
1540            .object_store(keys::WITHHELD_SESSIONS)?
1541            .get_all()
1542            .with_query(&range)
1543            .await?
1544            .map(|val| self.serializer.deserialize_value(val?).map_err(Into::into))
1545            .collect()
1546    }
1547
1548    async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1549        let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1550        self.inner
1551            .transaction(keys::ROOM_SETTINGS)
1552            .with_mode(TransactionMode::Readonly)
1553            .build()?
1554            .object_store(keys::ROOM_SETTINGS)?
1555            .get(&key)
1556            .await?
1557            .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1558            .transpose()
1559    }
1560
1561    async fn get_received_room_key_bundle_data(
1562        &self,
1563        room_id: &RoomId,
1564        user_id: &UserId,
1565    ) -> Result<Option<StoredRoomKeyBundleData>> {
1566        let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1567        let result = self
1568            .inner
1569            .transaction(keys::RECEIVED_ROOM_KEY_BUNDLES)
1570            .with_mode(TransactionMode::Readonly)
1571            .build()?
1572            .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1573            .get(&key)
1574            .await?
1575            .map(|v| self.serializer.deserialize_value(v))
1576            .transpose()?;
1577
1578        Ok(result)
1579    }
1580
1581    async fn has_downloaded_all_room_keys(&self, room_id: &RoomId) -> Result<bool> {
1582        let key = self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id);
1583        let result = self
1584            .inner
1585            .transaction(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)
1586            .with_mode(TransactionMode::Readonly)
1587            .build()?
1588            .object_store(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)?
1589            .get::<JsValue, _, _>(&key)
1590            .await?
1591            .is_some();
1592
1593        Ok(result)
1594    }
1595
1596    async fn get_pending_key_bundle_details_for_room(&self, room_id: &RoomId) -> Result<Option<RoomPendingKeyBundleDetails >> {
1597        let key = self.serializer.encode_key(keys::ROOMS_PENDING_KEY_BUNDLE, room_id);
1598        let result = self
1599            .inner
1600            .transaction(keys::ROOMS_PENDING_KEY_BUNDLE)
1601            .with_mode(TransactionMode::Readonly)
1602            .build()?
1603            .object_store(keys::ROOMS_PENDING_KEY_BUNDLE)?
1604            .get(&key)
1605            .await?
1606            .map(|v| self.serializer.deserialize_value(v))
1607            .transpose()?;
1608        Ok(result)
1609    }
1610
1611    async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1612        self.inner
1613            .transaction(keys::CORE)
1614            .with_mode(TransactionMode::Readonly)
1615            .build()?
1616            .object_store(keys::CORE)?
1617            .get(&JsValue::from_str(key))
1618            .await?
1619            .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1620            .transpose()
1621    }
1622
1623    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1624    async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1625        let transaction =
1626            self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1627        transaction
1628            .object_store(keys::CORE)?
1629            .put(&self.serializer.serialize_value(&value)?)
1630            .with_key(JsValue::from_str(key))
1631            .build()?;
1632        transaction.commit().await?;
1633        Ok(())
1634    }
1635
1636    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1637    async fn remove_custom_value(&self, key: &str) -> Result<()> {
1638        let transaction =
1639            self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1640        transaction.object_store(keys::CORE)?.delete(&JsValue::from_str(key)).build()?;
1641        transaction.commit().await?;
1642        Ok(())
1643    }
1644
1645    async fn try_take_leased_lock(
1646        &self,
1647        lease_duration_ms: u32,
1648        key: &str,
1649        holder: &str,
1650    ) -> Result<Option<CrossProcessLockGeneration>> {
1651        // As of 2023-06-23, the code below hasn't been tested yet.
1652        let key = JsValue::from_str(key);
1653        let txn = self
1654            .inner
1655            .transaction(keys::LEASE_LOCKS)
1656            .with_mode(TransactionMode::Readwrite)
1657            .build()?;
1658        let object_store = txn.object_store(keys::LEASE_LOCKS)?;
1659
1660        #[derive(Deserialize, Serialize)]
1661        struct Lease {
1662            holder: String,
1663            expiration: u64,
1664            generation: CrossProcessLockGeneration,
1665        }
1666
1667        let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1668        let expiration = now + lease_duration_ms as u64;
1669
1670        let lease = match object_store.get(&key).await? {
1671            Some(entry) => {
1672                let mut lease: Lease = self.serializer.deserialize_value(entry)?;
1673
1674                if lease.holder == holder {
1675                    // We had the lease before, extend it.
1676                    lease.expiration = expiration;
1677
1678                    Some(lease)
1679                } else {
1680                    // We didn't have it.
1681                    if lease.expiration < now {
1682                        // Steal it!
1683                        lease.holder = holder.to_owned();
1684                        lease.expiration = expiration;
1685                        lease.generation += 1;
1686
1687                        Some(lease)
1688                    } else {
1689                        // We tried our best.
1690                        None
1691                    }
1692                }
1693            }
1694            None => {
1695                let lease = Lease {
1696                    holder: holder.to_owned(),
1697                    expiration,
1698                    generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1699                };
1700
1701                Some(lease)
1702            }
1703        };
1704
1705        Ok(if let Some(lease) = lease {
1706            object_store.put(&self.serializer.serialize_value(&lease)?).with_key(key).build()?;
1707
1708            Some(lease.generation)
1709        } else {
1710            None
1711        })
1712    }
1713
1714    #[allow(clippy::unused_async)]
1715    async fn get_size(&self) -> Result<Option<usize>> {
1716        Ok(None)
1717    }
1718}
1719
1720impl Drop for IndexeddbCryptoStore {
1721    fn drop(&mut self) {
1722        // Must release the database access manually as it's not done when
1723        // dropping it.
1724        self.inner.as_sys().close();
1725    }
1726}
1727
1728/// Open the meta store.
1729///
1730/// The meta store contains details about the encryption of the main store.
1731async fn open_meta_db(prefix: &str) -> Result<Database, IndexeddbCryptoStoreError> {
1732    let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1733
1734    debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1735    Database::open(&name)
1736        .with_version(1u32)
1737        .with_on_upgrade_needed(|evt, tx| {
1738            let old_version = evt.old_version() as u32;
1739            if old_version < 1 {
1740                // migrating to version 1
1741                tx.db().create_object_store("matrix-sdk-crypto").build()?;
1742            }
1743            Ok(())
1744        })
1745        .await
1746        .map_err(Into::into)
1747}
1748
1749/// Load the serialised store cipher from the meta store.
1750///
1751/// # Arguments:
1752///
1753/// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1754///
1755/// # Returns:
1756///
1757/// The serialised `StoreCipher` object.
1758async fn load_store_cipher(
1759    meta_db: &Database,
1760) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1761    let tx: Transaction<'_> =
1762        meta_db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readonly).build()?;
1763    let ob = tx.object_store("matrix-sdk-crypto")?;
1764
1765    let store_cipher: Option<Vec<u8>> = ob
1766        .get(&JsValue::from_str(keys::STORE_CIPHER))
1767        .await?
1768        .map(|k: JsValue| k.into_serde())
1769        .transpose()?;
1770    Ok(store_cipher)
1771}
1772
1773/// Save the serialised store cipher to the meta store.
1774///
1775/// # Arguments:
1776///
1777/// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1778/// * `store_cipher`: The serialised `StoreCipher` object.
1779async fn save_store_cipher(
1780    db: &Database,
1781    export: &Vec<u8>,
1782) -> Result<(), IndexeddbCryptoStoreError> {
1783    let tx: Transaction<'_> =
1784        db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readwrite).build()?;
1785    let ob = tx.object_store("matrix-sdk-crypto")?;
1786
1787    ob.put(&JsValue::from_serde(&export)?)
1788        .with_key(JsValue::from_str(keys::STORE_CIPHER))
1789        .build()?;
1790    tx.commit().await?;
1791    Ok(())
1792}
1793
1794/// Given a serialised store cipher, try importing with the given key.
1795///
1796/// This is a helper for [`IndexeddbCryptoStore::open_with_key`].
1797///
1798/// # Arguments
1799///
1800/// * `chacha_key`: The key to use with [`StoreCipher::import_with_key`].
1801///   Derived from `original_key` via an HKDF.
1802/// * `original_key`: The key provided by the application. Used to provide a
1803///   migration path from an older key derivation system.
1804/// * `serialised_cipher`: The serialized `EncryptedStoreCipher`, retrieved from
1805///   the database.
1806/// * `db`: Connection to the database.
1807async fn import_store_cipher_with_key(
1808    chacha_key: &[u8; 32],
1809    original_key: &[u8],
1810    serialised_cipher: &[u8],
1811    db: &Database,
1812) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1813    let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1814        Ok(cipher) => cipher,
1815        Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1816            // Old versions of the matrix-js-sdk used to base64-encode their encryption
1817            // key, and pass it into [`IndexeddbCryptoStore::open_with_passphrase`]. For
1818            // backwards compatibility, we fall back to that if we discover we have a cipher
1819            // encrypted with a KDF when we expected it to be encrypted directly with a key.
1820            let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1821                .map_err(|_| CryptoStoreError::UnpicklingError)?;
1822
1823            // Loading the cipher with the passphrase was successful. Let's update the
1824            // stored version of the cipher so that it is encrypted with a key,
1825            // to save doing this again.
1826            debug!(
1827                "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1828            );
1829
1830            let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1831            save_store_cipher(db, &export).await?;
1832            cipher
1833        }
1834        Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1835    };
1836    Ok(cipher)
1837}
1838
1839/// Fetch items from an object store in batches, transform each item using
1840/// the supplied function, and stuff the transformed items into a single
1841/// vector to return.
1842async fn fetch_from_object_store_batched<R, F>(
1843    object_store: ObjectStore<'_>,
1844    f: F,
1845    batch_size: usize,
1846) -> Result<Vec<R>>
1847where
1848    F: Fn(JsValue) -> Result<R>,
1849{
1850    let mut result = Vec::new();
1851    let mut batch_n = 0;
1852
1853    // The empty string is before all keys in Indexed DB - first batch starts there.
1854    let mut latest_key: JsValue = "".into();
1855
1856    loop {
1857        debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1858
1859        // See https://github.com/Alorel/rust-indexed-db/issues/31 - we
1860        // would like to use `get_all_with_key_and_limit` if it ever exists
1861        // but for now we use a cursor and manually limit batch size.
1862
1863        // Get hold of a cursor for this batch. (This should not panic in expect()
1864        // because we always use "", or the result of cursor.key(), both of
1865        // which are valid keys.)
1866        let after_latest_key = KeyRange::LowerBound(&latest_key, true);
1867        let cursor = object_store.open_cursor().with_query(&after_latest_key).await?;
1868
1869        // Fetch batch_size records into result
1870        let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1871        if let Some(next_key) = next_key {
1872            latest_key = next_key;
1873        } else {
1874            break;
1875        }
1876
1877        batch_n += 1;
1878    }
1879
1880    Ok(result)
1881}
1882
1883/// Fetch batch_size records from the supplied cursor,
1884/// and return the last key we processed, or None if
1885/// we reached the end of the cursor.
1886async fn fetch_batch<R, F, Q>(
1887    cursor: Option<Cursor<'_, Q>>,
1888    batch_size: usize,
1889    f: &F,
1890    result: &mut Vec<R>,
1891) -> Result<Option<JsValue>>
1892where
1893    F: Fn(JsValue) -> Result<R>,
1894    Q: QuerySource,
1895{
1896    let Some(mut cursor) = cursor else {
1897        // Cursor was None - there are no more records
1898        return Ok(None);
1899    };
1900
1901    let mut latest_key = None;
1902
1903    for _ in 0..batch_size {
1904        let Some(value) = cursor.next_record().await? else {
1905            return Ok(None);
1906        };
1907
1908        // Process the record
1909        let processed = f(value);
1910        if let Ok(processed) = processed {
1911            result.push(processed);
1912        }
1913        // else processing failed: don't return this record at all
1914
1915        // Remember that we have processed this record, so if we hit
1916        // the end of the batch, the next batch can start after this one
1917        if let Some(key) = cursor.key()? {
1918            latest_key = Some(key);
1919        }
1920    }
1921
1922    // We finished the batch but there are more records -
1923    // return the key of the last one we processed
1924    Ok(latest_key)
1925}
1926
1927/// The objects we store in the gossip_requests indexeddb object store
1928#[derive(Debug, Serialize, Deserialize)]
1929struct GossipRequestIndexedDbObject {
1930    /// Encrypted hash of the [`SecretInfo`] structure.
1931    info: String,
1932
1933    /// Encrypted serialised representation of the [`GossipRequest`] as a whole.
1934    request: Vec<u8>,
1935
1936    /// Whether the request has yet to be sent out.
1937    ///
1938    /// Since we only need to be able to find requests where this is `true`, we
1939    /// skip serialization in cases where it is `false`. That has the effect
1940    /// of omitting it from the indexeddb index.
1941    ///
1942    /// We also use a custom serializer because bools can't be used as keys in
1943    /// indexeddb.
1944    #[serde(
1945        default,
1946        skip_serializing_if = "std::ops::Not::not",
1947        with = "crate::serializer::foreign::bool"
1948    )]
1949    unsent: bool,
1950}
1951
1952/// The objects we store in the inbound_group_sessions3 indexeddb object store
1953#[derive(Serialize, Deserialize)]
1954struct InboundGroupSessionIndexedDbObject {
1955    /// Possibly encrypted
1956    /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
1957    pickled_session: MaybeEncrypted,
1958
1959    /// The (hashed) session ID of this session. This is somewhat redundant, but
1960    /// we have to pull it out to its own object so that we can do batched
1961    /// queries such as
1962    /// [`IndexeddbStore::get_inbound_group_sessions_for_device_batch`].
1963    ///
1964    /// Added in database schema v12, and lazily populated, so it is only
1965    /// present for sessions received or modified since DB schema v12.
1966    #[serde(default, skip_serializing_if = "Option::is_none")]
1967    session_id: Option<String>,
1968
1969    /// Whether the session data has yet to be backed up.
1970    ///
1971    /// Since we only need to be able to find entries where this is `true`, we
1972    /// skip serialization in cases where it is `false`. That has the effect
1973    /// of omitting it from the indexeddb index.
1974    ///
1975    /// We also use a custom serializer because bools can't be used as keys in
1976    /// indexeddb.
1977    #[serde(
1978        default,
1979        skip_serializing_if = "std::ops::Not::not",
1980        with = "crate::serializer::foreign::bool"
1981    )]
1982    needs_backup: bool,
1983
1984    /// Unused: for future compatibility. In future, will contain the order
1985    /// number (not the ID!) of the backup for which this key has been
1986    /// backed up. This will replace `needs_backup`, fixing the performance
1987    /// problem identified in
1988    /// https://github.com/element-hq/element-web/issues/26892
1989    /// because we won't need to update all records when we spot a new backup
1990    /// version.
1991    /// In this version of the code, this is always set to -1, meaning:
1992    /// "refer to the `needs_backup` property". See:
1993    /// https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
1994    backed_up_to: i32,
1995
1996    /// The (hashed) curve25519 key of the device that sent us this room key,
1997    /// base64-encoded.
1998    ///
1999    /// Added in database schema v12, and lazily populated, so it is only
2000    /// present for sessions received or modified since DB schema v12.
2001    #[serde(default, skip_serializing_if = "Option::is_none")]
2002    sender_key: Option<String>,
2003
2004    /// The type of the [`SenderData`] within this session, converted to a u8
2005    /// from [`SenderDataType`].
2006    ///
2007    /// Added in database schema v12, and lazily populated, so it is only
2008    /// present for sessions received or modified since DB schema v12.
2009    #[serde(default, skip_serializing_if = "Option::is_none")]
2010    sender_data_type: Option<u8>,
2011}
2012
2013impl InboundGroupSessionIndexedDbObject {
2014    /// Build an [`InboundGroupSessionIndexedDbObject`] wrapping the given
2015    /// session.
2016    pub async fn from_session(
2017        session: &InboundGroupSession,
2018        serializer: &SafeEncodeSerializer,
2019    ) -> Result<Self, CryptoStoreError> {
2020        let session_id =
2021            serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
2022
2023        let sender_key = serializer.encode_key_as_string(
2024            keys::INBOUND_GROUP_SESSIONS_V3,
2025            session.sender_key().to_base64(),
2026        );
2027
2028        Ok(InboundGroupSessionIndexedDbObject {
2029            pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
2030            session_id: Some(session_id),
2031            needs_backup: !session.backed_up(),
2032            backed_up_to: -1,
2033            sender_key: Some(sender_key),
2034            sender_data_type: Some(session.sender_data_type() as u8),
2035        })
2036    }
2037}
2038
2039#[cfg(test)]
2040mod unit_tests {
2041    use matrix_sdk_crypto::{
2042        olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
2043        types::EventEncryptionAlgorithm,
2044        vodozemac::Ed25519Keypair,
2045    };
2046    use matrix_sdk_store_encryption::EncryptedValueBase64;
2047    use matrix_sdk_test::async_test;
2048    use ruma::{device_id, room_id, user_id};
2049
2050    use super::InboundGroupSessionIndexedDbObject;
2051    use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
2052
2053    #[test]
2054    fn needs_backup_is_serialized_as_a_u8_in_json() {
2055        let session_needs_backup = backup_test_session(true);
2056
2057        // Testing the exact JSON here is theoretically flaky in the face of
2058        // serialization changes in serde_json but it seems unlikely, and it's
2059        // simple enough to fix if we need to.
2060        assert!(
2061            serde_json::to_string(&session_needs_backup).unwrap().contains(r#""needs_backup":1"#),
2062        );
2063    }
2064
2065    #[test]
2066    fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
2067        let session_backed_up = backup_test_session(false);
2068
2069        assert!(
2070            !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
2071            "The needs_backup field should be missing!"
2072        );
2073    }
2074
2075    pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
2076        InboundGroupSessionIndexedDbObject {
2077            pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
2078            session_id: None,
2079            needs_backup,
2080            backed_up_to: -1,
2081            sender_key: None,
2082            sender_data_type: None,
2083        }
2084    }
2085
2086    #[async_test]
2087    async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
2088        let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2089
2090        let sender_data = SenderData::sender_verified(
2091            user_id!("@test:user"),
2092            device_id!("ABC"),
2093            Ed25519Keypair::new().public_key(),
2094        );
2095
2096        let db_object = sender_data_test_session(sender_key, sender_data).await;
2097        let serialized = serde_json::to_string(&db_object).unwrap();
2098
2099        assert!(
2100            serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
2101        );
2102        assert!(serialized.contains(r#""sender_data_type":5"#));
2103    }
2104
2105    pub async fn sender_data_test_session(
2106        sender_key: Curve25519PublicKey,
2107        sender_data: SenderData,
2108    ) -> InboundGroupSessionIndexedDbObject {
2109        let session = InboundGroupSession::new(
2110            sender_key,
2111            Ed25519Keypair::new().public_key(),
2112            room_id!("!test:localhost"),
2113            // Arbitrary session data
2114            &SessionKey::from_base64(
2115                "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
2116                 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
2117                 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
2118                 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
2119                 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
2120            )
2121            .unwrap(),
2122            sender_data,
2123            None,
2124            EventEncryptionAlgorithm::MegolmV1AesSha2,
2125            None,
2126            false,
2127        )
2128        .unwrap();
2129
2130        InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
2131            .await
2132            .unwrap()
2133    }
2134}
2135
2136#[cfg(all(test, target_family = "wasm"))]
2137mod wasm_unit_tests {
2138    use std::collections::BTreeMap;
2139
2140    use matrix_sdk_crypto::{
2141        olm::{Curve25519PublicKey, SenderData},
2142        types::{DeviceKeys, Signatures},
2143    };
2144    use matrix_sdk_test::async_test;
2145    use ruma::{owned_device_id, owned_user_id};
2146    use wasm_bindgen::JsValue;
2147
2148    use crate::crypto_store::unit_tests::sender_data_test_session;
2149
2150    fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
2151        assert_eq!(
2152            js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
2153            JsValue::from_f64(expected.into())
2154        );
2155    }
2156
2157    #[async_test]
2158    fn test_needs_backup_is_serialized_as_a_u8_in_js() {
2159        let session_needs_backup = super::unit_tests::backup_test_session(true);
2160
2161        let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
2162
2163        assert!(js_value.is_object());
2164        assert_field_equals(&js_value, "needs_backup", 1);
2165    }
2166
2167    #[async_test]
2168    fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
2169        let session_backed_up = super::unit_tests::backup_test_session(false);
2170
2171        let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
2172
2173        assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
2174    }
2175
2176    #[async_test]
2177    async fn test_sender_key_and_device_type_are_serialized_in_js() {
2178        let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2179
2180        let sender_data = SenderData::device_info(DeviceKeys::new(
2181            owned_user_id!("@test:user"),
2182            owned_device_id!("ABC"),
2183            vec![],
2184            BTreeMap::new(),
2185            Signatures::new(),
2186        ));
2187        let db_object = sender_data_test_session(sender_key, sender_data).await;
2188
2189        let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
2190
2191        assert!(js_value.is_object());
2192        assert_field_equals(&js_value, "sender_data_type", 2);
2193        assert_eq!(
2194            js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
2195            JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
2196        );
2197    }
2198}
2199
2200#[cfg(all(test, target_family = "wasm"))]
2201mod tests {
2202    use matrix_sdk_crypto::cryptostore_integration_tests;
2203
2204    use super::IndexeddbCryptoStore;
2205
2206    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2207
2208    async fn get_store(
2209        name: &str,
2210        passphrase: Option<&str>,
2211        clear_data: bool,
2212    ) -> IndexeddbCryptoStore {
2213        if clear_data {
2214            IndexeddbCryptoStore::delete_stores(name).unwrap();
2215        }
2216        match passphrase {
2217            Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2218                .await
2219                .expect("Can't create a passphrase protected store"),
2220            None => IndexeddbCryptoStore::open_with_name(name)
2221                .await
2222                .expect("Can't create store without passphrase"),
2223        }
2224    }
2225
2226    cryptostore_integration_tests!();
2227}
2228
2229#[cfg(all(test, target_family = "wasm"))]
2230mod encrypted_tests {
2231    use matrix_sdk_crypto::{
2232        cryptostore_integration_tests,
2233        olm::Account,
2234        store::{CryptoStore, types::PendingChanges},
2235        vodozemac::base64_encode,
2236    };
2237    use matrix_sdk_test::async_test;
2238    use ruma::{device_id, user_id};
2239
2240    use super::IndexeddbCryptoStore;
2241
2242    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2243
2244    async fn get_store(
2245        name: &str,
2246        passphrase: Option<&str>,
2247        clear_data: bool,
2248    ) -> IndexeddbCryptoStore {
2249        if clear_data {
2250            IndexeddbCryptoStore::delete_stores(name).unwrap();
2251        }
2252
2253        let pass = passphrase.unwrap_or(name);
2254        IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2255            .await
2256            .expect("Can't create a passphrase protected store")
2257    }
2258    cryptostore_integration_tests!();
2259
2260    /// Test that we can migrate a store created with a passphrase, to being
2261    /// encrypted with a key instead.
2262    #[async_test]
2263    async fn test_migrate_passphrase_to_key() {
2264        let store_name = "test_migrate_passphrase_to_key";
2265        let passdata: [u8; 32] = rand::random();
2266        let b64_passdata = base64_encode(passdata);
2267
2268        // Initialise the store with some account data
2269        IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2270        let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2271            .await
2272            .expect("Can't create a passphrase-protected store");
2273
2274        store
2275            .save_pending_changes(PendingChanges {
2276                account: Some(Account::with_device_id(
2277                    user_id!("@alice:example.org"),
2278                    device_id!("ALICEDEVICE"),
2279                )),
2280            })
2281            .await
2282            .expect("Can't save account");
2283
2284        // Now reopen the store, passing the key directly rather than as a b64 string.
2285        let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2286            .await
2287            .expect("Can't create a key-protected store");
2288        let loaded_account =
2289            store.load_account().await.expect("Can't load account").expect("Account was not saved");
2290        assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2291    }
2292}