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_crypto::{
34    olm::{
35        Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
36        PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session,
37        StaticAccountData,
38    },
39    store::{
40        types::{
41            BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, RoomSettings,
42            StoredRoomKeyBundleData,
43        },
44        CryptoStore, CryptoStoreError,
45    },
46    types::events::room_key_withheld::RoomKeyWithheldEvent,
47    vodozemac::base64_encode,
48    Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData,
49};
50use matrix_sdk_store_encryption::StoreCipher;
51use ruma::{
52    events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
53    RoomId, TransactionId, UserId,
54};
55use sha2::Sha256;
56use tokio::sync::Mutex;
57use tracing::{debug, warn};
58use wasm_bindgen::JsValue;
59
60use crate::{
61    crypto_store::migrations::open_and_upgrade_db,
62    error::GenericError,
63    serializer::{MaybeEncrypted, SafeEncodeSerializer, SafeEncodeSerializerError},
64};
65
66mod migrations;
67
68mod keys {
69    // stores
70    pub const CORE: &str = "core";
71
72    pub const SESSION: &str = "session";
73
74    pub const INBOUND_GROUP_SESSIONS_V3: &str = "inbound_group_sessions3";
75    pub const INBOUND_GROUP_SESSIONS_BACKUP_INDEX: &str = "backup";
76    pub const INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX: &str = "backed_up_to";
77    pub const INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX: &str =
78        "inbound_group_session_sender_key_sender_data_type_idx";
79
80    pub const OUTBOUND_GROUP_SESSIONS: &str = "outbound_group_sessions";
81
82    pub const TRACKED_USERS: &str = "tracked_users";
83    pub const OLM_HASHES: &str = "olm_hashes";
84
85    pub const DEVICES: &str = "devices";
86    pub const IDENTITIES: &str = "identities";
87
88    pub const GOSSIP_REQUESTS: &str = "gossip_requests";
89    pub const GOSSIP_REQUESTS_UNSENT_INDEX: &str = "unsent";
90    pub const GOSSIP_REQUESTS_BY_INFO_INDEX: &str = "by_info";
91
92    pub const ROOM_SETTINGS: &str = "room_settings";
93
94    pub const SECRETS_INBOX: &str = "secrets_inbox";
95
96    pub const DIRECT_WITHHELD_INFO: &str = "direct_withheld_info";
97
98    pub const RECEIVED_ROOM_KEY_BUNDLES: &str = "received_room_key_bundles";
99
100    // keys
101    pub const STORE_CIPHER: &str = "store_cipher";
102    pub const ACCOUNT: &str = "account";
103    pub const NEXT_BATCH_TOKEN: &str = "next_batch_token";
104    pub const PRIVATE_IDENTITY: &str = "private_identity";
105
106    // backup v1
107    pub const BACKUP_KEYS: &str = "backup_keys";
108
109    /// Indexeddb key for the key backup version that [`RECOVERY_KEY_V1`]
110    /// corresponds to.
111    pub const BACKUP_VERSION_V1: &str = "backup_version_v1";
112
113    /// Indexeddb key for the backup decryption key.
114    ///
115    /// Known, for historical reasons, as the recovery key. Not to be confused
116    /// with the client-side recovery key, which is actually an AES key for use
117    /// with SSSS.
118    pub const RECOVERY_KEY_V1: &str = "recovery_key_v1";
119
120    /// Indexeddb key for the dehydrated device pickle key.
121    pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key";
122}
123
124/// An implementation of [CryptoStore] that uses [IndexedDB] for persistent
125/// storage.
126///
127/// [IndexedDB]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
128pub struct IndexeddbCryptoStore {
129    static_account: RwLock<Option<StaticAccountData>>,
130    name: String,
131    pub(crate) inner: Database,
132
133    serializer: SafeEncodeSerializer,
134    save_changes_lock: Arc<Mutex<()>>,
135}
136
137#[cfg(not(tarpaulin_include))]
138impl std::fmt::Debug for IndexeddbCryptoStore {
139    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140        f.debug_struct("IndexeddbCryptoStore").field("name", &self.name).finish()
141    }
142}
143
144#[derive(Debug, thiserror::Error)]
145pub enum IndexeddbCryptoStoreError {
146    #[error(transparent)]
147    Serialization(#[from] serde_json::Error),
148    #[error("DomException {name} ({code}): {message}")]
149    DomException {
150        /// DomException code
151        code: u16,
152        /// Specific name of the DomException
153        name: String,
154        /// Message given to the DomException
155        message: String,
156    },
157    #[error(transparent)]
158    CryptoStoreError(#[from] CryptoStoreError),
159    #[error(
160        "The schema version of the crypto store is too new. \
161         Existing version: {current_version}; max supported version: {max_supported_version}"
162    )]
163    SchemaTooNewError { max_supported_version: u32, current_version: u32 },
164}
165
166impl From<SafeEncodeSerializerError> for IndexeddbCryptoStoreError {
167    fn from(value: SafeEncodeSerializerError) -> Self {
168        match value {
169            SafeEncodeSerializerError::Serialization(error) => Self::Serialization(error),
170            SafeEncodeSerializerError::DomException { code, name, message } => {
171                Self::DomException { code, name, message }
172            }
173            SafeEncodeSerializerError::CryptoStoreError(crypto_store_error) => {
174                Self::CryptoStoreError(crypto_store_error)
175            }
176        }
177    }
178}
179
180impl From<web_sys::DomException> for IndexeddbCryptoStoreError {
181    fn from(frm: web_sys::DomException) -> IndexeddbCryptoStoreError {
182        IndexeddbCryptoStoreError::DomException {
183            name: frm.name(),
184            message: frm.message(),
185            code: frm.code(),
186        }
187    }
188}
189
190impl From<serde_wasm_bindgen::Error> for IndexeddbCryptoStoreError {
191    fn from(e: serde_wasm_bindgen::Error) -> Self {
192        IndexeddbCryptoStoreError::Serialization(serde::de::Error::custom(e.to_string()))
193    }
194}
195
196impl From<IndexeddbCryptoStoreError> for CryptoStoreError {
197    fn from(frm: IndexeddbCryptoStoreError) -> CryptoStoreError {
198        match frm {
199            IndexeddbCryptoStoreError::Serialization(e) => CryptoStoreError::Serialization(e),
200            IndexeddbCryptoStoreError::CryptoStoreError(e) => e,
201            _ => CryptoStoreError::backend(frm),
202        }
203    }
204}
205
206impl From<indexed_db_futures::error::DomException> for IndexeddbCryptoStoreError {
207    fn from(value: indexed_db_futures::error::DomException) -> Self {
208        web_sys::DomException::from(value).into()
209    }
210}
211
212impl From<indexed_db_futures::error::SerialisationError> for IndexeddbCryptoStoreError {
213    fn from(value: indexed_db_futures::error::SerialisationError) -> Self {
214        Self::Serialization(serde::de::Error::custom(value.to_string()))
215    }
216}
217
218impl From<indexed_db_futures::error::UnexpectedDataError> for IndexeddbCryptoStoreError {
219    fn from(value: indexed_db_futures::error::UnexpectedDataError) -> Self {
220        Self::CryptoStoreError(CryptoStoreError::backend(value))
221    }
222}
223
224impl From<GenericError> for IndexeddbCryptoStoreError {
225    fn from(value: GenericError) -> Self {
226        Self::CryptoStoreError(value.into())
227    }
228}
229
230impl From<indexed_db_futures::error::JSError> for IndexeddbCryptoStoreError {
231    fn from(value: indexed_db_futures::error::JSError) -> Self {
232        GenericError::from(value.to_string()).into()
233    }
234}
235
236impl From<indexed_db_futures::error::Error> for IndexeddbCryptoStoreError {
237    fn from(value: indexed_db_futures::error::Error) -> Self {
238        use indexed_db_futures::error::Error;
239        match value {
240            Error::DomException(e) => e.into(),
241            Error::Serialisation(e) => e.into(),
242            Error::MissingData(e) => e.into(),
243            Error::Unknown(e) => e.into(),
244        }
245    }
246}
247
248impl From<indexed_db_futures::error::OpenDbError> for IndexeddbCryptoStoreError {
249    fn from(value: indexed_db_futures::error::OpenDbError) -> Self {
250        use indexed_db_futures::error::OpenDbError;
251        match value {
252            OpenDbError::Base(error) => error.into(),
253            _ => GenericError::from(value.to_string()).into(),
254        }
255    }
256}
257
258type Result<A, E = IndexeddbCryptoStoreError> = std::result::Result<A, E>;
259
260/// Defines an operation to perform on the database.
261enum PendingOperation {
262    Put { key: JsValue, value: JsValue },
263    Delete(JsValue),
264}
265
266/// A struct that represents all the operations that need to be done to the
267/// database when calls to the store `save_changes` are made.
268/// The idea is to do all the serialization and encryption before the
269/// transaction, and then just do the actual Indexeddb operations in the
270/// transaction.
271struct PendingIndexeddbChanges {
272    /// A map of the object store names to the operations to perform on that
273    /// store.
274    store_to_key_values: BTreeMap<&'static str, Vec<PendingOperation>>,
275}
276
277/// Represents the changes on a single object store.
278struct PendingStoreChanges<'a> {
279    operations: &'a mut Vec<PendingOperation>,
280}
281
282impl PendingStoreChanges<'_> {
283    fn put(&mut self, key: JsValue, value: JsValue) {
284        self.operations.push(PendingOperation::Put { key, value });
285    }
286
287    fn delete(&mut self, key: JsValue) {
288        self.operations.push(PendingOperation::Delete(key));
289    }
290}
291
292impl PendingIndexeddbChanges {
293    fn get(&mut self, store: &'static str) -> PendingStoreChanges<'_> {
294        PendingStoreChanges { operations: self.store_to_key_values.entry(store).or_default() }
295    }
296}
297
298impl PendingIndexeddbChanges {
299    fn new() -> Self {
300        Self { store_to_key_values: BTreeMap::new() }
301    }
302
303    /// Returns the list of stores that have pending operations.
304    /// Should be used as the list of store names when starting the indexeddb
305    /// transaction (`transaction_on_multi_with_mode`).
306    fn touched_stores(&self) -> Vec<&str> {
307        self.store_to_key_values
308            .iter()
309            .filter_map(
310                |(store, pending_operations)| {
311                    if !pending_operations.is_empty() {
312                        Some(*store)
313                    } else {
314                        None
315                    }
316                },
317            )
318            .collect()
319    }
320
321    /// Applies all the pending operations to the store.
322    fn apply(self, tx: &Transaction<'_>) -> Result<()> {
323        for (store, operations) in self.store_to_key_values {
324            if operations.is_empty() {
325                continue;
326            }
327            let object_store = tx.object_store(store)?;
328            for op in operations {
329                match op {
330                    PendingOperation::Put { key, value } => {
331                        object_store.put(&value).with_key(key).build()?;
332                    }
333                    PendingOperation::Delete(key) => {
334                        object_store.delete(&key).build()?;
335                    }
336                }
337            }
338        }
339        Ok(())
340    }
341}
342
343impl IndexeddbCryptoStore {
344    pub(crate) async fn open_with_store_cipher(
345        prefix: &str,
346        store_cipher: Option<Arc<StoreCipher>>,
347    ) -> Result<Self> {
348        let name = format!("{prefix:0}::matrix-sdk-crypto");
349
350        let serializer = SafeEncodeSerializer::new(store_cipher);
351        debug!("IndexedDbCryptoStore: opening main store {name}");
352        let db = open_and_upgrade_db(&name, &serializer).await?;
353
354        Ok(Self {
355            name,
356            inner: db,
357            serializer,
358            static_account: RwLock::new(None),
359            save_changes_lock: Default::default(),
360        })
361    }
362
363    /// Open a new `IndexeddbCryptoStore` with default name and no passphrase
364    pub async fn open() -> Result<Self> {
365        IndexeddbCryptoStore::open_with_store_cipher("crypto", None).await
366    }
367
368    /// Open an `IndexeddbCryptoStore` with given name and passphrase.
369    ///
370    /// If the store previously existed, the encryption cipher is initialised
371    /// using the given passphrase and the details from the meta store. If the
372    /// store did not previously exist, a new encryption cipher is derived
373    /// from the passphrase, and the details are stored to the metastore.
374    ///
375    /// The store is then opened, or a new one created, using the encryption
376    /// cipher.
377    ///
378    /// # Arguments
379    ///
380    /// * `prefix` - Common prefix for the names of the two IndexedDB stores.
381    /// * `passphrase` - Passphrase which is used to derive a key to encrypt the
382    ///   key which is used to encrypt the store. Must be the same each time the
383    ///   store is opened.
384    pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result<Self> {
385        let db = open_meta_db(prefix).await?;
386        let store_cipher = load_store_cipher(&db).await?;
387
388        let store_cipher = match store_cipher {
389            Some(cipher) => {
390                debug!("IndexedDbCryptoStore: decrypting store cipher");
391                StoreCipher::import(passphrase, &cipher)
392                    .map_err(|_| CryptoStoreError::UnpicklingError)?
393            }
394            None => {
395                debug!("IndexedDbCryptoStore: encrypting new store cipher");
396                let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
397                #[cfg(not(test))]
398                let export = cipher.export(passphrase);
399                #[cfg(test)]
400                let export = cipher._insecure_export_fast_for_testing(passphrase);
401
402                let export = export.map_err(CryptoStoreError::backend)?;
403
404                save_store_cipher(&db, &export).await?;
405                cipher
406            }
407        };
408
409        // Must release the database access manually as it's not done when
410        // dropping it.
411        db.close();
412
413        IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
414    }
415
416    /// Open an `IndexeddbCryptoStore` with given name and key.
417    ///
418    /// If the store previously existed, the encryption cipher is initialised
419    /// using the given key and the details from the meta store. If the store
420    /// did not previously exist, a new encryption cipher is derived from
421    /// the passphrase, and the details are stored to the metastore.
422    ///
423    /// The store is then opened, or a new one created, using the encryption
424    /// cipher.
425    ///
426    /// # Arguments
427    ///
428    /// * `prefix` - Common prefix for the names of the two IndexedDB stores.
429    /// * `key` - Key with which to encrypt the key which is used to encrypt the
430    ///   store. Must be the same each time the store is opened.
431    pub async fn open_with_key(prefix: &str, key: &[u8; 32]) -> Result<Self> {
432        // The application might also use the provided key for something else, so to
433        // avoid key reuse, we pass the provided key through an HKDF
434        let mut chacha_key = zeroize::Zeroizing::new([0u8; 32]);
435        const HKDF_INFO: &[u8] = b"CRYPTOSTORE_CIPHER";
436        let hkdf = Hkdf::<Sha256>::new(None, key);
437        hkdf.expand(HKDF_INFO, &mut *chacha_key)
438            .expect("We should be able to generate a 32-byte key");
439
440        let db = open_meta_db(prefix).await?;
441        let store_cipher = load_store_cipher(&db).await?;
442
443        let store_cipher = match store_cipher {
444            Some(cipher) => {
445                debug!("IndexedDbCryptoStore: decrypting store cipher");
446                import_store_cipher_with_key(&chacha_key, key, &cipher, &db).await?
447            }
448            None => {
449                debug!("IndexedDbCryptoStore: encrypting new store cipher");
450                let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
451                let export =
452                    cipher.export_with_key(&chacha_key).map_err(CryptoStoreError::backend)?;
453                save_store_cipher(&db, &export).await?;
454                cipher
455            }
456        };
457
458        // Must release the database access manually as it's not done when
459        // dropping it.
460        db.close();
461
462        IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
463    }
464
465    /// Open a new `IndexeddbCryptoStore` with given name and no passphrase
466    pub async fn open_with_name(name: &str) -> Result<Self> {
467        IndexeddbCryptoStore::open_with_store_cipher(name, None).await
468    }
469
470    /// Delete the IndexedDB databases for the given name.
471    #[cfg(test)]
472    pub fn delete_stores(prefix: &str) -> Result<()> {
473        Database::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto-meta"))?;
474        Database::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto"))?;
475        Ok(())
476    }
477
478    fn get_static_account(&self) -> Option<StaticAccountData> {
479        self.static_account.read().unwrap().clone()
480    }
481
482    /// Transform an [`InboundGroupSession`] into a `JsValue` holding a
483    /// [`InboundGroupSessionIndexedDbObject`], ready for storing.
484    async fn serialize_inbound_group_session(
485        &self,
486        session: &InboundGroupSession,
487    ) -> Result<JsValue> {
488        let obj =
489            InboundGroupSessionIndexedDbObject::from_session(session, &self.serializer).await?;
490        Ok(serde_wasm_bindgen::to_value(&obj)?)
491    }
492
493    /// Transform a JsValue holding a [`InboundGroupSessionIndexedDbObject`]
494    /// back into a [`InboundGroupSession`].
495    fn deserialize_inbound_group_session(
496        &self,
497        stored_value: JsValue,
498    ) -> Result<InboundGroupSession> {
499        let idb_object: InboundGroupSessionIndexedDbObject =
500            serde_wasm_bindgen::from_value(stored_value)?;
501        let pickled_session: PickledInboundGroupSession =
502            self.serializer.maybe_decrypt_value(idb_object.pickled_session)?;
503        let session = InboundGroupSession::from_pickle(pickled_session)
504            .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
505
506        // Although a "backed up" flag is stored inside `idb_object.pickled_session`, it
507        // is not maintained when backups are reset. Overwrite the flag with the
508        // needs_backup value from the IDB object.
509        if idb_object.needs_backup {
510            session.reset_backup_state();
511        } else {
512            session.mark_as_backed_up();
513        }
514
515        Ok(session)
516    }
517
518    /// Transform a [`GossipRequest`] into a `JsValue` holding a
519    /// [`GossipRequestIndexedDbObject`], ready for storing.
520    fn serialize_gossip_request(&self, gossip_request: &GossipRequest) -> Result<JsValue> {
521        let obj = GossipRequestIndexedDbObject {
522            // hash the info as a key so that it can be used in index lookups.
523            info: self
524                .serializer
525                .encode_key_as_string(keys::GOSSIP_REQUESTS, gossip_request.info.as_key()),
526
527            // serialize and encrypt the data about the request
528            request: self.serializer.serialize_value_as_bytes(gossip_request)?,
529
530            unsent: !gossip_request.sent_out,
531        };
532
533        Ok(serde_wasm_bindgen::to_value(&obj)?)
534    }
535
536    /// Transform a JsValue holding a [`GossipRequestIndexedDbObject`] back into
537    /// a [`GossipRequest`].
538    fn deserialize_gossip_request(&self, stored_request: JsValue) -> Result<GossipRequest> {
539        let idb_object: GossipRequestIndexedDbObject =
540            serde_wasm_bindgen::from_value(stored_request)?;
541        Ok(self.serializer.deserialize_value_from_bytes(&idb_object.request)?)
542    }
543
544    /// Process all the changes and do all encryption/serialization before the
545    /// actual transaction.
546    ///
547    /// Returns a tuple where the first item is a `PendingIndexeddbChanges`
548    /// struct, and the second item is a boolean indicating whether the session
549    /// cache should be cleared.
550    async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
551        let mut indexeddb_changes = PendingIndexeddbChanges::new();
552
553        let private_identity_pickle =
554            if let Some(i) = &changes.private_identity { Some(i.pickle().await) } else { None };
555
556        let decryption_key_pickle = &changes.backup_decryption_key;
557        let backup_version = &changes.backup_version;
558        let dehydration_pickle_key = &changes.dehydrated_device_pickle_key;
559
560        let mut core = indexeddb_changes.get(keys::CORE);
561        if let Some(next_batch) = &changes.next_batch_token {
562            core.put(
563                JsValue::from_str(keys::NEXT_BATCH_TOKEN),
564                self.serializer.serialize_value(next_batch)?,
565            );
566        }
567
568        if let Some(i) = &private_identity_pickle {
569            core.put(
570                JsValue::from_str(keys::PRIVATE_IDENTITY),
571                self.serializer.serialize_value(i)?,
572            );
573        }
574
575        if let Some(i) = &dehydration_pickle_key {
576            core.put(
577                JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY),
578                self.serializer.serialize_value(i)?,
579            );
580        }
581
582        if let Some(a) = &decryption_key_pickle {
583            indexeddb_changes.get(keys::BACKUP_KEYS).put(
584                JsValue::from_str(keys::RECOVERY_KEY_V1),
585                self.serializer.serialize_value(&a)?,
586            );
587        }
588
589        if let Some(a) = &backup_version {
590            indexeddb_changes.get(keys::BACKUP_KEYS).put(
591                JsValue::from_str(keys::BACKUP_VERSION_V1),
592                self.serializer.serialize_value(&a)?,
593            );
594        }
595
596        if !changes.sessions.is_empty() {
597            let mut sessions = indexeddb_changes.get(keys::SESSION);
598
599            for session in &changes.sessions {
600                let sender_key = session.sender_key().to_base64();
601                let session_id = session.session_id();
602
603                let pickle = session.pickle().await;
604                let key = self.serializer.encode_key(keys::SESSION, (&sender_key, session_id));
605
606                sessions.put(key, self.serializer.serialize_value(&pickle)?);
607            }
608        }
609
610        if !changes.inbound_group_sessions.is_empty() {
611            let mut sessions = indexeddb_changes.get(keys::INBOUND_GROUP_SESSIONS_V3);
612
613            for session in &changes.inbound_group_sessions {
614                let room_id = session.room_id();
615                let session_id = session.session_id();
616                let key = self
617                    .serializer
618                    .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
619                let value = self.serialize_inbound_group_session(session).await?;
620                sessions.put(key, value);
621            }
622        }
623
624        if !changes.outbound_group_sessions.is_empty() {
625            let mut sessions = indexeddb_changes.get(keys::OUTBOUND_GROUP_SESSIONS);
626
627            for session in &changes.outbound_group_sessions {
628                let room_id = session.room_id();
629                let pickle = session.pickle().await;
630                sessions.put(
631                    self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
632                    self.serializer.serialize_value(&pickle)?,
633                );
634            }
635        }
636
637        let device_changes = &changes.devices;
638        let identity_changes = &changes.identities;
639        let olm_hashes = &changes.message_hashes;
640        let key_requests = &changes.key_requests;
641        let withheld_session_info = &changes.withheld_session_info;
642        let room_settings_changes = &changes.room_settings;
643
644        let mut device_store = indexeddb_changes.get(keys::DEVICES);
645
646        for device in device_changes.new.iter().chain(&device_changes.changed) {
647            let key =
648                self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
649            let device = self.serializer.serialize_value(&device)?;
650
651            device_store.put(key, device);
652        }
653
654        for device in &device_changes.deleted {
655            let key =
656                self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
657            device_store.delete(key);
658        }
659
660        if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
661            let mut identities = indexeddb_changes.get(keys::IDENTITIES);
662            for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
663                identities.put(
664                    self.serializer.encode_key(keys::IDENTITIES, identity.user_id()),
665                    self.serializer.serialize_value(&identity)?,
666                );
667            }
668        }
669
670        if !olm_hashes.is_empty() {
671            let mut hashes = indexeddb_changes.get(keys::OLM_HASHES);
672            for hash in olm_hashes {
673                hashes.put(
674                    self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
675                    JsValue::TRUE,
676                );
677            }
678        }
679
680        if !key_requests.is_empty() {
681            let mut gossip_requests = indexeddb_changes.get(keys::GOSSIP_REQUESTS);
682
683            for gossip_request in key_requests {
684                let key_request_id = self
685                    .serializer
686                    .encode_key(keys::GOSSIP_REQUESTS, gossip_request.request_id.as_str());
687                let key_request_value = self.serialize_gossip_request(gossip_request)?;
688                gossip_requests.put(key_request_id, key_request_value);
689            }
690        }
691
692        if !withheld_session_info.is_empty() {
693            let mut withhelds = indexeddb_changes.get(keys::DIRECT_WITHHELD_INFO);
694
695            for (room_id, data) in withheld_session_info {
696                for (session_id, event) in data {
697                    let key = self
698                        .serializer
699                        .encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, &room_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 below, and we're
777        // pickling data as we go, so we don't want to invalidate data we've previously read and
778        // 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 we're
815        // pickling data as we go, so we don't want to invalidate data we've previously read and
816        // 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(
1106            lower_bound, true,
1107            upper_bound, true);
1108
1109        let tx = self
1110            .inner
1111            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1112            .with_mode(TransactionMode::Readonly)
1113            .build()?;
1114
1115        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1116        let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1117        let serialized_sessions =
1118            idx.get_all().with_query::<Array, _>(key).with_limit(limit as u32).await?;
1119
1120        // Deserialize and decrypt after the transaction is complete.
1121        let result = serialized_sessions
1122            .filter_map(Result::ok)
1123            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1124                Ok(session) => Some(session),
1125                Err(e) => {
1126                    warn!("Failed to deserialize inbound group session: {e}");
1127                    None
1128                }
1129            })
1130            .collect::<Vec<InboundGroupSession>>();
1131
1132        Ok(result)
1133    }
1134
1135    async fn inbound_group_session_counts(
1136        &self,
1137        _backup_version: Option<&str>,
1138    ) -> Result<RoomKeyCounts> {
1139        let tx = self
1140            .inner
1141            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1142            .with_mode(TransactionMode::Readonly)
1143            .build()?;
1144        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1145        let all = store.count().await? as usize;
1146        let not_backed_up =
1147            store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count().await? as usize;
1148        tx.commit().await?;
1149        Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1150    }
1151
1152    async fn inbound_group_sessions_for_backup(
1153        &self,
1154        _backup_version: &str,
1155        limit: usize,
1156    ) -> Result<Vec<InboundGroupSession>> {
1157        let tx = self
1158            .inner
1159            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1160            .with_mode(TransactionMode::Readonly)
1161            .build()?;
1162
1163        let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1164        let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1165
1166        // XXX ideally we would use `get_all_with_key_and_limit`, but that doesn't appear to be
1167        //   exposed (https://github.com/Alorel/rust-indexed-db/issues/31). Instead we replicate
1168        //   the behaviour with a cursor.
1169        let Some(mut cursor) = idx.open_cursor().await? else {
1170            return Ok(vec![]);
1171        };
1172
1173        let mut serialized_sessions = Vec::with_capacity(limit);
1174        for _ in 0..limit {
1175            let Some(value) = cursor.next_record().await? else {
1176                break;
1177            };
1178            serialized_sessions.push(value)
1179        }
1180
1181        tx.commit().await?;
1182
1183        // Deserialize and decrypt after the transaction is complete.
1184        let result = serialized_sessions
1185            .into_iter()
1186            .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1187                Ok(session) => Some(session),
1188                Err(e) => {
1189                    warn!("Failed to deserialize inbound group session: {e}");
1190                    None
1191                }
1192            })
1193            .collect::<Vec<InboundGroupSession>>();
1194
1195        Ok(result)
1196    }
1197
1198    async fn mark_inbound_group_sessions_as_backed_up(
1199        &self,
1200        _backup_version: &str,
1201        room_and_session_ids: &[(&RoomId, &str)],
1202    ) -> Result<()> {
1203        let tx = self
1204            .inner
1205            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1206            .with_mode(TransactionMode::Readwrite)
1207            .build()?;
1208
1209        let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1210
1211        for (room_id, session_id) in room_and_session_ids {
1212            let key =
1213                self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1214            if let Some(idb_object_js) = object_store.get(&key).await? {
1215                let mut idb_object: InboundGroupSessionIndexedDbObject =
1216                    serde_wasm_bindgen::from_value(idb_object_js)?;
1217                idb_object.needs_backup = false;
1218                object_store
1219                    .put(&serde_wasm_bindgen::to_value(&idb_object)?)
1220                    .with_key(key)
1221                    .build()?;
1222            } else {
1223                warn!(?key, "Could not find inbound group session to mark it as backed up.");
1224            }
1225        }
1226
1227        Ok(tx.commit().await?)
1228    }
1229
1230    async fn reset_backup_state(&self) -> Result<()> {
1231        let tx = self
1232            .inner
1233            .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1234            .with_mode(TransactionMode::Readwrite)
1235            .build()?;
1236
1237        if let Some(mut cursor) =
1238            tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor().await?
1239        {
1240            while let Some(value) = cursor.next_record().await? {
1241                let mut idb_object: InboundGroupSessionIndexedDbObject =
1242                    serde_wasm_bindgen::from_value(value)?;
1243                if !idb_object.needs_backup {
1244                    idb_object.needs_backup = true;
1245                    // We don't bother to update the encrypted `InboundGroupSession` object stored
1246                    // inside `idb_object.data`, since that would require decryption and encryption.
1247                    // Instead, it will be patched up by `deserialize_inbound_group_session`.
1248                    let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1249                    cursor.update(&idb_object).await?;
1250                }
1251            }
1252        }
1253
1254        Ok(tx.commit().await?)
1255    }
1256
1257    async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1258        let tx = self
1259            .inner
1260            .transaction(keys::TRACKED_USERS)
1261            .with_mode(TransactionMode::Readwrite)
1262            .build()?;
1263        let os = tx.object_store(keys::TRACKED_USERS)?;
1264
1265        for (user, dirty) in users {
1266            os.put(&JsValue::from(*dirty)).with_key(JsValue::from_str(user.as_str())).build()?;
1267        }
1268
1269        tx.commit().await?;
1270        Ok(())
1271    }
1272
1273    async fn get_device(
1274        &self,
1275        user_id: &UserId,
1276        device_id: &DeviceId,
1277    ) -> Result<Option<DeviceData>> {
1278        let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1279        self.inner
1280            .transaction(keys::DEVICES)
1281            .with_mode(TransactionMode::Readonly)
1282            .build()?
1283            .object_store(keys::DEVICES)?
1284            .get(&key)
1285            .await?
1286            .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1287            .transpose()
1288    }
1289
1290    async fn get_user_devices(
1291        &self,
1292        user_id: &UserId,
1293    ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1294        let range = self.serializer.encode_to_range(keys::DEVICES, user_id);
1295        Ok(self
1296            .inner
1297            .transaction(keys::DEVICES)
1298            .with_mode(TransactionMode::Readonly)
1299            .build()?
1300            .object_store(keys::DEVICES)?
1301            .get_all()
1302            .with_query(&range)
1303            .await?
1304            .filter_map(Result::ok)
1305            .filter_map(|d| {
1306                let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1307                Some((d.device_id().to_owned(), d))
1308            })
1309            .collect::<HashMap<_, _>>())
1310    }
1311
1312    async fn get_own_device(&self) -> Result<DeviceData> {
1313        let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1314        Ok(self.get_device(&account_info.user_id, &account_info.device_id).await?.unwrap())
1315    }
1316
1317    async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1318        self.inner
1319            .transaction(keys::IDENTITIES)
1320            .with_mode(TransactionMode::Readonly)
1321            .build()?
1322            .object_store(keys::IDENTITIES)?
1323            .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))
1324            .await?
1325            .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1326            .transpose()
1327    }
1328
1329    async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1330        Ok(self
1331            .inner
1332            .transaction(keys::OLM_HASHES)
1333            .with_mode(TransactionMode::Readonly)
1334            .build()?
1335            .object_store(keys::OLM_HASHES)?
1336            .get::<JsValue, _, _>(&self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)))
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.inner
1368            .transaction(keys::SECRETS_INBOX)
1369            .with_mode(TransactionMode::Readwrite)
1370            .build()?;
1371        transaction
1372            .object_store(keys::SECRETS_INBOX)?
1373            .delete(&range)
1374            .build()?;
1375        transaction.commit().await?;
1376
1377        Ok(())
1378    }
1379
1380    async fn get_secret_request_by_info(
1381        &self,
1382        key_info: &SecretInfo,
1383    ) -> Result<Option<GossipRequest>> {
1384        let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1385
1386        let val = self
1387            .inner
1388            .transaction(keys::GOSSIP_REQUESTS).with_mode( TransactionMode::Readonly).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<RoomKeyWithheldEvent>> {
1485        let key = self.serializer.encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, room_id));
1486        if let Some(pickle) = self
1487            .inner
1488            .transaction(keys::DIRECT_WITHHELD_INFO).with_mode( TransactionMode::Readonly).build()?
1489            .object_store(keys::DIRECT_WITHHELD_INFO)?
1490            .get(&key)
1491            .await?
1492        {
1493            let info = self.serializer.deserialize_value(pickle)?;
1494            Ok(Some(info))
1495        } else {
1496            Ok(None)
1497        }
1498    }
1499
1500    async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1501        let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1502        self.inner
1503            .transaction(keys::ROOM_SETTINGS)
1504            .with_mode(TransactionMode::Readonly)
1505            .build()?
1506            .object_store(keys::ROOM_SETTINGS)?
1507            .get(&key)
1508            .await?
1509            .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1510            .transpose()
1511    }
1512
1513    async fn get_received_room_key_bundle_data(
1514        &self,
1515        room_id: &RoomId,
1516        user_id: &UserId,
1517    ) -> Result<Option<StoredRoomKeyBundleData>> {
1518        let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1519        let result = self
1520            .inner
1521            .transaction(keys::RECEIVED_ROOM_KEY_BUNDLES)
1522            .with_mode(TransactionMode::Readonly)
1523            .build()?
1524            .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1525            .get(&key)
1526            .await?
1527            .map(|v| self.serializer.deserialize_value(v))
1528            .transpose()?;
1529
1530        Ok(result)
1531    }
1532
1533    async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1534        self.inner
1535            .transaction(keys::CORE)
1536            .with_mode(TransactionMode::Readonly)
1537            .build()?
1538            .object_store(keys::CORE)?
1539            .get(&JsValue::from_str(key))
1540            .await?
1541            .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1542            .transpose()
1543    }
1544
1545    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1546    async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1547        let transaction = self.inner
1548            .transaction(keys::CORE)
1549            .with_mode(TransactionMode::Readwrite)
1550            .build()?;
1551        transaction
1552            .object_store(keys::CORE)?
1553            .put(&self.serializer.serialize_value(&value)?)
1554            .with_key(JsValue::from_str(key))
1555            .build()?;
1556        transaction.commit().await?;
1557        Ok(())
1558    }
1559
1560    #[allow(clippy::unused_async)] // Mandated by trait on wasm.
1561    async fn remove_custom_value(&self, key: &str) -> Result<()> {
1562        let transaction = self.inner
1563            .transaction(keys::CORE)
1564            .with_mode(TransactionMode::Readwrite)
1565            .build()?;
1566        transaction
1567            .object_store(keys::CORE)?
1568            .delete(&JsValue::from_str(key))
1569            .build()?;
1570        transaction.commit().await?;
1571        Ok(())
1572    }
1573
1574    async fn try_take_leased_lock(
1575        &self,
1576        lease_duration_ms: u32,
1577        key: &str,
1578        holder: &str,
1579    ) -> Result<bool> {
1580        // As of 2023-06-23, the code below hasn't been tested yet.
1581        let key = JsValue::from_str(key);
1582        let txn =
1583            self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1584        let object_store = txn.object_store(keys::CORE)?;
1585
1586        #[derive(serde::Deserialize, serde::Serialize)]
1587        struct Lease {
1588            holder: String,
1589            expiration_ts: u64,
1590        }
1591
1592        let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1593        let expiration_ts = now_ts + lease_duration_ms as u64;
1594
1595        let prev = object_store.get(&key).await?;
1596        match prev {
1597            Some(prev) => {
1598                let lease: Lease = self.serializer.deserialize_value(prev)?;
1599                if lease.holder == holder || lease.expiration_ts < now_ts {
1600                    object_store
1601                        .put(
1602                            &self.serializer.serialize_value(&Lease {
1603                                holder: holder.to_owned(),
1604                                expiration_ts,
1605                            })?,
1606                        )
1607                        .with_key(key)
1608                        .build()?;
1609                    Ok(true)
1610                } else {
1611                    Ok(false)
1612                }
1613            }
1614            None => {
1615                object_store
1616                    .put(
1617                        &self
1618                            .serializer
1619                            .serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?,
1620                    )
1621                    .with_key(key)
1622                    .build()?;
1623                Ok(true)
1624            }
1625        }
1626    }
1627}
1628
1629impl Drop for IndexeddbCryptoStore {
1630    fn drop(&mut self) {
1631        // Must release the database access manually as it's not done when
1632        // dropping it.
1633        self.inner.as_sys().close();
1634    }
1635}
1636
1637/// Open the meta store.
1638///
1639/// The meta store contains details about the encryption of the main store.
1640async fn open_meta_db(prefix: &str) -> Result<Database, IndexeddbCryptoStoreError> {
1641    let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1642
1643    debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1644    Database::open(&name)
1645        .with_version(1u32)
1646        .with_on_upgrade_needed(|evt, tx| {
1647            let old_version = evt.old_version() as u32;
1648            if old_version < 1 {
1649                // migrating to version 1
1650                tx.db().create_object_store("matrix-sdk-crypto").build()?;
1651            }
1652            Ok(())
1653        })
1654        .await
1655        .map_err(Into::into)
1656}
1657
1658/// Load the serialised store cipher from the meta store.
1659///
1660/// # Arguments:
1661///
1662/// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1663///
1664/// # Returns:
1665///
1666/// The serialised `StoreCipher` object.
1667async fn load_store_cipher(
1668    meta_db: &Database,
1669) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1670    let tx: Transaction<'_> =
1671        meta_db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readonly).build()?;
1672    let ob = tx.object_store("matrix-sdk-crypto")?;
1673
1674    let store_cipher: Option<Vec<u8>> = ob
1675        .get(&JsValue::from_str(keys::STORE_CIPHER))
1676        .await?
1677        .map(|k: JsValue| k.into_serde())
1678        .transpose()?;
1679    Ok(store_cipher)
1680}
1681
1682/// Save the serialised store cipher to the meta store.
1683///
1684/// # Arguments:
1685///
1686/// * `meta_db`: Connection to the meta store, as returned by [`open_meta_db`].
1687/// * `store_cipher`: The serialised `StoreCipher` object.
1688async fn save_store_cipher(
1689    db: &Database,
1690    export: &Vec<u8>,
1691) -> Result<(), IndexeddbCryptoStoreError> {
1692    let tx: Transaction<'_> =
1693        db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readwrite).build()?;
1694    let ob = tx.object_store("matrix-sdk-crypto")?;
1695
1696    ob.put(&JsValue::from_serde(&export)?)
1697        .with_key(JsValue::from_str(keys::STORE_CIPHER))
1698        .build()?;
1699    tx.commit().await?;
1700    Ok(())
1701}
1702
1703/// Given a serialised store cipher, try importing with the given key.
1704///
1705/// This is a helper for [`IndexeddbCryptoStore::open_with_key`].
1706///
1707/// # Arguments
1708///
1709/// * `chacha_key`: The key to use with [`StoreCipher::import_with_key`].
1710///   Derived from `original_key` via an HKDF.
1711/// * `original_key`: The key provided by the application. Used to provide a
1712///   migration path from an older key derivation system.
1713/// * `serialised_cipher`: The serialized `EncryptedStoreCipher`, retrieved from
1714///   the database.
1715/// * `db`: Connection to the database.
1716async fn import_store_cipher_with_key(
1717    chacha_key: &[u8; 32],
1718    original_key: &[u8],
1719    serialised_cipher: &[u8],
1720    db: &Database,
1721) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1722    let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1723        Ok(cipher) => cipher,
1724        Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1725            // Old versions of the matrix-js-sdk used to base64-encode their encryption
1726            // key, and pass it into [`IndexeddbCryptoStore::open_with_passphrase`]. For
1727            // backwards compatibility, we fall back to that if we discover we have a cipher
1728            // encrypted with a KDF when we expected it to be encrypted directly with a key.
1729            let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1730                .map_err(|_| CryptoStoreError::UnpicklingError)?;
1731
1732            // Loading the cipher with the passphrase was successful. Let's update the
1733            // stored version of the cipher so that it is encrypted with a key,
1734            // to save doing this again.
1735            debug!(
1736                "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1737            );
1738
1739            let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1740            save_store_cipher(db, &export).await?;
1741            cipher
1742        }
1743        Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1744    };
1745    Ok(cipher)
1746}
1747
1748/// Fetch items from an object store in batches, transform each item using
1749/// the supplied function, and stuff the transformed items into a single
1750/// vector to return.
1751async fn fetch_from_object_store_batched<R, F>(
1752    object_store: ObjectStore<'_>,
1753    f: F,
1754    batch_size: usize,
1755) -> Result<Vec<R>>
1756where
1757    F: Fn(JsValue) -> Result<R>,
1758{
1759    let mut result = Vec::new();
1760    let mut batch_n = 0;
1761
1762    // The empty string is before all keys in Indexed DB - first batch starts there.
1763    let mut latest_key: JsValue = "".into();
1764
1765    loop {
1766        debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1767
1768        // See https://github.com/Alorel/rust-indexed-db/issues/31 - we
1769        // would like to use `get_all_with_key_and_limit` if it ever exists
1770        // but for now we use a cursor and manually limit batch size.
1771
1772        // Get hold of a cursor for this batch. (This should not panic in expect()
1773        // because we always use "", or the result of cursor.key(), both of
1774        // which are valid keys.)
1775        let after_latest_key = KeyRange::LowerBound(&latest_key, true);
1776        let cursor = object_store.open_cursor().with_query(&after_latest_key).await?;
1777
1778        // Fetch batch_size records into result
1779        let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1780        if let Some(next_key) = next_key {
1781            latest_key = next_key;
1782        } else {
1783            break;
1784        }
1785
1786        batch_n += 1;
1787    }
1788
1789    Ok(result)
1790}
1791
1792/// Fetch batch_size records from the supplied cursor,
1793/// and return the last key we processed, or None if
1794/// we reached the end of the cursor.
1795async fn fetch_batch<R, F, Q>(
1796    cursor: Option<Cursor<'_, Q>>,
1797    batch_size: usize,
1798    f: &F,
1799    result: &mut Vec<R>,
1800) -> Result<Option<JsValue>>
1801where
1802    F: Fn(JsValue) -> Result<R>,
1803    Q: QuerySource,
1804{
1805    let Some(mut cursor) = cursor else {
1806        // Cursor was None - there are no more records
1807        return Ok(None);
1808    };
1809
1810    let mut latest_key = None;
1811
1812    for _ in 0..batch_size {
1813        let Some(value) = cursor.next_record().await? else {
1814            return Ok(None);
1815        };
1816
1817        // Process the record
1818        let processed = f(value);
1819        if let Ok(processed) = processed {
1820            result.push(processed);
1821        }
1822        // else processing failed: don't return this record at all
1823
1824        // Remember that we have processed this record, so if we hit
1825        // the end of the batch, the next batch can start after this one
1826        if let Some(key) = cursor.key()? {
1827            latest_key = Some(key);
1828        }
1829    }
1830
1831    // We finished the batch but there are more records -
1832    // return the key of the last one we processed
1833    Ok(latest_key)
1834}
1835
1836/// The objects we store in the gossip_requests indexeddb object store
1837#[derive(Debug, serde::Serialize, serde::Deserialize)]
1838struct GossipRequestIndexedDbObject {
1839    /// Encrypted hash of the [`SecretInfo`] structure.
1840    info: String,
1841
1842    /// Encrypted serialised representation of the [`GossipRequest`] as a whole.
1843    request: Vec<u8>,
1844
1845    /// Whether the request has yet to be sent out.
1846    ///
1847    /// Since we only need to be able to find requests where this is `true`, we
1848    /// skip serialization in cases where it is `false`. That has the effect
1849    /// of omitting it from the indexeddb index.
1850    ///
1851    /// We also use a custom serializer because bools can't be used as keys in
1852    /// indexeddb.
1853    #[serde(
1854        default,
1855        skip_serializing_if = "std::ops::Not::not",
1856        with = "crate::serializer::foreign::bool"
1857    )]
1858    unsent: bool,
1859}
1860
1861/// The objects we store in the inbound_group_sessions3 indexeddb object store
1862#[derive(serde::Serialize, serde::Deserialize)]
1863struct InboundGroupSessionIndexedDbObject {
1864    /// Possibly encrypted
1865    /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`]
1866    pickled_session: MaybeEncrypted,
1867
1868    /// The (hashed) session ID of this session. This is somewhat redundant, but
1869    /// we have to pull it out to its own object so that we can do batched
1870    /// queries such as
1871    /// [`IndexeddbStore::get_inbound_group_sessions_for_device_batch`].
1872    ///
1873    /// Added in database schema v12, and lazily populated, so it is only
1874    /// present for sessions received or modified since DB schema v12.
1875    #[serde(default, skip_serializing_if = "Option::is_none")]
1876    session_id: Option<String>,
1877
1878    /// Whether the session data has yet to be backed up.
1879    ///
1880    /// Since we only need to be able to find entries where this is `true`, we
1881    /// skip serialization in cases where it is `false`. That has the effect
1882    /// of omitting it from the indexeddb index.
1883    ///
1884    /// We also use a custom serializer because bools can't be used as keys in
1885    /// indexeddb.
1886    #[serde(
1887        default,
1888        skip_serializing_if = "std::ops::Not::not",
1889        with = "crate::serializer::foreign::bool"
1890    )]
1891    needs_backup: bool,
1892
1893    /// Unused: for future compatibility. In future, will contain the order
1894    /// number (not the ID!) of the backup for which this key has been
1895    /// backed up. This will replace `needs_backup`, fixing the performance
1896    /// problem identified in
1897    /// https://github.com/element-hq/element-web/issues/26892
1898    /// because we won't need to update all records when we spot a new backup
1899    /// version.
1900    /// In this version of the code, this is always set to -1, meaning:
1901    /// "refer to the `needs_backup` property". See:
1902    /// https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076
1903    backed_up_to: i32,
1904
1905    /// The (hashed) curve25519 key of the device that sent us this room key,
1906    /// base64-encoded.
1907    ///
1908    /// Added in database schema v12, and lazily populated, so it is only
1909    /// present for sessions received or modified since DB schema v12.
1910    #[serde(default, skip_serializing_if = "Option::is_none")]
1911    sender_key: Option<String>,
1912
1913    /// The type of the [`SenderData`] within this session, converted to a u8
1914    /// from [`SenderDataType`].
1915    ///
1916    /// Added in database schema v12, and lazily populated, so it is only
1917    /// present for sessions received or modified since DB schema v12.
1918    #[serde(default, skip_serializing_if = "Option::is_none")]
1919    sender_data_type: Option<u8>,
1920}
1921
1922impl InboundGroupSessionIndexedDbObject {
1923    /// Build an [`InboundGroupSessionIndexedDbObject`] wrapping the given
1924    /// session.
1925    pub async fn from_session(
1926        session: &InboundGroupSession,
1927        serializer: &SafeEncodeSerializer,
1928    ) -> Result<Self, CryptoStoreError> {
1929        let session_id =
1930            serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
1931
1932        let sender_key = serializer.encode_key_as_string(
1933            keys::INBOUND_GROUP_SESSIONS_V3,
1934            session.sender_key().to_base64(),
1935        );
1936
1937        Ok(InboundGroupSessionIndexedDbObject {
1938            pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
1939            session_id: Some(session_id),
1940            needs_backup: !session.backed_up(),
1941            backed_up_to: -1,
1942            sender_key: Some(sender_key),
1943            sender_data_type: Some(session.sender_data_type() as u8),
1944        })
1945    }
1946}
1947
1948#[cfg(test)]
1949mod unit_tests {
1950    use matrix_sdk_crypto::{
1951        olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
1952        types::EventEncryptionAlgorithm,
1953        vodozemac::Ed25519Keypair,
1954    };
1955    use matrix_sdk_store_encryption::EncryptedValueBase64;
1956    use matrix_sdk_test::async_test;
1957    use ruma::{device_id, room_id, user_id};
1958
1959    use super::InboundGroupSessionIndexedDbObject;
1960    use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
1961
1962    #[test]
1963    fn needs_backup_is_serialized_as_a_u8_in_json() {
1964        let session_needs_backup = backup_test_session(true);
1965
1966        // Testing the exact JSON here is theoretically flaky in the face of
1967        // serialization changes in serde_json but it seems unlikely, and it's
1968        // simple enough to fix if we need to.
1969        assert!(serde_json::to_string(&session_needs_backup)
1970            .unwrap()
1971            .contains(r#""needs_backup":1"#),);
1972    }
1973
1974    #[test]
1975    fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
1976        let session_backed_up = backup_test_session(false);
1977
1978        assert!(
1979            !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
1980            "The needs_backup field should be missing!"
1981        );
1982    }
1983
1984    pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
1985        InboundGroupSessionIndexedDbObject {
1986            pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
1987            session_id: None,
1988            needs_backup,
1989            backed_up_to: -1,
1990            sender_key: None,
1991            sender_data_type: None,
1992        }
1993    }
1994
1995    #[async_test]
1996    async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
1997        let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1998
1999        let sender_data = SenderData::sender_verified(
2000            user_id!("@test:user"),
2001            device_id!("ABC"),
2002            Ed25519Keypair::new().public_key(),
2003        );
2004
2005        let db_object = sender_data_test_session(sender_key, sender_data).await;
2006        let serialized = serde_json::to_string(&db_object).unwrap();
2007
2008        assert!(
2009            serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
2010        );
2011        assert!(serialized.contains(r#""sender_data_type":5"#));
2012    }
2013
2014    pub async fn sender_data_test_session(
2015        sender_key: Curve25519PublicKey,
2016        sender_data: SenderData,
2017    ) -> InboundGroupSessionIndexedDbObject {
2018        let session = InboundGroupSession::new(
2019            sender_key,
2020            Ed25519Keypair::new().public_key(),
2021            room_id!("!test:localhost"),
2022            // Arbitrary session data
2023            &SessionKey::from_base64(
2024                "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
2025                 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
2026                 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
2027                 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
2028                 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
2029            )
2030            .unwrap(),
2031            sender_data,
2032            EventEncryptionAlgorithm::MegolmV1AesSha2,
2033            None,
2034            false,
2035        )
2036        .unwrap();
2037
2038        InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
2039            .await
2040            .unwrap()
2041    }
2042}
2043
2044#[cfg(all(test, target_family = "wasm"))]
2045mod wasm_unit_tests {
2046    use std::collections::BTreeMap;
2047
2048    use matrix_sdk_crypto::{
2049        olm::{Curve25519PublicKey, SenderData},
2050        types::{DeviceKeys, Signatures},
2051    };
2052    use matrix_sdk_test::async_test;
2053    use ruma::{device_id, user_id};
2054    use wasm_bindgen::JsValue;
2055
2056    use crate::crypto_store::unit_tests::sender_data_test_session;
2057
2058    fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
2059        assert_eq!(
2060            js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
2061            JsValue::from_f64(expected.into())
2062        );
2063    }
2064
2065    #[async_test]
2066    fn test_needs_backup_is_serialized_as_a_u8_in_js() {
2067        let session_needs_backup = super::unit_tests::backup_test_session(true);
2068
2069        let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
2070
2071        assert!(js_value.is_object());
2072        assert_field_equals(&js_value, "needs_backup", 1);
2073    }
2074
2075    #[async_test]
2076    fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
2077        let session_backed_up = super::unit_tests::backup_test_session(false);
2078
2079        let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
2080
2081        assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
2082    }
2083
2084    #[async_test]
2085    async fn test_sender_key_and_device_type_are_serialized_in_js() {
2086        let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2087
2088        let sender_data = SenderData::device_info(DeviceKeys::new(
2089            user_id!("@test:user").to_owned(),
2090            device_id!("ABC").to_owned(),
2091            vec![],
2092            BTreeMap::new(),
2093            Signatures::new(),
2094        ));
2095        let db_object = sender_data_test_session(sender_key, sender_data).await;
2096
2097        let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
2098
2099        assert!(js_value.is_object());
2100        assert_field_equals(&js_value, "sender_data_type", 2);
2101        assert_eq!(
2102            js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
2103            JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
2104        );
2105    }
2106}
2107
2108#[cfg(all(test, target_family = "wasm"))]
2109mod tests {
2110    use matrix_sdk_crypto::cryptostore_integration_tests;
2111
2112    use super::IndexeddbCryptoStore;
2113
2114    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2115
2116    async fn get_store(
2117        name: &str,
2118        passphrase: Option<&str>,
2119        clear_data: bool,
2120    ) -> IndexeddbCryptoStore {
2121        if clear_data {
2122            IndexeddbCryptoStore::delete_stores(name).unwrap();
2123        }
2124        match passphrase {
2125            Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2126                .await
2127                .expect("Can't create a passphrase protected store"),
2128            None => IndexeddbCryptoStore::open_with_name(name)
2129                .await
2130                .expect("Can't create store without passphrase"),
2131        }
2132    }
2133
2134    cryptostore_integration_tests!();
2135}
2136
2137#[cfg(all(test, target_family = "wasm"))]
2138mod encrypted_tests {
2139    use matrix_sdk_crypto::{
2140        cryptostore_integration_tests,
2141        olm::Account,
2142        store::{types::PendingChanges, CryptoStore},
2143        vodozemac::base64_encode,
2144    };
2145    use matrix_sdk_test::async_test;
2146    use ruma::{device_id, user_id};
2147
2148    use super::IndexeddbCryptoStore;
2149
2150    wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2151
2152    async fn get_store(
2153        name: &str,
2154        passphrase: Option<&str>,
2155        clear_data: bool,
2156    ) -> IndexeddbCryptoStore {
2157        if clear_data {
2158            IndexeddbCryptoStore::delete_stores(name).unwrap();
2159        }
2160
2161        let pass = passphrase.unwrap_or(name);
2162        IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2163            .await
2164            .expect("Can't create a passphrase protected store")
2165    }
2166    cryptostore_integration_tests!();
2167
2168    /// Test that we can migrate a store created with a passphrase, to being
2169    /// encrypted with a key instead.
2170    #[async_test]
2171    async fn test_migrate_passphrase_to_key() {
2172        let store_name = "test_migrate_passphrase_to_key";
2173        let passdata: [u8; 32] = rand::random();
2174        let b64_passdata = base64_encode(passdata);
2175
2176        // Initialise the store with some account data
2177        IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2178        let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2179            .await
2180            .expect("Can't create a passphrase-protected store");
2181
2182        store
2183            .save_pending_changes(PendingChanges {
2184                account: Some(Account::with_device_id(
2185                    user_id!("@alice:example.org"),
2186                    device_id!("ALICEDEVICE"),
2187                )),
2188            })
2189            .await
2190            .expect("Can't save account");
2191
2192        // Now reopen the store, passing the key directly rather than as a b64 string.
2193        let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2194            .await
2195            .expect("Can't create a key-protected store");
2196        let loaded_account =
2197            store.load_account().await.expect("Can't load account").expect("Account was not saved");
2198        assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2199    }
2200}