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 {
282 store_to_key_values: BTreeMap<&'static str, Vec<PendingOperation>>,
285}
286
287struct PendingStoreChanges<'a> {
289 operations: &'a mut Vec<PendingOperation>,
290}
291
292impl PendingStoreChanges<'_> {
293 fn put(&mut self, key: JsValue, value: JsValue) {
294 self.operations.push(PendingOperation::Put { key, value });
295 }
296
297 fn delete(&mut self, key: JsValue) {
298 self.operations.push(PendingOperation::Delete(key));
299 }
300
301 fn delete_by_index(&mut self, index: &'static str, key: JsValue) {
302 self.operations.push(PendingOperation::DeleteByIndex { index, key });
303 }
304}
305
306impl PendingIndexeddbChanges {
307 fn get(&mut self, store: &'static str) -> PendingStoreChanges<'_> {
308 PendingStoreChanges { operations: self.store_to_key_values.entry(store).or_default() }
309 }
310}
311
312impl PendingIndexeddbChanges {
313 fn new() -> Self {
314 Self { store_to_key_values: BTreeMap::new() }
315 }
316
317 fn touched_stores(&self) -> Vec<&str> {
321 self.store_to_key_values
322 .iter()
323 .filter_map(
324 |(store, pending_operations)| {
325 if !pending_operations.is_empty() { Some(*store) } else { None }
326 },
327 )
328 .collect()
329 }
330
331 async fn apply(self, tx: &Transaction<'_>) -> Result<()> {
333 for (store, operations) in self.store_to_key_values {
334 if operations.is_empty() {
335 continue;
336 }
337 let object_store = tx.object_store(store)?;
338 for op in operations {
339 match op {
340 PendingOperation::Put { key, value } => {
341 object_store.put(&value).with_key(key).build()?;
342 }
343 PendingOperation::Delete(key) => {
344 object_store.delete(&key).build()?;
345 }
346 PendingOperation::DeleteByIndex { index, key } => {
347 let range = KeyRange::Only(key);
348 let ids = object_store
349 .index(index)?
350 .get_all_keys::<JsValue>()
351 .with_query::<JsValue, _>(range)
352 .await?;
353 for id in ids {
354 object_store.delete(id.unwrap()).await?;
355 }
356 }
357 }
358 }
359 }
360 Ok(())
361 }
362}
363
364impl IndexeddbCryptoStore {
365 pub(crate) async fn open_with_store_cipher(
366 prefix: &str,
367 store_cipher: Option<Arc<StoreCipher>>,
368 ) -> Result<Self> {
369 let name = format!("{prefix:0}::matrix-sdk-crypto");
370
371 let serializer = SafeEncodeSerializer::new(store_cipher);
372 debug!("IndexedDbCryptoStore: opening main store {name}");
373 let db = open_and_upgrade_db(&name, &serializer).await?;
374
375 Ok(Self {
376 name,
377 inner: db,
378 serializer,
379 static_account: RwLock::new(None),
380 save_changes_lock: Default::default(),
381 })
382 }
383
384 pub async fn open() -> Result<Self> {
386 IndexeddbCryptoStore::open_with_store_cipher("crypto", None).await
387 }
388
389 pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result<Self> {
406 let db = open_meta_db(prefix).await?;
407 let store_cipher = load_store_cipher(&db).await?;
408
409 let store_cipher = match store_cipher {
410 Some(cipher) => {
411 debug!("IndexedDbCryptoStore: decrypting store cipher");
412 StoreCipher::import(passphrase, &cipher)
413 .map_err(|_| CryptoStoreError::UnpicklingError)?
414 }
415 None => {
416 debug!("IndexedDbCryptoStore: encrypting new store cipher");
417 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
418 #[cfg(not(test))]
419 let export = cipher.export(passphrase);
420 #[cfg(test)]
421 let export = cipher._insecure_export_fast_for_testing(passphrase);
422
423 let export = export.map_err(CryptoStoreError::backend)?;
424
425 save_store_cipher(&db, &export).await?;
426 cipher
427 }
428 };
429
430 db.close();
433
434 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
435 }
436
437 pub async fn open_with_key(prefix: &str, key: &[u8; 32]) -> Result<Self> {
453 let mut chacha_key = zeroize::Zeroizing::new([0u8; 32]);
456 const HKDF_INFO: &[u8] = b"CRYPTOSTORE_CIPHER";
457 let hkdf = Hkdf::<Sha256>::new(None, key);
458 hkdf.expand(HKDF_INFO, &mut *chacha_key)
459 .expect("We should be able to generate a 32-byte key");
460
461 let db = open_meta_db(prefix).await?;
462 let store_cipher = load_store_cipher(&db).await?;
463
464 let store_cipher = match store_cipher {
465 Some(cipher) => {
466 debug!("IndexedDbCryptoStore: decrypting store cipher");
467 import_store_cipher_with_key(&chacha_key, key, &cipher, &db).await?
468 }
469 None => {
470 debug!("IndexedDbCryptoStore: encrypting new store cipher");
471 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
472 let export =
473 cipher.export_with_key(&chacha_key).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 {
531 session.reset_backup_state();
532 } else {
533 session.mark_as_backed_up();
534 }
535
536 Ok(session)
537 }
538
539 fn serialize_gossip_request(&self, gossip_request: &GossipRequest) -> Result<JsValue> {
542 let obj = GossipRequestIndexedDbObject {
543 info: self
545 .serializer
546 .encode_key_as_string(keys::GOSSIP_REQUESTS, gossip_request.info.as_key()),
547
548 request: self.serializer.serialize_value_as_bytes(gossip_request)?,
550
551 unsent: !gossip_request.sent_out,
552 };
553
554 Ok(serde_wasm_bindgen::to_value(&obj)?)
555 }
556
557 fn deserialize_gossip_request(&self, stored_request: JsValue) -> Result<GossipRequest> {
560 let idb_object: GossipRequestIndexedDbObject =
561 serde_wasm_bindgen::from_value(stored_request)?;
562 Ok(self.serializer.deserialize_value_from_bytes(&idb_object.request)?)
563 }
564
565 async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
572 let mut indexeddb_changes = PendingIndexeddbChanges::new();
573
574 let private_identity_pickle =
575 if let Some(i) = &changes.private_identity { Some(i.pickle().await) } else { None };
576
577 let decryption_key_pickle = &changes.backup_decryption_key;
578 let backup_version = &changes.backup_version;
579 let dehydration_pickle_key = &changes.dehydrated_device_pickle_key;
580
581 let mut core = indexeddb_changes.get(keys::CORE);
582 if let Some(next_batch) = &changes.next_batch_token {
583 core.put(
584 JsValue::from_str(keys::NEXT_BATCH_TOKEN),
585 self.serializer.serialize_value(next_batch)?,
586 );
587 }
588
589 if let Some(i) = &private_identity_pickle {
590 core.put(
591 JsValue::from_str(keys::PRIVATE_IDENTITY),
592 self.serializer.serialize_value(i)?,
593 );
594 }
595
596 if let Some(i) = &dehydration_pickle_key {
597 core.put(
598 JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY),
599 self.serializer.serialize_value(i)?,
600 );
601 }
602
603 if let Some(a) = &decryption_key_pickle {
604 indexeddb_changes.get(keys::BACKUP_KEYS).put(
605 JsValue::from_str(keys::RECOVERY_KEY_V1),
606 self.serializer.serialize_value(&a)?,
607 );
608 }
609
610 if let Some(a) = &backup_version {
611 indexeddb_changes.get(keys::BACKUP_KEYS).put(
612 JsValue::from_str(keys::BACKUP_VERSION_V1),
613 self.serializer.serialize_value(&a)?,
614 );
615 }
616
617 if !changes.sessions.is_empty() {
618 let mut sessions = indexeddb_changes.get(keys::SESSION);
619
620 for session in &changes.sessions {
621 let sender_key = session.sender_key().to_base64();
622 let session_id = session.session_id();
623
624 let pickle = session.pickle().await;
625 let key = self.serializer.encode_key(keys::SESSION, (&sender_key, session_id));
626
627 sessions.put(key, self.serializer.serialize_value(&pickle)?);
628 }
629 }
630
631 if !changes.inbound_group_sessions.is_empty() {
632 let mut sessions = indexeddb_changes.get(keys::INBOUND_GROUP_SESSIONS_V3);
633
634 for session in &changes.inbound_group_sessions {
635 let room_id = session.room_id();
636 let session_id = session.session_id();
637 let key = self
638 .serializer
639 .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
640 let value = self.serialize_inbound_group_session(session).await?;
641 sessions.put(key, value);
642 }
643 }
644
645 if !changes.outbound_group_sessions.is_empty() {
646 let mut sessions = indexeddb_changes.get(keys::OUTBOUND_GROUP_SESSIONS);
647
648 for session in &changes.outbound_group_sessions {
649 let room_id = session.room_id();
650 let pickle = session.pickle().await;
651 sessions.put(
652 self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
653 self.serializer.serialize_value(&pickle)?,
654 );
655 }
656 }
657
658 let device_changes = &changes.devices;
659 let identity_changes = &changes.identities;
660 let olm_hashes = &changes.message_hashes;
661 let key_requests = &changes.key_requests;
662 let withheld_session_info = &changes.withheld_session_info;
663 let room_settings_changes = &changes.room_settings;
664
665 let mut device_store = indexeddb_changes.get(keys::DEVICES);
666
667 for device in device_changes.new.iter().chain(&device_changes.changed) {
668 let key =
669 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
670 let device = self.serializer.serialize_value(&device)?;
671
672 device_store.put(key, device);
673 }
674
675 for device in &device_changes.deleted {
676 let key =
677 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
678 device_store.delete(key);
679 }
680
681 if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
682 let mut identities = indexeddb_changes.get(keys::IDENTITIES);
683 for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
684 identities.put(
685 self.serializer.encode_key(keys::IDENTITIES, identity.user_id()),
686 self.serializer.serialize_value(&identity)?,
687 );
688 }
689 }
690
691 if !olm_hashes.is_empty() {
692 let mut hashes = indexeddb_changes.get(keys::OLM_HASHES);
693 for hash in olm_hashes {
694 hashes.put(
695 self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
696 JsValue::TRUE,
697 );
698 }
699 }
700
701 if !key_requests.is_empty() {
702 let mut gossip_requests = indexeddb_changes.get(keys::GOSSIP_REQUESTS);
703
704 for gossip_request in key_requests {
705 let key_request_info =
707 self.serializer.encode_key(keys::GOSSIP_REQUESTS, gossip_request.info.as_key());
708 gossip_requests
709 .delete_by_index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX, key_request_info);
710
711 let key_request_id = self
712 .serializer
713 .encode_key(keys::GOSSIP_REQUESTS, gossip_request.request_id.as_str());
714 let key_request_value = self.serialize_gossip_request(gossip_request)?;
715 gossip_requests.put(key_request_id, key_request_value);
716 }
717 }
718
719 if !withheld_session_info.is_empty() {
720 let mut withhelds = indexeddb_changes.get(keys::WITHHELD_SESSIONS);
721
722 for (room_id, data) in withheld_session_info {
723 for (session_id, event) in data {
724 let key =
725 self.serializer.encode_key(keys::WITHHELD_SESSIONS, (&room_id, session_id));
726 withhelds.put(key, self.serializer.serialize_value(&event)?);
727 }
728 }
729 }
730
731 if !room_settings_changes.is_empty() {
732 let mut settings_store = indexeddb_changes.get(keys::ROOM_SETTINGS);
733
734 for (room_id, settings) in room_settings_changes {
735 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
736 let value = self.serializer.serialize_value(&settings)?;
737 settings_store.put(key, value);
738 }
739 }
740
741 if !changes.secrets.is_empty() {
742 let mut secret_store = indexeddb_changes.get(keys::SECRETS_INBOX_V2);
743
744 for secret in &changes.secrets {
745 use std::ops::Deref;
746 let key = self.serializer.encode_key(
751 keys::SECRETS_INBOX_V2,
752 (secret.secret_name.as_str(), secret.secret.as_str()),
753 );
754 let value = self.serializer.serialize_value(secret.secret.deref())?;
755
756 secret_store.put(key, value);
757 }
758 }
759
760 if !changes.received_room_key_bundles.is_empty() {
761 let mut bundle_store = indexeddb_changes.get(keys::RECEIVED_ROOM_KEY_BUNDLES);
762 for bundle in &changes.received_room_key_bundles {
763 let key = self.serializer.encode_key(
764 keys::RECEIVED_ROOM_KEY_BUNDLES,
765 (&bundle.bundle_data.room_id, &bundle.sender_user),
766 );
767 let value = self.serializer.serialize_value(&bundle)?;
768 bundle_store.put(key, value);
769 }
770 }
771
772 if !changes.room_key_backups_fully_downloaded.is_empty() {
773 let mut room_store = indexeddb_changes.get(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED);
774 for room_id in &changes.room_key_backups_fully_downloaded {
775 room_store.put(
776 self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id),
777 JsValue::TRUE,
778 );
779 }
780 }
781
782 if !changes.rooms_pending_key_bundle.is_empty() {
783 let mut room_store = indexeddb_changes.get(keys::ROOMS_PENDING_KEY_BUNDLE);
784 for (room_id, details) in &changes.rooms_pending_key_bundle {
785 let key = self.serializer.encode_key(keys::ROOMS_PENDING_KEY_BUNDLE, room_id);
786 if let Some(details) = details {
787 let value = self.serializer.serialize_value(details)?;
788 room_store.put(key, value);
789 } else {
790 room_store.delete(key);
791 }
792 }
793 }
794
795 Ok(indexeddb_changes)
796 }
797}
798
799#[cfg(target_family = "wasm")]
808macro_rules! impl_crypto_store {
809 ( $($body:tt)* ) => {
810 #[async_trait(?Send)]
811 impl CryptoStore for IndexeddbCryptoStore {
812 type Error = IndexeddbCryptoStoreError;
813
814 $($body)*
815 }
816 };
817}
818
819#[cfg(not(target_family = "wasm"))]
820macro_rules! impl_crypto_store {
821 ( $($body:tt)* ) => {
822 impl IndexeddbCryptoStore {
823 $($body)*
824 }
825 };
826}
827
828impl_crypto_store! {
829 async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> {
830 let _guard = self.save_changes_lock.lock().await;
835
836 let stores: Vec<&str> = [(changes.account.is_some(), keys::CORE)]
837 .iter()
838 .filter_map(|(id, key)| if *id { Some(*key) } else { None })
839 .collect();
840
841 if stores.is_empty() {
842 return Ok(());
844 }
845
846 let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
847
848 let account_pickle = if let Some(account) = changes.account {
849 *self.static_account.write().unwrap() = Some(account.static_data().clone());
850 Some(account.pickle())
851 } else {
852 None
853 };
854
855 if let Some(a) = &account_pickle {
856 tx.object_store(keys::CORE)?
857 .put(&self.serializer.serialize_value(&a)?)
858 .with_key(JsValue::from_str(keys::ACCOUNT))
859 .build()?;
860 }
861
862 tx.commit().await?;
863
864 Ok(())
865 }
866
867 async fn save_changes(&self, changes: Changes) -> Result<()> {
868 let _guard = self.save_changes_lock.lock().await;
873
874 let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
875
876 let stores = indexeddb_changes.touched_stores();
877
878 if stores.is_empty() {
879 return Ok(());
881 }
882
883 let tx = self.inner.transaction(stores).with_mode(TransactionMode::Readwrite).build()?;
884
885 indexeddb_changes.apply(&tx).await?;
886
887 tx.commit().await?;
888
889 Ok(())
890 }
891
892 async fn save_inbound_group_sessions(
893 &self,
894 sessions: Vec<InboundGroupSession>,
895 backed_up_to_version: Option<&str>,
896 ) -> Result<()> {
897 sessions.iter().for_each(|s| {
899 let backed_up = s.backed_up();
900 if backed_up != backed_up_to_version.is_some() {
901 warn!(
902 backed_up,
903 backed_up_to_version,
904 "Session backed-up flag does not correspond to backup version setting",
905 );
906 }
907 });
908
909 self.save_changes(Changes { inbound_group_sessions: sessions, ..Changes::default() }).await
912 }
913
914 async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
915 let tx = self
916 .inner
917 .transaction(keys::TRACKED_USERS)
918 .with_mode(TransactionMode::Readonly)
919 .build()?;
920 let os = tx.object_store(keys::TRACKED_USERS)?;
921 let user_ids = os.get_all_keys::<JsValue>().await?;
922
923 let mut users = Vec::new();
924
925 for result in user_ids {
926 let user_id = result?;
927 let dirty: bool = !matches!(
928 os.get(&user_id).await?.map(|v: JsValue| v.into_serde()),
929 Some(Ok(false))
930 );
931 let Some(Ok(user_id)) = user_id.as_string().map(UserId::parse) else { continue };
932
933 users.push(TrackedUser { user_id, dirty });
934 }
935
936 Ok(users)
937 }
938
939 async fn get_outbound_group_session(
940 &self,
941 room_id: &RoomId,
942 ) -> Result<Option<OutboundGroupSession>> {
943 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
944 if let Some(value) = self
945 .inner
946 .transaction(keys::OUTBOUND_GROUP_SESSIONS)
947 .with_mode(TransactionMode::Readonly)
948 .build()?
949 .object_store(keys::OUTBOUND_GROUP_SESSIONS)?
950 .get(&self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))
951 .await?
952 {
953 Ok(Some(
954 OutboundGroupSession::from_pickle(
955 account_info.device_id,
956 account_info.identity_keys,
957 self.serializer.deserialize_value(value)?,
958 )
959 .map_err(CryptoStoreError::from)?,
960 ))
961 } else {
962 Ok(None)
963 }
964 }
965
966 async fn get_outgoing_secret_requests(
967 &self,
968 request_id: &TransactionId,
969 ) -> Result<Option<GossipRequest>> {
970 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id.as_str());
971 self.inner
972 .transaction(keys::GOSSIP_REQUESTS)
973 .with_mode(TransactionMode::Readonly)
974 .build()?
975 .object_store(keys::GOSSIP_REQUESTS)?
976 .get(jskey)
977 .await?
978 .map(|val| self.deserialize_gossip_request(val))
979 .transpose()
980 }
981
982 async fn load_account(&self) -> Result<Option<Account>> {
983 if let Some(pickle) = self
984 .inner
985 .transaction(keys::CORE)
986 .with_mode(TransactionMode::Readonly)
987 .build()?
988 .object_store(keys::CORE)?
989 .get(&JsValue::from_str(keys::ACCOUNT))
990 .await?
991 {
992 let pickle = self.serializer.deserialize_value(pickle)?;
993
994 let account = Account::from_pickle(pickle).map_err(CryptoStoreError::from)?;
995
996 *self.static_account.write().unwrap() = Some(account.static_data().clone());
997
998 Ok(Some(account))
999 } else {
1000 Ok(None)
1001 }
1002 }
1003
1004 async fn next_batch_token(&self) -> Result<Option<String>> {
1005 if let Some(serialized) = self
1006 .inner
1007 .transaction(keys::CORE)
1008 .with_mode(TransactionMode::Readonly)
1009 .build()?
1010 .object_store(keys::CORE)?
1011 .get(&JsValue::from_str(keys::NEXT_BATCH_TOKEN))
1012 .await?
1013 {
1014 let token = self.serializer.deserialize_value(serialized)?;
1015 Ok(Some(token))
1016 } else {
1017 Ok(None)
1018 }
1019 }
1020
1021 async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
1022 if let Some(pickle) = self
1023 .inner
1024 .transaction(keys::CORE)
1025 .with_mode(TransactionMode::Readonly)
1026 .build()?
1027 .object_store(keys::CORE)?
1028 .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))
1029 .await?
1030 {
1031 let pickle = self.serializer.deserialize_value(pickle)?;
1032
1033 Ok(Some(
1034 PrivateCrossSigningIdentity::from_pickle(pickle)
1035 .map_err(|_| CryptoStoreError::UnpicklingError)?,
1036 ))
1037 } else {
1038 Ok(None)
1039 }
1040 }
1041
1042 async fn get_sessions(&self, sender_key: &str) -> Result<Option<Vec<Session>>> {
1043 let device_keys = self.get_own_device().await?.as_device_keys().clone();
1044
1045 let range = self.serializer.encode_to_range(keys::SESSION, sender_key);
1046 let sessions: Vec<Session> = self
1047 .inner
1048 .transaction(keys::SESSION)
1049 .with_mode(TransactionMode::Readonly)
1050 .build()?
1051 .object_store(keys::SESSION)?
1052 .get_all()
1053 .with_query(&range)
1054 .await?
1055 .filter_map(Result::ok)
1056 .filter_map(|f| {
1057 self.serializer.deserialize_value(f).ok().map(|p| {
1058 Session::from_pickle(device_keys.clone(), p).map_err(|_| {
1059 IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset)
1060 })
1061 })
1062 })
1063 .collect::<Result<Vec<Session>>>()?;
1064
1065 if sessions.is_empty() {
1066 Ok(None)
1067 } else {
1068 Ok(Some(sessions))
1069 }
1070 }
1071
1072 async fn get_inbound_group_session(
1073 &self,
1074 room_id: &RoomId,
1075 session_id: &str,
1076 ) -> Result<Option<InboundGroupSession>> {
1077 let key =
1078 self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1079 if let Some(value) = self
1080 .inner
1081 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1082 .with_mode(TransactionMode::Readonly)
1083 .build()?
1084 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1085 .get(&key)
1086 .await?
1087 {
1088 Ok(Some(self.deserialize_inbound_group_session(value)?))
1089 } else {
1090 Ok(None)
1091 }
1092 }
1093
1094 async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
1095 const INBOUND_GROUP_SESSIONS_BATCH_SIZE: usize = 1000;
1096
1097 let transaction = self
1098 .inner
1099 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1100 .with_mode(TransactionMode::Readonly)
1101 .build()?;
1102
1103 let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1104
1105 fetch_from_object_store_batched(
1106 object_store,
1107 |value| self.deserialize_inbound_group_session(value),
1108 INBOUND_GROUP_SESSIONS_BATCH_SIZE,
1109 )
1110 .await
1111 }
1112
1113 async fn get_inbound_group_sessions_by_room_id(
1114 &self,
1115 room_id: &RoomId,
1116 ) -> Result<Vec<InboundGroupSession>> {
1117 let range = self.serializer.encode_to_range(keys::INBOUND_GROUP_SESSIONS_V3, room_id);
1118 Ok(self
1119 .inner
1120 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1121 .with_mode(TransactionMode::Readonly)
1122 .build()?
1123 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1124 .get_all()
1125 .with_query(&range)
1126 .await?
1127 .filter_map(Result::ok)
1128 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1129 Ok(session) => Some(session),
1130 Err(e) => {
1131 warn!("Failed to deserialize inbound group session: {e}");
1132 None
1133 }
1134 })
1135 .collect::<Vec<InboundGroupSession>>())
1136 }
1137
1138 async fn get_inbound_group_sessions_for_device_batch(
1139 &self,
1140 sender_key: Curve25519PublicKey,
1141 sender_data_type: SenderDataType,
1142 after_session_id: Option<String>,
1143 limit: usize,
1144 ) -> Result<Vec<InboundGroupSession>> {
1145 let sender_key =
1146 self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
1147
1148 let after_session_id = after_session_id
1150 .map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s))
1151 .unwrap_or("".into());
1152
1153 let lower_bound: Array =
1154 [sender_key.clone(), (sender_data_type as u8).into(), after_session_id]
1155 .iter()
1156 .collect();
1157 let upper_bound: Array =
1158 [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
1159 let key = KeyRange::Bound(lower_bound, true, upper_bound, true);
1160
1161 let tx = self
1162 .inner
1163 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1164 .with_mode(TransactionMode::Readonly)
1165 .build()?;
1166
1167 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1168 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1169 let serialized_sessions =
1170 idx.get_all().with_query::<Array, _>(key).with_limit(limit as u32).await?;
1171
1172 let result = serialized_sessions
1174 .filter_map(Result::ok)
1175 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1176 Ok(session) => Some(session),
1177 Err(e) => {
1178 warn!("Failed to deserialize inbound group session: {e}");
1179 None
1180 }
1181 })
1182 .collect::<Vec<InboundGroupSession>>();
1183
1184 Ok(result)
1185 }
1186
1187 async fn inbound_group_session_counts(
1188 &self,
1189 _backup_version: Option<&str>,
1190 ) -> Result<RoomKeyCounts> {
1191 let tx = self
1192 .inner
1193 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1194 .with_mode(TransactionMode::Readonly)
1195 .build()?;
1196 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1197 let all = store.count().await? as usize;
1198 let not_backed_up =
1199 store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count().await? as usize;
1200 tx.commit().await?;
1201 Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1202 }
1203
1204 async fn inbound_group_sessions_for_backup(
1205 &self,
1206 _backup_version: &str,
1207 limit: usize,
1208 ) -> Result<Vec<InboundGroupSession>> {
1209 let tx = self
1210 .inner
1211 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1212 .with_mode(TransactionMode::Readonly)
1213 .build()?;
1214
1215 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1216 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1217
1218 let Some(mut cursor) = idx.open_cursor().await? else {
1222 return Ok(vec![]);
1223 };
1224
1225 let mut serialized_sessions = Vec::with_capacity(limit);
1226 for _ in 0..limit {
1227 let Some(value) = cursor.next_record().await? else {
1228 break;
1229 };
1230 serialized_sessions.push(value)
1231 }
1232
1233 tx.commit().await?;
1234
1235 let result = serialized_sessions
1237 .into_iter()
1238 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1239 Ok(session) => Some(session),
1240 Err(e) => {
1241 warn!("Failed to deserialize inbound group session: {e}");
1242 None
1243 }
1244 })
1245 .collect::<Vec<InboundGroupSession>>();
1246
1247 Ok(result)
1248 }
1249
1250 async fn mark_inbound_group_sessions_as_backed_up(
1251 &self,
1252 _backup_version: &str,
1253 room_and_session_ids: &[(&RoomId, &str)],
1254 ) -> Result<()> {
1255 let tx = self
1256 .inner
1257 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1258 .with_mode(TransactionMode::Readwrite)
1259 .build()?;
1260
1261 let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1262
1263 for (room_id, session_id) in room_and_session_ids {
1264 let key =
1265 self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1266 if let Some(idb_object_js) = object_store.get(&key).await? {
1267 let mut idb_object: InboundGroupSessionIndexedDbObject =
1268 serde_wasm_bindgen::from_value(idb_object_js)?;
1269 idb_object.needs_backup = false;
1270 object_store
1271 .put(&serde_wasm_bindgen::to_value(&idb_object)?)
1272 .with_key(key)
1273 .build()?;
1274 } else {
1275 warn!(?key, "Could not find inbound group session to mark it as backed up.");
1276 }
1277 }
1278
1279 Ok(tx.commit().await?)
1280 }
1281
1282 async fn reset_backup_state(&self) -> Result<()> {
1283 let tx = self
1284 .inner
1285 .transaction(keys::INBOUND_GROUP_SESSIONS_V3)
1286 .with_mode(TransactionMode::Readwrite)
1287 .build()?;
1288
1289 if let Some(mut cursor) =
1290 tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor().await?
1291 {
1292 while let Some(value) = cursor.next_record().await? {
1293 let mut idb_object: InboundGroupSessionIndexedDbObject =
1294 serde_wasm_bindgen::from_value(value)?;
1295 if !idb_object.needs_backup {
1296 idb_object.needs_backup = true;
1297 let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1301 cursor.update(&idb_object).await?;
1302 }
1303 }
1304 }
1305
1306 Ok(tx.commit().await?)
1307 }
1308
1309 async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1310 let tx = self
1311 .inner
1312 .transaction(keys::TRACKED_USERS)
1313 .with_mode(TransactionMode::Readwrite)
1314 .build()?;
1315 let os = tx.object_store(keys::TRACKED_USERS)?;
1316
1317 for (user, dirty) in users {
1318 os.put(&JsValue::from(*dirty)).with_key(JsValue::from_str(user.as_str())).build()?;
1319 }
1320
1321 tx.commit().await?;
1322 Ok(())
1323 }
1324
1325 async fn get_device(
1326 &self,
1327 user_id: &UserId,
1328 device_id: &DeviceId,
1329 ) -> Result<Option<DeviceData>> {
1330 let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1331 self.inner
1332 .transaction(keys::DEVICES)
1333 .with_mode(TransactionMode::Readonly)
1334 .build()?
1335 .object_store(keys::DEVICES)?
1336 .get(&key)
1337 .await?
1338 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1339 .transpose()
1340 }
1341
1342 async fn get_user_devices(
1343 &self,
1344 user_id: &UserId,
1345 ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1346 let range = self.serializer.encode_to_range(keys::DEVICES, user_id);
1347 Ok(self
1348 .inner
1349 .transaction(keys::DEVICES)
1350 .with_mode(TransactionMode::Readonly)
1351 .build()?
1352 .object_store(keys::DEVICES)?
1353 .get_all()
1354 .with_query(&range)
1355 .await?
1356 .filter_map(Result::ok)
1357 .filter_map(|d| {
1358 let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1359 Some((d.device_id().to_owned(), d))
1360 })
1361 .collect::<HashMap<_, _>>())
1362 }
1363
1364 async fn get_own_device(&self) -> Result<DeviceData> {
1365 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1366 Ok(self.get_device(&account_info.user_id, &account_info.device_id).await?.unwrap())
1367 }
1368
1369 async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1370 self.inner
1371 .transaction(keys::IDENTITIES)
1372 .with_mode(TransactionMode::Readonly)
1373 .build()?
1374 .object_store(keys::IDENTITIES)?
1375 .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))
1376 .await?
1377 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1378 .transpose()
1379 }
1380
1381 async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1382 Ok(self
1383 .inner
1384 .transaction(keys::OLM_HASHES)
1385 .with_mode(TransactionMode::Readonly)
1386 .build()?
1387 .object_store(keys::OLM_HASHES)?
1388 .get::<JsValue, _, _>(
1389 &self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
1390 )
1391 .await?
1392 .is_some())
1393 }
1394
1395 async fn get_secrets_from_inbox(
1396 &self,
1397 secret_name: &SecretName,
1398 ) -> Result<Vec<zeroize::Zeroizing<String>>> {
1399 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX_V2, secret_name.as_str());
1400
1401 self.inner
1402 .transaction(keys::SECRETS_INBOX_V2)
1403 .with_mode(TransactionMode::Readonly)
1404 .build()?
1405 .object_store(keys::SECRETS_INBOX_V2)?
1406 .get_all()
1407 .with_query(&range)
1408 .await?
1409 .map(|result| {
1410 let d = result?;
1411 let secret: String = self.serializer.deserialize_value(d)?;
1412 Ok(secret.into())
1413 })
1414 .collect()
1415 }
1416
1417 #[allow(clippy::unused_async)] async fn delete_secrets_from_inbox(&self, secret_name: &SecretName) -> Result<()> {
1419 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX_V2, secret_name.as_str());
1420
1421 let transaction = self
1422 .inner
1423 .transaction(keys::SECRETS_INBOX_V2)
1424 .with_mode(TransactionMode::Readwrite)
1425 .build()?;
1426 transaction.object_store(keys::SECRETS_INBOX_V2)?.delete(&range).build()?;
1427 transaction.commit().await?;
1428
1429 Ok(())
1430 }
1431
1432 async fn get_secret_request_by_info(
1433 &self,
1434 key_info: &SecretInfo,
1435 ) -> Result<Option<GossipRequest>> {
1436 let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1437
1438 let val = self
1439 .inner
1440 .transaction(keys::GOSSIP_REQUESTS)
1441 .with_mode(TransactionMode::Readonly)
1442 .build()?
1443 .object_store(keys::GOSSIP_REQUESTS)?
1444 .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1445 .get(key)
1446 .await?;
1447
1448 if let Some(val) = val {
1449 let deser = self.deserialize_gossip_request(val)?;
1450 Ok(Some(deser))
1451 } else {
1452 Ok(None)
1453 }
1454 }
1455
1456 async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1457 let results = self
1458 .inner
1459 .transaction(keys::GOSSIP_REQUESTS)
1460 .with_mode(TransactionMode::Readonly)
1461 .build()?
1462 .object_store(keys::GOSSIP_REQUESTS)?
1463 .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1464 .get_all()
1465 .await?
1466 .filter_map(Result::ok)
1467 .filter_map(|val| self.deserialize_gossip_request(val).ok())
1468 .collect();
1469
1470 Ok(results)
1471 }
1472
1473 async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1474 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1475 let tx = self
1476 .inner
1477 .transaction(keys::GOSSIP_REQUESTS)
1478 .with_mode(TransactionMode::Readwrite)
1479 .build()?;
1480 tx.object_store(keys::GOSSIP_REQUESTS)?.delete(jskey).build()?;
1481 tx.commit().await.map_err(|e| e.into())
1482 }
1483
1484 async fn load_backup_keys(&self) -> Result<BackupKeys> {
1485 let key = {
1486 let tx = self
1487 .inner
1488 .transaction(keys::BACKUP_KEYS)
1489 .with_mode(TransactionMode::Readonly)
1490 .build()?;
1491 let store = tx.object_store(keys::BACKUP_KEYS)?;
1492
1493 let backup_version = store
1494 .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))
1495 .await?
1496 .map(|i| self.serializer.deserialize_value(i))
1497 .transpose()?;
1498
1499 let decryption_key = store
1500 .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))
1501 .await?
1502 .map(|i| self.serializer.deserialize_value(i))
1503 .transpose()?;
1504
1505 BackupKeys { backup_version, decryption_key }
1506 };
1507
1508 Ok(key)
1509 }
1510
1511 async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1512 if let Some(pickle) = self
1513 .inner
1514 .transaction(keys::CORE)
1515 .with_mode(TransactionMode::Readonly)
1516 .build()?
1517 .object_store(keys::CORE)?
1518 .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))
1519 .await?
1520 {
1521 let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1522
1523 Ok(Some(pickle))
1524 } else {
1525 Ok(None)
1526 }
1527 }
1528
1529 async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1530 self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1531 Ok(())
1532 }
1533
1534 async fn get_withheld_info(
1535 &self,
1536 room_id: &RoomId,
1537 session_id: &str,
1538 ) -> Result<Option<RoomKeyWithheldEntry>> {
1539 let key = self.serializer.encode_key(keys::WITHHELD_SESSIONS, (room_id, session_id));
1540 if let Some(pickle) = self
1541 .inner
1542 .transaction(keys::WITHHELD_SESSIONS)
1543 .with_mode(TransactionMode::Readonly)
1544 .build()?
1545 .object_store(keys::WITHHELD_SESSIONS)?
1546 .get(&key)
1547 .await?
1548 {
1549 let info = self.serializer.deserialize_value(pickle)?;
1550 Ok(Some(info))
1551 } else {
1552 Ok(None)
1553 }
1554 }
1555
1556 async fn get_withheld_sessions_by_room_id(
1557 &self,
1558 room_id: &RoomId,
1559 ) -> Result<Vec<RoomKeyWithheldEntry>> {
1560 let range = self.serializer.encode_to_range(keys::WITHHELD_SESSIONS, room_id);
1561
1562 self
1563 .inner
1564 .transaction(keys::WITHHELD_SESSIONS)
1565 .with_mode(TransactionMode::Readonly)
1566 .build()?
1567 .object_store(keys::WITHHELD_SESSIONS)?
1568 .get_all()
1569 .with_query(&range)
1570 .await?
1571 .map(|val| self.serializer.deserialize_value(val?).map_err(Into::into))
1572 .collect()
1573 }
1574
1575 async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1576 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1577 self.inner
1578 .transaction(keys::ROOM_SETTINGS)
1579 .with_mode(TransactionMode::Readonly)
1580 .build()?
1581 .object_store(keys::ROOM_SETTINGS)?
1582 .get(&key)
1583 .await?
1584 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1585 .transpose()
1586 }
1587
1588 async fn get_received_room_key_bundle_data(
1589 &self,
1590 room_id: &RoomId,
1591 user_id: &UserId,
1592 ) -> Result<Option<StoredRoomKeyBundleData>> {
1593 let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1594 let result = self
1595 .inner
1596 .transaction(keys::RECEIVED_ROOM_KEY_BUNDLES)
1597 .with_mode(TransactionMode::Readonly)
1598 .build()?
1599 .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1600 .get(&key)
1601 .await?
1602 .map(|v| self.serializer.deserialize_value(v))
1603 .transpose()?;
1604
1605 Ok(result)
1606 }
1607
1608 async fn has_downloaded_all_room_keys(&self, room_id: &RoomId) -> Result<bool> {
1609 let key = self.serializer.encode_key(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED, room_id);
1610 let result = self
1611 .inner
1612 .transaction(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)
1613 .with_mode(TransactionMode::Readonly)
1614 .build()?
1615 .object_store(keys::ROOM_KEY_BACKUPS_FULLY_DOWNLOADED)?
1616 .get::<JsValue, _, _>(&key)
1617 .await?
1618 .is_some();
1619
1620 Ok(result)
1621 }
1622
1623 async fn get_pending_key_bundle_details_for_room(&self, room_id: &RoomId) -> Result<Option<RoomPendingKeyBundleDetails >> {
1624 let key = self.serializer.encode_key(keys::ROOMS_PENDING_KEY_BUNDLE, room_id);
1625 let result = self
1626 .inner
1627 .transaction(keys::ROOMS_PENDING_KEY_BUNDLE)
1628 .with_mode(TransactionMode::Readonly)
1629 .build()?
1630 .object_store(keys::ROOMS_PENDING_KEY_BUNDLE)?
1631 .get(&key)
1632 .await?
1633 .map(|v| self.serializer.deserialize_value(v))
1634 .transpose()?;
1635 Ok(result)
1636 }
1637
1638 async fn get_all_rooms_pending_key_bundles(&self) -> Result<Vec<RoomPendingKeyBundleDetails>> {
1639 let result = self
1640 .inner
1641 .transaction(keys::ROOMS_PENDING_KEY_BUNDLE)
1642 .with_mode(TransactionMode::Readonly)
1643 .build()?
1644 .object_store(keys::ROOMS_PENDING_KEY_BUNDLE)?
1645 .get_all()
1646 .await?
1647 .map(|result| {
1648 result
1649 .map_err(Into::into)
1650 .and_then(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1651 })
1652 .collect::<Result<Vec<_>>>()?;
1653 Ok(result)
1654 }
1655
1656 async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1657 self.inner
1658 .transaction(keys::CORE)
1659 .with_mode(TransactionMode::Readonly)
1660 .build()?
1661 .object_store(keys::CORE)?
1662 .get(&JsValue::from_str(key))
1663 .await?
1664 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1665 .transpose()
1666 }
1667
1668 #[allow(clippy::unused_async)] async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1670 let transaction =
1671 self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1672 transaction
1673 .object_store(keys::CORE)?
1674 .put(&self.serializer.serialize_value(&value)?)
1675 .with_key(JsValue::from_str(key))
1676 .build()?;
1677 transaction.commit().await?;
1678 Ok(())
1679 }
1680
1681 #[allow(clippy::unused_async)] async fn remove_custom_value(&self, key: &str) -> Result<()> {
1683 let transaction =
1684 self.inner.transaction(keys::CORE).with_mode(TransactionMode::Readwrite).build()?;
1685 transaction.object_store(keys::CORE)?.delete(&JsValue::from_str(key)).build()?;
1686 transaction.commit().await?;
1687 Ok(())
1688 }
1689
1690 async fn try_take_leased_lock(
1691 &self,
1692 lease_duration_ms: u32,
1693 key: &str,
1694 holder: &str,
1695 ) -> Result<Option<CrossProcessLockGeneration>> {
1696 let key = JsValue::from_str(key);
1698 let txn = self
1699 .inner
1700 .transaction(keys::LEASE_LOCKS)
1701 .with_mode(TransactionMode::Readwrite)
1702 .build()?;
1703 let object_store = txn.object_store(keys::LEASE_LOCKS)?;
1704
1705 #[derive(Deserialize, Serialize)]
1706 struct Lease {
1707 holder: String,
1708 expiration: u64,
1709 generation: CrossProcessLockGeneration,
1710 }
1711
1712 let now: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1713 let expiration = now + lease_duration_ms as u64;
1714
1715 let lease = match object_store.get(&key).await? {
1716 Some(entry) => {
1717 let mut lease: Lease = self.serializer.deserialize_value(entry)?;
1718
1719 if lease.holder == holder {
1720 lease.expiration = expiration;
1722
1723 Some(lease)
1724 } else {
1725 if lease.expiration < now {
1727 lease.holder = holder.to_owned();
1729 lease.expiration = expiration;
1730 lease.generation += 1;
1731
1732 Some(lease)
1733 } else {
1734 None
1736 }
1737 }
1738 }
1739 None => {
1740 let lease = Lease {
1741 holder: holder.to_owned(),
1742 expiration,
1743 generation: FIRST_CROSS_PROCESS_LOCK_GENERATION,
1744 };
1745
1746 Some(lease)
1747 }
1748 };
1749
1750 Ok(if let Some(lease) = lease {
1751 object_store.put(&self.serializer.serialize_value(&lease)?).with_key(key).build()?;
1752
1753 Some(lease.generation)
1754 } else {
1755 None
1756 })
1757 }
1758
1759 #[allow(clippy::unused_async)]
1760 async fn get_size(&self) -> Result<Option<usize>> {
1761 Ok(None)
1762 }
1763
1764 #[allow(clippy::unused_async)]
1765 async fn close(&self) -> Result<()> {
1766 Ok(())
1767 }
1768
1769 #[allow(clippy::unused_async)]
1770 async fn reopen(&self) -> Result<()> {
1771 Ok(())
1772 }
1773}
1774
1775impl Drop for IndexeddbCryptoStore {
1776 fn drop(&mut self) {
1777 self.inner.as_sys().close();
1780 }
1781}
1782
1783async fn open_meta_db(prefix: &str) -> Result<Database, IndexeddbCryptoStoreError> {
1787 let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1788
1789 debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1790 Database::open(&name)
1791 .with_version(1u32)
1792 .with_on_upgrade_needed(|evt, tx| {
1793 let old_version = evt.old_version() as u32;
1794 if old_version < 1 {
1795 tx.db().create_object_store("matrix-sdk-crypto").build()?;
1797 }
1798 Ok(())
1799 })
1800 .await
1801 .map_err(Into::into)
1802}
1803
1804async fn load_store_cipher(
1814 meta_db: &Database,
1815) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1816 let tx: Transaction<'_> =
1817 meta_db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readonly).build()?;
1818 let ob = tx.object_store("matrix-sdk-crypto")?;
1819
1820 let store_cipher: Option<Vec<u8>> = ob
1821 .get(&JsValue::from_str(keys::STORE_CIPHER))
1822 .await?
1823 .map(|k: JsValue| k.into_serde())
1824 .transpose()?;
1825 Ok(store_cipher)
1826}
1827
1828async fn save_store_cipher(
1835 db: &Database,
1836 export: &Vec<u8>,
1837) -> Result<(), IndexeddbCryptoStoreError> {
1838 let tx: Transaction<'_> =
1839 db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readwrite).build()?;
1840 let ob = tx.object_store("matrix-sdk-crypto")?;
1841
1842 ob.put(&JsValue::from_serde(&export)?)
1843 .with_key(JsValue::from_str(keys::STORE_CIPHER))
1844 .build()?;
1845 tx.commit().await?;
1846 Ok(())
1847}
1848
1849async fn import_store_cipher_with_key(
1863 chacha_key: &[u8; 32],
1864 original_key: &[u8],
1865 serialised_cipher: &[u8],
1866 db: &Database,
1867) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1868 let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1869 Ok(cipher) => cipher,
1870 Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1871 let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1876 .map_err(|_| CryptoStoreError::UnpicklingError)?;
1877
1878 debug!(
1882 "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1883 );
1884
1885 let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1886 save_store_cipher(db, &export).await?;
1887 cipher
1888 }
1889 Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1890 };
1891 Ok(cipher)
1892}
1893
1894async fn fetch_from_object_store_batched<R, F>(
1898 object_store: ObjectStore<'_>,
1899 f: F,
1900 batch_size: usize,
1901) -> Result<Vec<R>>
1902where
1903 F: Fn(JsValue) -> Result<R>,
1904{
1905 let mut result = Vec::new();
1906 let mut batch_n = 0;
1907
1908 let mut latest_key: JsValue = "".into();
1910
1911 loop {
1912 debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1913
1914 let after_latest_key = KeyRange::LowerBound(&latest_key, true);
1922 let cursor = object_store.open_cursor().with_query(&after_latest_key).await?;
1923
1924 let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1926 if let Some(next_key) = next_key {
1927 latest_key = next_key;
1928 } else {
1929 break;
1930 }
1931
1932 batch_n += 1;
1933 }
1934
1935 Ok(result)
1936}
1937
1938async fn fetch_batch<R, F, Q>(
1942 cursor: Option<Cursor<'_, Q>>,
1943 batch_size: usize,
1944 f: &F,
1945 result: &mut Vec<R>,
1946) -> Result<Option<JsValue>>
1947where
1948 F: Fn(JsValue) -> Result<R>,
1949 Q: QuerySource,
1950{
1951 let Some(mut cursor) = cursor else {
1952 return Ok(None);
1954 };
1955
1956 let mut latest_key = None;
1957
1958 for _ in 0..batch_size {
1959 let Some(value) = cursor.next_record().await? else {
1960 return Ok(None);
1961 };
1962
1963 let processed = f(value);
1965 if let Ok(processed) = processed {
1966 result.push(processed);
1967 }
1968 if let Some(key) = cursor.key()? {
1973 latest_key = Some(key);
1974 }
1975 }
1976
1977 Ok(latest_key)
1980}
1981
1982#[derive(Debug, Serialize, Deserialize)]
1984struct GossipRequestIndexedDbObject {
1985 info: String,
1987
1988 request: Vec<u8>,
1990
1991 #[serde(
2000 default,
2001 skip_serializing_if = "std::ops::Not::not",
2002 with = "crate::serializer::foreign::bool"
2003 )]
2004 unsent: bool,
2005}
2006
2007#[derive(Serialize, Deserialize)]
2009struct InboundGroupSessionIndexedDbObject {
2010 pickled_session: MaybeEncrypted,
2013
2014 #[serde(default, skip_serializing_if = "Option::is_none")]
2022 session_id: Option<String>,
2023
2024 #[serde(
2033 default,
2034 skip_serializing_if = "std::ops::Not::not",
2035 with = "crate::serializer::foreign::bool"
2036 )]
2037 needs_backup: bool,
2038
2039 backed_up_to: i32,
2050
2051 #[serde(default, skip_serializing_if = "Option::is_none")]
2057 sender_key: Option<String>,
2058
2059 #[serde(default, skip_serializing_if = "Option::is_none")]
2065 sender_data_type: Option<u8>,
2066}
2067
2068impl InboundGroupSessionIndexedDbObject {
2069 pub async fn from_session(
2072 session: &InboundGroupSession,
2073 serializer: &SafeEncodeSerializer,
2074 ) -> Result<Self, CryptoStoreError> {
2075 let session_id =
2076 serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
2077
2078 let sender_key = serializer.encode_key_as_string(
2079 keys::INBOUND_GROUP_SESSIONS_V3,
2080 session.sender_key().to_base64(),
2081 );
2082
2083 Ok(InboundGroupSessionIndexedDbObject {
2084 pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
2085 session_id: Some(session_id),
2086 needs_backup: !session.backed_up(),
2087 backed_up_to: -1,
2088 sender_key: Some(sender_key),
2089 sender_data_type: Some(session.sender_data_type() as u8),
2090 })
2091 }
2092}
2093
2094#[cfg(test)]
2095mod unit_tests {
2096 use matrix_sdk_crypto::{
2097 olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
2098 types::EventEncryptionAlgorithm,
2099 vodozemac::Ed25519Keypair,
2100 };
2101 use matrix_sdk_store_encryption::EncryptedValueBase64;
2102 use matrix_sdk_test::async_test;
2103 use ruma::{device_id, room_id, user_id};
2104
2105 use super::InboundGroupSessionIndexedDbObject;
2106 use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
2107
2108 #[test]
2109 fn needs_backup_is_serialized_as_a_u8_in_json() {
2110 let session_needs_backup = backup_test_session(true);
2111
2112 assert!(
2116 serde_json::to_string(&session_needs_backup).unwrap().contains(r#""needs_backup":1"#),
2117 );
2118 }
2119
2120 #[test]
2121 fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
2122 let session_backed_up = backup_test_session(false);
2123
2124 assert!(
2125 !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
2126 "The needs_backup field should be missing!"
2127 );
2128 }
2129
2130 pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
2131 InboundGroupSessionIndexedDbObject {
2132 pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
2133 session_id: None,
2134 needs_backup,
2135 backed_up_to: -1,
2136 sender_key: None,
2137 sender_data_type: None,
2138 }
2139 }
2140
2141 #[async_test]
2142 async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
2143 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2144
2145 let sender_data = SenderData::sender_verified(
2146 user_id!("@test:user"),
2147 device_id!("ABC"),
2148 Ed25519Keypair::new().public_key(),
2149 );
2150
2151 let db_object = sender_data_test_session(sender_key, sender_data).await;
2152 let serialized = serde_json::to_string(&db_object).unwrap();
2153
2154 assert!(
2155 serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
2156 );
2157 assert!(serialized.contains(r#""sender_data_type":5"#));
2158 }
2159
2160 pub async fn sender_data_test_session(
2161 sender_key: Curve25519PublicKey,
2162 sender_data: SenderData,
2163 ) -> InboundGroupSessionIndexedDbObject {
2164 let session = InboundGroupSession::new(
2165 sender_key,
2166 Ed25519Keypair::new().public_key(),
2167 room_id!("!test:localhost"),
2168 &SessionKey::from_base64(
2170 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
2171 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
2172 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
2173 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
2174 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
2175 )
2176 .unwrap(),
2177 sender_data,
2178 None,
2179 EventEncryptionAlgorithm::MegolmV1AesSha2,
2180 None,
2181 false,
2182 )
2183 .unwrap();
2184
2185 InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
2186 .await
2187 .unwrap()
2188 }
2189}
2190
2191#[cfg(all(test, target_family = "wasm"))]
2192mod wasm_unit_tests {
2193 use std::collections::BTreeMap;
2194
2195 use matrix_sdk_crypto::{
2196 olm::{Curve25519PublicKey, SenderData},
2197 types::{DeviceKeys, Signatures},
2198 };
2199 use matrix_sdk_test::async_test;
2200 use ruma::{owned_device_id, owned_user_id};
2201 use wasm_bindgen::JsValue;
2202
2203 use crate::crypto_store::unit_tests::sender_data_test_session;
2204
2205 fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
2206 assert_eq!(
2207 js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
2208 JsValue::from_f64(expected.into())
2209 );
2210 }
2211
2212 #[async_test]
2213 fn test_needs_backup_is_serialized_as_a_u8_in_js() {
2214 let session_needs_backup = super::unit_tests::backup_test_session(true);
2215
2216 let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
2217
2218 assert!(js_value.is_object());
2219 assert_field_equals(&js_value, "needs_backup", 1);
2220 }
2221
2222 #[async_test]
2223 fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
2224 let session_backed_up = super::unit_tests::backup_test_session(false);
2225
2226 let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
2227
2228 assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
2229 }
2230
2231 #[async_test]
2232 async fn test_sender_key_and_device_type_are_serialized_in_js() {
2233 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2234
2235 let sender_data = SenderData::device_info(DeviceKeys::new(
2236 owned_user_id!("@test:user"),
2237 owned_device_id!("ABC"),
2238 vec![],
2239 BTreeMap::new(),
2240 Signatures::new(),
2241 ));
2242 let db_object = sender_data_test_session(sender_key, sender_data).await;
2243
2244 let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
2245
2246 assert!(js_value.is_object());
2247 assert_field_equals(&js_value, "sender_data_type", 2);
2248 assert_eq!(
2249 js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
2250 JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
2251 );
2252 }
2253}
2254
2255#[cfg(all(test, target_family = "wasm"))]
2256mod tests {
2257 use matrix_sdk_crypto::cryptostore_integration_tests;
2258
2259 use super::IndexeddbCryptoStore;
2260
2261 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2262
2263 async fn get_store(
2264 name: &str,
2265 passphrase: Option<&str>,
2266 clear_data: bool,
2267 ) -> IndexeddbCryptoStore {
2268 if clear_data {
2269 IndexeddbCryptoStore::delete_stores(name).unwrap();
2270 }
2271 match passphrase {
2272 Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2273 .await
2274 .expect("Can't create a passphrase protected store"),
2275 None => IndexeddbCryptoStore::open_with_name(name)
2276 .await
2277 .expect("Can't create store without passphrase"),
2278 }
2279 }
2280
2281 cryptostore_integration_tests!();
2282}
2283
2284#[cfg(all(test, target_family = "wasm"))]
2285mod encrypted_tests {
2286 use matrix_sdk_crypto::{
2287 cryptostore_integration_tests,
2288 olm::Account,
2289 store::{CryptoStore, types::PendingChanges},
2290 vodozemac::base64_encode,
2291 };
2292 use matrix_sdk_test::async_test;
2293 use ruma::{device_id, user_id};
2294
2295 use super::IndexeddbCryptoStore;
2296
2297 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2298
2299 async fn get_store(
2300 name: &str,
2301 passphrase: Option<&str>,
2302 clear_data: bool,
2303 ) -> IndexeddbCryptoStore {
2304 if clear_data {
2305 IndexeddbCryptoStore::delete_stores(name).unwrap();
2306 }
2307
2308 let pass = passphrase.unwrap_or(name);
2309 IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2310 .await
2311 .expect("Can't create a passphrase protected store")
2312 }
2313 cryptostore_integration_tests!();
2314
2315 #[async_test]
2318 async fn test_migrate_passphrase_to_key() {
2319 let store_name = "test_migrate_passphrase_to_key";
2320 let passdata: [u8; 32] = rand::random();
2321 let b64_passdata = base64_encode(passdata);
2322
2323 IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2325 let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2326 .await
2327 .expect("Can't create a passphrase-protected store");
2328
2329 store
2330 .save_pending_changes(PendingChanges {
2331 account: Some(Account::with_device_id(
2332 user_id!("@alice:example.org"),
2333 device_id!("ALICEDEVICE"),
2334 )),
2335 })
2336 .await
2337 .expect("Can't save account");
2338
2339 let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2341 .await
2342 .expect("Can't create a key-protected store");
2343 let loaded_account =
2344 store.load_account().await.expect("Can't load account").expect("Account was not saved");
2345 assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2346 }
2347}