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