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