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