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::prelude::*;
24use js_sys::Array;
25use matrix_sdk_crypto::{
26 olm::{
27 Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
28 PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session,
29 StaticAccountData,
30 },
31 store::{
32 types::{
33 BackupKeys, Changes, DehydratedDeviceKey, PendingChanges, RoomKeyCounts, RoomSettings,
34 StoredRoomKeyBundleData,
35 },
36 CryptoStore, CryptoStoreError,
37 },
38 types::events::room_key_withheld::RoomKeyWithheldEvent,
39 vodozemac::base64_encode,
40 Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData,
41};
42use matrix_sdk_store_encryption::StoreCipher;
43use ruma::{
44 events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
45 RoomId, TransactionId, UserId,
46};
47use sha2::Sha256;
48use tokio::sync::Mutex;
49use tracing::{debug, warn};
50use wasm_bindgen::JsValue;
51use web_sys::IdbKeyRange;
52
53use crate::{
54 crypto_store::migrations::open_and_upgrade_db,
55 serializer::{MaybeEncrypted, SafeEncodeSerializer, SafeEncodeSerializerError},
56};
57
58mod migrations;
59
60mod keys {
61 pub const CORE: &str = "core";
63
64 pub const SESSION: &str = "session";
65
66 pub const INBOUND_GROUP_SESSIONS_V3: &str = "inbound_group_sessions3";
67 pub const INBOUND_GROUP_SESSIONS_BACKUP_INDEX: &str = "backup";
68 pub const INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX: &str = "backed_up_to";
69 pub const INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX: &str =
70 "inbound_group_session_sender_key_sender_data_type_idx";
71
72 pub const OUTBOUND_GROUP_SESSIONS: &str = "outbound_group_sessions";
73
74 pub const TRACKED_USERS: &str = "tracked_users";
75 pub const OLM_HASHES: &str = "olm_hashes";
76
77 pub const DEVICES: &str = "devices";
78 pub const IDENTITIES: &str = "identities";
79
80 pub const GOSSIP_REQUESTS: &str = "gossip_requests";
81 pub const GOSSIP_REQUESTS_UNSENT_INDEX: &str = "unsent";
82 pub const GOSSIP_REQUESTS_BY_INFO_INDEX: &str = "by_info";
83
84 pub const ROOM_SETTINGS: &str = "room_settings";
85
86 pub const SECRETS_INBOX: &str = "secrets_inbox";
87
88 pub const DIRECT_WITHHELD_INFO: &str = "direct_withheld_info";
89
90 pub const RECEIVED_ROOM_KEY_BUNDLES: &str = "received_room_key_bundles";
91
92 pub const STORE_CIPHER: &str = "store_cipher";
94 pub const ACCOUNT: &str = "account";
95 pub const NEXT_BATCH_TOKEN: &str = "next_batch_token";
96 pub const PRIVATE_IDENTITY: &str = "private_identity";
97
98 pub const BACKUP_KEYS: &str = "backup_keys";
100
101 pub const BACKUP_VERSION_V1: &str = "backup_version_v1";
104
105 pub const RECOVERY_KEY_V1: &str = "recovery_key_v1";
111
112 pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key";
114}
115
116pub struct IndexeddbCryptoStore {
121 static_account: RwLock<Option<StaticAccountData>>,
122 name: String,
123 pub(crate) inner: IdbDatabase,
124
125 serializer: SafeEncodeSerializer,
126 save_changes_lock: Arc<Mutex<()>>,
127}
128
129#[cfg(not(tarpaulin_include))]
130impl std::fmt::Debug for IndexeddbCryptoStore {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 f.debug_struct("IndexeddbCryptoStore").field("name", &self.name).finish()
133 }
134}
135
136#[derive(Debug, thiserror::Error)]
137pub enum IndexeddbCryptoStoreError {
138 #[error(transparent)]
139 Serialization(#[from] serde_json::Error),
140 #[error("DomException {name} ({code}): {message}")]
141 DomException {
142 code: u16,
144 name: String,
146 message: String,
148 },
149 #[error(transparent)]
150 CryptoStoreError(#[from] CryptoStoreError),
151 #[error(
152 "The schema version of the crypto store is too new. \
153 Existing version: {current_version}; max supported version: {max_supported_version}"
154 )]
155 SchemaTooNewError { max_supported_version: u32, current_version: u32 },
156}
157
158impl From<SafeEncodeSerializerError> for IndexeddbCryptoStoreError {
159 fn from(value: SafeEncodeSerializerError) -> Self {
160 match value {
161 SafeEncodeSerializerError::Serialization(error) => Self::Serialization(error),
162 SafeEncodeSerializerError::DomException { code, name, message } => {
163 Self::DomException { code, name, message }
164 }
165 SafeEncodeSerializerError::CryptoStoreError(crypto_store_error) => {
166 Self::CryptoStoreError(crypto_store_error)
167 }
168 }
169 }
170}
171
172impl From<web_sys::DomException> for IndexeddbCryptoStoreError {
173 fn from(frm: web_sys::DomException) -> IndexeddbCryptoStoreError {
174 IndexeddbCryptoStoreError::DomException {
175 name: frm.name(),
176 message: frm.message(),
177 code: frm.code(),
178 }
179 }
180}
181
182impl From<serde_wasm_bindgen::Error> for IndexeddbCryptoStoreError {
183 fn from(e: serde_wasm_bindgen::Error) -> Self {
184 IndexeddbCryptoStoreError::Serialization(serde::de::Error::custom(e.to_string()))
185 }
186}
187
188impl From<IndexeddbCryptoStoreError> for CryptoStoreError {
189 fn from(frm: IndexeddbCryptoStoreError) -> CryptoStoreError {
190 match frm {
191 IndexeddbCryptoStoreError::Serialization(e) => CryptoStoreError::Serialization(e),
192 IndexeddbCryptoStoreError::CryptoStoreError(e) => e,
193 _ => CryptoStoreError::backend(frm),
194 }
195 }
196}
197
198type Result<A, E = IndexeddbCryptoStoreError> = std::result::Result<A, E>;
199
200enum PendingOperation {
202 Put { key: JsValue, value: JsValue },
203 Delete(JsValue),
204}
205
206struct PendingIndexeddbChanges {
212 store_to_key_values: BTreeMap<&'static str, Vec<PendingOperation>>,
215}
216
217struct PendingStoreChanges<'a> {
219 operations: &'a mut Vec<PendingOperation>,
220}
221
222impl PendingStoreChanges<'_> {
223 fn put(&mut self, key: JsValue, value: JsValue) {
224 self.operations.push(PendingOperation::Put { key, value });
225 }
226
227 fn delete(&mut self, key: JsValue) {
228 self.operations.push(PendingOperation::Delete(key));
229 }
230}
231
232impl PendingIndexeddbChanges {
233 fn get(&mut self, store: &'static str) -> PendingStoreChanges<'_> {
234 PendingStoreChanges { operations: self.store_to_key_values.entry(store).or_default() }
235 }
236}
237
238impl PendingIndexeddbChanges {
239 fn new() -> Self {
240 Self { store_to_key_values: BTreeMap::new() }
241 }
242
243 fn touched_stores(&self) -> Vec<&str> {
247 self.store_to_key_values
248 .iter()
249 .filter_map(
250 |(store, pending_operations)| {
251 if !pending_operations.is_empty() {
252 Some(*store)
253 } else {
254 None
255 }
256 },
257 )
258 .collect()
259 }
260
261 fn apply(self, tx: &IdbTransaction<'_>) -> Result<()> {
263 for (store, operations) in self.store_to_key_values {
264 if operations.is_empty() {
265 continue;
266 }
267 let object_store = tx.object_store(store)?;
268 for op in operations {
269 match op {
270 PendingOperation::Put { key, value } => {
271 object_store.put_key_val(&key, &value)?;
272 }
273 PendingOperation::Delete(key) => {
274 object_store.delete(&key)?;
275 }
276 }
277 }
278 }
279 Ok(())
280 }
281}
282
283impl IndexeddbCryptoStore {
284 pub(crate) async fn open_with_store_cipher(
285 prefix: &str,
286 store_cipher: Option<Arc<StoreCipher>>,
287 ) -> Result<Self> {
288 let name = format!("{prefix:0}::matrix-sdk-crypto");
289
290 let serializer = SafeEncodeSerializer::new(store_cipher);
291 debug!("IndexedDbCryptoStore: opening main store {name}");
292 let db = open_and_upgrade_db(&name, &serializer).await?;
293
294 Ok(Self {
295 name,
296 inner: db,
297 serializer,
298 static_account: RwLock::new(None),
299 save_changes_lock: Default::default(),
300 })
301 }
302
303 pub async fn open() -> Result<Self> {
305 IndexeddbCryptoStore::open_with_store_cipher("crypto", None).await
306 }
307
308 pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result<Self> {
325 let db = open_meta_db(prefix).await?;
326 let store_cipher = load_store_cipher(&db).await?;
327
328 let store_cipher = match store_cipher {
329 Some(cipher) => {
330 debug!("IndexedDbCryptoStore: decrypting store cipher");
331 StoreCipher::import(passphrase, &cipher)
332 .map_err(|_| CryptoStoreError::UnpicklingError)?
333 }
334 None => {
335 debug!("IndexedDbCryptoStore: encrypting new store cipher");
336 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
337 #[cfg(not(test))]
338 let export = cipher.export(passphrase);
339 #[cfg(test)]
340 let export = cipher._insecure_export_fast_for_testing(passphrase);
341
342 let export = export.map_err(CryptoStoreError::backend)?;
343
344 save_store_cipher(&db, &export).await?;
345 cipher
346 }
347 };
348
349 db.close();
352
353 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
354 }
355
356 pub async fn open_with_key(prefix: &str, key: &[u8; 32]) -> Result<Self> {
372 let mut chacha_key = zeroize::Zeroizing::new([0u8; 32]);
375 const HKDF_INFO: &[u8] = b"CRYPTOSTORE_CIPHER";
376 let hkdf = Hkdf::<Sha256>::new(None, key);
377 hkdf.expand(HKDF_INFO, &mut *chacha_key)
378 .expect("We should be able to generate a 32-byte key");
379
380 let db = open_meta_db(prefix).await?;
381 let store_cipher = load_store_cipher(&db).await?;
382
383 let store_cipher = match store_cipher {
384 Some(cipher) => {
385 debug!("IndexedDbCryptoStore: decrypting store cipher");
386 import_store_cipher_with_key(&chacha_key, key, &cipher, &db).await?
387 }
388 None => {
389 debug!("IndexedDbCryptoStore: encrypting new store cipher");
390 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
391 let export =
392 cipher.export_with_key(&chacha_key).map_err(CryptoStoreError::backend)?;
393 save_store_cipher(&db, &export).await?;
394 cipher
395 }
396 };
397
398 db.close();
401
402 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
403 }
404
405 pub async fn open_with_name(name: &str) -> Result<Self> {
407 IndexeddbCryptoStore::open_with_store_cipher(name, None).await
408 }
409
410 #[cfg(test)]
412 pub fn delete_stores(prefix: &str) -> Result<()> {
413 IdbDatabase::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto-meta"))?;
414 IdbDatabase::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto"))?;
415 Ok(())
416 }
417
418 fn get_static_account(&self) -> Option<StaticAccountData> {
419 self.static_account.read().unwrap().clone()
420 }
421
422 async fn serialize_inbound_group_session(
425 &self,
426 session: &InboundGroupSession,
427 ) -> Result<JsValue> {
428 let obj =
429 InboundGroupSessionIndexedDbObject::from_session(session, &self.serializer).await?;
430 Ok(serde_wasm_bindgen::to_value(&obj)?)
431 }
432
433 fn deserialize_inbound_group_session(
436 &self,
437 stored_value: JsValue,
438 ) -> Result<InboundGroupSession> {
439 let idb_object: InboundGroupSessionIndexedDbObject =
440 serde_wasm_bindgen::from_value(stored_value)?;
441 let pickled_session: PickledInboundGroupSession =
442 self.serializer.maybe_decrypt_value(idb_object.pickled_session)?;
443 let session = InboundGroupSession::from_pickle(pickled_session)
444 .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
445
446 if idb_object.needs_backup {
450 session.reset_backup_state();
451 } else {
452 session.mark_as_backed_up();
453 }
454
455 Ok(session)
456 }
457
458 fn serialize_gossip_request(&self, gossip_request: &GossipRequest) -> Result<JsValue> {
461 let obj = GossipRequestIndexedDbObject {
462 info: self
464 .serializer
465 .encode_key_as_string(keys::GOSSIP_REQUESTS, gossip_request.info.as_key()),
466
467 request: self.serializer.serialize_value_as_bytes(gossip_request)?,
469
470 unsent: !gossip_request.sent_out,
471 };
472
473 Ok(serde_wasm_bindgen::to_value(&obj)?)
474 }
475
476 fn deserialize_gossip_request(&self, stored_request: JsValue) -> Result<GossipRequest> {
479 let idb_object: GossipRequestIndexedDbObject =
480 serde_wasm_bindgen::from_value(stored_request)?;
481 Ok(self.serializer.deserialize_value_from_bytes(&idb_object.request)?)
482 }
483
484 async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
491 let mut indexeddb_changes = PendingIndexeddbChanges::new();
492
493 let private_identity_pickle =
494 if let Some(i) = &changes.private_identity { Some(i.pickle().await) } else { None };
495
496 let decryption_key_pickle = &changes.backup_decryption_key;
497 let backup_version = &changes.backup_version;
498 let dehydration_pickle_key = &changes.dehydrated_device_pickle_key;
499
500 let mut core = indexeddb_changes.get(keys::CORE);
501 if let Some(next_batch) = &changes.next_batch_token {
502 core.put(
503 JsValue::from_str(keys::NEXT_BATCH_TOKEN),
504 self.serializer.serialize_value(next_batch)?,
505 );
506 }
507
508 if let Some(i) = &private_identity_pickle {
509 core.put(
510 JsValue::from_str(keys::PRIVATE_IDENTITY),
511 self.serializer.serialize_value(i)?,
512 );
513 }
514
515 if let Some(i) = &dehydration_pickle_key {
516 core.put(
517 JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY),
518 self.serializer.serialize_value(i)?,
519 );
520 }
521
522 if let Some(a) = &decryption_key_pickle {
523 indexeddb_changes.get(keys::BACKUP_KEYS).put(
524 JsValue::from_str(keys::RECOVERY_KEY_V1),
525 self.serializer.serialize_value(&a)?,
526 );
527 }
528
529 if let Some(a) = &backup_version {
530 indexeddb_changes.get(keys::BACKUP_KEYS).put(
531 JsValue::from_str(keys::BACKUP_VERSION_V1),
532 self.serializer.serialize_value(&a)?,
533 );
534 }
535
536 if !changes.sessions.is_empty() {
537 let mut sessions = indexeddb_changes.get(keys::SESSION);
538
539 for session in &changes.sessions {
540 let sender_key = session.sender_key().to_base64();
541 let session_id = session.session_id();
542
543 let pickle = session.pickle().await;
544 let key = self.serializer.encode_key(keys::SESSION, (&sender_key, session_id));
545
546 sessions.put(key, self.serializer.serialize_value(&pickle)?);
547 }
548 }
549
550 if !changes.inbound_group_sessions.is_empty() {
551 let mut sessions = indexeddb_changes.get(keys::INBOUND_GROUP_SESSIONS_V3);
552
553 for session in &changes.inbound_group_sessions {
554 let room_id = session.room_id();
555 let session_id = session.session_id();
556 let key = self
557 .serializer
558 .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
559 let value = self.serialize_inbound_group_session(session).await?;
560 sessions.put(key, value);
561 }
562 }
563
564 if !changes.outbound_group_sessions.is_empty() {
565 let mut sessions = indexeddb_changes.get(keys::OUTBOUND_GROUP_SESSIONS);
566
567 for session in &changes.outbound_group_sessions {
568 let room_id = session.room_id();
569 let pickle = session.pickle().await;
570 sessions.put(
571 self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
572 self.serializer.serialize_value(&pickle)?,
573 );
574 }
575 }
576
577 let device_changes = &changes.devices;
578 let identity_changes = &changes.identities;
579 let olm_hashes = &changes.message_hashes;
580 let key_requests = &changes.key_requests;
581 let withheld_session_info = &changes.withheld_session_info;
582 let room_settings_changes = &changes.room_settings;
583
584 let mut device_store = indexeddb_changes.get(keys::DEVICES);
585
586 for device in device_changes.new.iter().chain(&device_changes.changed) {
587 let key =
588 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
589 let device = self.serializer.serialize_value(&device)?;
590
591 device_store.put(key, device);
592 }
593
594 for device in &device_changes.deleted {
595 let key =
596 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
597 device_store.delete(key);
598 }
599
600 if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
601 let mut identities = indexeddb_changes.get(keys::IDENTITIES);
602 for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
603 identities.put(
604 self.serializer.encode_key(keys::IDENTITIES, identity.user_id()),
605 self.serializer.serialize_value(&identity)?,
606 );
607 }
608 }
609
610 if !olm_hashes.is_empty() {
611 let mut hashes = indexeddb_changes.get(keys::OLM_HASHES);
612 for hash in olm_hashes {
613 hashes.put(
614 self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
615 JsValue::TRUE,
616 );
617 }
618 }
619
620 if !key_requests.is_empty() {
621 let mut gossip_requests = indexeddb_changes.get(keys::GOSSIP_REQUESTS);
622
623 for gossip_request in key_requests {
624 let key_request_id = self
625 .serializer
626 .encode_key(keys::GOSSIP_REQUESTS, gossip_request.request_id.as_str());
627 let key_request_value = self.serialize_gossip_request(gossip_request)?;
628 gossip_requests.put(key_request_id, key_request_value);
629 }
630 }
631
632 if !withheld_session_info.is_empty() {
633 let mut withhelds = indexeddb_changes.get(keys::DIRECT_WITHHELD_INFO);
634
635 for (room_id, data) in withheld_session_info {
636 for (session_id, event) in data {
637 let key = self
638 .serializer
639 .encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, &room_id));
640 withhelds.put(key, self.serializer.serialize_value(&event)?);
641 }
642 }
643 }
644
645 if !room_settings_changes.is_empty() {
646 let mut settings_store = indexeddb_changes.get(keys::ROOM_SETTINGS);
647
648 for (room_id, settings) in room_settings_changes {
649 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
650 let value = self.serializer.serialize_value(&settings)?;
651 settings_store.put(key, value);
652 }
653 }
654
655 if !changes.secrets.is_empty() {
656 let mut secret_store = indexeddb_changes.get(keys::SECRETS_INBOX);
657
658 for secret in &changes.secrets {
659 let key = self.serializer.encode_key(
660 keys::SECRETS_INBOX,
661 (secret.secret_name.as_str(), secret.event.content.request_id.as_str()),
662 );
663 let value = self.serializer.serialize_value(&secret)?;
664
665 secret_store.put(key, value);
666 }
667 }
668
669 if !changes.received_room_key_bundles.is_empty() {
670 let mut bundle_store = indexeddb_changes.get(keys::RECEIVED_ROOM_KEY_BUNDLES);
671 for bundle in &changes.received_room_key_bundles {
672 let key = self.serializer.encode_key(
673 keys::RECEIVED_ROOM_KEY_BUNDLES,
674 (&bundle.bundle_data.room_id, &bundle.sender_user),
675 );
676 let value = self.serializer.serialize_value(&bundle)?;
677 bundle_store.put(key, value);
678 }
679 }
680
681 Ok(indexeddb_changes)
682 }
683}
684
685#[cfg(target_family = "wasm")]
694macro_rules! impl_crypto_store {
695 ( $($body:tt)* ) => {
696 #[async_trait(?Send)]
697 impl CryptoStore for IndexeddbCryptoStore {
698 type Error = IndexeddbCryptoStoreError;
699
700 $($body)*
701 }
702 };
703}
704
705#[cfg(not(target_family = "wasm"))]
706macro_rules! impl_crypto_store {
707 ( $($body:tt)* ) => {
708 impl IndexeddbCryptoStore {
709 $($body)*
710 }
711 };
712}
713
714impl_crypto_store! {
715 async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> {
716 let _guard = self.save_changes_lock.lock().await;
721
722 let stores: Vec<&str> = [
723 (changes.account.is_some() , keys::CORE),
724 ]
725 .iter()
726 .filter_map(|(id, key)| if *id { Some(*key) } else { None })
727 .collect();
728
729 if stores.is_empty() {
730 return Ok(());
732 }
733
734 let tx =
735 self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?;
736
737 let account_pickle = if let Some(account) = changes.account {
738 *self.static_account.write().unwrap() = Some(account.static_data().clone());
739 Some(account.pickle())
740 } else {
741 None
742 };
743
744 if let Some(a) = &account_pickle {
745 tx.object_store(keys::CORE)?
746 .put_key_val(&JsValue::from_str(keys::ACCOUNT), &self.serializer.serialize_value(&a)?)?;
747 }
748
749 tx.await.into_result()?;
750
751 Ok(())
752 }
753
754 async fn save_changes(&self, changes: Changes) -> Result<()> {
755 let _guard = self.save_changes_lock.lock().await;
760
761 let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
762
763 let stores = indexeddb_changes.touched_stores();
764
765 if stores.is_empty() {
766 return Ok(());
768 }
769
770 let tx =
771 self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?;
772
773 indexeddb_changes.apply(&tx)?;
774
775 tx.await.into_result()?;
776
777 Ok(())
778 }
779
780 async fn save_inbound_group_sessions(
781 &self,
782 sessions: Vec<InboundGroupSession>,
783 backed_up_to_version: Option<&str>,
784 ) -> Result<()> {
785 sessions.iter().for_each(|s| {
787 let backed_up = s.backed_up();
788 if backed_up != backed_up_to_version.is_some() {
789 warn!(
790 backed_up, backed_up_to_version,
791 "Session backed-up flag does not correspond to backup version setting",
792 );
793 }
794 });
795
796 self.save_changes(Changes { inbound_group_sessions: sessions, ..Changes::default() }).await
799 }
800
801 async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
802 let tx = self
803 .inner
804 .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readonly)?;
805 let os = tx.object_store(keys::TRACKED_USERS)?;
806 let user_ids = os.get_all_keys()?.await?;
807
808 let mut users = Vec::new();
809
810 for user_id in user_ids.iter() {
811 let dirty: bool =
812 !matches!(os.get(&user_id)?.await?.map(|v| v.into_serde()), Some(Ok(false)));
813 let Some(Ok(user_id)) = user_id.as_string().map(UserId::parse) else { continue };
814
815 users.push(TrackedUser { user_id, dirty });
816 }
817
818 Ok(users)
819 }
820
821 async fn get_outbound_group_session(
822 &self,
823 room_id: &RoomId,
824 ) -> Result<Option<OutboundGroupSession>> {
825 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
826 if let Some(value) = self
827 .inner
828 .transaction_on_one_with_mode(
829 keys::OUTBOUND_GROUP_SESSIONS,
830 IdbTransactionMode::Readonly,
831 )?
832 .object_store(keys::OUTBOUND_GROUP_SESSIONS)?
833 .get(&self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))?
834 .await?
835 {
836 Ok(Some(
837 OutboundGroupSession::from_pickle(
838 account_info.device_id,
839 account_info.identity_keys,
840 self.serializer.deserialize_value(value)?,
841 )
842 .map_err(CryptoStoreError::from)?,
843 ))
844 } else {
845 Ok(None)
846 }
847 }
848
849 async fn get_outgoing_secret_requests(
850 &self,
851 request_id: &TransactionId,
852 ) -> Result<Option<GossipRequest>> {
853 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id.as_str());
854 self
855 .inner
856 .transaction_on_one_with_mode(keys::GOSSIP_REQUESTS, IdbTransactionMode::Readonly)?
857 .object_store(keys::GOSSIP_REQUESTS)?
858 .get_owned(jskey)?
859 .await?
860 .map(|val| self.deserialize_gossip_request(val))
861 .transpose()
862 }
863
864 async fn load_account(&self) -> Result<Option<Account>> {
865 if let Some(pickle) = self
866 .inner
867 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
868 .object_store(keys::CORE)?
869 .get(&JsValue::from_str(keys::ACCOUNT))?
870 .await?
871 {
872 let pickle = self.serializer.deserialize_value(pickle)?;
873
874 let account = Account::from_pickle(pickle).map_err(CryptoStoreError::from)?;
875
876 *self.static_account.write().unwrap() = Some(account.static_data().clone());
877
878 Ok(Some(account))
879 } else {
880 Ok(None)
881 }
882 }
883
884 async fn next_batch_token(&self) -> Result<Option<String>> {
885 if let Some(serialized) = self
886 .inner
887 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
888 .object_store(keys::CORE)?
889 .get(&JsValue::from_str(keys::NEXT_BATCH_TOKEN))?
890 .await?
891 {
892 let token = self.serializer.deserialize_value(serialized)?;
893 Ok(Some(token))
894 } else {
895 Ok(None)
896 }
897 }
898
899 async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
900 if let Some(pickle) = self
901 .inner
902 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
903 .object_store(keys::CORE)?
904 .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))?
905 .await?
906 {
907 let pickle = self.serializer.deserialize_value(pickle)?;
908
909 Ok(Some(
910 PrivateCrossSigningIdentity::from_pickle(pickle)
911 .map_err(|_| CryptoStoreError::UnpicklingError)?,
912 ))
913 } else {
914 Ok(None)
915 }
916 }
917
918 async fn get_sessions(&self, sender_key: &str) -> Result<Option<Vec<Session>>> {
919 let device_keys = self.get_own_device()
920 .await?
921 .as_device_keys()
922 .clone();
923
924 let range = self.serializer.encode_to_range(keys::SESSION, sender_key)?;
925 let sessions: Vec<Session> = self
926 .inner
927 .transaction_on_one_with_mode(keys::SESSION, IdbTransactionMode::Readonly)?
928 .object_store(keys::SESSION)?
929 .get_all_with_key(&range)?
930 .await?
931 .iter()
932 .filter_map(|f| self.serializer.deserialize_value(f).ok().map(|p| {
933 Session::from_pickle(
934 device_keys.clone(),
935 p,
936 )
937 .map_err(|_| IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset))
938 }))
939 .collect::<Result<Vec<Session>>>()?;
940
941 if sessions.is_empty() {
942 Ok(None)
943 } else {
944 Ok(Some(sessions))
945 }
946 }
947
948 async fn get_inbound_group_session(
949 &self,
950 room_id: &RoomId,
951 session_id: &str,
952 ) -> Result<Option<InboundGroupSession>> {
953 let key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
954 if let Some(value) = self
955 .inner
956 .transaction_on_one_with_mode(
957 keys::INBOUND_GROUP_SESSIONS_V3,
958 IdbTransactionMode::Readonly,
959 )?
960 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
961 .get(&key)?
962 .await?
963 {
964 Ok(Some(self.deserialize_inbound_group_session(value)?))
965 } else {
966 Ok(None)
967 }
968 }
969
970 async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
971 const INBOUND_GROUP_SESSIONS_BATCH_SIZE: usize = 1000;
972
973 let transaction = self
974 .inner
975 .transaction_on_one_with_mode(
976 keys::INBOUND_GROUP_SESSIONS_V3,
977 IdbTransactionMode::Readonly,
978 )?;
979
980 let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
981
982 fetch_from_object_store_batched(
983 object_store,
984 |value| self.deserialize_inbound_group_session(value),
985 INBOUND_GROUP_SESSIONS_BATCH_SIZE
986 ).await
987 }
988
989 async fn get_inbound_group_sessions_by_room_id(
990 &self,
991 room_id: &RoomId,
992 ) -> Result<Vec<InboundGroupSession>> {
993 let range = self.serializer.encode_to_range(keys::INBOUND_GROUP_SESSIONS_V3, room_id)?;
994 Ok(self
995 .inner
996 .transaction_on_one_with_mode(
997 keys::INBOUND_GROUP_SESSIONS_V3,
998 IdbTransactionMode::Readonly,
999 )?
1000 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
1001 .get_all_with_key(&range)?
1002 .await?
1003 .into_iter()
1004 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1005 Ok(session) => Some(session),
1006 Err(e) => {
1007 warn!("Failed to deserialize inbound group session: {e}");
1008 None
1009 }
1010 })
1011 .collect::<Vec<InboundGroupSession>>())
1012 }
1013
1014 async fn get_inbound_group_sessions_for_device_batch(
1015 &self,
1016 sender_key: Curve25519PublicKey,
1017 sender_data_type: SenderDataType,
1018 after_session_id: Option<String>,
1019 limit: usize,
1020 ) -> Result<Vec<InboundGroupSession>> {
1021 let sender_key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
1022
1023 let after_session_id = after_session_id.map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s)).unwrap_or("".into());
1025
1026 let lower_bound: Array = [sender_key.clone(), (sender_data_type as u8).into(), after_session_id].iter().collect();
1027 let upper_bound: Array = [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
1028 let key = IdbKeyRange::bound_with_lower_open_and_upper_open(
1029 &lower_bound,
1030 &upper_bound,
1031 true, true
1032 ).expect("Key was not valid!");
1033
1034 let tx = self
1035 .inner
1036 .transaction_on_one_with_mode(
1037 keys::INBOUND_GROUP_SESSIONS_V3,
1038 IdbTransactionMode::Readonly,
1039 )?;
1040
1041 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1042 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1043 let serialized_sessions = idx.get_all_with_key_and_limit_owned(key, limit as u32)?.await?;
1044
1045 let result = serialized_sessions.into_iter()
1047 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1048 Ok(session) => Some(session),
1049 Err(e) => {
1050 warn!("Failed to deserialize inbound group session: {e}");
1051 None
1052 }
1053 })
1054 .collect::<Vec<InboundGroupSession>>();
1055
1056 Ok(result)
1057 }
1058
1059 async fn inbound_group_session_counts(&self, _backup_version: Option<&str>) -> Result<RoomKeyCounts> {
1060 let tx = self
1061 .inner
1062 .transaction_on_one_with_mode(
1063 keys::INBOUND_GROUP_SESSIONS_V3,
1064 IdbTransactionMode::Readonly,
1065 )?;
1066 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1067 let all = store.count()?.await? as usize;
1068 let not_backed_up = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count()?.await? as usize;
1069 tx.await.into_result()?;
1070 Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1071 }
1072
1073 async fn inbound_group_sessions_for_backup(
1074 &self,
1075 _backup_version: &str,
1076 limit: usize,
1077 ) -> Result<Vec<InboundGroupSession>> {
1078 let tx = self
1079 .inner
1080 .transaction_on_one_with_mode(
1081 keys::INBOUND_GROUP_SESSIONS_V3,
1082 IdbTransactionMode::Readonly,
1083 )?;
1084
1085
1086 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1087 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1088
1089 let Some(cursor) = idx.open_cursor()?.await? else {
1093 return Ok(vec![]);
1094 };
1095
1096 let mut serialized_sessions = Vec::with_capacity(limit);
1097 for _ in 0..limit {
1098 serialized_sessions.push(cursor.value());
1099 if !cursor.continue_cursor()?.await? {
1100 break;
1101 }
1102 }
1103
1104 tx.await.into_result()?;
1105
1106 let result = serialized_sessions.into_iter()
1108 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1109 Ok(session) => Some(session),
1110 Err(e) => {
1111 warn!("Failed to deserialize inbound group session: {e}");
1112 None
1113 }
1114 })
1115 .collect::<Vec<InboundGroupSession>>();
1116
1117 Ok(result)
1118 }
1119
1120 async fn mark_inbound_group_sessions_as_backed_up(&self,
1121 _backup_version: &str,
1122 room_and_session_ids: &[(&RoomId, &str)]
1123 ) -> Result<()> {
1124 let tx = self
1125 .inner
1126 .transaction_on_one_with_mode(
1127 keys::INBOUND_GROUP_SESSIONS_V3,
1128 IdbTransactionMode::Readwrite,
1129 )?;
1130
1131 let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1132
1133 for (room_id, session_id) in room_and_session_ids {
1134 let key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1135 if let Some(idb_object_js) = object_store.get(&key)?.await? {
1136 let mut idb_object: InboundGroupSessionIndexedDbObject = serde_wasm_bindgen::from_value(idb_object_js)?;
1137 idb_object.needs_backup = false;
1138 object_store.put_key_val(&key, &serde_wasm_bindgen::to_value(&idb_object)?)?;
1139 } else {
1140 warn!(?key, "Could not find inbound group session to mark it as backed up.");
1141 }
1142 }
1143
1144 Ok(tx.await.into_result()?)
1145 }
1146
1147 async fn reset_backup_state(&self) -> Result<()> {
1148 let tx = self
1149 .inner
1150 .transaction_on_one_with_mode(
1151 keys::INBOUND_GROUP_SESSIONS_V3,
1152 IdbTransactionMode::Readwrite,
1153 )?;
1154
1155 if let Some(cursor) = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor()?.await? {
1156 loop {
1157 let mut idb_object: InboundGroupSessionIndexedDbObject = serde_wasm_bindgen::from_value(cursor.value())?;
1158 if !idb_object.needs_backup {
1159 idb_object.needs_backup = true;
1160 let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1164 cursor.update(&idb_object)?.await?;
1165 }
1166
1167 if !cursor.continue_cursor()?.await? {
1168 break;
1169 }
1170 }
1171 }
1172
1173 Ok(tx.await.into_result()?)
1174 }
1175
1176 async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1177 let tx = self
1178 .inner
1179 .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readwrite)?;
1180 let os = tx.object_store(keys::TRACKED_USERS)?;
1181
1182 for (user, dirty) in users {
1183 os.put_key_val(&JsValue::from_str(user.as_str()), &JsValue::from(*dirty))?;
1184 }
1185
1186 tx.await.into_result()?;
1187 Ok(())
1188 }
1189
1190 async fn get_device(
1191 &self,
1192 user_id: &UserId,
1193 device_id: &DeviceId,
1194 ) -> Result<Option<DeviceData>> {
1195 let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1196 self
1197 .inner
1198 .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
1199 .object_store(keys::DEVICES)?
1200 .get(&key)?
1201 .await?
1202 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1203 .transpose()
1204 }
1205
1206 async fn get_user_devices(
1207 &self,
1208 user_id: &UserId,
1209 ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1210 let range = self.serializer.encode_to_range(keys::DEVICES, user_id)?;
1211 Ok(self
1212 .inner
1213 .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
1214 .object_store(keys::DEVICES)?
1215 .get_all_with_key(&range)?
1216 .await?
1217 .iter()
1218 .filter_map(|d| {
1219 let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1220 Some((d.device_id().to_owned(), d))
1221 })
1222 .collect::<HashMap<_, _>>())
1223 }
1224
1225 async fn get_own_device(&self) -> Result<DeviceData> {
1226 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1227 Ok(self.get_device(&account_info.user_id, &account_info.device_id)
1228 .await?
1229 .unwrap())
1230 }
1231
1232 async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1233 self
1234 .inner
1235 .transaction_on_one_with_mode(keys::IDENTITIES, IdbTransactionMode::Readonly)?
1236 .object_store(keys::IDENTITIES)?
1237 .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))?
1238 .await?
1239 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1240 .transpose()
1241 }
1242
1243 async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1244 Ok(self
1245 .inner
1246 .transaction_on_one_with_mode(keys::OLM_HASHES, IdbTransactionMode::Readonly)?
1247 .object_store(keys::OLM_HASHES)?
1248 .get(&self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)))?
1249 .await?
1250 .is_some())
1251 }
1252
1253 async fn get_secrets_from_inbox(
1254 &self,
1255 secret_name: &SecretName,
1256 ) -> Result<Vec<GossippedSecret>> {
1257 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str())?;
1258
1259 self
1260 .inner
1261 .transaction_on_one_with_mode(keys::SECRETS_INBOX, IdbTransactionMode::Readonly)?
1262 .object_store(keys::SECRETS_INBOX)?
1263 .get_all_with_key(&range)?
1264 .await?
1265 .iter()
1266 .map(|d| {
1267 let secret = self.serializer.deserialize_value(d)?;
1268 Ok(secret)
1269 }).collect()
1270 }
1271
1272 #[allow(clippy::unused_async)] async fn delete_secrets_from_inbox(
1274 &self,
1275 secret_name: &SecretName,
1276 ) -> Result<()> {
1277 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str())?;
1278
1279 self
1280 .inner
1281 .transaction_on_one_with_mode(keys::SECRETS_INBOX, IdbTransactionMode::Readwrite)?
1282 .object_store(keys::SECRETS_INBOX)?
1283 .delete(&range)?;
1284
1285 Ok(())
1286 }
1287
1288 async fn get_secret_request_by_info(
1289 &self,
1290 key_info: &SecretInfo,
1291 ) -> Result<Option<GossipRequest>> {
1292 let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1293
1294 let val = self
1295 .inner
1296 .transaction_on_one_with_mode(
1297 keys::GOSSIP_REQUESTS,
1298 IdbTransactionMode::Readonly,
1299 )?
1300 .object_store(keys::GOSSIP_REQUESTS)?
1301 .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1302 .get_owned(key)?
1303 .await?;
1304
1305 if let Some(val) = val {
1306 let deser = self.deserialize_gossip_request(val)?;
1307 Ok(Some(deser))
1308 } else {
1309 Ok(None)
1310 }
1311 }
1312
1313 async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1314 let results = self
1315 .inner
1316 .transaction_on_one_with_mode(
1317 keys::GOSSIP_REQUESTS,
1318 IdbTransactionMode::Readonly,
1319 )?
1320 .object_store(keys::GOSSIP_REQUESTS)?
1321 .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1322 .get_all()?
1323 .await?
1324 .iter()
1325 .filter_map(|val| self.deserialize_gossip_request(val).ok())
1326 .collect();
1327
1328 Ok(results)
1329 }
1330
1331 async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1332 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1333 let tx = self.inner.transaction_on_one_with_mode(keys::GOSSIP_REQUESTS, IdbTransactionMode::Readwrite)?;
1334 tx.object_store(keys::GOSSIP_REQUESTS)?.delete_owned(jskey)?;
1335 tx.await.into_result().map_err(|e| e.into())
1336 }
1337
1338 async fn load_backup_keys(&self) -> Result<BackupKeys> {
1339 let key = {
1340 let tx = self
1341 .inner
1342 .transaction_on_one_with_mode(keys::BACKUP_KEYS, IdbTransactionMode::Readonly)?;
1343 let store = tx.object_store(keys::BACKUP_KEYS)?;
1344
1345 let backup_version = store
1346 .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))?
1347 .await?
1348 .map(|i| self.serializer.deserialize_value(i))
1349 .transpose()?;
1350
1351 let decryption_key = store
1352 .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))?
1353 .await?
1354 .map(|i| self.serializer.deserialize_value(i))
1355 .transpose()?;
1356
1357 BackupKeys { backup_version, decryption_key }
1358 };
1359
1360 Ok(key)
1361 }
1362
1363
1364 async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1365 if let Some(pickle) = self
1366 .inner
1367 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
1368 .object_store(keys::CORE)?
1369 .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))?
1370 .await?
1371 {
1372 let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1373
1374 Ok(Some(pickle))
1375 } else {
1376 Ok(None)
1377 }
1378 }
1379
1380 async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1381 self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1382 Ok(())
1383 }
1384
1385 async fn get_withheld_info(
1386 &self,
1387 room_id: &RoomId,
1388 session_id: &str,
1389 ) -> Result<Option<RoomKeyWithheldEvent>> {
1390 let key = self.serializer.encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, room_id));
1391 if let Some(pickle) = self
1392 .inner
1393 .transaction_on_one_with_mode(
1394 keys::DIRECT_WITHHELD_INFO,
1395 IdbTransactionMode::Readonly,
1396 )?
1397 .object_store(keys::DIRECT_WITHHELD_INFO)?
1398 .get(&key)?
1399 .await?
1400 {
1401 let info = self.serializer.deserialize_value(pickle)?;
1402 Ok(Some(info))
1403 } else {
1404 Ok(None)
1405 }
1406 }
1407
1408 async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1409 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1410 self
1411 .inner
1412 .transaction_on_one_with_mode(keys::ROOM_SETTINGS, IdbTransactionMode::Readonly)?
1413 .object_store(keys::ROOM_SETTINGS)?
1414 .get(&key)?
1415 .await?
1416 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1417 .transpose()
1418 }
1419
1420 async fn get_received_room_key_bundle_data(&self, room_id: &RoomId, user_id: &UserId) -> Result<Option<StoredRoomKeyBundleData>> {
1421 let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1422 let result = self
1423 .inner
1424 .transaction_on_one_with_mode(keys::RECEIVED_ROOM_KEY_BUNDLES, IdbTransactionMode::Readonly)?
1425 .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1426 .get(&key)?
1427 .await?
1428 .map(|v| self.serializer.deserialize_value(v))
1429 .transpose()?;
1430
1431 Ok(result)
1432 }
1433
1434 async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1435 self
1436 .inner
1437 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
1438 .object_store(keys::CORE)?
1439 .get(&JsValue::from_str(key))?
1440 .await?
1441 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1442 .transpose()
1443 }
1444
1445 #[allow(clippy::unused_async)] async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1447 self
1448 .inner
1449 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?
1450 .object_store(keys::CORE)?
1451 .put_key_val(&JsValue::from_str(key), &self.serializer.serialize_value(&value)?)?;
1452 Ok(())
1453 }
1454
1455 #[allow(clippy::unused_async)] async fn remove_custom_value(&self, key: &str) -> Result<()> {
1457 self
1458 .inner
1459 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?
1460 .object_store(keys::CORE)?
1461 .delete(&JsValue::from_str(key))?;
1462 Ok(())
1463 }
1464
1465 async fn try_take_leased_lock(
1466 &self,
1467 lease_duration_ms: u32,
1468 key: &str,
1469 holder: &str,
1470 ) -> Result<bool> {
1471 let key = JsValue::from_str(key);
1473 let txn = self
1474 .inner
1475 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?;
1476 let object_store = txn
1477 .object_store(keys::CORE)?;
1478
1479 #[derive(serde::Deserialize, serde::Serialize)]
1480 struct Lease {
1481 holder: String,
1482 expiration_ts: u64,
1483 }
1484
1485 let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1486 let expiration_ts = now_ts + lease_duration_ms as u64;
1487
1488 let prev = object_store.get(&key)?.await?;
1489 match prev {
1490 Some(prev) => {
1491 let lease: Lease = self.serializer.deserialize_value(prev)?;
1492 if lease.holder == holder || lease.expiration_ts < now_ts {
1493 object_store.put_key_val(&key, &self.serializer.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?)?;
1494 Ok(true)
1495 } else {
1496 Ok(false)
1497 }
1498 }
1499 None => {
1500 object_store.put_key_val(&key, &self.serializer.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?)?;
1501 Ok(true)
1502 }
1503 }
1504 }
1505}
1506
1507impl Drop for IndexeddbCryptoStore {
1508 fn drop(&mut self) {
1509 self.inner.close();
1512 }
1513}
1514
1515async fn open_meta_db(prefix: &str) -> Result<IdbDatabase, IndexeddbCryptoStoreError> {
1519 let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1520
1521 debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1522 let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&name, 1)?;
1523 db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
1524 let old_version = evt.old_version() as u32;
1525 if old_version < 1 {
1526 let db = evt.db();
1528
1529 db.create_object_store("matrix-sdk-crypto")?;
1530 }
1531 Ok(())
1532 }));
1533
1534 Ok(db_req.await?)
1535}
1536
1537async fn load_store_cipher(
1547 meta_db: &IdbDatabase,
1548) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1549 let tx: IdbTransaction<'_> =
1550 meta_db.transaction_on_one_with_mode("matrix-sdk-crypto", IdbTransactionMode::Readonly)?;
1551 let ob = tx.object_store("matrix-sdk-crypto")?;
1552
1553 let store_cipher: Option<Vec<u8>> = ob
1554 .get(&JsValue::from_str(keys::STORE_CIPHER))?
1555 .await?
1556 .map(|k| k.into_serde())
1557 .transpose()?;
1558 Ok(store_cipher)
1559}
1560
1561async fn save_store_cipher(
1568 db: &IdbDatabase,
1569 export: &Vec<u8>,
1570) -> Result<(), IndexeddbCryptoStoreError> {
1571 let tx: IdbTransaction<'_> =
1572 db.transaction_on_one_with_mode("matrix-sdk-crypto", IdbTransactionMode::Readwrite)?;
1573 let ob = tx.object_store("matrix-sdk-crypto")?;
1574
1575 ob.put_key_val(&JsValue::from_str(keys::STORE_CIPHER), &JsValue::from_serde(&export)?)?;
1576 tx.await.into_result()?;
1577 Ok(())
1578}
1579
1580async fn import_store_cipher_with_key(
1594 chacha_key: &[u8; 32],
1595 original_key: &[u8],
1596 serialised_cipher: &[u8],
1597 db: &IdbDatabase,
1598) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1599 let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1600 Ok(cipher) => cipher,
1601 Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1602 let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1607 .map_err(|_| CryptoStoreError::UnpicklingError)?;
1608
1609 debug!(
1613 "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1614 );
1615
1616 let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1617 save_store_cipher(db, &export).await?;
1618 cipher
1619 }
1620 Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1621 };
1622 Ok(cipher)
1623}
1624
1625async fn fetch_from_object_store_batched<R, F>(
1629 object_store: IdbObjectStore<'_>,
1630 f: F,
1631 batch_size: usize,
1632) -> Result<Vec<R>>
1633where
1634 F: Fn(JsValue) -> Result<R>,
1635{
1636 let mut result = Vec::new();
1637 let mut batch_n = 0;
1638
1639 let mut latest_key: JsValue = "".into();
1641
1642 loop {
1643 debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1644
1645 let after_latest_key =
1653 IdbKeyRange::lower_bound_with_open(&latest_key, true).expect("Key was not valid!");
1654 let cursor = object_store.open_cursor_with_range(&after_latest_key)?.await?;
1655
1656 let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1658 if let Some(next_key) = next_key {
1659 latest_key = next_key;
1660 } else {
1661 break;
1662 }
1663
1664 batch_n += 1;
1665 }
1666
1667 Ok(result)
1668}
1669
1670async fn fetch_batch<R, F, Q>(
1674 cursor: Option<IdbCursorWithValue<'_, Q>>,
1675 batch_size: usize,
1676 f: &F,
1677 result: &mut Vec<R>,
1678) -> Result<Option<JsValue>>
1679where
1680 F: Fn(JsValue) -> Result<R>,
1681 Q: IdbQuerySource,
1682{
1683 let Some(cursor) = cursor else {
1684 return Ok(None);
1686 };
1687
1688 let mut latest_key = None;
1689
1690 for _ in 0..batch_size {
1691 let processed = f(cursor.value());
1693 if let Ok(processed) = processed {
1694 result.push(processed);
1695 }
1696 if let Some(key) = cursor.key() {
1701 latest_key = Some(key);
1702 }
1703
1704 let more_records = cursor.continue_cursor()?.await?;
1706 if !more_records {
1707 return Ok(None);
1708 }
1709 }
1710
1711 Ok(latest_key)
1714}
1715
1716#[derive(Debug, serde::Serialize, serde::Deserialize)]
1718struct GossipRequestIndexedDbObject {
1719 info: String,
1721
1722 request: Vec<u8>,
1724
1725 #[serde(
1734 default,
1735 skip_serializing_if = "std::ops::Not::not",
1736 with = "crate::serializer::foreign::bool"
1737 )]
1738 unsent: bool,
1739}
1740
1741#[derive(serde::Serialize, serde::Deserialize)]
1743struct InboundGroupSessionIndexedDbObject {
1744 pickled_session: MaybeEncrypted,
1747
1748 #[serde(default, skip_serializing_if = "Option::is_none")]
1756 session_id: Option<String>,
1757
1758 #[serde(
1767 default,
1768 skip_serializing_if = "std::ops::Not::not",
1769 with = "crate::serializer::foreign::bool"
1770 )]
1771 needs_backup: bool,
1772
1773 backed_up_to: i32,
1784
1785 #[serde(default, skip_serializing_if = "Option::is_none")]
1791 sender_key: Option<String>,
1792
1793 #[serde(default, skip_serializing_if = "Option::is_none")]
1799 sender_data_type: Option<u8>,
1800}
1801
1802impl InboundGroupSessionIndexedDbObject {
1803 pub async fn from_session(
1806 session: &InboundGroupSession,
1807 serializer: &SafeEncodeSerializer,
1808 ) -> Result<Self, CryptoStoreError> {
1809 let session_id =
1810 serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
1811
1812 let sender_key = serializer.encode_key_as_string(
1813 keys::INBOUND_GROUP_SESSIONS_V3,
1814 session.sender_key().to_base64(),
1815 );
1816
1817 Ok(InboundGroupSessionIndexedDbObject {
1818 pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
1819 session_id: Some(session_id),
1820 needs_backup: !session.backed_up(),
1821 backed_up_to: -1,
1822 sender_key: Some(sender_key),
1823 sender_data_type: Some(session.sender_data_type() as u8),
1824 })
1825 }
1826}
1827
1828#[cfg(test)]
1829mod unit_tests {
1830 use matrix_sdk_crypto::{
1831 olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
1832 types::EventEncryptionAlgorithm,
1833 vodozemac::Ed25519Keypair,
1834 };
1835 use matrix_sdk_store_encryption::EncryptedValueBase64;
1836 use matrix_sdk_test::async_test;
1837 use ruma::{device_id, room_id, user_id};
1838
1839 use super::InboundGroupSessionIndexedDbObject;
1840 use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
1841
1842 #[test]
1843 fn needs_backup_is_serialized_as_a_u8_in_json() {
1844 let session_needs_backup = backup_test_session(true);
1845
1846 assert!(serde_json::to_string(&session_needs_backup)
1850 .unwrap()
1851 .contains(r#""needs_backup":1"#),);
1852 }
1853
1854 #[test]
1855 fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
1856 let session_backed_up = backup_test_session(false);
1857
1858 assert!(
1859 !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
1860 "The needs_backup field should be missing!"
1861 );
1862 }
1863
1864 pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
1865 InboundGroupSessionIndexedDbObject {
1866 pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
1867 session_id: None,
1868 needs_backup,
1869 backed_up_to: -1,
1870 sender_key: None,
1871 sender_data_type: None,
1872 }
1873 }
1874
1875 #[async_test]
1876 async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
1877 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1878
1879 let sender_data = SenderData::sender_verified(
1880 user_id!("@test:user"),
1881 device_id!("ABC"),
1882 Ed25519Keypair::new().public_key(),
1883 );
1884
1885 let db_object = sender_data_test_session(sender_key, sender_data).await;
1886 let serialized = serde_json::to_string(&db_object).unwrap();
1887
1888 assert!(
1889 serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
1890 );
1891 assert!(serialized.contains(r#""sender_data_type":5"#));
1892 }
1893
1894 pub async fn sender_data_test_session(
1895 sender_key: Curve25519PublicKey,
1896 sender_data: SenderData,
1897 ) -> InboundGroupSessionIndexedDbObject {
1898 let session = InboundGroupSession::new(
1899 sender_key,
1900 Ed25519Keypair::new().public_key(),
1901 room_id!("!test:localhost"),
1902 &SessionKey::from_base64(
1904 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
1905 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
1906 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
1907 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
1908 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
1909 )
1910 .unwrap(),
1911 sender_data,
1912 EventEncryptionAlgorithm::MegolmV1AesSha2,
1913 None,
1914 false,
1915 )
1916 .unwrap();
1917
1918 InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
1919 .await
1920 .unwrap()
1921 }
1922}
1923
1924#[cfg(all(test, target_family = "wasm"))]
1925mod wasm_unit_tests {
1926 use std::collections::BTreeMap;
1927
1928 use matrix_sdk_crypto::{
1929 olm::{Curve25519PublicKey, SenderData},
1930 types::{DeviceKeys, Signatures},
1931 };
1932 use matrix_sdk_test::async_test;
1933 use ruma::{device_id, user_id};
1934 use wasm_bindgen::JsValue;
1935
1936 use crate::crypto_store::unit_tests::sender_data_test_session;
1937
1938 fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
1939 assert_eq!(
1940 js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
1941 JsValue::from_f64(expected.into())
1942 );
1943 }
1944
1945 #[async_test]
1946 fn test_needs_backup_is_serialized_as_a_u8_in_js() {
1947 let session_needs_backup = super::unit_tests::backup_test_session(true);
1948
1949 let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
1950
1951 assert!(js_value.is_object());
1952 assert_field_equals(&js_value, "needs_backup", 1);
1953 }
1954
1955 #[async_test]
1956 fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
1957 let session_backed_up = super::unit_tests::backup_test_session(false);
1958
1959 let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
1960
1961 assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
1962 }
1963
1964 #[async_test]
1965 async fn test_sender_key_and_device_type_are_serialized_in_js() {
1966 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1967
1968 let sender_data = SenderData::device_info(DeviceKeys::new(
1969 user_id!("@test:user").to_owned(),
1970 device_id!("ABC").to_owned(),
1971 vec![],
1972 BTreeMap::new(),
1973 Signatures::new(),
1974 ));
1975 let db_object = sender_data_test_session(sender_key, sender_data).await;
1976
1977 let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
1978
1979 assert!(js_value.is_object());
1980 assert_field_equals(&js_value, "sender_data_type", 2);
1981 assert_eq!(
1982 js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
1983 JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
1984 );
1985 }
1986}
1987
1988#[cfg(all(test, target_family = "wasm"))]
1989mod tests {
1990 use matrix_sdk_crypto::cryptostore_integration_tests;
1991
1992 use super::IndexeddbCryptoStore;
1993
1994 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1995
1996 async fn get_store(
1997 name: &str,
1998 passphrase: Option<&str>,
1999 clear_data: bool,
2000 ) -> IndexeddbCryptoStore {
2001 if clear_data {
2002 IndexeddbCryptoStore::delete_stores(name).unwrap();
2003 }
2004 match passphrase {
2005 Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2006 .await
2007 .expect("Can't create a passphrase protected store"),
2008 None => IndexeddbCryptoStore::open_with_name(name)
2009 .await
2010 .expect("Can't create store without passphrase"),
2011 }
2012 }
2013
2014 cryptostore_integration_tests!();
2015}
2016
2017#[cfg(all(test, target_family = "wasm"))]
2018mod encrypted_tests {
2019 use matrix_sdk_crypto::{
2020 cryptostore_integration_tests,
2021 olm::Account,
2022 store::{types::PendingChanges, CryptoStore},
2023 vodozemac::base64_encode,
2024 };
2025 use matrix_sdk_test::async_test;
2026 use ruma::{device_id, user_id};
2027
2028 use super::IndexeddbCryptoStore;
2029
2030 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2031
2032 async fn get_store(
2033 name: &str,
2034 passphrase: Option<&str>,
2035 clear_data: bool,
2036 ) -> IndexeddbCryptoStore {
2037 if clear_data {
2038 IndexeddbCryptoStore::delete_stores(name).unwrap();
2039 }
2040
2041 let pass = passphrase.unwrap_or(name);
2042 IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2043 .await
2044 .expect("Can't create a passphrase protected store")
2045 }
2046 cryptostore_integration_tests!();
2047
2048 #[async_test]
2051 async fn test_migrate_passphrase_to_key() {
2052 let store_name = "test_migrate_passphrase_to_key";
2053 let passdata: [u8; 32] = rand::random();
2054 let b64_passdata = base64_encode(passdata);
2055
2056 IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2058 let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2059 .await
2060 .expect("Can't create a passphrase-protected store");
2061
2062 store
2063 .save_pending_changes(PendingChanges {
2064 account: Some(Account::with_device_id(
2065 user_id!("@alice:example.org"),
2066 device_id!("ALICEDEVICE"),
2067 )),
2068 })
2069 .await
2070 .expect("Can't save account");
2071
2072 let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2074 .await
2075 .expect("Can't create a key-protected store");
2076 let loaded_account =
2077 store.load_account().await.expect("Can't load account").expect("Account was not saved");
2078 assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2079 }
2080}