1use std::{
16 collections::{BTreeMap, HashMap},
17 sync::{Arc, RwLock},
18};
19
20use async_trait::async_trait;
21use gloo_utils::format::JsValueSerdeExt;
22use hkdf::Hkdf;
23use indexed_db_futures::prelude::*;
24use js_sys::Array;
25use matrix_sdk_crypto::{
26 olm::{
27 Curve25519PublicKey, InboundGroupSession, OlmMessageHash, OutboundGroupSession,
28 PickledInboundGroupSession, PrivateCrossSigningIdentity, SenderDataType, Session,
29 StaticAccountData,
30 },
31 store::{
32 BackupKeys, Changes, CryptoStore, CryptoStoreError, DehydratedDeviceKey, PendingChanges,
33 RoomKeyCounts, RoomSettings,
34 },
35 types::events::room_key_withheld::RoomKeyWithheldEvent,
36 vodozemac::base64_encode,
37 Account, DeviceData, GossipRequest, GossippedSecret, SecretInfo, TrackedUser, UserIdentityData,
38};
39use matrix_sdk_store_encryption::StoreCipher;
40use ruma::{
41 events::secret::request::SecretName, DeviceId, MilliSecondsSinceUnixEpoch, OwnedDeviceId,
42 RoomId, TransactionId, UserId,
43};
44use sha2::Sha256;
45use tokio::sync::Mutex;
46use tracing::{debug, warn};
47use wasm_bindgen::JsValue;
48use web_sys::IdbKeyRange;
49
50use self::indexeddb_serializer::MaybeEncrypted;
51use crate::crypto_store::{
52 indexeddb_serializer::IndexeddbSerializer, migrations::open_and_upgrade_db,
53};
54
55mod indexeddb_serializer;
56mod migrations;
57
58mod keys {
59 pub const CORE: &str = "core";
61
62 pub const SESSION: &str = "session";
63
64 pub const INBOUND_GROUP_SESSIONS_V3: &str = "inbound_group_sessions3";
65 pub const INBOUND_GROUP_SESSIONS_BACKUP_INDEX: &str = "backup";
66 pub const INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX: &str = "backed_up_to";
67 pub const INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX: &str =
68 "inbound_group_session_sender_key_sender_data_type_idx";
69
70 pub const OUTBOUND_GROUP_SESSIONS: &str = "outbound_group_sessions";
71
72 pub const TRACKED_USERS: &str = "tracked_users";
73 pub const OLM_HASHES: &str = "olm_hashes";
74
75 pub const DEVICES: &str = "devices";
76 pub const IDENTITIES: &str = "identities";
77
78 pub const GOSSIP_REQUESTS: &str = "gossip_requests";
79 pub const GOSSIP_REQUESTS_UNSENT_INDEX: &str = "unsent";
80 pub const GOSSIP_REQUESTS_BY_INFO_INDEX: &str = "by_info";
81
82 pub const ROOM_SETTINGS: &str = "room_settings";
83
84 pub const SECRETS_INBOX: &str = "secrets_inbox";
85
86 pub const DIRECT_WITHHELD_INFO: &str = "direct_withheld_info";
87
88 pub const STORE_CIPHER: &str = "store_cipher";
90 pub const ACCOUNT: &str = "account";
91 pub const NEXT_BATCH_TOKEN: &str = "next_batch_token";
92 pub const PRIVATE_IDENTITY: &str = "private_identity";
93
94 pub const BACKUP_KEYS: &str = "backup_keys";
96
97 pub const BACKUP_VERSION_V1: &str = "backup_version_v1";
100
101 pub const RECOVERY_KEY_V1: &str = "recovery_key_v1";
107
108 pub const DEHYDRATION_PICKLE_KEY: &str = "dehydration_pickle_key";
110}
111
112pub struct IndexeddbCryptoStore {
117 static_account: RwLock<Option<StaticAccountData>>,
118 name: String,
119 pub(crate) inner: IdbDatabase,
120
121 serializer: IndexeddbSerializer,
122 save_changes_lock: Arc<Mutex<()>>,
123}
124
125#[cfg(not(tarpaulin_include))]
126impl std::fmt::Debug for IndexeddbCryptoStore {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("IndexeddbCryptoStore").field("name", &self.name).finish()
129 }
130}
131
132#[derive(Debug, thiserror::Error)]
133pub enum IndexeddbCryptoStoreError {
134 #[error(transparent)]
135 Serialization(#[from] serde_json::Error),
136 #[error("DomException {name} ({code}): {message}")]
137 DomException {
138 code: u16,
140 name: String,
142 message: String,
144 },
145 #[error(transparent)]
146 CryptoStoreError(#[from] CryptoStoreError),
147 #[error("The schema version of the crypto store is too new. Existing version: {current_version}; max supported version: {max_supported_version}")]
148 SchemaTooNewError { max_supported_version: u32, current_version: u32 },
149}
150
151impl From<web_sys::DomException> for IndexeddbCryptoStoreError {
152 fn from(frm: web_sys::DomException) -> IndexeddbCryptoStoreError {
153 IndexeddbCryptoStoreError::DomException {
154 name: frm.name(),
155 message: frm.message(),
156 code: frm.code(),
157 }
158 }
159}
160
161impl From<serde_wasm_bindgen::Error> for IndexeddbCryptoStoreError {
162 fn from(e: serde_wasm_bindgen::Error) -> Self {
163 IndexeddbCryptoStoreError::Serialization(serde::de::Error::custom(e.to_string()))
164 }
165}
166
167impl From<IndexeddbCryptoStoreError> for CryptoStoreError {
168 fn from(frm: IndexeddbCryptoStoreError) -> CryptoStoreError {
169 match frm {
170 IndexeddbCryptoStoreError::Serialization(e) => CryptoStoreError::Serialization(e),
171 IndexeddbCryptoStoreError::CryptoStoreError(e) => e,
172 _ => CryptoStoreError::backend(frm),
173 }
174 }
175}
176
177type Result<A, E = IndexeddbCryptoStoreError> = std::result::Result<A, E>;
178
179enum PendingOperation {
181 Put { key: JsValue, value: JsValue },
182 Delete(JsValue),
183}
184
185struct PendingIndexeddbChanges {
191 store_to_key_values: BTreeMap<&'static str, Vec<PendingOperation>>,
194}
195
196struct PendingStoreChanges<'a> {
198 operations: &'a mut Vec<PendingOperation>,
199}
200
201impl PendingStoreChanges<'_> {
202 fn put(&mut self, key: JsValue, value: JsValue) {
203 self.operations.push(PendingOperation::Put { key, value });
204 }
205
206 fn delete(&mut self, key: JsValue) {
207 self.operations.push(PendingOperation::Delete(key));
208 }
209}
210
211impl PendingIndexeddbChanges {
212 fn get(&mut self, store: &'static str) -> PendingStoreChanges<'_> {
213 PendingStoreChanges { operations: self.store_to_key_values.entry(store).or_default() }
214 }
215}
216
217impl PendingIndexeddbChanges {
218 fn new() -> Self {
219 Self { store_to_key_values: BTreeMap::new() }
220 }
221
222 fn touched_stores(&self) -> Vec<&str> {
226 self.store_to_key_values
227 .iter()
228 .filter_map(
229 |(store, pending_operations)| {
230 if !pending_operations.is_empty() {
231 Some(*store)
232 } else {
233 None
234 }
235 },
236 )
237 .collect()
238 }
239
240 fn apply(self, tx: &IdbTransaction<'_>) -> Result<()> {
242 for (store, operations) in self.store_to_key_values {
243 if operations.is_empty() {
244 continue;
245 }
246 let object_store = tx.object_store(store)?;
247 for op in operations {
248 match op {
249 PendingOperation::Put { key, value } => {
250 object_store.put_key_val(&key, &value)?;
251 }
252 PendingOperation::Delete(key) => {
253 object_store.delete(&key)?;
254 }
255 }
256 }
257 }
258 Ok(())
259 }
260}
261
262impl IndexeddbCryptoStore {
263 pub(crate) async fn open_with_store_cipher(
264 prefix: &str,
265 store_cipher: Option<Arc<StoreCipher>>,
266 ) -> Result<Self> {
267 let name = format!("{prefix:0}::matrix-sdk-crypto");
268
269 let serializer = IndexeddbSerializer::new(store_cipher);
270 debug!("IndexedDbCryptoStore: opening main store {name}");
271 let db = open_and_upgrade_db(&name, &serializer).await?;
272
273 Ok(Self {
274 name,
275 inner: db,
276 serializer,
277 static_account: RwLock::new(None),
278 save_changes_lock: Default::default(),
279 })
280 }
281
282 pub async fn open() -> Result<Self> {
284 IndexeddbCryptoStore::open_with_store_cipher("crypto", None).await
285 }
286
287 pub async fn open_with_passphrase(prefix: &str, passphrase: &str) -> Result<Self> {
304 let db = open_meta_db(prefix).await?;
305 let store_cipher = load_store_cipher(&db).await?;
306
307 let store_cipher = match store_cipher {
308 Some(cipher) => {
309 debug!("IndexedDbCryptoStore: decrypting store cipher");
310 StoreCipher::import(passphrase, &cipher)
311 .map_err(|_| CryptoStoreError::UnpicklingError)?
312 }
313 None => {
314 debug!("IndexedDbCryptoStore: encrypting new store cipher");
315 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
316 #[cfg(not(test))]
317 let export = cipher.export(passphrase);
318 #[cfg(test)]
319 let export = cipher._insecure_export_fast_for_testing(passphrase);
320
321 let export = export.map_err(CryptoStoreError::backend)?;
322
323 save_store_cipher(&db, &export).await?;
324 cipher
325 }
326 };
327
328 db.close();
331
332 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
333 }
334
335 pub async fn open_with_key(prefix: &str, key: &[u8; 32]) -> Result<Self> {
351 let mut chacha_key = zeroize::Zeroizing::new([0u8; 32]);
354 const HKDF_INFO: &[u8] = b"CRYPTOSTORE_CIPHER";
355 let hkdf = Hkdf::<Sha256>::new(None, key);
356 hkdf.expand(HKDF_INFO, &mut *chacha_key)
357 .expect("We should be able to generate a 32-byte key");
358
359 let db = open_meta_db(prefix).await?;
360 let store_cipher = load_store_cipher(&db).await?;
361
362 let store_cipher = match store_cipher {
363 Some(cipher) => {
364 debug!("IndexedDbCryptoStore: decrypting store cipher");
365 import_store_cipher_with_key(&chacha_key, key, &cipher, &db).await?
366 }
367 None => {
368 debug!("IndexedDbCryptoStore: encrypting new store cipher");
369 let cipher = StoreCipher::new().map_err(CryptoStoreError::backend)?;
370 let export =
371 cipher.export_with_key(&chacha_key).map_err(CryptoStoreError::backend)?;
372 save_store_cipher(&db, &export).await?;
373 cipher
374 }
375 };
376
377 db.close();
380
381 IndexeddbCryptoStore::open_with_store_cipher(prefix, Some(store_cipher.into())).await
382 }
383
384 pub async fn open_with_name(name: &str) -> Result<Self> {
386 IndexeddbCryptoStore::open_with_store_cipher(name, None).await
387 }
388
389 #[cfg(test)]
391 pub fn delete_stores(prefix: &str) -> Result<()> {
392 IdbDatabase::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto-meta"))?;
393 IdbDatabase::delete_by_name(&format!("{prefix:0}::matrix-sdk-crypto"))?;
394 Ok(())
395 }
396
397 fn get_static_account(&self) -> Option<StaticAccountData> {
398 self.static_account.read().unwrap().clone()
399 }
400
401 async fn serialize_inbound_group_session(
404 &self,
405 session: &InboundGroupSession,
406 ) -> Result<JsValue> {
407 let obj =
408 InboundGroupSessionIndexedDbObject::from_session(session, &self.serializer).await?;
409 Ok(serde_wasm_bindgen::to_value(&obj)?)
410 }
411
412 fn deserialize_inbound_group_session(
415 &self,
416 stored_value: JsValue,
417 ) -> Result<InboundGroupSession> {
418 let idb_object: InboundGroupSessionIndexedDbObject =
419 serde_wasm_bindgen::from_value(stored_value)?;
420 let pickled_session: PickledInboundGroupSession =
421 self.serializer.maybe_decrypt_value(idb_object.pickled_session)?;
422 let session = InboundGroupSession::from_pickle(pickled_session)
423 .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?;
424
425 if idb_object.needs_backup {
429 session.reset_backup_state();
430 } else {
431 session.mark_as_backed_up();
432 }
433
434 Ok(session)
435 }
436
437 fn serialize_gossip_request(&self, gossip_request: &GossipRequest) -> Result<JsValue> {
440 let obj = GossipRequestIndexedDbObject {
441 info: self
443 .serializer
444 .encode_key_as_string(keys::GOSSIP_REQUESTS, gossip_request.info.as_key()),
445
446 request: self.serializer.serialize_value_as_bytes(gossip_request)?,
448
449 unsent: !gossip_request.sent_out,
450 };
451
452 Ok(serde_wasm_bindgen::to_value(&obj)?)
453 }
454
455 fn deserialize_gossip_request(&self, stored_request: JsValue) -> Result<GossipRequest> {
458 let idb_object: GossipRequestIndexedDbObject =
459 serde_wasm_bindgen::from_value(stored_request)?;
460 Ok(self.serializer.deserialize_value_from_bytes(&idb_object.request)?)
461 }
462
463 async fn prepare_for_transaction(&self, changes: &Changes) -> Result<PendingIndexeddbChanges> {
470 let mut indexeddb_changes = PendingIndexeddbChanges::new();
471
472 let private_identity_pickle =
473 if let Some(i) = &changes.private_identity { Some(i.pickle().await) } else { None };
474
475 let decryption_key_pickle = &changes.backup_decryption_key;
476 let backup_version = &changes.backup_version;
477 let dehydration_pickle_key = &changes.dehydrated_device_pickle_key;
478
479 let mut core = indexeddb_changes.get(keys::CORE);
480 if let Some(next_batch) = &changes.next_batch_token {
481 core.put(
482 JsValue::from_str(keys::NEXT_BATCH_TOKEN),
483 self.serializer.serialize_value(next_batch)?,
484 );
485 }
486
487 if let Some(i) = &private_identity_pickle {
488 core.put(
489 JsValue::from_str(keys::PRIVATE_IDENTITY),
490 self.serializer.serialize_value(i)?,
491 );
492 }
493
494 if let Some(i) = &dehydration_pickle_key {
495 core.put(
496 JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY),
497 self.serializer.serialize_value(i)?,
498 );
499 }
500
501 if let Some(a) = &decryption_key_pickle {
502 indexeddb_changes.get(keys::BACKUP_KEYS).put(
503 JsValue::from_str(keys::RECOVERY_KEY_V1),
504 self.serializer.serialize_value(&a)?,
505 );
506 }
507
508 if let Some(a) = &backup_version {
509 indexeddb_changes.get(keys::BACKUP_KEYS).put(
510 JsValue::from_str(keys::BACKUP_VERSION_V1),
511 self.serializer.serialize_value(&a)?,
512 );
513 }
514
515 if !changes.sessions.is_empty() {
516 let mut sessions = indexeddb_changes.get(keys::SESSION);
517
518 for session in &changes.sessions {
519 let sender_key = session.sender_key().to_base64();
520 let session_id = session.session_id();
521
522 let pickle = session.pickle().await;
523 let key = self.serializer.encode_key(keys::SESSION, (&sender_key, session_id));
524
525 sessions.put(key, self.serializer.serialize_value(&pickle)?);
526 }
527 }
528
529 if !changes.inbound_group_sessions.is_empty() {
530 let mut sessions = indexeddb_changes.get(keys::INBOUND_GROUP_SESSIONS_V3);
531
532 for session in &changes.inbound_group_sessions {
533 let room_id = session.room_id();
534 let session_id = session.session_id();
535 let key = self
536 .serializer
537 .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
538 let value = self.serialize_inbound_group_session(session).await?;
539 sessions.put(key, value);
540 }
541 }
542
543 if !changes.outbound_group_sessions.is_empty() {
544 let mut sessions = indexeddb_changes.get(keys::OUTBOUND_GROUP_SESSIONS);
545
546 for session in &changes.outbound_group_sessions {
547 let room_id = session.room_id();
548 let pickle = session.pickle().await;
549 sessions.put(
550 self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id),
551 self.serializer.serialize_value(&pickle)?,
552 );
553 }
554 }
555
556 let device_changes = &changes.devices;
557 let identity_changes = &changes.identities;
558 let olm_hashes = &changes.message_hashes;
559 let key_requests = &changes.key_requests;
560 let withheld_session_info = &changes.withheld_session_info;
561 let room_settings_changes = &changes.room_settings;
562
563 let mut device_store = indexeddb_changes.get(keys::DEVICES);
564
565 for device in device_changes.new.iter().chain(&device_changes.changed) {
566 let key =
567 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
568 let device = self.serializer.serialize_value(&device)?;
569
570 device_store.put(key, device);
571 }
572
573 for device in &device_changes.deleted {
574 let key =
575 self.serializer.encode_key(keys::DEVICES, (device.user_id(), device.device_id()));
576 device_store.delete(key);
577 }
578
579 if !identity_changes.changed.is_empty() || !identity_changes.new.is_empty() {
580 let mut identities = indexeddb_changes.get(keys::IDENTITIES);
581 for identity in identity_changes.changed.iter().chain(&identity_changes.new) {
582 identities.put(
583 self.serializer.encode_key(keys::IDENTITIES, identity.user_id()),
584 self.serializer.serialize_value(&identity)?,
585 );
586 }
587 }
588
589 if !olm_hashes.is_empty() {
590 let mut hashes = indexeddb_changes.get(keys::OLM_HASHES);
591 for hash in olm_hashes {
592 hashes.put(
593 self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)),
594 JsValue::TRUE,
595 );
596 }
597 }
598
599 if !key_requests.is_empty() {
600 let mut gossip_requests = indexeddb_changes.get(keys::GOSSIP_REQUESTS);
601
602 for gossip_request in key_requests {
603 let key_request_id = self
604 .serializer
605 .encode_key(keys::GOSSIP_REQUESTS, gossip_request.request_id.as_str());
606 let key_request_value = self.serialize_gossip_request(gossip_request)?;
607 gossip_requests.put(key_request_id, key_request_value);
608 }
609 }
610
611 if !withheld_session_info.is_empty() {
612 let mut withhelds = indexeddb_changes.get(keys::DIRECT_WITHHELD_INFO);
613
614 for (room_id, data) in withheld_session_info {
615 for (session_id, event) in data {
616 let key = self
617 .serializer
618 .encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, &room_id));
619 withhelds.put(key, self.serializer.serialize_value(&event)?);
620 }
621 }
622 }
623
624 if !room_settings_changes.is_empty() {
625 let mut settings_store = indexeddb_changes.get(keys::ROOM_SETTINGS);
626
627 for (room_id, settings) in room_settings_changes {
628 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
629 let value = self.serializer.serialize_value(&settings)?;
630 settings_store.put(key, value);
631 }
632 }
633
634 if !changes.secrets.is_empty() {
635 let mut secret_store = indexeddb_changes.get(keys::SECRETS_INBOX);
636
637 for secret in &changes.secrets {
638 let key = self.serializer.encode_key(
639 keys::SECRETS_INBOX,
640 (secret.secret_name.as_str(), secret.event.content.request_id.as_str()),
641 );
642 let value = self.serializer.serialize_value(&secret)?;
643
644 secret_store.put(key, value);
645 }
646 }
647
648 Ok(indexeddb_changes)
649 }
650}
651
652#[cfg(target_arch = "wasm32")]
661macro_rules! impl_crypto_store {
662 ( $($body:tt)* ) => {
663 #[async_trait(?Send)]
664 impl CryptoStore for IndexeddbCryptoStore {
665 type Error = IndexeddbCryptoStoreError;
666
667 $($body)*
668 }
669 };
670}
671
672#[cfg(not(target_arch = "wasm32"))]
673macro_rules! impl_crypto_store {
674 ( $($body:tt)* ) => {
675 impl IndexeddbCryptoStore {
676 $($body)*
677 }
678 };
679}
680
681impl_crypto_store! {
682 async fn save_pending_changes(&self, changes: PendingChanges) -> Result<()> {
683 let _guard = self.save_changes_lock.lock().await;
688
689 let stores: Vec<&str> = [
690 (changes.account.is_some() , keys::CORE),
691 ]
692 .iter()
693 .filter_map(|(id, key)| if *id { Some(*key) } else { None })
694 .collect();
695
696 if stores.is_empty() {
697 return Ok(());
699 }
700
701 let tx =
702 self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?;
703
704 let account_pickle = if let Some(account) = changes.account {
705 *self.static_account.write().unwrap() = Some(account.static_data().clone());
706 Some(account.pickle())
707 } else {
708 None
709 };
710
711 if let Some(a) = &account_pickle {
712 tx.object_store(keys::CORE)?
713 .put_key_val(&JsValue::from_str(keys::ACCOUNT), &self.serializer.serialize_value(&a)?)?;
714 }
715
716 tx.await.into_result()?;
717
718 Ok(())
719 }
720
721 async fn save_changes(&self, changes: Changes) -> Result<()> {
722 let _guard = self.save_changes_lock.lock().await;
727
728 let indexeddb_changes = self.prepare_for_transaction(&changes).await?;
729
730 let stores = indexeddb_changes.touched_stores();
731
732 if stores.is_empty() {
733 return Ok(());
735 }
736
737 let tx =
738 self.inner.transaction_on_multi_with_mode(&stores, IdbTransactionMode::Readwrite)?;
739
740 indexeddb_changes.apply(&tx)?;
741
742 tx.await.into_result()?;
743
744 Ok(())
745 }
746
747 async fn save_inbound_group_sessions(
748 &self,
749 sessions: Vec<InboundGroupSession>,
750 backed_up_to_version: Option<&str>,
751 ) -> Result<()> {
752 sessions.iter().for_each(|s| {
754 let backed_up = s.backed_up();
755 if backed_up != backed_up_to_version.is_some() {
756 warn!(
757 backed_up, backed_up_to_version,
758 "Session backed-up flag does not correspond to backup version setting",
759 );
760 }
761 });
762
763 self.save_changes(Changes { inbound_group_sessions: sessions, ..Changes::default() }).await
766 }
767
768 async fn load_tracked_users(&self) -> Result<Vec<TrackedUser>> {
769 let tx = self
770 .inner
771 .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readonly)?;
772 let os = tx.object_store(keys::TRACKED_USERS)?;
773 let user_ids = os.get_all_keys()?.await?;
774
775 let mut users = Vec::new();
776
777 for user_id in user_ids.iter() {
778 let dirty: bool =
779 !matches!(os.get(&user_id)?.await?.map(|v| v.into_serde()), Some(Ok(false)));
780 let Some(Ok(user_id)) = user_id.as_string().map(UserId::parse) else { continue };
781
782 users.push(TrackedUser { user_id, dirty });
783 }
784
785 Ok(users)
786 }
787
788 async fn get_outbound_group_session(
789 &self,
790 room_id: &RoomId,
791 ) -> Result<Option<OutboundGroupSession>> {
792 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
793 if let Some(value) = self
794 .inner
795 .transaction_on_one_with_mode(
796 keys::OUTBOUND_GROUP_SESSIONS,
797 IdbTransactionMode::Readonly,
798 )?
799 .object_store(keys::OUTBOUND_GROUP_SESSIONS)?
800 .get(&self.serializer.encode_key(keys::OUTBOUND_GROUP_SESSIONS, room_id))?
801 .await?
802 {
803 Ok(Some(
804 OutboundGroupSession::from_pickle(
805 account_info.device_id,
806 account_info.identity_keys,
807 self.serializer.deserialize_value(value)?,
808 )
809 .map_err(CryptoStoreError::from)?,
810 ))
811 } else {
812 Ok(None)
813 }
814 }
815
816 async fn get_outgoing_secret_requests(
817 &self,
818 request_id: &TransactionId,
819 ) -> Result<Option<GossipRequest>> {
820 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id.as_str());
821 self
822 .inner
823 .transaction_on_one_with_mode(keys::GOSSIP_REQUESTS, IdbTransactionMode::Readonly)?
824 .object_store(keys::GOSSIP_REQUESTS)?
825 .get_owned(jskey)?
826 .await?
827 .map(|val| self.deserialize_gossip_request(val))
828 .transpose()
829 }
830
831 async fn load_account(&self) -> Result<Option<Account>> {
832 if let Some(pickle) = self
833 .inner
834 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
835 .object_store(keys::CORE)?
836 .get(&JsValue::from_str(keys::ACCOUNT))?
837 .await?
838 {
839 let pickle = self.serializer.deserialize_value(pickle)?;
840
841 let account = Account::from_pickle(pickle).map_err(CryptoStoreError::from)?;
842
843 *self.static_account.write().unwrap() = Some(account.static_data().clone());
844
845 Ok(Some(account))
846 } else {
847 Ok(None)
848 }
849 }
850
851 async fn next_batch_token(&self) -> Result<Option<String>> {
852 if let Some(serialized) = self
853 .inner
854 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
855 .object_store(keys::CORE)?
856 .get(&JsValue::from_str(keys::NEXT_BATCH_TOKEN))?
857 .await?
858 {
859 let token = self.serializer.deserialize_value(serialized)?;
860 Ok(Some(token))
861 } else {
862 Ok(None)
863 }
864 }
865
866 async fn load_identity(&self) -> Result<Option<PrivateCrossSigningIdentity>> {
867 if let Some(pickle) = self
868 .inner
869 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
870 .object_store(keys::CORE)?
871 .get(&JsValue::from_str(keys::PRIVATE_IDENTITY))?
872 .await?
873 {
874 let pickle = self.serializer.deserialize_value(pickle)?;
875
876 Ok(Some(
877 PrivateCrossSigningIdentity::from_pickle(pickle)
878 .map_err(|_| CryptoStoreError::UnpicklingError)?,
879 ))
880 } else {
881 Ok(None)
882 }
883 }
884
885 async fn get_sessions(&self, sender_key: &str) -> Result<Option<Vec<Session>>> {
886 let device_keys = self.get_own_device()
887 .await?
888 .as_device_keys()
889 .clone();
890
891 let range = self.serializer.encode_to_range(keys::SESSION, sender_key)?;
892 let sessions: Vec<Session> = self
893 .inner
894 .transaction_on_one_with_mode(keys::SESSION, IdbTransactionMode::Readonly)?
895 .object_store(keys::SESSION)?
896 .get_all_with_key(&range)?
897 .await?
898 .iter()
899 .filter_map(|f| self.serializer.deserialize_value(f).ok().map(|p| {
900 Session::from_pickle(
901 device_keys.clone(),
902 p,
903 )
904 .map_err(|_| IndexeddbCryptoStoreError::CryptoStoreError(CryptoStoreError::AccountUnset))
905 }))
906 .collect::<Result<Vec<Session>>>()?;
907
908 if sessions.is_empty() {
909 Ok(None)
910 } else {
911 Ok(Some(sessions))
912 }
913 }
914
915 async fn get_inbound_group_session(
916 &self,
917 room_id: &RoomId,
918 session_id: &str,
919 ) -> Result<Option<InboundGroupSession>> {
920 let key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
921 if let Some(value) = self
922 .inner
923 .transaction_on_one_with_mode(
924 keys::INBOUND_GROUP_SESSIONS_V3,
925 IdbTransactionMode::Readonly,
926 )?
927 .object_store(keys::INBOUND_GROUP_SESSIONS_V3)?
928 .get(&key)?
929 .await?
930 {
931 Ok(Some(self.deserialize_inbound_group_session(value)?))
932 } else {
933 Ok(None)
934 }
935 }
936
937 async fn get_inbound_group_sessions(&self) -> Result<Vec<InboundGroupSession>> {
938 const INBOUND_GROUP_SESSIONS_BATCH_SIZE: usize = 1000;
939
940 let transaction = self
941 .inner
942 .transaction_on_one_with_mode(
943 keys::INBOUND_GROUP_SESSIONS_V3,
944 IdbTransactionMode::Readonly,
945 )?;
946
947 let object_store = transaction.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
948
949 fetch_from_object_store_batched(
950 object_store,
951 |value| self.deserialize_inbound_group_session(value),
952 INBOUND_GROUP_SESSIONS_BATCH_SIZE
953 ).await
954 }
955
956 async fn get_inbound_group_sessions_for_device_batch(
957 &self,
958 sender_key: Curve25519PublicKey,
959 sender_data_type: SenderDataType,
960 after_session_id: Option<String>,
961 limit: usize,
962 ) -> Result<Vec<InboundGroupSession>> {
963 let sender_key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, sender_key.to_base64());
964
965 let after_session_id = after_session_id.map(|s| self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, s)).unwrap_or("".into());
967
968 let lower_bound: Array = [sender_key.clone(), (sender_data_type as u8).into(), after_session_id].iter().collect();
969 let upper_bound: Array = [sender_key, ((sender_data_type as u8) + 1).into()].iter().collect();
970 let key = IdbKeyRange::bound_with_lower_open_and_upper_open(
971 &lower_bound,
972 &upper_bound,
973 true, true
974 ).expect("Key was not valid!");
975
976 let tx = self
977 .inner
978 .transaction_on_one_with_mode(
979 keys::INBOUND_GROUP_SESSIONS_V3,
980 IdbTransactionMode::Readonly,
981 )?;
982
983 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
984 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_SENDER_KEY_INDEX)?;
985 let serialized_sessions = idx.get_all_with_key_and_limit_owned(key, limit as u32)?.await?;
986
987 let result = serialized_sessions.into_iter()
989 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
990 Ok(session) => Some(session),
991 Err(e) => {
992 warn!("Failed to deserialize inbound group session: {e}");
993 None
994 }
995 })
996 .collect::<Vec<InboundGroupSession>>();
997
998 Ok(result)
999 }
1000
1001 async fn inbound_group_session_counts(&self, _backup_version: Option<&str>) -> Result<RoomKeyCounts> {
1002 let tx = self
1003 .inner
1004 .transaction_on_one_with_mode(
1005 keys::INBOUND_GROUP_SESSIONS_V3,
1006 IdbTransactionMode::Readonly,
1007 )?;
1008 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1009 let all = store.count()?.await? as usize;
1010 let not_backed_up = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?.count()?.await? as usize;
1011 tx.await.into_result()?;
1012 Ok(RoomKeyCounts { total: all, backed_up: all - not_backed_up })
1013 }
1014
1015 async fn inbound_group_sessions_for_backup(
1016 &self,
1017 _backup_version: &str,
1018 limit: usize,
1019 ) -> Result<Vec<InboundGroupSession>> {
1020 let tx = self
1021 .inner
1022 .transaction_on_one_with_mode(
1023 keys::INBOUND_GROUP_SESSIONS_V3,
1024 IdbTransactionMode::Readonly,
1025 )?;
1026
1027
1028 let store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1029 let idx = store.index(keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX)?;
1030
1031 let Some(cursor) = idx.open_cursor()?.await? else {
1035 return Ok(vec![]);
1036 };
1037
1038 let mut serialized_sessions = Vec::with_capacity(limit);
1039 for _ in 0..limit {
1040 serialized_sessions.push(cursor.value());
1041 if !cursor.continue_cursor()?.await? {
1042 break;
1043 }
1044 }
1045
1046 tx.await.into_result()?;
1047
1048 let result = serialized_sessions.into_iter()
1050 .filter_map(|v| match self.deserialize_inbound_group_session(v) {
1051 Ok(session) => Some(session),
1052 Err(e) => {
1053 warn!("Failed to deserialize inbound group session: {e}");
1054 None
1055 }
1056 })
1057 .collect::<Vec<InboundGroupSession>>();
1058
1059 Ok(result)
1060 }
1061
1062 async fn mark_inbound_group_sessions_as_backed_up(&self,
1063 _backup_version: &str,
1064 room_and_session_ids: &[(&RoomId, &str)]
1065 ) -> Result<()> {
1066 let tx = self
1067 .inner
1068 .transaction_on_one_with_mode(
1069 keys::INBOUND_GROUP_SESSIONS_V3,
1070 IdbTransactionMode::Readwrite,
1071 )?;
1072
1073 let object_store = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?;
1074
1075 for (room_id, session_id) in room_and_session_ids {
1076 let key = self.serializer.encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (room_id, session_id));
1077 if let Some(idb_object_js) = object_store.get(&key)?.await? {
1078 let mut idb_object: InboundGroupSessionIndexedDbObject = serde_wasm_bindgen::from_value(idb_object_js)?;
1079 idb_object.needs_backup = false;
1080 object_store.put_key_val(&key, &serde_wasm_bindgen::to_value(&idb_object)?)?;
1081 } else {
1082 warn!("Could not find inbound group session to mark it as backed up. key={:?}", key);
1083 }
1084 }
1085
1086 Ok(tx.await.into_result()?)
1087 }
1088
1089 async fn reset_backup_state(&self) -> Result<()> {
1090 let tx = self
1091 .inner
1092 .transaction_on_one_with_mode(
1093 keys::INBOUND_GROUP_SESSIONS_V3,
1094 IdbTransactionMode::Readwrite,
1095 )?;
1096
1097 if let Some(cursor) = tx.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?.open_cursor()?.await? {
1098 loop {
1099 let mut idb_object: InboundGroupSessionIndexedDbObject = serde_wasm_bindgen::from_value(cursor.value())?;
1100 if !idb_object.needs_backup {
1101 idb_object.needs_backup = true;
1102 let idb_object = serde_wasm_bindgen::to_value(&idb_object)?;
1106 cursor.update(&idb_object)?.await?;
1107 }
1108
1109 if !cursor.continue_cursor()?.await? {
1110 break;
1111 }
1112 }
1113 }
1114
1115 Ok(tx.await.into_result()?)
1116 }
1117
1118 async fn save_tracked_users(&self, users: &[(&UserId, bool)]) -> Result<()> {
1119 let tx = self
1120 .inner
1121 .transaction_on_one_with_mode(keys::TRACKED_USERS, IdbTransactionMode::Readwrite)?;
1122 let os = tx.object_store(keys::TRACKED_USERS)?;
1123
1124 for (user, dirty) in users {
1125 os.put_key_val(&JsValue::from_str(user.as_str()), &JsValue::from(*dirty))?;
1126 }
1127
1128 tx.await.into_result()?;
1129 Ok(())
1130 }
1131
1132 async fn get_device(
1133 &self,
1134 user_id: &UserId,
1135 device_id: &DeviceId,
1136 ) -> Result<Option<DeviceData>> {
1137 let key = self.serializer.encode_key(keys::DEVICES, (user_id, device_id));
1138 self
1139 .inner
1140 .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
1141 .object_store(keys::DEVICES)?
1142 .get(&key)?
1143 .await?
1144 .map(|i| self.serializer.deserialize_value(i))
1145 .transpose()
1146 }
1147
1148 async fn get_user_devices(
1149 &self,
1150 user_id: &UserId,
1151 ) -> Result<HashMap<OwnedDeviceId, DeviceData>> {
1152 let range = self.serializer.encode_to_range(keys::DEVICES, user_id)?;
1153 Ok(self
1154 .inner
1155 .transaction_on_one_with_mode(keys::DEVICES, IdbTransactionMode::Readonly)?
1156 .object_store(keys::DEVICES)?
1157 .get_all_with_key(&range)?
1158 .await?
1159 .iter()
1160 .filter_map(|d| {
1161 let d: DeviceData = self.serializer.deserialize_value(d).ok()?;
1162 Some((d.device_id().to_owned(), d))
1163 })
1164 .collect::<HashMap<_, _>>())
1165 }
1166
1167 async fn get_own_device(&self) -> Result<DeviceData> {
1168 let account_info = self.get_static_account().ok_or(CryptoStoreError::AccountUnset)?;
1169 Ok(self.get_device(&account_info.user_id, &account_info.device_id)
1170 .await?
1171 .unwrap())
1172 }
1173
1174 async fn get_user_identity(&self, user_id: &UserId) -> Result<Option<UserIdentityData>> {
1175 self
1176 .inner
1177 .transaction_on_one_with_mode(keys::IDENTITIES, IdbTransactionMode::Readonly)?
1178 .object_store(keys::IDENTITIES)?
1179 .get(&self.serializer.encode_key(keys::IDENTITIES, user_id))?
1180 .await?
1181 .map(|i| self.serializer.deserialize_value(i))
1182 .transpose()
1183 }
1184
1185 async fn is_message_known(&self, hash: &OlmMessageHash) -> Result<bool> {
1186 Ok(self
1187 .inner
1188 .transaction_on_one_with_mode(keys::OLM_HASHES, IdbTransactionMode::Readonly)?
1189 .object_store(keys::OLM_HASHES)?
1190 .get(&self.serializer.encode_key(keys::OLM_HASHES, (&hash.sender_key, &hash.hash)))?
1191 .await?
1192 .is_some())
1193 }
1194
1195 async fn get_secrets_from_inbox(
1196 &self,
1197 secret_name: &SecretName,
1198 ) -> Result<Vec<GossippedSecret>> {
1199 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str())?;
1200
1201 self
1202 .inner
1203 .transaction_on_one_with_mode(keys::SECRETS_INBOX, IdbTransactionMode::Readonly)?
1204 .object_store(keys::SECRETS_INBOX)?
1205 .get_all_with_key(&range)?
1206 .await?
1207 .iter()
1208 .map(|d| {
1209 let secret = self.serializer.deserialize_value(d)?;
1210 Ok(secret)
1211 }).collect()
1212 }
1213
1214 #[allow(clippy::unused_async)] async fn delete_secrets_from_inbox(
1216 &self,
1217 secret_name: &SecretName,
1218 ) -> Result<()> {
1219 let range = self.serializer.encode_to_range(keys::SECRETS_INBOX, secret_name.as_str())?;
1220
1221 self
1222 .inner
1223 .transaction_on_one_with_mode(keys::SECRETS_INBOX, IdbTransactionMode::Readwrite)?
1224 .object_store(keys::SECRETS_INBOX)?
1225 .delete(&range)?;
1226
1227 Ok(())
1228 }
1229
1230 async fn get_secret_request_by_info(
1231 &self,
1232 key_info: &SecretInfo,
1233 ) -> Result<Option<GossipRequest>> {
1234 let key = self.serializer.encode_key(keys::GOSSIP_REQUESTS, key_info.as_key());
1235
1236 let val = self
1237 .inner
1238 .transaction_on_one_with_mode(
1239 keys::GOSSIP_REQUESTS,
1240 IdbTransactionMode::Readonly,
1241 )?
1242 .object_store(keys::GOSSIP_REQUESTS)?
1243 .index(keys::GOSSIP_REQUESTS_BY_INFO_INDEX)?
1244 .get_owned(key)?
1245 .await?;
1246
1247 if let Some(val) = val {
1248 let deser = self.deserialize_gossip_request(val)?;
1249 Ok(Some(deser))
1250 } else {
1251 Ok(None)
1252 }
1253 }
1254
1255 async fn get_unsent_secret_requests(&self) -> Result<Vec<GossipRequest>> {
1256 let results = self
1257 .inner
1258 .transaction_on_one_with_mode(
1259 keys::GOSSIP_REQUESTS,
1260 IdbTransactionMode::Readonly,
1261 )?
1262 .object_store(keys::GOSSIP_REQUESTS)?
1263 .index(keys::GOSSIP_REQUESTS_UNSENT_INDEX)?
1264 .get_all()?
1265 .await?
1266 .iter()
1267 .filter_map(|val| self.deserialize_gossip_request(val).ok())
1268 .collect();
1269
1270 Ok(results)
1271 }
1272
1273 async fn delete_outgoing_secret_requests(&self, request_id: &TransactionId) -> Result<()> {
1274 let jskey = self.serializer.encode_key(keys::GOSSIP_REQUESTS, request_id);
1275 let tx = self.inner.transaction_on_one_with_mode(keys::GOSSIP_REQUESTS, IdbTransactionMode::Readwrite)?;
1276 tx.object_store(keys::GOSSIP_REQUESTS)?.delete_owned(jskey)?;
1277 tx.await.into_result().map_err(|e| e.into())
1278 }
1279
1280 async fn load_backup_keys(&self) -> Result<BackupKeys> {
1281 let key = {
1282 let tx = self
1283 .inner
1284 .transaction_on_one_with_mode(keys::BACKUP_KEYS, IdbTransactionMode::Readonly)?;
1285 let store = tx.object_store(keys::BACKUP_KEYS)?;
1286
1287 let backup_version = store
1288 .get(&JsValue::from_str(keys::BACKUP_VERSION_V1))?
1289 .await?
1290 .map(|i| self.serializer.deserialize_value(i))
1291 .transpose()?;
1292
1293 let decryption_key = store
1294 .get(&JsValue::from_str(keys::RECOVERY_KEY_V1))?
1295 .await?
1296 .map(|i| self.serializer.deserialize_value(i))
1297 .transpose()?;
1298
1299 BackupKeys { backup_version, decryption_key }
1300 };
1301
1302 Ok(key)
1303 }
1304
1305
1306 async fn load_dehydrated_device_pickle_key(&self) -> Result<Option<DehydratedDeviceKey>> {
1307 if let Some(pickle) = self
1308 .inner
1309 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
1310 .object_store(keys::CORE)?
1311 .get(&JsValue::from_str(keys::DEHYDRATION_PICKLE_KEY))?
1312 .await?
1313 {
1314 let pickle: DehydratedDeviceKey = self.serializer.deserialize_value(pickle)?;
1315
1316 Ok(Some(pickle))
1317 } else {
1318 Ok(None)
1319 }
1320 }
1321
1322 async fn delete_dehydrated_device_pickle_key(&self) -> Result<()> {
1323 self.remove_custom_value(keys::DEHYDRATION_PICKLE_KEY).await?;
1324 Ok(())
1325 }
1326
1327 async fn get_withheld_info(
1328 &self,
1329 room_id: &RoomId,
1330 session_id: &str,
1331 ) -> Result<Option<RoomKeyWithheldEvent>> {
1332 let key = self.serializer.encode_key(keys::DIRECT_WITHHELD_INFO, (session_id, room_id));
1333 if let Some(pickle) = self
1334 .inner
1335 .transaction_on_one_with_mode(
1336 keys::DIRECT_WITHHELD_INFO,
1337 IdbTransactionMode::Readonly,
1338 )?
1339 .object_store(keys::DIRECT_WITHHELD_INFO)?
1340 .get(&key)?
1341 .await?
1342 {
1343 let info = self.serializer.deserialize_value(pickle)?;
1344 Ok(Some(info))
1345 } else {
1346 Ok(None)
1347 }
1348 }
1349
1350 async fn get_room_settings(&self, room_id: &RoomId) -> Result<Option<RoomSettings>> {
1351 let key = self.serializer.encode_key(keys::ROOM_SETTINGS, room_id);
1352 self
1353 .inner
1354 .transaction_on_one_with_mode(keys::ROOM_SETTINGS, IdbTransactionMode::Readonly)?
1355 .object_store(keys::ROOM_SETTINGS)?
1356 .get(&key)?
1357 .await?
1358 .map(|v| self.serializer.deserialize_value(v))
1359 .transpose()
1360 }
1361
1362 async fn get_custom_value(&self, key: &str) -> Result<Option<Vec<u8>>> {
1363 self
1364 .inner
1365 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readonly)?
1366 .object_store(keys::CORE)?
1367 .get(&JsValue::from_str(key))?
1368 .await?
1369 .map(|v| self.serializer.deserialize_value(v))
1370 .transpose()
1371 }
1372
1373 #[allow(clippy::unused_async)] async fn set_custom_value(&self, key: &str, value: Vec<u8>) -> Result<()> {
1375 self
1376 .inner
1377 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?
1378 .object_store(keys::CORE)?
1379 .put_key_val(&JsValue::from_str(key), &self.serializer.serialize_value(&value)?)?;
1380 Ok(())
1381 }
1382
1383 #[allow(clippy::unused_async)] async fn remove_custom_value(&self, key: &str) -> Result<()> {
1385 self
1386 .inner
1387 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?
1388 .object_store(keys::CORE)?
1389 .delete(&JsValue::from_str(key))?;
1390 Ok(())
1391 }
1392
1393 async fn try_take_leased_lock(
1394 &self,
1395 lease_duration_ms: u32,
1396 key: &str,
1397 holder: &str,
1398 ) -> Result<bool> {
1399 let key = JsValue::from_str(key);
1401 let txn = self
1402 .inner
1403 .transaction_on_one_with_mode(keys::CORE, IdbTransactionMode::Readwrite)?;
1404 let object_store = txn
1405 .object_store(keys::CORE)?;
1406
1407 #[derive(serde::Deserialize, serde::Serialize)]
1408 struct Lease {
1409 holder: String,
1410 expiration_ts: u64,
1411 }
1412
1413 let now_ts: u64 = MilliSecondsSinceUnixEpoch::now().get().into();
1414 let expiration_ts = now_ts + lease_duration_ms as u64;
1415
1416 let prev = object_store.get(&key)?.await?;
1417 match prev {
1418 Some(prev) => {
1419 let lease: Lease = self.serializer.deserialize_value(prev)?;
1420 if lease.holder == holder || lease.expiration_ts < now_ts {
1421 object_store.put_key_val(&key, &self.serializer.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?)?;
1422 Ok(true)
1423 } else {
1424 Ok(false)
1425 }
1426 }
1427 None => {
1428 object_store.put_key_val(&key, &self.serializer.serialize_value(&Lease { holder: holder.to_owned(), expiration_ts })?)?;
1429 Ok(true)
1430 }
1431 }
1432 }
1433}
1434
1435impl Drop for IndexeddbCryptoStore {
1436 fn drop(&mut self) {
1437 self.inner.close();
1440 }
1441}
1442
1443async fn open_meta_db(prefix: &str) -> Result<IdbDatabase, IndexeddbCryptoStoreError> {
1447 let name = format!("{prefix:0}::matrix-sdk-crypto-meta");
1448
1449 debug!("IndexedDbCryptoStore: Opening meta-store {name}");
1450 let mut db_req: OpenDbRequest = IdbDatabase::open_u32(&name, 1)?;
1451 db_req.set_on_upgrade_needed(Some(|evt: &IdbVersionChangeEvent| -> Result<(), JsValue> {
1452 let old_version = evt.old_version() as u32;
1453 if old_version < 1 {
1454 let db = evt.db();
1456
1457 db.create_object_store("matrix-sdk-crypto")?;
1458 }
1459 Ok(())
1460 }));
1461
1462 Ok(db_req.await?)
1463}
1464
1465async fn load_store_cipher(
1475 meta_db: &IdbDatabase,
1476) -> Result<Option<Vec<u8>>, IndexeddbCryptoStoreError> {
1477 let tx: IdbTransaction<'_> =
1478 meta_db.transaction_on_one_with_mode("matrix-sdk-crypto", IdbTransactionMode::Readonly)?;
1479 let ob = tx.object_store("matrix-sdk-crypto")?;
1480
1481 let store_cipher: Option<Vec<u8>> = ob
1482 .get(&JsValue::from_str(keys::STORE_CIPHER))?
1483 .await?
1484 .map(|k| k.into_serde())
1485 .transpose()?;
1486 Ok(store_cipher)
1487}
1488
1489async fn save_store_cipher(
1496 db: &IdbDatabase,
1497 export: &Vec<u8>,
1498) -> Result<(), IndexeddbCryptoStoreError> {
1499 let tx: IdbTransaction<'_> =
1500 db.transaction_on_one_with_mode("matrix-sdk-crypto", IdbTransactionMode::Readwrite)?;
1501 let ob = tx.object_store("matrix-sdk-crypto")?;
1502
1503 ob.put_key_val(&JsValue::from_str(keys::STORE_CIPHER), &JsValue::from_serde(&export)?)?;
1504 tx.await.into_result()?;
1505 Ok(())
1506}
1507
1508async fn import_store_cipher_with_key(
1522 chacha_key: &[u8; 32],
1523 original_key: &[u8],
1524 serialised_cipher: &[u8],
1525 db: &IdbDatabase,
1526) -> Result<StoreCipher, IndexeddbCryptoStoreError> {
1527 let cipher = match StoreCipher::import_with_key(chacha_key, serialised_cipher) {
1528 Ok(cipher) => cipher,
1529 Err(matrix_sdk_store_encryption::Error::KdfMismatch) => {
1530 let cipher = StoreCipher::import(&base64_encode(original_key), serialised_cipher)
1535 .map_err(|_| CryptoStoreError::UnpicklingError)?;
1536
1537 debug!("IndexedDbCryptoStore: Migrating passphrase-encrypted store cipher to key-encryption");
1541
1542 let export = cipher.export_with_key(chacha_key).map_err(CryptoStoreError::backend)?;
1543 save_store_cipher(db, &export).await?;
1544 cipher
1545 }
1546 Err(_) => Err(CryptoStoreError::UnpicklingError)?,
1547 };
1548 Ok(cipher)
1549}
1550
1551async fn fetch_from_object_store_batched<R, F>(
1555 object_store: IdbObjectStore<'_>,
1556 f: F,
1557 batch_size: usize,
1558) -> Result<Vec<R>>
1559where
1560 F: Fn(JsValue) -> Result<R>,
1561{
1562 let mut result = Vec::new();
1563 let mut batch_n = 0;
1564
1565 let mut latest_key: JsValue = "".into();
1567
1568 loop {
1569 debug!("Fetching Indexed DB records starting from {}", batch_n * batch_size);
1570
1571 let after_latest_key =
1579 IdbKeyRange::lower_bound_with_open(&latest_key, true).expect("Key was not valid!");
1580 let cursor = object_store.open_cursor_with_range(&after_latest_key)?.await?;
1581
1582 let next_key = fetch_batch(cursor, batch_size, &f, &mut result).await?;
1584 if let Some(next_key) = next_key {
1585 latest_key = next_key;
1586 } else {
1587 break;
1588 }
1589
1590 batch_n += 1;
1591 }
1592
1593 Ok(result)
1594}
1595
1596async fn fetch_batch<R, F, Q>(
1600 cursor: Option<IdbCursorWithValue<'_, Q>>,
1601 batch_size: usize,
1602 f: &F,
1603 result: &mut Vec<R>,
1604) -> Result<Option<JsValue>>
1605where
1606 F: Fn(JsValue) -> Result<R>,
1607 Q: IdbQuerySource,
1608{
1609 let Some(cursor) = cursor else {
1610 return Ok(None);
1612 };
1613
1614 let mut latest_key = None;
1615
1616 for _ in 0..batch_size {
1617 let processed = f(cursor.value());
1619 if let Ok(processed) = processed {
1620 result.push(processed);
1621 }
1622 if let Some(key) = cursor.key() {
1627 latest_key = Some(key);
1628 }
1629
1630 let more_records = cursor.continue_cursor()?.await?;
1632 if !more_records {
1633 return Ok(None);
1634 }
1635 }
1636
1637 Ok(latest_key)
1640}
1641
1642#[derive(Debug, serde::Serialize, serde::Deserialize)]
1644struct GossipRequestIndexedDbObject {
1645 info: String,
1647
1648 request: Vec<u8>,
1650
1651 #[serde(
1660 default,
1661 skip_serializing_if = "std::ops::Not::not",
1662 with = "crate::serialize_bool_for_indexeddb"
1663 )]
1664 unsent: bool,
1665}
1666
1667#[derive(serde::Serialize, serde::Deserialize)]
1669struct InboundGroupSessionIndexedDbObject {
1670 pickled_session: MaybeEncrypted,
1673
1674 #[serde(default, skip_serializing_if = "Option::is_none")]
1682 session_id: Option<String>,
1683
1684 #[serde(
1693 default,
1694 skip_serializing_if = "std::ops::Not::not",
1695 with = "crate::serialize_bool_for_indexeddb"
1696 )]
1697 needs_backup: bool,
1698
1699 backed_up_to: i32,
1710
1711 #[serde(default, skip_serializing_if = "Option::is_none")]
1717 sender_key: Option<String>,
1718
1719 #[serde(default, skip_serializing_if = "Option::is_none")]
1725 sender_data_type: Option<u8>,
1726}
1727
1728impl InboundGroupSessionIndexedDbObject {
1729 pub async fn from_session(
1732 session: &InboundGroupSession,
1733 serializer: &IndexeddbSerializer,
1734 ) -> Result<Self, CryptoStoreError> {
1735 let session_id =
1736 serializer.encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id());
1737
1738 let sender_key = serializer.encode_key_as_string(
1739 keys::INBOUND_GROUP_SESSIONS_V3,
1740 session.sender_key().to_base64(),
1741 );
1742
1743 Ok(InboundGroupSessionIndexedDbObject {
1744 pickled_session: serializer.maybe_encrypt_value(session.pickle().await)?,
1745 session_id: Some(session_id),
1746 needs_backup: !session.backed_up(),
1747 backed_up_to: -1,
1748 sender_key: Some(sender_key),
1749 sender_data_type: Some(session.sender_data_type() as u8),
1750 })
1751 }
1752}
1753
1754#[cfg(test)]
1755mod unit_tests {
1756 use matrix_sdk_crypto::{
1757 olm::{Curve25519PublicKey, InboundGroupSession, SenderData, SessionKey},
1758 types::EventEncryptionAlgorithm,
1759 vodozemac::Ed25519Keypair,
1760 };
1761 use matrix_sdk_store_encryption::EncryptedValueBase64;
1762 use matrix_sdk_test::async_test;
1763 use ruma::{device_id, room_id, user_id};
1764
1765 use super::InboundGroupSessionIndexedDbObject;
1766 use crate::crypto_store::indexeddb_serializer::{IndexeddbSerializer, MaybeEncrypted};
1767
1768 #[test]
1769 fn needs_backup_is_serialized_as_a_u8_in_json() {
1770 let session_needs_backup = backup_test_session(true);
1771
1772 assert!(serde_json::to_string(&session_needs_backup)
1776 .unwrap()
1777 .contains(r#""needs_backup":1"#),);
1778 }
1779
1780 #[test]
1781 fn doesnt_need_backup_is_serialized_with_missing_field_in_json() {
1782 let session_backed_up = backup_test_session(false);
1783
1784 assert!(
1785 !serde_json::to_string(&session_backed_up).unwrap().contains("needs_backup"),
1786 "The needs_backup field should be missing!"
1787 );
1788 }
1789
1790 pub fn backup_test_session(needs_backup: bool) -> InboundGroupSessionIndexedDbObject {
1791 InboundGroupSessionIndexedDbObject {
1792 pickled_session: MaybeEncrypted::Encrypted(EncryptedValueBase64::new(1, "", "")),
1793 session_id: None,
1794 needs_backup,
1795 backed_up_to: -1,
1796 sender_key: None,
1797 sender_data_type: None,
1798 }
1799 }
1800
1801 #[async_test]
1802 async fn test_sender_key_and_sender_data_type_are_serialized_in_json() {
1803 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1804
1805 let sender_data = SenderData::sender_verified(
1806 user_id!("@test:user"),
1807 device_id!("ABC"),
1808 Ed25519Keypair::new().public_key(),
1809 );
1810
1811 let db_object = sender_data_test_session(sender_key, sender_data).await;
1812 let serialized = serde_json::to_string(&db_object).unwrap();
1813
1814 assert!(
1815 serialized.contains(r#""sender_key":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA""#)
1816 );
1817 assert!(serialized.contains(r#""sender_data_type":5"#));
1818 }
1819
1820 pub async fn sender_data_test_session(
1821 sender_key: Curve25519PublicKey,
1822 sender_data: SenderData,
1823 ) -> InboundGroupSessionIndexedDbObject {
1824 let session = InboundGroupSession::new(
1825 sender_key,
1826 Ed25519Keypair::new().public_key(),
1827 room_id!("!test:localhost"),
1828 &SessionKey::from_base64(
1830 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
1831 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
1832 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
1833 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
1834 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
1835 )
1836 .unwrap(),
1837 sender_data,
1838 EventEncryptionAlgorithm::MegolmV1AesSha2,
1839 None,
1840 )
1841 .unwrap();
1842
1843 InboundGroupSessionIndexedDbObject::from_session(&session, &IndexeddbSerializer::new(None))
1844 .await
1845 .unwrap()
1846 }
1847}
1848
1849#[cfg(all(test, target_arch = "wasm32"))]
1850mod wasm_unit_tests {
1851 use std::collections::BTreeMap;
1852
1853 use matrix_sdk_crypto::{
1854 olm::{Curve25519PublicKey, SenderData},
1855 types::{DeviceKeys, Signatures},
1856 };
1857 use matrix_sdk_test::async_test;
1858 use ruma::{device_id, user_id};
1859 use wasm_bindgen::JsValue;
1860
1861 use crate::crypto_store::unit_tests::sender_data_test_session;
1862
1863 fn assert_field_equals(js_value: &JsValue, field: &str, expected: u32) {
1864 assert_eq!(
1865 js_sys::Reflect::get(&js_value, &field.into()).unwrap(),
1866 JsValue::from_f64(expected.into())
1867 );
1868 }
1869
1870 #[async_test]
1871 fn test_needs_backup_is_serialized_as_a_u8_in_js() {
1872 let session_needs_backup = super::unit_tests::backup_test_session(true);
1873
1874 let js_value = serde_wasm_bindgen::to_value(&session_needs_backup).unwrap();
1875
1876 assert!(js_value.is_object());
1877 assert_field_equals(&js_value, "needs_backup", 1);
1878 }
1879
1880 #[async_test]
1881 fn test_doesnt_need_backup_is_serialized_with_missing_field_in_js() {
1882 let session_backed_up = super::unit_tests::backup_test_session(false);
1883
1884 let js_value = serde_wasm_bindgen::to_value(&session_backed_up).unwrap();
1885
1886 assert!(!js_sys::Reflect::has(&js_value, &"needs_backup".into()).unwrap());
1887 }
1888
1889 #[async_test]
1890 async fn test_sender_key_and_device_type_are_serialized_in_js() {
1891 let sender_key = Curve25519PublicKey::from_bytes([0; 32]);
1892
1893 let sender_data = SenderData::device_info(DeviceKeys::new(
1894 user_id!("@test:user").to_owned(),
1895 device_id!("ABC").to_owned(),
1896 vec![],
1897 BTreeMap::new(),
1898 Signatures::new(),
1899 ));
1900 let db_object = sender_data_test_session(sender_key, sender_data).await;
1901
1902 let js_value = serde_wasm_bindgen::to_value(&db_object).unwrap();
1903
1904 assert!(js_value.is_object());
1905 assert_field_equals(&js_value, "sender_data_type", 2);
1906 assert_eq!(
1907 js_sys::Reflect::get(&js_value, &"sender_key".into()).unwrap(),
1908 JsValue::from_str("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
1909 );
1910 }
1911}
1912
1913#[cfg(all(test, target_arch = "wasm32"))]
1914mod tests {
1915 use matrix_sdk_crypto::cryptostore_integration_tests;
1916
1917 use super::IndexeddbCryptoStore;
1918
1919 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1920
1921 async fn get_store(
1922 name: &str,
1923 passphrase: Option<&str>,
1924 clear_data: bool,
1925 ) -> IndexeddbCryptoStore {
1926 if clear_data {
1927 IndexeddbCryptoStore::delete_stores(name).unwrap();
1928 }
1929 match passphrase {
1930 Some(pass) => IndexeddbCryptoStore::open_with_passphrase(name, pass)
1931 .await
1932 .expect("Can't create a passphrase protected store"),
1933 None => IndexeddbCryptoStore::open_with_name(name)
1934 .await
1935 .expect("Can't create store without passphrase"),
1936 }
1937 }
1938
1939 cryptostore_integration_tests!();
1940}
1941
1942#[cfg(all(test, target_arch = "wasm32"))]
1943mod encrypted_tests {
1944 use matrix_sdk_crypto::{
1945 cryptostore_integration_tests,
1946 olm::Account,
1947 store::{CryptoStore, PendingChanges},
1948 vodozemac::base64_encode,
1949 };
1950 use matrix_sdk_test::async_test;
1951 use ruma::{device_id, user_id};
1952
1953 use super::IndexeddbCryptoStore;
1954
1955 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
1956
1957 async fn get_store(
1958 name: &str,
1959 passphrase: Option<&str>,
1960 clear_data: bool,
1961 ) -> IndexeddbCryptoStore {
1962 if clear_data {
1963 IndexeddbCryptoStore::delete_stores(name).unwrap();
1964 }
1965
1966 let pass = passphrase.unwrap_or(name);
1967 IndexeddbCryptoStore::open_with_passphrase(&name, pass)
1968 .await
1969 .expect("Can't create a passphrase protected store")
1970 }
1971 cryptostore_integration_tests!();
1972
1973 #[async_test]
1976 async fn test_migrate_passphrase_to_key() {
1977 let store_name = "test_migrate_passphrase_to_key";
1978 let passdata: [u8; 32] = rand::random();
1979 let b64_passdata = base64_encode(passdata);
1980
1981 IndexeddbCryptoStore::delete_stores(store_name).unwrap();
1983 let store = IndexeddbCryptoStore::open_with_passphrase(&store_name, &b64_passdata)
1984 .await
1985 .expect("Can't create a passphrase-protected store");
1986
1987 store
1988 .save_pending_changes(PendingChanges {
1989 account: Some(Account::with_device_id(
1990 user_id!("@alice:example.org"),
1991 device_id!("ALICEDEVICE"),
1992 )),
1993 })
1994 .await
1995 .expect("Can't save account");
1996
1997 let store = IndexeddbCryptoStore::open_with_key(&store_name, &passdata)
1999 .await
2000 .expect("Can't create a key-protected store");
2001 let loaded_account =
2002 store.load_account().await.expect("Can't load account").expect("Account was not saved");
2003 assert_eq!(loaded_account.user_id, user_id!("@alice:example.org"));
2004 }
2005}