1use std::{
16 collections::{BTreeMap, HashMap},
17 sync::{Arc, RwLock},
18};
19
20use async_trait::async_trait;
21use gloo_utils::format::JsValueSerdeExt;
22use hkdf::Hkdf;
23use indexed_db_futures::{
24 KeyRange,
25 cursor::Cursor,
26 database::Database,
27 internals::SystemRepr,
28 object_store::ObjectStore,
29 prelude::*,
30 transaction::{Transaction, TransactionMode},
31};
32use js_sys::Array;
33use matrix_sdk_base::cross_process_lock::{
34 CrossProcessLockGeneration, FIRST_CROSS_PROCESS_LOCK_GENERATION,
35};
36use matrix_sdk_crypto::{
37 Account, DeviceData, GossipRequest, SecretInfo, TrackedUser, UserIdentityData,
38 olm::{
39 Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
40 PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session,
41 StaticAccountData,
42 },
43 store::{
44 CryptoStore, CryptoStoreError,
45 types::{
46 BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts,
47 RoomKeyWithheldEntry, RoomPendingKeyBundleDetails, RoomSettings,
48 StoredRoomKeyBundleData,
49 },
50 },
51 vodozemac::base64_encode,
52};
53use matrix_sdk_store_encryption::StoreCipher;
54use ruma::{
55 DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId, RoomId, TransactionId, UserId,
56 events::secret::request::SecretName,
57};
58use serde::{Deserialize, Serialize};
59use sha2::Sha256;
60use tokio::sync::Mutex;
61use tracing::{debug, warn};
62use wasm_bindgen::JsValue;
63
64use crate::{
65 crypto_store::migrations::open_and_upgrade_db,
66 error::GenericError,
67 serializer::{MaybeEncrypted, SafeEncodeSerializer, SafeEncodeSerializerError},
68};
69
70mod migrations;
71
72mod keys {
73 pub const CORE: &str = "core";
75
76 pub const SESSION: &str = "session";
77
78 pub const INBOUND_GROUP_SESSIONS_V3: &str = "inbound_group_sessions3";
79 pub const INBOUND_GROUP_SESSIONS_BACKUP_INDEX: &str = "backup";
80 pub const INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX: &str = "backed_up_to";
81 pub const INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX: &str =
82 "inbound_group_session_sender_key_sender_data_type_idx";
83
84 pub const OUTBOUND_GROUP_SESSIONS: &str = "outbound_group_sessions";
85
86 pub const TRACKED_USERS: &str = "tracked_users";
87 pub const OLM_HASHES: &str = "olm_hashes";
88
89 pub const DEVICES: &str = "devices";
90 pub const IDENTITIES: &str = "identities";
91
92 pub const GOSSIP_REQUESTS: &str = "gossip_requests";
93 pub const GOSSIP_REQUESTS_UNSENT_INDEX: &str = "unsent";
94 pub const GOSSIP_REQUESTS_BY_INFO_INDEX: &str = "by_info";
95
96 pub const ROOM_SETTINGS: &str = "room_settings";
97
98 pub const SECRETS_INBOX_V2: &str = "secrets_inbox2";
99
100 pub const WITHHELD_SESSIONS: &str = "withheld_sessions";
101
102 pub const RECEIVED_ROOM_KEY_BUNDLES: &str = "received_room_key_bundles";
103
104 pub const LEASE_LOCKS: &str = "lease_locks";
105
106 pub const ROOM_KEY_BACKUPS_FULLY_DOWNLOADED: &str = "room_key_backups_fully_downloaded";
107 pub const ROOMS_PENDING_KEY_BUNDLE: &str = "rooms_pending_key_bundle";
108
109 pub const STORE_CIPHER: &str = "store_cipher";
111 pub const ACCOUNT: &str = "account";
112 pub const NEXT_BATCH_TOKEN: &str = "next_batch_token";
113 pub const PRIVATE_IDENTITY: &str = "private_identity";
114
115 pub const BACKUP_KEYS: &str = "backup_keys";
117
118 pub const BACKUP_VERSION_V1: &str = "backup_version_v1";
121
122 pub const RECOVERY_KEY_V1: &str = "recovery_key_v1";
128
129 pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key";
131}
132
133pub struct IndexeddbCryptoStore {
138 static_account: RwLock<Option<StaticAccountData>>,
139 name: String,
140 pub(crate) inner: Database,
141
142 serializer: SafeEncodeSerializer,
143 save_changes_lock: Arc<Mutex<()>>,
144}
145
146#[cfg(not(tarpaulin_include))]
147impl std::fmt::Debug for IndexeddbCryptoStore {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("IndexeddbCryptoStore").field("name", &self.name).finish()
150 }
151}
152
153#[derive(Debug, thiserror::Error)]
154pub enum IndexeddbCryptoStoreError {
155 #[error(transparent)]
156 Serialization(#[from] serde_json::Error),
157 #[error("DomException {name} ({code}): {message}")]
158 DomException {
159 code: u16,
161 name: String,
163 message: String,
165 },
166 #[error(transparent)]
167 CryptoStoreError(#[from] CryptoStoreError),
168 #[error(
169 "The schema version of the crypto store is too new. \
170 Existing version: {current_version}; max supported version: {max_supported_version}"
171 )]
172 SchemaTooNewError { max_supported_version: u32, current_version: u32 },
173}
174
175impl From<SafeEncodeSerializerError> for IndexeddbCryptoStoreError {
176 fn from(value: SafeEncodeSerializerError) -> Self {
177 match value {
178 SafeEncodeSerializerError::Serialization(error) => Self::Serialization(error),
179 SafeEncodeSerializerError::DomException { code, name, message } => {
180 Self::DomException { code, name, message }
181 }
182 SafeEncodeSerializerError::CryptoStoreError(crypto_store_error) => {
183 Self::CryptoStoreError(crypto_store_error)
184 }
185 }
186 }
187}
188
189impl From<web_sys::DomException> for IndexeddbCryptoStoreError {
190 fn from(frm: web_sys::DomException) -> IndexeddbCryptoStoreError {
191 IndexeddbCryptoStoreError::DomException {
192 name: frm.name(),
193 message: frm.message(),
194 code: frm.code(),
195 }
196 }
197}
198
199impl From<serde_wasm_bindgen::Error> for IndexeddbCryptoStoreError {
200 fn from(e: serde_wasm_bindgen::Error) -> Self {
201 IndexeddbCryptoStoreError::Serialization(serde::de::Error::custom(e.to_string()))
202 }
203}
204
205impl From<IndexeddbCryptoStoreError> for CryptoStoreError {
206 fn from(frm: IndexeddbCryptoStoreError) -> CryptoStoreError {
207 match frm {
208 IndexeddbCryptoStoreError::Serialization(e) => CryptoStoreError::Serialization(e),
209 IndexeddbCryptoStoreError::CryptoStoreError(e) => e,
210 _ => CryptoStoreError::backend(frm),
211 }
212 }
213}
214
215impl From<indexed_db_futures::error::DomException> for IndexeddbCryptoStoreError {
216 fn from(value: indexed_db_futures::error::DomException) -> Self {
217 web_sys::DomException::from(value).into()
218 }
219}
220
221impl From<indexed_db_futures::error::SerialisationError> for IndexeddbCryptoStoreError {
222 fn from(value: indexed_db_futures::error::SerialisationError) -> Self {
223 Self::Serialization(serde::de::Error::custom(value.to_string()))
224 }
225}
226
227impl From<indexed_db_futures::error::UnexpectedDataError> for IndexeddbCryptoStoreError {
228 fn from(value: indexed_db_futures::error::UnexpectedDataError) -> Self {
229 Self::CryptoStoreError(CryptoStoreError::backend(value))
230 }
231}
232
233impl From<GenericError> for IndexeddbCryptoStoreError {
234 fn from(value: GenericError) -> Self {
235 Self::CryptoStoreError(value.into())
236 }
237}
238
239impl From<indexed_db_futures::error::JSError> for IndexeddbCryptoStoreError {
240 fn from(value: indexed_db_futures::error::JSError) -> Self {
241 GenericError::from(value.to_string()).into()
242 }
243}
244
245impl From<indexed_db_futures::error::Error> for IndexeddbCryptoStoreError {
246 fn from(value: indexed_db_futures::error::Error) -> Self {
247 use indexed_db_futures::error::Error;
248 match value {
249 Error::DomException(e) => e.into(),
250 Error::Serialisation(e) => e.into(),
251 Error::MissingData(e) => e.into(),
252 Error::Unknown(e) => e.into(),
253 }
254 }
255}
256
257impl From<indexed_db_futures::error::OpenDbError> for IndexeddbCryptoStoreError {
258 fn from(value: indexed_db_futures::error::OpenDbError) -> Self {
259 use indexed_db_futures::error::OpenDbError;
260 match value {
261 OpenDbError::Base(error) => error.into(),
262 _ => GenericError::from(value.to_string()).into(),
263 }
264 }
265}
266
267type Result<A, E = IndexeddbCryptoStoreError> = std::result::Result<A, E>;
268
269enum PendingOperation {
271 Put { key: JsValue, value: JsValue },
272 Delete(JsValue),
273 DeleteByIndex { index: &'static str, key: JsValue },
274}
275
276struct PendingIndexeddbChanges {
281 store_to_key_values: BTreeMap<&'static str, Vec<PendingOperation>>,
284}
285
286struct PendingStoreChanges<'a> {
288 operations: &'a mut Vec<PendingOperation>,
289}
290
291impl PendingStoreChanges<'_> {
292 fn put(&mut self, key: JsValue, value: JsValue) {
293 self.operations.push(PendingOperation::Put { key, value });
294 }
295
296 fn delete(&mut self, key: JsValue) {
297 self.operations.push(PendingOperation::Delete(key));
298 }
299
300 fn delete_by_index(&mut self, index: &'static str, key: JsValue) {
301 self.operations.push(PendingOperation::DeleteByIndex { index, key });
302 }
303}
304
305impl PendingIndexeddbChanges {
306 fn get(&mut self, store: &'static str) -> PendingStoreChanges<'_> {
307 PendingStoreChanges { operations: self.store_to_key_values.entry(store).or_default() }
308 }
309}
310
311impl PendingIndexeddbChanges {
312 fn new() -> Self {
313 Self { store_to_key_values: BTreeMap::new() }
314 }
315
316 fn touched_stores(&self) -> Vec<&str> {
320 self.store_to_key_values
321 .iter()
322 .filter_map(
323 |(store, pending_operations)| {
324 if !pending_operations.is_empty() { Some(*store) } else { None }
325 },
326 )
327 .collect()
328 }
329
330 async fn apply(self, tx: &Transaction<'_>) -> Result<()> {
332 for (store, operations) in self.store_to_key_values {
333 if operations.is_empty() {
334 continue;
335 }
336 let object_store = tx.object_store(store)?;
337 for op in operations {
338 match op {
339 PendingOperation::Put { key, value } => {
340 object_store.put(&value).with_key(key).build()?;
341 }
342 PendingOperation::Delete(key) => {
343 object_store.delete(&key).build()?;
344 }
345 PendingOperation::DeleteByIndex { index, key } => {
346 let range = KeyRange::Only(key);
347 let ids = object_store
348 .index(index)?
349 .get_all_keys::<JsValue>()
350 .with_query::<JsValue, _>(range)
351 .await?;
352 for id in ids {
353 object_store.delete(id.unwrap()).await?;
354 }
355 }
356 }
357 }
358 }
359 Ok(())
360 }
361}
362
363impl IndexeddbCryptoStore {
364 pub(crate) async fn open_with_store_cipher(
365 prefix: &str,
366 store_cipher: Option<Arc<StoreCipher>>,
367 ) -> Result<Self> {
368 let name = format!("{prefix:0}::matrix-sdk-crypto");
369
370 let serializer = SafeEncodeSerializer::new(store_cipher);
371 debug!("IndexedDbCryptoStore: opening main store {name}");
372 let db = open_and_upgrade_db(&name, &serializer).await?;
373
374 Ok(Self {
375 name,
376 inner: db,
377 serializer,
378 static_account: RwLock::new(None),
379 save_changes_lock: Default::default(),
380 })
381 }
382
383 pub async fn open() -> Result<Self> {
385 IndexeddbCryptoStore::open_with_store_cipher("crypto", None).await
386 }
387
388 pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result<Self> {
405 let db = open_meta_db(prefix).await?;
406 let store_cipher = load_store_cipher(&db).await?;
407
408 let store_cipher = match store_cipher {
409 Some(cipher) => {
410 debug!("IndexedDbCryptoStore: decrypting store cipher");
411 StoreCipher::import(passphrase, &cipher)
412 .map_err(|_| CryptoStoreError::UnpicklingError)?
413 }
414 None => {
415 debug!("IndexedDbCryptoStore: encrypting new store cipher");
416 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
417 #[cfg(not(test))]
418 let export = cipher.export(passphrase);
419 #[cfg(test)]
420 let export = cipher._insecure_export_fast_for_testing(passphrase);
421
422 let export = export.map_err(CryptoStoreError::backend)?;
423
424 save_store_cipher(&db, &export).await?;
425 cipher
426 }
427 };
428
429 db.close();
432
433 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
434 }
435
436 pub async fn open_with_key(prefix: &str, key: &[u8; 32]) -> Result<Self> {
452 let mut chacha_key = zeroize::Zeroizing::new([0u8; 32]);
455 const HKDF_INFO: &[u8] = b"CRYPTOSTORE_CIPHER";
456 let hkdf = Hkdf::<Sha256>::new(None, key);
457 hkdf.expand(HKDF_INFO, &mut *chacha_key)
458 .expect("We should be able to generate a 32-byte key");
459
460 let db = open_meta_db(prefix).await?;
461 let store_cipher = load_store_cipher(&db).await?;
462
463 let store_cipher = match store_cipher {
464 Some(cipher) => {
465 debug!("IndexedDbCryptoStore: decrypting store cipher");
466 import_store_cipher_with_key(&chacha_key, key, &cipher, &db).await?
467 }
468 None => {
469 debug!("IndexedDbCryptoStore: encrypting new store cipher");
470 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
471 let export = cipher
472 .export_with_key(chacha_key.as_slice())
473 .map_err(CryptoStoreError::backend)?;
474 save_store_cipher(&db, &export).await?;
475 cipher
476 }
477 };
478
479 db.close();
482
483 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
484 }
485
486 pub async fn open_with_name(name: &str) -> Result<Self> {
488 IndexeddbCryptoStore::open_with_store_cipher(name, None).await
489 }
490
491 #[cfg(test)]
493 pub fn delete_stores(prefix: &str) -> Result<()> {
494 Database::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto-meta"))?;
495 Database::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto"))?;
496 Ok(())
497 }
498
499 fn get_static_account(&self) -> Option<StaticAccountData> {
500 self.static_account.read().unwrap().clone()
501 }
502
503 async fn serialize_inbound_group_session(
506 &self,
507 session: &InboundGroupSession,
508 ) -> Result<JsValue> {
509 let obj =
510 InboundGroupSessionIndexedDbObject::from_session(session, &self.serializer).await?;
511 Ok(serde_wasm_bindgen::to_value(&obj)?)
512 }
513
514 fn deserialize_inbound_group_session(
517 &self,
518 stored_value: JsValue,
519 ) -> Result<InboundGroupSession> {
520 let idb_object: InboundGroupSessionIndexedDbObject =
521 serde_wasm_bindgen::from_value(stored_value)?;
522 let pickled_session: PickledInboundGroupSession =
523 self.serializer.maybe_decrypt_value(idb_object.pickled_session)?;
524 let session = InboundGroupSession::from_pickle(pickled_session)
525 .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
526
527 if idb_object.needs_backup {
532 session.reset_backup_state();
533 } else {
534 session.mark_as_backed_up();
535 }
536
537 Ok(session)
538 }
539
540 fn serialize_gossip_request(&self, gossip_request: &GossipRequest) -> Result<JsValue> {
543 let obj = GossipRequestIndexedDbObject {
544 info: self
546 .serializer
547 .encode_key_as_string(keys::GOSSIP_REQUESTS, gossip_request.info.as_key()),
548
549 request: self.serializer.serialize_value_as_bytes(gossip_request)?,
551
552 unsent: !gossip_request.sent_out,
553 };
554
555 Ok(serde_wasm_bindgen::to_value(&obj)?)
556 }
557
558 fn deserialize_gossip_request(&self, stored_request: JsValue) -> Result<GossipRequest> {
561 let idb_object: GossipRequestIndexedDbObject =
562 serde_wasm_bindgen::from_value(stored_request)?;
563 Ok(self.serializer.deserialize_value_from_bytes(&idb_object.request)?)
564 }
565
566 async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
573 let mut indexeddb_changes = PendingIndexeddbChanges::new();
574
575 let private_identity_pickle =
576 if let Some(i) = &changes.private_identity { Some(i.pickle().await) } else { None };
577
578 let decryption_key_pickle = &changes.backup_decryption_key;
579 let backup_version = &changes.backup_version;
580 let dehydration_pickle_key = &changes.dehydrated_device_pickle_key;
581
582 let mut core = indexeddb_changes.get(keys::CORE);
583 if let Some(next_batch) = &changes.next_batch_token {
584 core.put(
585 JsValue::from_str(keys::NEXT_BATCH_TOKEN),
586 self.serializer.serialize_value(next_batch)?,
587 );
588 }
589
590 if let Some(i) = &private_identity_pickle {
591 core.put(
592 JsValue::from_str(keys::PRIVATE_IDENTITY),
593 self.serializer.serialize_value(i)?,
594 );
595 }
596
597 if let Some(i) = &dehydration_pickle_key {
598 core.put(
599 JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY),
600 self.serializer.serialize_value(i)?,
601 );
602 }
603
604 if let Some(a) = &decryption_key_pickle {
605 indexeddb_changes.get(keys::BACKUP_KEYS).put(
606 JsValue::from_str(keys::RECOVERY_KEY_V1),
607 self.serializer.serialize_value(&a)?,
608 );
609 }
610
611 if let Some(a) = &backup_version {
612 indexeddb_changes.get(keys::BACKUP_KEYS).put(
613 JsValue::from_str(keys::BACKUP_VERSION_V1),
614 self.serializer.serialize_value(&a)?,
615 );
616 }
617
618 if !changes.sessions.is_empty() {
619 let mut sessions = indexeddb_changes.get(keys::SESSION);
620
621 for session in &changes.sessions {
622 let sender_key = session.sender_key().to_base64();
623 let session_id = session.session_id();
624
625 let pickle = session.pickle().await;
626 let key = self.serializer.encode_key(keys::SESSION, (&sender_key, session_id));
627
628 sessions.put(key, self.serializer.serialize_value(&pickle)?);
629 }
630 }
631
632 if !changes.inbound_group_sessions.is_empty() {
633 let mut sessions = indexeddb_changes.get(keys::INBOUND_GROUP_SESSIONS_V3);
634
635 for session in &changes.inbound_group_sessions {
636 let room_id = session.room_id();
637 let session_id = session.session_id();
638 let key = self
639 .serializer
640 .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
641 let value = self.serialize_inbound_group_session(session).await?;
642 sessions.put(key, value);
643 }
644 }
645
646 if !changes.outbound_group_sessions.is_empty() {
647 let mut sessions = indexeddb_changes.get(keys::OUTBOUND_GROUP_SESSIONS);
648
649 for session in &changes.outbound_group_sessions {
650 let room_id = session.room_id();
651 let pickle = session.pickle().await;
652 sessions.put(
653 self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
654 self.serializer.serialize_value(&pickle)?,
655 );
656 }
657 }
658
659 let device_changes = &changes.devices;
660 let identity_changes = &changes.identities;
661 let olm_hashes = &changes.message_hashes;
662 let key_requests = &changes.key_requests;
663 let withheld_session_info = &changes.withheld_session_info;
664 let room_settings_changes = &changes.room_settings;
665
666 let mut device_store = indexeddb_changes.get(keys::DEVICES);
667
668 for device in device_changes.new.iter().chain(&device_changes.changed) {
669 let key =
670 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
671 let device = self.serializer.serialize_value(&device)?;
672
673 device_store.put(key, device);
674 }
675
676 for device in &device_changes.deleted {
677 let key =
678 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
679 device_store.delete(key);
680 }
681
682 if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
683 let mut identities = indexeddb_changes.get(keys::IDENTITIES);
684 for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
685 identities.put(
686 self.serializer.encode_key(keys::IDENTITIES, identity.user_id()),
687 self.serializer.serialize_value(&identity)?,
688 );
689 }
690 }
691
692 if !olm_hashes.is_empty() {
693 let mut hashes = indexeddb_changes.get(keys::OLM_HASHES);
694 for hash in olm_hashes {
695 hashes.put(
696 self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
697 JsValue::TRUE,
698 );
699 }
700 }
701
702 if !key_requests.is_empty() {
703 let mut gossip_requests = indexeddb_changes.get(keys::GOSSIP_REQUESTS);
704
705 for gossip_request in key_requests {
706 let key_request_info =
708 self.serializer.encode_key(keys::GOSSIP_REQUESTS, gossip_request.info.as_key());
709 gossip_requests
710 .delete_by_index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX, key_request_info);
711
712 let key_request_id = self
713 .serializer
714 .encode_key(keys::GOSSIP_REQUESTS, gossip_request.request_id.as_str());
715 let key_request_value = self.serialize_gossip_request(gossip_request)?;
716 gossip_requests.put(key_request_id, key_request_value);
717 }
718 }
719
720 if !withheld_session_info.is_empty() {
721 let mut withhelds = indexeddb_changes.get(keys::WITHHELD_SESSIONS);
722
723 for (room_id, data) in withheld_session_info {
724 for (session_id, event) in data {
725 let key =
726 self.serializer.encode_key(keys::WITHHELD_SESSIONS, (&room_id, session_id));
727 withhelds.put(key, self.serializer.serialize_value(&event)?);
728 }
729 }
730 }
731
732 if !room_settings_changes.is_empty() {
733 let mut settings_store = indexeddb_changes.get(keys::ROOM_SETTINGS);
734
735 for (room_id, settings) in room_settings_changes {
736 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
737 let value = self.serializer.serialize_value(&settings)?;
738 settings_store.put(key, value);
739 }
740 }
741
742 if !changes.secrets.is_empty() {
743 let mut secret_store = indexeddb_changes.get(keys::SECRETS_INBOX_V2);
744
745 for secret in &changes.secrets {
746 use std::ops::Deref;
747 let key = self.serializer.encode_key(
752 keys::SECRETS_INBOX_V2,
753 (secret.secret_name.as_str(), secret.secret.as_str()),
754 );
755 let value = self.serializer.serialize_value(secret.secret.deref())?;
756
757 secret_store.put(key, value);
758 }
759 }
760
761 if !changes.received_room_key_bundles.is_empty() {
762 let mut bundle_store = indexeddb_changes.get(keys::RECEIVED_ROOM_KEY_BUNDLES);
763 for bundle in &changes.received_room_key_bundles {
764 let key = self.serializer.encode_key(
765 keys::RECEIVED_ROOM_KEY_BUNDLES,
766 (&bundle.bundle_data.room_id, &bundle.sender_user),
767 );
768 let value = self.serializer.serialize_value(&bundle)?;
769 bundle_store.put(key, value);
770 }
771 }
772
773 if !changes.room_key_backups_fully_downloaded.is_empty() {
774 let mut room_store = indexeddb_changes.get(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED);
775 for room_id in &changes.room_key_backups_fully_downloaded {
776 room_store.put(
777 self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id),
778 JsValue::TRUE,
779 );
780 }
781 }
782
783 if !changes.rooms_pending_key_bundle.is_empty() {
784 let mut room_store = indexeddb_changes.get(keys::ROOMS_PENDING_KEY_BUNDLE);
785 for (room_id, details) in &changes.rooms_pending_key_bundle {
786 let key = self.serializer.encode_key(keys::ROOMS_PENDING_KEY_BUNDLE, room_id);
787 if let Some(details) = details {
788 let value = self.serializer.serialize_value(details)?;
789 room_store.put(key, value);
790 } else {
791 room_store.delete(key);
792 }
793 }
794 }
795
796 Ok(indexeddb_changes)
797 }
798}
799
800#[cfg(target_family = "wasm")]
809macro_rules! impl_crypto_store {
810 ( $($body:tt)* ) => {
811 #[async_trait(?Send)]
812 impl CryptoStore for IndexeddbCryptoStore {
813 type Error = IndexeddbCryptoStoreError;
814
815 $($body)*
816 }
817 };
818}
819
820#[cfg(not(target_family = "wasm"))]
821macro_rules! impl_crypto_store {
822 ( $($body:tt)* ) => {
823 impl IndexeddbCryptoStore {
824 $($body)*
825 }
826 };
827}
828
829impl_crypto_store! {
830 async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> {
831 let _guard = self.save_changes_lock.lock().await;
836
837 let stores: Vec<&str> = [(changes.account.is_some(), keys::CORE)]
838 .iter()
839 .filter_map(|(id, key)| if *id { Some(*key) } else { None })
840 .collect();
841
842 if stores.is_empty() {
843 return Ok(());
845 }
846
847 let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
848
849 let account_pickle = if let Some(account) = changes.account {
850 *self.static_account.write().unwrap() = Some(account.static_data().clone());
851 Some(account.pickle())
852 } else {
853 None
854 };
855
856 if let Some(a) = &account_pickle {
857 tx.object_store(keys::CORE)?
858 .put(&self.serializer.serialize_value(&a)?)
859 .with_key(JsValue::from_str(keys::ACCOUNT))
860 .build()?;
861 }
862
863 tx.commit().await?;
864
865 Ok(())
866 }
867
868 async fn save_changes(&self, changes: Changes) -> Result<()> {
869 let _guard = self.save_changes_lock.lock().await;
874
875 let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
876
877 let stores = indexeddb_changes.touched_stores();
878
879 if stores.is_empty() {
880 return Ok(());
882 }
883
884 let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
885
886 indexeddb_changes.apply(&tx).await?;
887
888 tx.commit().await?;
889
890 Ok(())
891 }
892
893 async fn save_inbound_group_sessions(
894 &self,
895 sessions: Vec<InboundGroupSession>,
896 backed_up_to_version: Option<&str>,
897 ) -> Result<()> {
898 sessions.iter().for_each(|s| {
900 let backed_up = s.backed_up();
901 if backed_up != backed_up_to_version.is_some() {
902 warn!(
903 backed_up,
904 backed_up_to_version,
905 "Session backed-up flag does not correspond to backup version setting",
906 );
907 }
908 });
909
910 self.save_changes(Changes { inbound_group_sessions: sessions, ..Changes::default() }).await
913 }
914
915 async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
916 let tx = self
917 .inner
918 .transaction(keys::TRACKED_USERS)
919 .with_mode(TransactionMode::Readonly)
920 .build()?;
921 let os = tx.object_store(keys::TRACKED_USERS)?;
922 let user_ids = os.get_all_keys::<JsValue>().await?;
923
924 let mut users = Vec::new();
925
926 for result in user_ids {
927 let user_id = result?;
928 let dirty: bool = !matches!(
929 os.get(&user_id).await?.map(|v: JsValue| v.into_serde()),
930 Some(Ok(false))
931 );
932 let Some(Ok(user_id)) = user_id.as_string().map(UserId::parse) else { continue };
933
934 users.push(TrackedUser { user_id, dirty });
935 }
936
937 Ok(users)
938 }
939
940 async fn get_outbound_group_session(
941 &self,
942 room_id: &RoomId,
943 ) -> Result<Option<OutboundGroupSession>> {
944 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
945 if let Some(value) = self
946 .inner
947 .transaction(keys::OUTBOUND_GROUP_SESSIONS)
948 .with_mode(TransactionMode::Readonly)
949 .build()?
950 .object_store(keys::OUTBOUND_GROUP_SESSIONS)?
951 .get(&self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))
952 .await?
953 {
954 Ok(Some(
955 OutboundGroupSession::from_pickle(
956 account_info.device_id,
957 account_info.identity_keys,
958 self.serializer.deserialize_value(value)?,
959 )
960 .map_err(CryptoStoreError::from)?,
961 ))
962 } else {
963 Ok(None)
964 }
965 }
966
967 async fn get_outgoing_secret_requests(
968 &self,
969 request_id: &TransactionId,
970 ) -> Result<Option<GossipRequest>> {
971 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id.as_str());
972 self.inner
973 .transaction(keys::GOSSIP_REQUESTS)
974 .with_mode(TransactionMode::Readonly)
975 .build()?
976 .object_store(keys::GOSSIP_REQUESTS)?
977 .get(jskey)
978 .await?
979 .map(|val| self.deserialize_gossip_request(val))
980 .transpose()
981 }
982
983 async fn load_account(&self) -> Result<Option<Account>> {
984 if let Some(pickle) = self
985 .inner
986 .transaction(keys::CORE)
987 .with_mode(TransactionMode::Readonly)
988 .build()?
989 .object_store(keys::CORE)?
990 .get(&JsValue::from_str(keys::ACCOUNT))
991 .await?
992 {
993 let pickle = self.serializer.deserialize_value(pickle)?;
994
995 let account = Account::from_pickle(pickle).map_err(CryptoStoreError::from)?;
996
997 *self.static_account.write().unwrap() = Some(account.static_data().clone());
998
999 Ok(Some(account))
1000 } else {
1001 Ok(None)
1002 }
1003 }
1004
1005 async fn next_batch_token(&self) -> Result<Option<String>> {
1006 if let Some(serialized) = self
1007 .inner
1008 .transaction(keys::CORE)
1009 .with_mode(TransactionMode::Readonly)
1010 .build()?
1011 .object_store(keys::CORE)?
1012 .get(&JsValue::from_str(keys::NEXT_BATCH_TOKEN))
1013 .await?
1014 {
1015 let token = self.serializer.deserialize_value(serialized)?;
1016 Ok(Some(token))
1017 } else {
1018 Ok(None)
1019 }
1020 }
1021
1022 async fn delete_next_batch_token(&self) -> Result<()> {
1023 let transaction =
1024 self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1025 transaction.object_store(keys::CORE)?.delete(&JsValue::from_str(keys::NEXT_BATCH_TOKEN)).build()?;
1026 transaction.commit().await?;
1027 Ok(())
1028 }
1029
1030 async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
1031 if let Some(pickle) = self
1032 .inner
1033 .transaction(keys::CORE)
1034 .with_mode(TransactionMode::Readonly)
1035 .build()?
1036 .object_store(keys::CORE)?
1037 .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))
1038 .await?
1039 {
1040 let pickle = self.serializer.deserialize_value(pickle)?;
1041
1042 Ok(Some(
1043 PrivateCrossSigningIdentity::from_pickle(pickle)
1044 .map_err(|_| CryptoStoreError::UnpicklingError)?,
1045 ))
1046 } else {
1047 Ok(None)
1048 }
1049 }
1050
1051 async fn get_sessions(&self, sender_key: &str) -> Result<Option<Vec<Session>>> {
1052 let device_keys = self.get_own_device().await?.as_device_keys().clone();
1053
1054 let range = self.serializer.encode_to_range(keys::SESSION, sender_key);
1055 let sessions: Vec<Session> = self
1056 .inner
1057 .transaction(keys::SESSION)
1058 .with_mode(TransactionMode::Readonly)
1059 .build()?
1060 .object_store(keys::SESSION)?
1061 .get_all()
1062 .with_query(&range)
1063 .await?
1064 .filter_map(Result::ok)
1065 .filter_map(|f| {
1066 self.serializer.deserialize_value(f).ok().map(|p| {
1067 Session::from_pickle(device_keys.clone(), p).map_err(|_| {
1068 IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset)
1069 })
1070 })
1071 })
1072 .collect::<Result<Vec<Session>>>()?;
1073
1074 if sessions.is_empty() {
1075 Ok(None)
1076 } else {
1077 Ok(Some(sessions))
1078 }
1079 }
1080
1081 async fn get_inbound_group_session(
1082 &self,
1083 room_id: &RoomId,
1084 session_id: &str,
1085 ) -> Result<Option<InboundGroupSession>> {
1086 let key =
1087 self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1088 if let Some(value) = self
1089 .inner
1090 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1091 .with_mode(TransactionMode::Readonly)
1092 .build()?
1093 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1094 .get(&key)
1095 .await?
1096 {
1097 Ok(Some(self.deserialize_inbound_group_session(value)?))
1098 } else {
1099 Ok(None)
1100 }
1101 }
1102
1103 async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
1104 const INBOUND_GROUP_SESSIONS_BATCH_SIZE: usize = 1000;
1105
1106 let transaction = self
1107 .inner
1108 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1109 .with_mode(TransactionMode::Readonly)
1110 .build()?;
1111
1112 let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1113
1114 fetch_from_object_store_batched(
1115 object_store,
1116 |value| self.deserialize_inbound_group_session(value),
1117 INBOUND_GROUP_SESSIONS_BATCH_SIZE,
1118 )
1119 .await
1120 }
1121
1122 async fn get_inbound_group_sessions_by_room_id(
1123 &self,
1124 room_id: &RoomId,
1125 ) -> Result<Vec<InboundGroupSession>> {
1126 let range = self.serializer.encode_to_range(keys::INBOUND_GROUP_SESSIONS_V3, room_id);
1127 Ok(self
1128 .inner
1129 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1130 .with_mode(TransactionMode::Readonly)
1131 .build()?
1132 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1133 .get_all()
1134 .with_query(&range)
1135 .await?
1136 .filter_map(Result::ok)
1137 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1138 Ok(session) => Some(session),
1139 Err(e) => {
1140 warn!("Failed to deserialize inbound group session: {e}");
1141 None
1142 }
1143 })
1144 .collect::<Vec<InboundGroupSession>>())
1145 }
1146
1147 async fn get_inbound_group_sessions_for_device_batch(
1148 &self,
1149 sender_key: Curve25519PublicKey,
1150 sender_data_type: SenderDataType,
1151 after_session_id: Option<String>,
1152 limit: usize,
1153 ) -> Result<Vec<InboundGroupSession>> {
1154 let sender_key =
1155 self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
1156
1157 let after_session_id = after_session_id
1159 .map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s))
1160 .unwrap_or("".into());
1161
1162 let lower_bound: Array =
1163 [sender_key.clone(), (sender_data_type as u8).into(), after_session_id]
1164 .iter()
1165 .collect();
1166 let upper_bound: Array =
1167 [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
1168 let key = KeyRange::Bound(lower_bound, true, upper_bound, true);
1169
1170 let tx = self
1171 .inner
1172 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1173 .with_mode(TransactionMode::Readonly)
1174 .build()?;
1175
1176 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1177 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1178 let serialized_sessions =
1179 idx.get_all().with_query::<Array, _>(key).with_limit(limit as u32).await?;
1180
1181 let result = serialized_sessions
1183 .filter_map(Result::ok)
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 inbound_group_session_counts(
1197 &self,
1198 _backup_version: Option<&str>,
1199 ) -> Result<RoomKeyCounts> {
1200 let tx = self
1201 .inner
1202 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1203 .with_mode(TransactionMode::Readonly)
1204 .build()?;
1205 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1206 let all = store.count().await? as usize;
1207 let not_backed_up =
1208 store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count().await? as usize;
1209 tx.commit().await?;
1210 Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1211 }
1212
1213 async fn inbound_group_sessions_for_backup(
1214 &self,
1215 _backup_version: &str,
1216 limit: usize,
1217 ) -> Result<Vec<InboundGroupSession>> {
1218 let tx = self
1219 .inner
1220 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1221 .with_mode(TransactionMode::Readonly)
1222 .build()?;
1223
1224 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1225 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1226
1227 let Some(mut cursor) = idx.open_cursor().await? else {
1232 return Ok(vec![]);
1233 };
1234
1235 let mut serialized_sessions = Vec::with_capacity(limit);
1236 for _ in 0..limit {
1237 let Some(value) = cursor.next_record().await? else {
1238 break;
1239 };
1240 serialized_sessions.push(value)
1241 }
1242
1243 tx.commit().await?;
1244
1245 let result = serialized_sessions
1247 .into_iter()
1248 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1249 Ok(session) => Some(session),
1250 Err(e) => {
1251 warn!("Failed to deserialize inbound group session: {e}");
1252 None
1253 }
1254 })
1255 .collect::<Vec<InboundGroupSession>>();
1256
1257 Ok(result)
1258 }
1259
1260 async fn mark_inbound_group_sessions_as_backed_up(
1261 &self,
1262 _backup_version: &str,
1263 room_and_session_ids: &[(&RoomId, &str)],
1264 ) -> Result<()> {
1265 let tx = self
1266 .inner
1267 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1268 .with_mode(TransactionMode::Readwrite)
1269 .build()?;
1270
1271 let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1272
1273 for (room_id, session_id) in room_and_session_ids {
1274 let key =
1275 self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1276 if let Some(idb_object_js) = object_store.get(&key).await? {
1277 let mut idb_object: InboundGroupSessionIndexedDbObject =
1278 serde_wasm_bindgen::from_value(idb_object_js)?;
1279 idb_object.needs_backup = false;
1280 object_store
1281 .put(&serde_wasm_bindgen::to_value(&idb_object)?)
1282 .with_key(key)
1283 .build()?;
1284 } else {
1285 warn!(?key, "Could not find inbound group session to mark it as backed up.");
1286 }
1287 }
1288
1289 Ok(tx.commit().await?)
1290 }
1291
1292 async fn reset_backup_state(&self) -> Result<()> {
1293 let tx = self
1294 .inner
1295 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1296 .with_mode(TransactionMode::Readwrite)
1297 .build()?;
1298
1299 if let Some(mut cursor) =
1300 tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor().await?
1301 {
1302 while let Some(value) = cursor.next_record().await? {
1303 let mut idb_object: InboundGroupSessionIndexedDbObject =
1304 serde_wasm_bindgen::from_value(value)?;
1305 if !idb_object.needs_backup {
1306 idb_object.needs_backup = true;
1307 let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1313 cursor.update(&idb_object).await?;
1314 }
1315 }
1316 }
1317
1318 Ok(tx.commit().await?)
1319 }
1320
1321 async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1322 let tx = self
1323 .inner
1324 .transaction(keys::TRACKED_USERS)
1325 .with_mode(TransactionMode::Readwrite)
1326 .build()?;
1327 let os = tx.object_store(keys::TRACKED_USERS)?;
1328
1329 for (user, dirty) in users {
1330 os.put(&JsValue::from(*dirty)).with_key(JsValue::from_str(user.as_str())).build()?;
1331 }
1332
1333 tx.commit().await?;
1334 Ok(())
1335 }
1336
1337 async fn get_device(
1338 &self,
1339 user_id: &UserId,
1340 device_id: &DeviceId,
1341 ) -> Result<Option<DeviceData>> {
1342 let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1343 self.inner
1344 .transaction(keys::DEVICES)
1345 .with_mode(TransactionMode::Readonly)
1346 .build()?
1347 .object_store(keys::DEVICES)?
1348 .get(&key)
1349 .await?
1350 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1351 .transpose()
1352 }
1353
1354 async fn get_user_devices(
1355 &self,
1356 user_id: &UserId,
1357 ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1358 let range = self.serializer.encode_to_range(keys::DEVICES, user_id);
1359 Ok(self
1360 .inner
1361 .transaction(keys::DEVICES)
1362 .with_mode(TransactionMode::Readonly)
1363 .build()?
1364 .object_store(keys::DEVICES)?
1365 .get_all()
1366 .with_query(&range)
1367 .await?
1368 .filter_map(Result::ok)
1369 .filter_map(|d| {
1370 let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1371 Some((d.device_id().to_owned(), d))
1372 })
1373 .collect::<HashMap<_, _>>())
1374 }
1375
1376 async fn get_own_device(&self) -> Result<DeviceData> {
1377 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1378 Ok(self.get_device(&account_info.user_id, &account_info.device_id).await?.unwrap())
1379 }
1380
1381 async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1382 self.inner
1383 .transaction(keys::IDENTITIES)
1384 .with_mode(TransactionMode::Readonly)
1385 .build()?
1386 .object_store(keys::IDENTITIES)?
1387 .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))
1388 .await?
1389 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1390 .transpose()
1391 }
1392
1393 async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1394 Ok(self
1395 .inner
1396 .transaction(keys::OLM_HASHES)
1397 .with_mode(TransactionMode::Readonly)
1398 .build()?
1399 .object_store(keys::OLM_HASHES)?
1400 .get::<JsValue, _, _>(
1401 &self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
1402 )
1403 .await?
1404 .is_some())
1405 }
1406
1407 async fn get_secrets_from_inbox(
1408 &self,
1409 secret_name: &SecretName,
1410 ) -> Result<Vec<zeroize::Zeroizing<String>>> {
1411 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX_V2, secret_name.as_str());
1412
1413 self.inner
1414 .transaction(keys::SECRETS_INBOX_V2)
1415 .with_mode(TransactionMode::Readonly)
1416 .build()?
1417 .object_store(keys::SECRETS_INBOX_V2)?
1418 .get_all()
1419 .with_query(&range)
1420 .await?
1421 .map(|result| {
1422 let d = result?;
1423 let secret: String = self.serializer.deserialize_value(d)?;
1424 Ok(secret.into())
1425 })
1426 .collect()
1427 }
1428
1429 #[allow(clippy::unused_async)] async fn delete_secrets_from_inbox(&self, secret_name: &SecretName) -> Result<()> {
1431 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX_V2, secret_name.as_str());
1432
1433 let transaction = self
1434 .inner
1435 .transaction(keys::SECRETS_INBOX_V2)
1436 .with_mode(TransactionMode::Readwrite)
1437 .build()?;
1438 transaction.object_store(keys::SECRETS_INBOX_V2)?.delete(&range).build()?;
1439 transaction.commit().await?;
1440
1441 Ok(())
1442 }
1443
1444 async fn get_secret_request_by_info(
1445 &self,
1446 key_info: &SecretInfo,
1447 ) -> Result<Option<GossipRequest>> {
1448 let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1449
1450 let val = self
1451 .inner
1452 .transaction(keys::GOSSIP_REQUESTS)
1453 .with_mode(TransactionMode::Readonly)
1454 .build()?
1455 .object_store(keys::GOSSIP_REQUESTS)?
1456 .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1457 .get(key)
1458 .await?;
1459
1460 if let Some(val) = val {
1461 let deser = self.deserialize_gossip_request(val)?;
1462 Ok(Some(deser))
1463 } else {
1464 Ok(None)
1465 }
1466 }
1467
1468 async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1469 let results = self
1470 .inner
1471 .transaction(keys::GOSSIP_REQUESTS)
1472 .with_mode(TransactionMode::Readonly)
1473 .build()?
1474 .object_store(keys::GOSSIP_REQUESTS)?
1475 .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1476 .get_all()
1477 .await?
1478 .filter_map(Result::ok)
1479 .filter_map(|val| self.deserialize_gossip_request(val).ok())
1480 .collect();
1481
1482 Ok(results)
1483 }
1484
1485 async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1486 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1487 let tx = self
1488 .inner
1489 .transaction(keys::GOSSIP_REQUESTS)
1490 .with_mode(TransactionMode::Readwrite)
1491 .build()?;
1492 tx.object_store(keys::GOSSIP_REQUESTS)?.delete(jskey).build()?;
1493 tx.commit().await.map_err(|e| e.into())
1494 }
1495
1496 async fn load_backup_keys(&self) -> Result<BackupKeys> {
1497 let key = {
1498 let tx = self
1499 .inner
1500 .transaction(keys::BACKUP_KEYS)
1501 .with_mode(TransactionMode::Readonly)
1502 .build()?;
1503 let store = tx.object_store(keys::BACKUP_KEYS)?;
1504
1505 let backup_version = store
1506 .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))
1507 .await?
1508 .map(|i| self.serializer.deserialize_value(i))
1509 .transpose()?;
1510
1511 let decryption_key = store
1512 .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))
1513 .await?
1514 .map(|i| self.serializer.deserialize_value(i))
1515 .transpose()?;
1516
1517 BackupKeys { backup_version, decryption_key }
1518 };
1519
1520 Ok(key)
1521 }
1522
1523 async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1524 if let Some(pickle) = self
1525 .inner
1526 .transaction(keys::CORE)
1527 .with_mode(TransactionMode::Readonly)
1528 .build()?
1529 .object_store(keys::CORE)?
1530 .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))
1531 .await?
1532 {
1533 let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1534
1535 Ok(Some(pickle))
1536 } else {
1537 Ok(None)
1538 }
1539 }
1540
1541 async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1542 self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1543 Ok(())
1544 }
1545
1546 async fn get_withheld_info(
1547 &self,
1548 room_id: &RoomId,
1549 session_id: &str,
1550 ) -> Result<Option<RoomKeyWithheldEntry>> {
1551 let key = self.serializer.encode_key(keys::WITHHELD_SESSIONS, (room_id, session_id));
1552 if let Some(pickle) = self
1553 .inner
1554 .transaction(keys::WITHHELD_SESSIONS)
1555 .with_mode(TransactionMode::Readonly)
1556 .build()?
1557 .object_store(keys::WITHHELD_SESSIONS)?
1558 .get(&key)
1559 .await?
1560 {
1561 let info = self.serializer.deserialize_value(pickle)?;
1562 Ok(Some(info))
1563 } else {
1564 Ok(None)
1565 }
1566 }
1567
1568 async fn get_withheld_sessions_by_room_id(
1569 &self,
1570 room_id: &RoomId,
1571 ) -> Result<Vec<RoomKeyWithheldEntry>> {
1572 let range = self.serializer.encode_to_range(keys::WITHHELD_SESSIONS, room_id);
1573
1574 self
1575 .inner
1576 .transaction(keys::WITHHELD_SESSIONS)
1577 .with_mode(TransactionMode::Readonly)
1578 .build()?
1579 .object_store(keys::WITHHELD_SESSIONS)?
1580 .get_all()
1581 .with_query(&range)
1582 .await?
1583 .map(|val| self.serializer.deserialize_value(val?).map_err(Into::into))
1584 .collect()
1585 }
1586
1587 async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1588 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1589 self.inner
1590 .transaction(keys::ROOM_SETTINGS)
1591 .with_mode(TransactionMode::Readonly)
1592 .build()?
1593 .object_store(keys::ROOM_SETTINGS)?
1594 .get(&key)
1595 .await?
1596 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1597 .transpose()
1598 }
1599
1600 async fn get_received_room_key_bundle_data(
1601 &self,
1602 room_id: &RoomId,
1603 user_id: &UserId,
1604 ) -> Result<Option<StoredRoomKeyBundleData>> {
1605 let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1606 let result = self
1607 .inner
1608 .transaction(keys::RECEIVED_ROOM_KEY_BUNDLES)
1609 .with_mode(TransactionMode::Readonly)
1610 .build()?
1611 .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1612 .get(&key)
1613 .await?
1614 .map(|v| self.serializer.deserialize_value(v))
1615 .transpose()?;
1616
1617 Ok(result)
1618 }
1619
1620 async fn has_downloaded_all_room_keys(&self, room_id: &RoomId) -> Result<bool> {
1621 let key = self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id);
1622 let result = self
1623 .inner
1624 .transaction(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)
1625 .with_mode(TransactionMode::Readonly)
1626 .build()?
1627 .object_store(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)?
1628 .get::<JsValue, _, _>(&key)
1629 .await?
1630 .is_some();
1631
1632 Ok(result)
1633 }
1634
1635 async fn get_pending_key_bundle_details_for_room(&self, room_id: &RoomId) -> Result<Option<RoomPendingKeyBundleDetails >> {
1636 let key = self.serializer.encode_key(keys::ROOMS_PENDING_KEY_BUNDLE, room_id);
1637 let result = self
1638 .inner
1639 .transaction(keys::ROOMS_PENDING_KEY_BUNDLE)
1640 .with_mode(TransactionMode::Readonly)
1641 .build()?
1642 .object_store(keys::ROOMS_PENDING_KEY_BUNDLE)?
1643 .get(&key)
1644 .await?
1645 .map(|v| self.serializer.deserialize_value(v))
1646 .transpose()?;
1647 Ok(result)
1648 }
1649
1650 async fn get_all_rooms_pending_key_bundles(&self) -> Result<Vec<RoomPendingKeyBundleDetails>> {
1651 let result = self
1652 .inner
1653 .transaction(keys::ROOMS_PENDING_KEY_BUNDLE)
1654 .with_mode(TransactionMode::Readonly)
1655 .build()?
1656 .object_store(keys::ROOMS_PENDING_KEY_BUNDLE)?
1657 .get_all()
1658 .await?
1659 .map(|result| {
1660 result
1661 .map_err(Into::into)
1662 .and_then(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1663 })
1664 .collect::<Result<Vec<_>>>()?;
1665 Ok(result)
1666 }
1667
1668 async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1669 self.inner
1670 .transaction(keys::CORE)
1671 .with_mode(TransactionMode::Readonly)
1672 .build()?
1673 .object_store(keys::CORE)?
1674 .get(&JsValue::from_str(key))
1675 .await?
1676 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1677 .transpose()
1678 }
1679
1680 #[allow(clippy::unused_async)] async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1682 let transaction =
1683 self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1684 transaction
1685 .object_store(keys::CORE)?
1686 .put(&self.serializer.serialize_value(&value)?)
1687 .with_key(JsValue::from_str(key))
1688 .build()?;
1689 transaction.commit().await?;
1690 Ok(())
1691 }
1692
1693 #[allow(clippy::unused_async)] async fn remove_custom_value(&self, key: &str) -> Result<()> {
1695 let transaction =
1696 self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1697 transaction.object_store(keys::CORE)?.delete(&JsValue::from_str(key)).build()?;
1698 transaction.commit().await?;
1699 Ok(())
1700 }
1701
1702 async fn try_take_leased_lock(
1703 &self,
1704 lease_duration_ms: u32,
1705 key: &str,
1706 holder: &str,
1707 ) -> Result<Option<CrossProcessLockGeneration>> {
1708 let key = JsValue::from_str(key);
1710 let txn = self
1711 .inner
1712 .transaction(keys::LEASE_LOCKS)
1713 .with_mode(TransactionMode::Readwrite)
1714 .build()?;
1715 let object_store = txn.object_store(keys::LEASE_LOCKS)?;
1716
1717 #[derive(Deserialize, Serialize)]
1718 struct Lease {
1719 holder: String,
1720 expiration: u64,
1721 generation: CrossProcessLockGeneration,
1722 }
1723
1724 let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1725 let expiration = now + lease_duration_ms as u64;
1726
1727 let lease = match object_store.get(&key).await? {
1728 Some(entry) => {
1729 let mut lease: Lease = self.serializer.deserialize_value(entry)?;
1730
1731 if lease.holder == holder {
1732 lease.expiration = expiration;
1734
1735 Some(lease)
1736 } else {
1737 if lease.expiration < now {
1739 lease.holder = holder.to_owned();
1741 lease.expiration = expiration;
1742 lease.generation += 1;
1743
1744 Some(lease)
1745 } else {
1746 None
1748 }
1749 }
1750 }
1751 None => {
1752 let lease = Lease {
1753 holder: holder.to_owned(),
1754 expiration,
1755 generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1756 };
1757
1758 Some(lease)
1759 }
1760 };
1761
1762 Ok(if let Some(lease) = lease {
1763 object_store.put(&self.serializer.serialize_value(&lease)?).with_key(key).build()?;
1764
1765 Some(lease.generation)
1766 } else {
1767 None
1768 })
1769 }
1770
1771 #[allow(clippy::unused_async)]
1772 async fn get_size(&self) -> Result<Option<usize>> {
1773 Ok(None)
1774 }
1775
1776 #[allow(clippy::unused_async)]
1777 async fn close(&self) -> Result<()> {
1778 Ok(())
1779 }
1780
1781 #[allow(clippy::unused_async)]
1782 async fn reopen(&self) -> Result<()> {
1783 Ok(())
1784 }
1785}
1786
1787impl Drop for IndexeddbCryptoStore {
1788 fn drop(&mut self) {
1789 self.inner.as_sys().close();
1792 }
1793}
1794
1795async fn open_meta_db(prefix: &str) -> Result<Database, IndexeddbCryptoStoreError> {
1799 let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1800
1801 debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1802 Database::open(&name)
1803 .with_version(1u32)
1804 .with_on_upgrade_needed(|evt, tx| {
1805 let old_version = evt.old_version() as u32;
1806 if old_version < 1 {
1807 tx.db().create_object_store("matrix-sdk-crypto").build()?;
1809 }
1810 Ok(())
1811 })
1812 .await
1813 .map_err(Into::into)
1814}
1815
1816async fn load_store_cipher(
1826 meta_db: &Database,
1827) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1828 let tx: Transaction<'_> =
1829 meta_db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readonly).build()?;
1830 let ob = tx.object_store("matrix-sdk-crypto")?;
1831
1832 let store_cipher: Option<Vec<u8>> = ob
1833 .get(&JsValue::from_str(keys::STORE_CIPHER))
1834 .await?
1835 .map(|k: JsValue| k.into_serde())
1836 .transpose()?;
1837 Ok(store_cipher)
1838}
1839
1840async fn save_store_cipher(
1847 db: &Database,
1848 export: &Vec<u8>,
1849) -> Result<(), IndexeddbCryptoStoreError> {
1850 let tx: Transaction<'_> =
1851 db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readwrite).build()?;
1852 let ob = tx.object_store("matrix-sdk-crypto")?;
1853
1854 ob.put(&JsValue::from_serde(&export)?)
1855 .with_key(JsValue::from_str(keys::STORE_CIPHER))
1856 .build()?;
1857 tx.commit().await?;
1858 Ok(())
1859}
1860
1861async fn import_store_cipher_with_key(
1875 chacha_key: &[u8; 32],
1876 original_key: &[u8],
1877 serialised_cipher: &[u8],
1878 db: &Database,
1879) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1880 let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1881 Ok(cipher) => cipher,
1882 Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1883 let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1890 .map_err(|_| CryptoStoreError::UnpicklingError)?;
1891
1892 debug!(
1896 "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1897 );
1898
1899 let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1900 save_store_cipher(db, &export).await?;
1901 cipher
1902 }
1903 Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1904 };
1905 Ok(cipher)
1906}
1907
1908async fn fetch_from_object_store_batched<R, F>(
1912 object_store: ObjectStore<'_>,
1913 f: F,
1914 batch_size: usize,
1915) -> Result<Vec<R>>
1916where
1917 F: Fn(JsValue) -> Result<R>,
1918{
1919 let mut result = Vec::new();
1920 let mut batch_n = 0;
1921
1922 let mut latest_key: JsValue = "".into();
1925
1926 loop {
1927 debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1928
1929 let after_latest_key = KeyRange::LowerBound(&latest_key, true);
1937 let cursor = object_store.open_cursor().with_query(&after_latest_key).await?;
1938
1939 let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1941 if let Some(next_key) = next_key {
1942 latest_key = next_key;
1943 } else {
1944 break;
1945 }
1946
1947 batch_n += 1;
1948 }
1949
1950 Ok(result)
1951}
1952
1953async fn fetch_batch<R, F, Q>(
1956 cursor: Option<Cursor<'_, Q>>,
1957 batch_size: usize,
1958 f: &F,
1959 result: &mut Vec<R>,
1960) -> Result<Option<JsValue>>
1961where
1962 F: Fn(JsValue) -> Result<R>,
1963 Q: QuerySource,
1964{
1965 let Some(mut cursor) = cursor else {
1966 return Ok(None);
1968 };
1969
1970 let mut latest_key = None;
1971
1972 for _ in 0..batch_size {
1973 let Some(value) = cursor.next_record().await? else {
1974 return Ok(None);
1975 };
1976
1977 let processed = f(value);
1979 if let Ok(processed) = processed {
1980 result.push(processed);
1981 }
1982 if let Some(key) = cursor.key()? {
1987 latest_key = Some(key);
1988 }
1989 }
1990
1991 Ok(latest_key)
1994}
1995
1996#[derive(Debug, Serialize, Deserialize)]
1998struct GossipRequestIndexedDbObject {
1999 info: String,
2001
2002 request: Vec<u8>,
2004
2005 #[serde(
2014 default,
2015 skip_serializing_if = "std::ops::Not::not",
2016 with = "crate::serializer::foreign::bool"
2017 )]
2018 unsent: bool,
2019}
2020
2021#[derive(Serialize, Deserialize)]
2023struct InboundGroupSessionIndexedDbObject {
2024 pickled_session: MaybeEncrypted,
2027
2028 #[serde(default, skip_serializing_if = "Option::is_none")]
2036 session_id: Option<String>,
2037
2038 #[serde(
2047 default,
2048 skip_serializing_if = "std::ops::Not::not",
2049 with = "crate::serializer::foreign::bool"
2050 )]
2051 needs_backup: bool,
2052
2053 backed_up_to: i32,
2062
2063 #[serde(default, skip_serializing_if = "Option::is_none")]
2069 sender_key: Option<String>,
2070
2071 #[serde(default, skip_serializing_if = "Option::is_none")]
2077 sender_data_type: Option<u8>,
2078}
2079
2080impl InboundGroupSessionIndexedDbObject {
2081 pub async fn from_session(
2084 session: &InboundGroupSession,
2085 serializer: &SafeEncodeSerializer,
2086 ) -> Result<Self, CryptoStoreError> {
2087 let session_id =
2088 serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
2089
2090 let sender_key = serializer.encode_key_as_string(
2091 keys::INBOUND_GROUP_SESSIONS_V3,
2092 session.sender_key().to_base64(),
2093 );
2094
2095 Ok(InboundGroupSessionIndexedDbObject {
2096 pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
2097 session_id: Some(session_id),
2098 needs_backup: !session.backed_up(),
2099 backed_up_to: -1,
2100 sender_key: Some(sender_key),
2101 sender_data_type: Some(session.sender_data_type() as u8),
2102 })
2103 }
2104}
2105
2106#[cfg(test)]
2107mod unit_tests {
2108 use matrix_sdk_crypto::{
2109 olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
2110 types::EventEncryptionAlgorithm,
2111 vodozemac::Ed25519Keypair,
2112 };
2113 use matrix_sdk_store_encryption::EncryptedValueBase64;
2114 use matrix_sdk_test::async_test;
2115 use ruma::{device_id, room_id, user_id};
2116
2117 use super::InboundGroupSessionIndexedDbObject;
2118 use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
2119
2120 #[test]
2121 fn needs_backup_is_serialized_as_a_u8_in_json() {
2122 let session_needs_backup = backup_test_session(true);
2123
2124 assert!(
2128 serde_json::to_string(&session_needs_backup).unwrap().contains(r#""needs_backup":1"#),
2129 );
2130 }
2131
2132 #[test]
2133 fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
2134 let session_backed_up = backup_test_session(false);
2135
2136 assert!(
2137 !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
2138 "The needs_backup field should be missing!"
2139 );
2140 }
2141
2142 pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
2143 InboundGroupSessionIndexedDbObject {
2144 pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
2145 session_id: None,
2146 needs_backup,
2147 backed_up_to: -1,
2148 sender_key: None,
2149 sender_data_type: None,
2150 }
2151 }
2152
2153 #[async_test]
2154 async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
2155 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2156
2157 let sender_data = SenderData::sender_verified(
2158 user_id!("@test:user"),
2159 device_id!("ABC"),
2160 Ed25519Keypair::new().public_key(),
2161 );
2162
2163 let db_object = sender_data_test_session(sender_key, sender_data).await;
2164 let serialized = serde_json::to_string(&db_object).unwrap();
2165
2166 assert!(
2167 serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
2168 );
2169 assert!(serialized.contains(r#""sender_data_type":5"#));
2170 }
2171
2172 pub async fn sender_data_test_session(
2173 sender_key: Curve25519PublicKey,
2174 sender_data: SenderData,
2175 ) -> InboundGroupSessionIndexedDbObject {
2176 let session = InboundGroupSession::new(
2177 sender_key,
2178 Ed25519Keypair::new().public_key(),
2179 room_id!("!test:localhost"),
2180 &SessionKey::from_base64(
2182 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
2183 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
2184 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
2185 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
2186 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
2187 )
2188 .unwrap(),
2189 sender_data,
2190 None,
2191 EventEncryptionAlgorithm::MegolmV1AesSha2,
2192 None,
2193 false,
2194 )
2195 .unwrap();
2196
2197 InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
2198 .await
2199 .unwrap()
2200 }
2201}
2202
2203#[cfg(all(test, target_family = "wasm"))]
2204mod wasm_unit_tests {
2205 use std::collections::BTreeMap;
2206
2207 use matrix_sdk_crypto::{
2208 olm::{Curve25519PublicKey, SenderData},
2209 types::{DeviceKeys, Signatures},
2210 };
2211 use matrix_sdk_test::async_test;
2212 use ruma::{owned_device_id, owned_user_id};
2213 use wasm_bindgen::JsValue;
2214
2215 use crate::crypto_store::unit_tests::sender_data_test_session;
2216
2217 fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
2218 assert_eq!(
2219 js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
2220 JsValue::from_f64(expected.into())
2221 );
2222 }
2223
2224 #[async_test]
2225 fn test_needs_backup_is_serialized_as_a_u8_in_js() {
2226 let session_needs_backup = super::unit_tests::backup_test_session(true);
2227
2228 let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
2229
2230 assert!(js_value.is_object());
2231 assert_field_equals(&js_value, "needs_backup", 1);
2232 }
2233
2234 #[async_test]
2235 fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
2236 let session_backed_up = super::unit_tests::backup_test_session(false);
2237
2238 let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
2239
2240 assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
2241 }
2242
2243 #[async_test]
2244 async fn test_sender_key_and_device_type_are_serialized_in_js() {
2245 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2246
2247 let sender_data = SenderData::device_info(DeviceKeys::new(
2248 owned_user_id!("@test:user"),
2249 owned_device_id!("ABC"),
2250 vec![],
2251 BTreeMap::new(),
2252 Signatures::new(),
2253 ));
2254 let db_object = sender_data_test_session(sender_key, sender_data).await;
2255
2256 let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
2257
2258 assert!(js_value.is_object());
2259 assert_field_equals(&js_value, "sender_data_type", 2);
2260 assert_eq!(
2261 js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
2262 JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
2263 );
2264 }
2265}
2266
2267#[cfg(all(test, target_family = "wasm"))]
2268mod tests {
2269 use matrix_sdk_crypto::cryptostore_integration_tests;
2270
2271 use super::IndexeddbCryptoStore;
2272
2273 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2274
2275 async fn get_store(
2276 name: &str,
2277 passphrase: Option<&str>,
2278 clear_data: bool,
2279 ) -> IndexeddbCryptoStore {
2280 if clear_data {
2281 IndexeddbCryptoStore::delete_stores(name).unwrap();
2282 }
2283 match passphrase {
2284 Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2285 .await
2286 .expect("Can't create a passphrase protected store"),
2287 None => IndexeddbCryptoStore::open_with_name(name)
2288 .await
2289 .expect("Can't create store without passphrase"),
2290 }
2291 }
2292
2293 cryptostore_integration_tests!();
2294}
2295
2296#[cfg(all(test, target_family = "wasm"))]
2297mod encrypted_tests {
2298 use matrix_sdk_crypto::{
2299 cryptostore_integration_tests,
2300 olm::Account,
2301 store::{CryptoStore, types::PendingChanges},
2302 vodozemac::base64_encode,
2303 };
2304 use matrix_sdk_test::async_test;
2305 use ruma::{device_id, user_id};
2306
2307 use super::IndexeddbCryptoStore;
2308
2309 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2310
2311 async fn get_store(
2312 name: &str,
2313 passphrase: Option<&str>,
2314 clear_data: bool,
2315 ) -> IndexeddbCryptoStore {
2316 if clear_data {
2317 IndexeddbCryptoStore::delete_stores(name).unwrap();
2318 }
2319
2320 let pass = passphrase.unwrap_or(name);
2321 IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2322 .await
2323 .expect("Can't create a passphrase protected store")
2324 }
2325 cryptostore_integration_tests!();
2326
2327 #[async_test]
2330 async fn test_migrate_passphrase_to_key() {
2331 let store_name = "test_migrate_passphrase_to_key";
2332 let passdata: [u8; 32] = rand::random();
2333 let b64_passdata = base64_encode(passdata);
2334
2335 IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2337 let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2338 .await
2339 .expect("Can't create a passphrase-protected store");
2340
2341 store
2342 .save_pending_changes(PendingChanges {
2343 account: Some(Account::with_device_id(
2344 user_id!("@alice:example.org"),
2345 device_id!("ALICEDEVICE"),
2346 )),
2347 })
2348 .await
2349 .expect("Can't save account");
2350
2351 let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2354 .await
2355 .expect("Can't create a key-protected store");
2356 let loaded_account =
2357 store.load_account().await.expect("Can't load account").expect("Account was not saved");
2358 assert_eq!(loaded_account.user_id, "@alice:example.org");
2359 }
2360}