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