matrix_sdk_indexeddb/crypto_store/
mod.rs

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