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