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