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