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