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::{IndexeddbSerializer, IndexeddbSerializerError, MaybeEncrypted},
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: IndexeddbSerializer,
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<IndexeddbSerializerError> for IndexeddbCryptoStoreError {
159 fn from(value: IndexeddbSerializerError) -> Self {
160 match value {
161 IndexeddbSerializerError::Serialization(error) => Self::Serialization(error),
162 IndexeddbSerializerError::DomException { code, name, message } => {
163 Self::DomException { code, name, message }
164 }
165 IndexeddbSerializerError::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 = IndexeddbSerializer::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_for_device_batch(
990 &self,
991 sender_key: Curve25519PublicKey,
992 sender_data_type: SenderDataType,
993 after_session_id: Option<String>,
994 limit: usize,
995 ) -> Result<Vec<InboundGroupSession>> {
996 let sender_key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
997
998 let after_session_id = after_session_id.map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s)).unwrap_or("".into());
1000
1001 let lower_bound: Array = [sender_key.clone(), (sender_data_type as u8).into(), after_session_id].iter().collect();
1002 let upper_bound: Array = [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
1003 let key = IdbKeyRange::bound_with_lower_open_and_upper_open(
1004 &lower_bound,
1005 &upper_bound,
1006 true, true
1007 ).expect("Key was not valid!");
1008
1009 let tx = self
1010 .inner
1011 .transaction_on_one_with_mode(
1012 keys::INBOUND_GROUP_SESSIONS_V3,
1013 IdbTransactionMode::Readonly,
1014 )?;
1015
1016 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1017 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
1018 let serialized_sessions = idx.get_all_with_key_and_limit_owned(key, limit as u32)?.await?;
1019
1020 let result = serialized_sessions.into_iter()
1022 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1023 Ok(session) => Some(session),
1024 Err(e) => {
1025 warn!("Failed to deserialize inbound group session: {e}");
1026 None
1027 }
1028 })
1029 .collect::<Vec<InboundGroupSession>>();
1030
1031 Ok(result)
1032 }
1033
1034 async fn inbound_group_session_counts(&self, _backup_version: Option<&str>) -> Result<RoomKeyCounts> {
1035 let tx = self
1036 .inner
1037 .transaction_on_one_with_mode(
1038 keys::INBOUND_GROUP_SESSIONS_V3,
1039 IdbTransactionMode::Readonly,
1040 )?;
1041 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1042 let all = store.count()?.await? as usize;
1043 let not_backed_up = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count()?.await? as usize;
1044 tx.await.into_result()?;
1045 Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1046 }
1047
1048 async fn inbound_group_sessions_for_backup(
1049 &self,
1050 _backup_version: &str,
1051 limit: usize,
1052 ) -> Result<Vec<InboundGroupSession>> {
1053 let tx = self
1054 .inner
1055 .transaction_on_one_with_mode(
1056 keys::INBOUND_GROUP_SESSIONS_V3,
1057 IdbTransactionMode::Readonly,
1058 )?;
1059
1060
1061 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1062 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1063
1064 let Some(cursor) = idx.open_cursor()?.await? else {
1068 return Ok(vec![]);
1069 };
1070
1071 let mut serialized_sessions = Vec::with_capacity(limit);
1072 for _ in 0..limit {
1073 serialized_sessions.push(cursor.value());
1074 if !cursor.continue_cursor()?.await? {
1075 break;
1076 }
1077 }
1078
1079 tx.await.into_result()?;
1080
1081 let result = serialized_sessions.into_iter()
1083 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1084 Ok(session) => Some(session),
1085 Err(e) => {
1086 warn!("Failed to deserialize inbound group session: {e}");
1087 None
1088 }
1089 })
1090 .collect::<Vec<InboundGroupSession>>();
1091
1092 Ok(result)
1093 }
1094
1095 async fn mark_inbound_group_sessions_as_backed_up(&self,
1096 _backup_version: &str,
1097 room_and_session_ids: &[(&RoomId, &str)]
1098 ) -> Result<()> {
1099 let tx = self
1100 .inner
1101 .transaction_on_one_with_mode(
1102 keys::INBOUND_GROUP_SESSIONS_V3,
1103 IdbTransactionMode::Readwrite,
1104 )?;
1105
1106 let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1107
1108 for (room_id, session_id) in room_and_session_ids {
1109 let key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1110 if let Some(idb_object_js) = object_store.get(&key)?.await? {
1111 let mut idb_object: InboundGroupSessionIndexedDbObject = serde_wasm_bindgen::from_value(idb_object_js)?;
1112 idb_object.needs_backup = false;
1113 object_store.put_key_val(&key, &serde_wasm_bindgen::to_value(&idb_object)?)?;
1114 } else {
1115 warn!(?key, "Could not find inbound group session to mark it as backed up.");
1116 }
1117 }
1118
1119 Ok(tx.await.into_result()?)
1120 }
1121
1122 async fn reset_backup_state(&self) -> Result<()> {
1123 let tx = self
1124 .inner
1125 .transaction_on_one_with_mode(
1126 keys::INBOUND_GROUP_SESSIONS_V3,
1127 IdbTransactionMode::Readwrite,
1128 )?;
1129
1130 if let Some(cursor) = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor()?.await? {
1131 loop {
1132 let mut idb_object: InboundGroupSessionIndexedDbObject = serde_wasm_bindgen::from_value(cursor.value())?;
1133 if !idb_object.needs_backup {
1134 idb_object.needs_backup = true;
1135 let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1139 cursor.update(&idb_object)?.await?;
1140 }
1141
1142 if !cursor.continue_cursor()?.await? {
1143 break;
1144 }
1145 }
1146 }
1147
1148 Ok(tx.await.into_result()?)
1149 }
1150
1151 async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1152 let tx = self
1153 .inner
1154 .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readwrite)?;
1155 let os = tx.object_store(keys::TRACKED_USERS)?;
1156
1157 for (user, dirty) in users {
1158 os.put_key_val(&JsValue::from_str(user.as_str()), &JsValue::from(*dirty))?;
1159 }
1160
1161 tx.await.into_result()?;
1162 Ok(())
1163 }
1164
1165 async fn get_device(
1166 &self,
1167 user_id: &UserId,
1168 device_id: &DeviceId,
1169 ) -> Result<Option<DeviceData>> {
1170 let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1171 self
1172 .inner
1173 .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
1174 .object_store(keys::DEVICES)?
1175 .get(&key)?
1176 .await?
1177 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1178 .transpose()
1179 }
1180
1181 async fn get_user_devices(
1182 &self,
1183 user_id: &UserId,
1184 ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1185 let range = self.serializer.encode_to_range(keys::DEVICES, user_id)?;
1186 Ok(self
1187 .inner
1188 .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
1189 .object_store(keys::DEVICES)?
1190 .get_all_with_key(&range)?
1191 .await?
1192 .iter()
1193 .filter_map(|d| {
1194 let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1195 Some((d.device_id().to_owned(), d))
1196 })
1197 .collect::<HashMap<_, _>>())
1198 }
1199
1200 async fn get_own_device(&self) -> Result<DeviceData> {
1201 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1202 Ok(self.get_device(&account_info.user_id, &account_info.device_id)
1203 .await?
1204 .unwrap())
1205 }
1206
1207 async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1208 self
1209 .inner
1210 .transaction_on_one_with_mode(keys::IDENTITIES, IdbTransactionMode::Readonly)?
1211 .object_store(keys::IDENTITIES)?
1212 .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))?
1213 .await?
1214 .map(|i| self.serializer.deserialize_value(i).map_err(Into::into))
1215 .transpose()
1216 }
1217
1218 async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1219 Ok(self
1220 .inner
1221 .transaction_on_one_with_mode(keys::OLM_HASHES, IdbTransactionMode::Readonly)?
1222 .object_store(keys::OLM_HASHES)?
1223 .get(&self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)))?
1224 .await?
1225 .is_some())
1226 }
1227
1228 async fn get_secrets_from_inbox(
1229 &self,
1230 secret_name: &SecretName,
1231 ) -> Result<Vec<GossippedSecret>> {
1232 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str())?;
1233
1234 self
1235 .inner
1236 .transaction_on_one_with_mode(keys::SECRETS_INBOX, IdbTransactionMode::Readonly)?
1237 .object_store(keys::SECRETS_INBOX)?
1238 .get_all_with_key(&range)?
1239 .await?
1240 .iter()
1241 .map(|d| {
1242 let secret = self.serializer.deserialize_value(d)?;
1243 Ok(secret)
1244 }).collect()
1245 }
1246
1247 #[allow(clippy::unused_async)] async fn delete_secrets_from_inbox(
1249 &self,
1250 secret_name: &SecretName,
1251 ) -> Result<()> {
1252 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str())?;
1253
1254 self
1255 .inner
1256 .transaction_on_one_with_mode(keys::SECRETS_INBOX, IdbTransactionMode::Readwrite)?
1257 .object_store(keys::SECRETS_INBOX)?
1258 .delete(&range)?;
1259
1260 Ok(())
1261 }
1262
1263 async fn get_secret_request_by_info(
1264 &self,
1265 key_info: &SecretInfo,
1266 ) -> Result<Option<GossipRequest>> {
1267 let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1268
1269 let val = self
1270 .inner
1271 .transaction_on_one_with_mode(
1272 keys::GOSSIP_REQUESTS,
1273 IdbTransactionMode::Readonly,
1274 )?
1275 .object_store(keys::GOSSIP_REQUESTS)?
1276 .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1277 .get_owned(key)?
1278 .await?;
1279
1280 if let Some(val) = val {
1281 let deser = self.deserialize_gossip_request(val)?;
1282 Ok(Some(deser))
1283 } else {
1284 Ok(None)
1285 }
1286 }
1287
1288 async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1289 let results = self
1290 .inner
1291 .transaction_on_one_with_mode(
1292 keys::GOSSIP_REQUESTS,
1293 IdbTransactionMode::Readonly,
1294 )?
1295 .object_store(keys::GOSSIP_REQUESTS)?
1296 .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1297 .get_all()?
1298 .await?
1299 .iter()
1300 .filter_map(|val| self.deserialize_gossip_request(val).ok())
1301 .collect();
1302
1303 Ok(results)
1304 }
1305
1306 async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1307 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1308 let tx = self.inner.transaction_on_one_with_mode(keys::GOSSIP_REQUESTS, IdbTransactionMode::Readwrite)?;
1309 tx.object_store(keys::GOSSIP_REQUESTS)?.delete_owned(jskey)?;
1310 tx.await.into_result().map_err(|e| e.into())
1311 }
1312
1313 async fn load_backup_keys(&self) -> Result<BackupKeys> {
1314 let key = {
1315 let tx = self
1316 .inner
1317 .transaction_on_one_with_mode(keys::BACKUP_KEYS, IdbTransactionMode::Readonly)?;
1318 let store = tx.object_store(keys::BACKUP_KEYS)?;
1319
1320 let backup_version = store
1321 .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))?
1322 .await?
1323 .map(|i| self.serializer.deserialize_value(i))
1324 .transpose()?;
1325
1326 let decryption_key = store
1327 .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))?
1328 .await?
1329 .map(|i| self.serializer.deserialize_value(i))
1330 .transpose()?;
1331
1332 BackupKeys { backup_version, decryption_key }
1333 };
1334
1335 Ok(key)
1336 }
1337
1338
1339 async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1340 if let Some(pickle) = self
1341 .inner
1342 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
1343 .object_store(keys::CORE)?
1344 .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))?
1345 .await?
1346 {
1347 let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1348
1349 Ok(Some(pickle))
1350 } else {
1351 Ok(None)
1352 }
1353 }
1354
1355 async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1356 self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1357 Ok(())
1358 }
1359
1360 async fn get_withheld_info(
1361 &self,
1362 room_id: &RoomId,
1363 session_id: &str,
1364 ) -> Result<Option<RoomKeyWithheldEvent>> {
1365 let key = self.serializer.encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, room_id));
1366 if let Some(pickle) = self
1367 .inner
1368 .transaction_on_one_with_mode(
1369 keys::DIRECT_WITHHELD_INFO,
1370 IdbTransactionMode::Readonly,
1371 )?
1372 .object_store(keys::DIRECT_WITHHELD_INFO)?
1373 .get(&key)?
1374 .await?
1375 {
1376 let info = self.serializer.deserialize_value(pickle)?;
1377 Ok(Some(info))
1378 } else {
1379 Ok(None)
1380 }
1381 }
1382
1383 async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1384 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1385 self
1386 .inner
1387 .transaction_on_one_with_mode(keys::ROOM_SETTINGS, IdbTransactionMode::Readonly)?
1388 .object_store(keys::ROOM_SETTINGS)?
1389 .get(&key)?
1390 .await?
1391 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1392 .transpose()
1393 }
1394
1395 async fn get_received_room_key_bundle_data(&self, room_id: &RoomId, user_id: &UserId) -> Result<Option<StoredRoomKeyBundleData>> {
1396 let key = self.serializer.encode_key(keys::RECEIVED_ROOM_KEY_BUNDLES, (room_id, user_id));
1397 let result = self
1398 .inner
1399 .transaction_on_one_with_mode(keys::RECEIVED_ROOM_KEY_BUNDLES, IdbTransactionMode::Readonly)?
1400 .object_store(keys::RECEIVED_ROOM_KEY_BUNDLES)?
1401 .get(&key)?
1402 .await?
1403 .map(|v| self.serializer.deserialize_value(v))
1404 .transpose()?;
1405
1406 Ok(result)
1407 }
1408
1409 async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1410 self
1411 .inner
1412 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
1413 .object_store(keys::CORE)?
1414 .get(&JsValue::from_str(key))?
1415 .await?
1416 .map(|v| self.serializer.deserialize_value(v).map_err(Into::into))
1417 .transpose()
1418 }
1419
1420 #[allow(clippy::unused_async)] async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1422 self
1423 .inner
1424 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?
1425 .object_store(keys::CORE)?
1426 .put_key_val(&JsValue::from_str(key), &self.serializer.serialize_value(&value)?)?;
1427 Ok(())
1428 }
1429
1430 #[allow(clippy::unused_async)] async fn remove_custom_value(&self, key: &str) -> Result<()> {
1432 self
1433 .inner
1434 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?
1435 .object_store(keys::CORE)?
1436 .delete(&JsValue::from_str(key))?;
1437 Ok(())
1438 }
1439
1440 async fn try_take_leased_lock(
1441 &self,
1442 lease_duration_ms: u32,
1443 key: &str,
1444 holder: &str,
1445 ) -> Result<bool> {
1446 let key = JsValue::from_str(key);
1448 let txn = self
1449 .inner
1450 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?;
1451 let object_store = txn
1452 .object_store(keys::CORE)?;
1453
1454 #[derive(serde::Deserialize, serde::Serialize)]
1455 struct Lease {
1456 holder: String,
1457 expiration_ts: u64,
1458 }
1459
1460 let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1461 let expiration_ts = now_ts + lease_duration_ms as u64;
1462
1463 let prev = object_store.get(&key)?.await?;
1464 match prev {
1465 Some(prev) => {
1466 let lease: Lease = self.serializer.deserialize_value(prev)?;
1467 if lease.holder == holder || lease.expiration_ts < now_ts {
1468 object_store.put_key_val(&key, &self.serializer.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?)?;
1469 Ok(true)
1470 } else {
1471 Ok(false)
1472 }
1473 }
1474 None => {
1475 object_store.put_key_val(&key, &self.serializer.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?)?;
1476 Ok(true)
1477 }
1478 }
1479 }
1480}
1481
1482impl Drop for IndexeddbCryptoStore {
1483 fn drop(&mut self) {
1484 self.inner.close();
1487 }
1488}
1489
1490async fn open_meta_db(prefix: &str) -> Result<IdbDatabase, IndexeddbCryptoStoreError> {
1494 let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1495
1496 debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1497 let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&name, 1)?;
1498 db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
1499 let old_version = evt.old_version() as u32;
1500 if old_version < 1 {
1501 let db = evt.db();
1503
1504 db.create_object_store("matrix-sdk-crypto")?;
1505 }
1506 Ok(())
1507 }));
1508
1509 Ok(db_req.await?)
1510}
1511
1512async fn load_store_cipher(
1522 meta_db: &IdbDatabase,
1523) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1524 let tx: IdbTransaction<'_> =
1525 meta_db.transaction_on_one_with_mode("matrix-sdk-crypto", IdbTransactionMode::Readonly)?;
1526 let ob = tx.object_store("matrix-sdk-crypto")?;
1527
1528 let store_cipher: Option<Vec<u8>> = ob
1529 .get(&JsValue::from_str(keys::STORE_CIPHER))?
1530 .await?
1531 .map(|k| k.into_serde())
1532 .transpose()?;
1533 Ok(store_cipher)
1534}
1535
1536async fn save_store_cipher(
1543 db: &IdbDatabase,
1544 export: &Vec<u8>,
1545) -> Result<(), IndexeddbCryptoStoreError> {
1546 let tx: IdbTransaction<'_> =
1547 db.transaction_on_one_with_mode("matrix-sdk-crypto", IdbTransactionMode::Readwrite)?;
1548 let ob = tx.object_store("matrix-sdk-crypto")?;
1549
1550 ob.put_key_val(&JsValue::from_str(keys::STORE_CIPHER), &JsValue::from_serde(&export)?)?;
1551 tx.await.into_result()?;
1552 Ok(())
1553}
1554
1555async fn import_store_cipher_with_key(
1569 chacha_key: &[u8; 32],
1570 original_key: &[u8],
1571 serialised_cipher: &[u8],
1572 db: &IdbDatabase,
1573) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1574 let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1575 Ok(cipher) => cipher,
1576 Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1577 let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1582 .map_err(|_| CryptoStoreError::UnpicklingError)?;
1583
1584 debug!(
1588 "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1589 );
1590
1591 let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1592 save_store_cipher(db, &export).await?;
1593 cipher
1594 }
1595 Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1596 };
1597 Ok(cipher)
1598}
1599
1600async fn fetch_from_object_store_batched<R, F>(
1604 object_store: IdbObjectStore<'_>,
1605 f: F,
1606 batch_size: usize,
1607) -> Result<Vec<R>>
1608where
1609 F: Fn(JsValue) -> Result<R>,
1610{
1611 let mut result = Vec::new();
1612 let mut batch_n = 0;
1613
1614 let mut latest_key: JsValue = "".into();
1616
1617 loop {
1618 debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1619
1620 let after_latest_key =
1628 IdbKeyRange::lower_bound_with_open(&latest_key, true).expect("Key was not valid!");
1629 let cursor = object_store.open_cursor_with_range(&after_latest_key)?.await?;
1630
1631 let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1633 if let Some(next_key) = next_key {
1634 latest_key = next_key;
1635 } else {
1636 break;
1637 }
1638
1639 batch_n += 1;
1640 }
1641
1642 Ok(result)
1643}
1644
1645async fn fetch_batch<R, F, Q>(
1649 cursor: Option<IdbCursorWithValue<'_, Q>>,
1650 batch_size: usize,
1651 f: &F,
1652 result: &mut Vec<R>,
1653) -> Result<Option<JsValue>>
1654where
1655 F: Fn(JsValue) -> Result<R>,
1656 Q: IdbQuerySource,
1657{
1658 let Some(cursor) = cursor else {
1659 return Ok(None);
1661 };
1662
1663 let mut latest_key = None;
1664
1665 for _ in 0..batch_size {
1666 let processed = f(cursor.value());
1668 if let Ok(processed) = processed {
1669 result.push(processed);
1670 }
1671 if let Some(key) = cursor.key() {
1676 latest_key = Some(key);
1677 }
1678
1679 let more_records = cursor.continue_cursor()?.await?;
1681 if !more_records {
1682 return Ok(None);
1683 }
1684 }
1685
1686 Ok(latest_key)
1689}
1690
1691#[derive(Debug, serde::Serialize, serde::Deserialize)]
1693struct GossipRequestIndexedDbObject {
1694 info: String,
1696
1697 request: Vec<u8>,
1699
1700 #[serde(
1709 default,
1710 skip_serializing_if = "std::ops::Not::not",
1711 with = "crate::serialize_bool_for_indexeddb"
1712 )]
1713 unsent: bool,
1714}
1715
1716#[derive(serde::Serialize, serde::Deserialize)]
1718struct InboundGroupSessionIndexedDbObject {
1719 pickled_session: MaybeEncrypted,
1722
1723 #[serde(default, skip_serializing_if = "Option::is_none")]
1731 session_id: Option<String>,
1732
1733 #[serde(
1742 default,
1743 skip_serializing_if = "std::ops::Not::not",
1744 with = "crate::serialize_bool_for_indexeddb"
1745 )]
1746 needs_backup: bool,
1747
1748 backed_up_to: i32,
1759
1760 #[serde(default, skip_serializing_if = "Option::is_none")]
1766 sender_key: Option<String>,
1767
1768 #[serde(default, skip_serializing_if = "Option::is_none")]
1774 sender_data_type: Option<u8>,
1775}
1776
1777impl InboundGroupSessionIndexedDbObject {
1778 pub async fn from_session(
1781 session: &InboundGroupSession,
1782 serializer: &IndexeddbSerializer,
1783 ) -> Result<Self, CryptoStoreError> {
1784 let session_id =
1785 serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
1786
1787 let sender_key = serializer.encode_key_as_string(
1788 keys::INBOUND_GROUP_SESSIONS_V3,
1789 session.sender_key().to_base64(),
1790 );
1791
1792 Ok(InboundGroupSessionIndexedDbObject {
1793 pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
1794 session_id: Some(session_id),
1795 needs_backup: !session.backed_up(),
1796 backed_up_to: -1,
1797 sender_key: Some(sender_key),
1798 sender_data_type: Some(session.sender_data_type() as u8),
1799 })
1800 }
1801}
1802
1803#[cfg(test)]
1804mod unit_tests {
1805 use matrix_sdk_crypto::{
1806 olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
1807 types::EventEncryptionAlgorithm,
1808 vodozemac::Ed25519Keypair,
1809 };
1810 use matrix_sdk_store_encryption::EncryptedValueBase64;
1811 use matrix_sdk_test::async_test;
1812 use ruma::{device_id, room_id, user_id};
1813
1814 use super::InboundGroupSessionIndexedDbObject;
1815 use crate::serializer::{IndexeddbSerializer, MaybeEncrypted};
1816
1817 #[test]
1818 fn needs_backup_is_serialized_as_a_u8_in_json() {
1819 let session_needs_backup = backup_test_session(true);
1820
1821 assert!(serde_json::to_string(&session_needs_backup)
1825 .unwrap()
1826 .contains(r#""needs_backup":1"#),);
1827 }
1828
1829 #[test]
1830 fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
1831 let session_backed_up = backup_test_session(false);
1832
1833 assert!(
1834 !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
1835 "The needs_backup field should be missing!"
1836 );
1837 }
1838
1839 pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
1840 InboundGroupSessionIndexedDbObject {
1841 pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
1842 session_id: None,
1843 needs_backup,
1844 backed_up_to: -1,
1845 sender_key: None,
1846 sender_data_type: None,
1847 }
1848 }
1849
1850 #[async_test]
1851 async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
1852 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1853
1854 let sender_data = SenderData::sender_verified(
1855 user_id!("@test:user"),
1856 device_id!("ABC"),
1857 Ed25519Keypair::new().public_key(),
1858 );
1859
1860 let db_object = sender_data_test_session(sender_key, sender_data).await;
1861 let serialized = serde_json::to_string(&db_object).unwrap();
1862
1863 assert!(
1864 serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
1865 );
1866 assert!(serialized.contains(r#""sender_data_type":5"#));
1867 }
1868
1869 pub async fn sender_data_test_session(
1870 sender_key: Curve25519PublicKey,
1871 sender_data: SenderData,
1872 ) -> InboundGroupSessionIndexedDbObject {
1873 let session = InboundGroupSession::new(
1874 sender_key,
1875 Ed25519Keypair::new().public_key(),
1876 room_id!("!test:localhost"),
1877 &SessionKey::from_base64(
1879 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
1880 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
1881 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
1882 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
1883 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
1884 )
1885 .unwrap(),
1886 sender_data,
1887 EventEncryptionAlgorithm::MegolmV1AesSha2,
1888 None,
1889 false,
1890 )
1891 .unwrap();
1892
1893 InboundGroupSessionIndexedDbObject::from_session(&session, &IndexeddbSerializer::new(None))
1894 .await
1895 .unwrap()
1896 }
1897}
1898
1899#[cfg(all(test, target_family = "wasm"))]
1900mod wasm_unit_tests {
1901 use std::collections::BTreeMap;
1902
1903 use matrix_sdk_crypto::{
1904 olm::{Curve25519PublicKey, SenderData},
1905 types::{DeviceKeys, Signatures},
1906 };
1907 use matrix_sdk_test::async_test;
1908 use ruma::{device_id, user_id};
1909 use wasm_bindgen::JsValue;
1910
1911 use crate::crypto_store::unit_tests::sender_data_test_session;
1912
1913 fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
1914 assert_eq!(
1915 js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
1916 JsValue::from_f64(expected.into())
1917 );
1918 }
1919
1920 #[async_test]
1921 fn test_needs_backup_is_serialized_as_a_u8_in_js() {
1922 let session_needs_backup = super::unit_tests::backup_test_session(true);
1923
1924 let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
1925
1926 assert!(js_value.is_object());
1927 assert_field_equals(&js_value, "needs_backup", 1);
1928 }
1929
1930 #[async_test]
1931 fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
1932 let session_backed_up = super::unit_tests::backup_test_session(false);
1933
1934 let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
1935
1936 assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
1937 }
1938
1939 #[async_test]
1940 async fn test_sender_key_and_device_type_are_serialized_in_js() {
1941 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1942
1943 let sender_data = SenderData::device_info(DeviceKeys::new(
1944 user_id!("@test:user").to_owned(),
1945 device_id!("ABC").to_owned(),
1946 vec![],
1947 BTreeMap::new(),
1948 Signatures::new(),
1949 ));
1950 let db_object = sender_data_test_session(sender_key, sender_data).await;
1951
1952 let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
1953
1954 assert!(js_value.is_object());
1955 assert_field_equals(&js_value, "sender_data_type", 2);
1956 assert_eq!(
1957 js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
1958 JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
1959 );
1960 }
1961}
1962
1963#[cfg(all(test, target_family = "wasm"))]
1964mod tests {
1965 use matrix_sdk_crypto::cryptostore_integration_tests;
1966
1967 use super::IndexeddbCryptoStore;
1968
1969 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1970
1971 async fn get_store(
1972 name: &str,
1973 passphrase: Option<&str>,
1974 clear_data: bool,
1975 ) -> IndexeddbCryptoStore {
1976 if clear_data {
1977 IndexeddbCryptoStore::delete_stores(name).unwrap();
1978 }
1979 match passphrase {
1980 Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
1981 .await
1982 .expect("Can't create a passphrase protected store"),
1983 None => IndexeddbCryptoStore::open_with_name(name)
1984 .await
1985 .expect("Can't create store without passphrase"),
1986 }
1987 }
1988
1989 cryptostore_integration_tests!();
1990}
1991
1992#[cfg(all(test, target_family = "wasm"))]
1993mod encrypted_tests {
1994 use matrix_sdk_crypto::{
1995 cryptostore_integration_tests,
1996 olm::Account,
1997 store::{types::PendingChanges, CryptoStore},
1998 vodozemac::base64_encode,
1999 };
2000 use matrix_sdk_test::async_test;
2001 use ruma::{device_id, user_id};
2002
2003 use super::IndexeddbCryptoStore;
2004
2005 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2006
2007 async fn get_store(
2008 name: &str,
2009 passphrase: Option<&str>,
2010 clear_data: bool,
2011 ) -> IndexeddbCryptoStore {
2012 if clear_data {
2013 IndexeddbCryptoStore::delete_stores(name).unwrap();
2014 }
2015
2016 let pass = passphrase.unwrap_or(name);
2017 IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2018 .await
2019 .expect("Can't create a passphrase protected store")
2020 }
2021 cryptostore_integration_tests!();
2022
2023 #[async_test]
2026 async fn test_migrate_passphrase_to_key() {
2027 let store_name = "test_migrate_passphrase_to_key";
2028 let passdata: [u8; 32] = rand::random();
2029 let b64_passdata = base64_encode(passdata);
2030
2031 IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2033 let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2034 .await
2035 .expect("Can't create a passphrase-protected store");
2036
2037 store
2038 .save_pending_changes(PendingChanges {
2039 account: Some(Account::with_device_id(
2040 user_id!("@alice:example.org"),
2041 device_id!("ALICEDEVICE"),
2042 )),
2043 })
2044 .await
2045 .expect("Can't save account");
2046
2047 let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2049 .await
2050 .expect("Can't create a key-protected store");
2051 let loaded_account =
2052 store.load_account().await.expect("Can't load account").expect("Account was not saved");
2053 assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2054 }
2055}