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