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 #[allow(clippy::unused_async)]
1662 async fn get_size(&self) -> Result<Option<usize>> {
1663 Ok(None)
1664 }
1665}
1666
1667impl Drop for IndexeddbCryptoStore {
1668 fn drop(&mut self) {
1669 self.inner.as_sys().close();
1672 }
1673}
1674
1675async fn open_meta_db(prefix: &str) -> Result<Database, IndexeddbCryptoStoreError> {
1679 let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1680
1681 debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1682 Database::open(&name)
1683 .with_version(1u32)
1684 .with_on_upgrade_needed(|evt, tx| {
1685 let old_version = evt.old_version() as u32;
1686 if old_version < 1 {
1687 tx.db().create_object_store("matrix-sdk-crypto").build()?;
1689 }
1690 Ok(())
1691 })
1692 .await
1693 .map_err(Into::into)
1694}
1695
1696async fn load_store_cipher(
1706 meta_db: &Database,
1707) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1708 let tx: Transaction<'_> =
1709 meta_db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readonly).build()?;
1710 let ob = tx.object_store("matrix-sdk-crypto")?;
1711
1712 let store_cipher: Option<Vec<u8>> = ob
1713 .get(&JsValue::from_str(keys::STORE_CIPHER))
1714 .await?
1715 .map(|k: JsValue| k.into_serde())
1716 .transpose()?;
1717 Ok(store_cipher)
1718}
1719
1720async fn save_store_cipher(
1727 db: &Database,
1728 export: &Vec<u8>,
1729) -> Result<(), IndexeddbCryptoStoreError> {
1730 let tx: Transaction<'_> =
1731 db.transaction("matrix-sdk-crypto").with_mode(TransactionMode::Readwrite).build()?;
1732 let ob = tx.object_store("matrix-sdk-crypto")?;
1733
1734 ob.put(&JsValue::from_serde(&export)?)
1735 .with_key(JsValue::from_str(keys::STORE_CIPHER))
1736 .build()?;
1737 tx.commit().await?;
1738 Ok(())
1739}
1740
1741async fn import_store_cipher_with_key(
1755 chacha_key: &[u8; 32],
1756 original_key: &[u8],
1757 serialised_cipher: &[u8],
1758 db: &Database,
1759) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1760 let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1761 Ok(cipher) => cipher,
1762 Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1763 let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1768 .map_err(|_| CryptoStoreError::UnpicklingError)?;
1769
1770 debug!(
1774 "IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption"
1775 );
1776
1777 let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1778 save_store_cipher(db, &export).await?;
1779 cipher
1780 }
1781 Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1782 };
1783 Ok(cipher)
1784}
1785
1786async fn fetch_from_object_store_batched<R, F>(
1790 object_store: ObjectStore<'_>,
1791 f: F,
1792 batch_size: usize,
1793) -> Result<Vec<R>>
1794where
1795 F: Fn(JsValue) -> Result<R>,
1796{
1797 let mut result = Vec::new();
1798 let mut batch_n = 0;
1799
1800 let mut latest_key: JsValue = "".into();
1802
1803 loop {
1804 debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1805
1806 let after_latest_key = KeyRange::LowerBound(&latest_key, true);
1814 let cursor = object_store.open_cursor().with_query(&after_latest_key).await?;
1815
1816 let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1818 if let Some(next_key) = next_key {
1819 latest_key = next_key;
1820 } else {
1821 break;
1822 }
1823
1824 batch_n += 1;
1825 }
1826
1827 Ok(result)
1828}
1829
1830async fn fetch_batch<R, F, Q>(
1834 cursor: Option<Cursor<'_, Q>>,
1835 batch_size: usize,
1836 f: &F,
1837 result: &mut Vec<R>,
1838) -> Result<Option<JsValue>>
1839where
1840 F: Fn(JsValue) -> Result<R>,
1841 Q: QuerySource,
1842{
1843 let Some(mut cursor) = cursor else {
1844 return Ok(None);
1846 };
1847
1848 let mut latest_key = None;
1849
1850 for _ in 0..batch_size {
1851 let Some(value) = cursor.next_record().await? else {
1852 return Ok(None);
1853 };
1854
1855 let processed = f(value);
1857 if let Ok(processed) = processed {
1858 result.push(processed);
1859 }
1860 if let Some(key) = cursor.key()? {
1865 latest_key = Some(key);
1866 }
1867 }
1868
1869 Ok(latest_key)
1872}
1873
1874#[derive(Debug, Serialize, Deserialize)]
1876struct GossipRequestIndexedDbObject {
1877 info: String,
1879
1880 request: Vec<u8>,
1882
1883 #[serde(
1892 default,
1893 skip_serializing_if = "std::ops::Not::not",
1894 with = "crate::serializer::foreign::bool"
1895 )]
1896 unsent: bool,
1897}
1898
1899#[derive(Serialize, Deserialize)]
1901struct InboundGroupSessionIndexedDbObject {
1902 pickled_session: MaybeEncrypted,
1905
1906 #[serde(default, skip_serializing_if = "Option::is_none")]
1914 session_id: Option<String>,
1915
1916 #[serde(
1925 default,
1926 skip_serializing_if = "std::ops::Not::not",
1927 with = "crate::serializer::foreign::bool"
1928 )]
1929 needs_backup: bool,
1930
1931 backed_up_to: i32,
1942
1943 #[serde(default, skip_serializing_if = "Option::is_none")]
1949 sender_key: Option<String>,
1950
1951 #[serde(default, skip_serializing_if = "Option::is_none")]
1957 sender_data_type: Option<u8>,
1958}
1959
1960impl InboundGroupSessionIndexedDbObject {
1961 pub async fn from_session(
1964 session: &InboundGroupSession,
1965 serializer: &SafeEncodeSerializer,
1966 ) -> Result<Self, CryptoStoreError> {
1967 let session_id =
1968 serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
1969
1970 let sender_key = serializer.encode_key_as_string(
1971 keys::INBOUND_GROUP_SESSIONS_V3,
1972 session.sender_key().to_base64(),
1973 );
1974
1975 Ok(InboundGroupSessionIndexedDbObject {
1976 pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
1977 session_id: Some(session_id),
1978 needs_backup: !session.backed_up(),
1979 backed_up_to: -1,
1980 sender_key: Some(sender_key),
1981 sender_data_type: Some(session.sender_data_type() as u8),
1982 })
1983 }
1984}
1985
1986#[cfg(test)]
1987mod unit_tests {
1988 use matrix_sdk_crypto::{
1989 olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
1990 types::EventEncryptionAlgorithm,
1991 vodozemac::Ed25519Keypair,
1992 };
1993 use matrix_sdk_store_encryption::EncryptedValueBase64;
1994 use matrix_sdk_test::async_test;
1995 use ruma::{device_id, room_id, user_id};
1996
1997 use super::InboundGroupSessionIndexedDbObject;
1998 use crate::serializer::{MaybeEncrypted, SafeEncodeSerializer};
1999
2000 #[test]
2001 fn needs_backup_is_serialized_as_a_u8_in_json() {
2002 let session_needs_backup = backup_test_session(true);
2003
2004 assert!(serde_json::to_string(&session_needs_backup)
2008 .unwrap()
2009 .contains(r#""needs_backup":1"#),);
2010 }
2011
2012 #[test]
2013 fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
2014 let session_backed_up = backup_test_session(false);
2015
2016 assert!(
2017 !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
2018 "The needs_backup field should be missing!"
2019 );
2020 }
2021
2022 pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
2023 InboundGroupSessionIndexedDbObject {
2024 pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
2025 session_id: None,
2026 needs_backup,
2027 backed_up_to: -1,
2028 sender_key: None,
2029 sender_data_type: None,
2030 }
2031 }
2032
2033 #[async_test]
2034 async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
2035 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2036
2037 let sender_data = SenderData::sender_verified(
2038 user_id!("@test:user"),
2039 device_id!("ABC"),
2040 Ed25519Keypair::new().public_key(),
2041 );
2042
2043 let db_object = sender_data_test_session(sender_key, sender_data).await;
2044 let serialized = serde_json::to_string(&db_object).unwrap();
2045
2046 assert!(
2047 serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
2048 );
2049 assert!(serialized.contains(r#""sender_data_type":5"#));
2050 }
2051
2052 pub async fn sender_data_test_session(
2053 sender_key: Curve25519PublicKey,
2054 sender_data: SenderData,
2055 ) -> InboundGroupSessionIndexedDbObject {
2056 let session = InboundGroupSession::new(
2057 sender_key,
2058 Ed25519Keypair::new().public_key(),
2059 room_id!("!test:localhost"),
2060 &SessionKey::from_base64(
2062 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
2063 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
2064 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
2065 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
2066 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
2067 )
2068 .unwrap(),
2069 sender_data,
2070 EventEncryptionAlgorithm::MegolmV1AesSha2,
2071 None,
2072 false,
2073 )
2074 .unwrap();
2075
2076 InboundGroupSessionIndexedDbObject::from_session(&session, &SafeEncodeSerializer::new(None))
2077 .await
2078 .unwrap()
2079 }
2080}
2081
2082#[cfg(all(test, target_family = "wasm"))]
2083mod wasm_unit_tests {
2084 use std::collections::BTreeMap;
2085
2086 use matrix_sdk_crypto::{
2087 olm::{Curve25519PublicKey, SenderData},
2088 types::{DeviceKeys, Signatures},
2089 };
2090 use matrix_sdk_test::async_test;
2091 use ruma::{device_id, user_id};
2092 use wasm_bindgen::JsValue;
2093
2094 use crate::crypto_store::unit_tests::sender_data_test_session;
2095
2096 fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
2097 assert_eq!(
2098 js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
2099 JsValue::from_f64(expected.into())
2100 );
2101 }
2102
2103 #[async_test]
2104 fn test_needs_backup_is_serialized_as_a_u8_in_js() {
2105 let session_needs_backup = super::unit_tests::backup_test_session(true);
2106
2107 let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
2108
2109 assert!(js_value.is_object());
2110 assert_field_equals(&js_value, "needs_backup", 1);
2111 }
2112
2113 #[async_test]
2114 fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
2115 let session_backed_up = super::unit_tests::backup_test_session(false);
2116
2117 let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
2118
2119 assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
2120 }
2121
2122 #[async_test]
2123 async fn test_sender_key_and_device_type_are_serialized_in_js() {
2124 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
2125
2126 let sender_data = SenderData::device_info(DeviceKeys::new(
2127 user_id!("@test:user").to_owned(),
2128 device_id!("ABC").to_owned(),
2129 vec![],
2130 BTreeMap::new(),
2131 Signatures::new(),
2132 ));
2133 let db_object = sender_data_test_session(sender_key, sender_data).await;
2134
2135 let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
2136
2137 assert!(js_value.is_object());
2138 assert_field_equals(&js_value, "sender_data_type", 2);
2139 assert_eq!(
2140 js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
2141 JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
2142 );
2143 }
2144}
2145
2146#[cfg(all(test, target_family = "wasm"))]
2147mod tests {
2148 use matrix_sdk_crypto::cryptostore_integration_tests;
2149
2150 use super::IndexeddbCryptoStore;
2151
2152 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2153
2154 async fn get_store(
2155 name: &str,
2156 passphrase: Option<&str>,
2157 clear_data: bool,
2158 ) -> IndexeddbCryptoStore {
2159 if clear_data {
2160 IndexeddbCryptoStore::delete_stores(name).unwrap();
2161 }
2162 match passphrase {
2163 Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
2164 .await
2165 .expect("Can't create a passphrase protected store"),
2166 None => IndexeddbCryptoStore::open_with_name(name)
2167 .await
2168 .expect("Can't create store without passphrase"),
2169 }
2170 }
2171
2172 cryptostore_integration_tests!();
2173}
2174
2175#[cfg(all(test, target_family = "wasm"))]
2176mod encrypted_tests {
2177 use matrix_sdk_crypto::{
2178 cryptostore_integration_tests,
2179 olm::Account,
2180 store::{types::PendingChanges, CryptoStore},
2181 vodozemac::base64_encode,
2182 };
2183 use matrix_sdk_test::async_test;
2184 use ruma::{device_id, user_id};
2185
2186 use super::IndexeddbCryptoStore;
2187
2188 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
2189
2190 async fn get_store(
2191 name: &str,
2192 passphrase: Option<&str>,
2193 clear_data: bool,
2194 ) -> IndexeddbCryptoStore {
2195 if clear_data {
2196 IndexeddbCryptoStore::delete_stores(name).unwrap();
2197 }
2198
2199 let pass = passphrase.unwrap_or(name);
2200 IndexeddbCryptoStore::open_with_passphrase(&name, pass)
2201 .await
2202 .expect("Can't create a passphrase protected store")
2203 }
2204 cryptostore_integration_tests!();
2205
2206 #[async_test]
2209 async fn test_migrate_passphrase_to_key() {
2210 let store_name = "test_migrate_passphrase_to_key";
2211 let passdata: [u8; 32] = rand::random();
2212 let b64_passdata = base64_encode(passdata);
2213
2214 IndexeddbCryptoStore::delete_stores(store_name).unwrap();
2216 let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
2217 .await
2218 .expect("Can't create a passphrase-protected store");
2219
2220 store
2221 .save_pending_changes(PendingChanges {
2222 account: Some(Account::with_device_id(
2223 user_id!("@alice:example.org"),
2224 device_id!("ALICEDEVICE"),
2225 )),
2226 })
2227 .await
2228 .expect("Can't save account");
2229
2230 let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
2232 .await
2233 .expect("Can't create a key-protected store");
2234 let loaded_account =
2235 store.load_account().await.expect("Can't load account").expect("Account was not saved");
2236 assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2237 }
2238}