1use std::ops::Deref;
16
17use indexed_db_futures::{
18 database::Database,
19 error::{Error, OpenDbError},
20 index::Index,
21 internals::SystemRepr,
22 object_store::ObjectStore,
23 prelude::*,
24 transaction::Transaction,
25};
26use tracing::info;
27
28use crate::{IndexeddbCryptoStoreError, crypto_store::Result, serializer::SafeEncodeSerializer};
29
30mod old_keys;
31mod v0_to_v5;
32mod v101_to_v102;
33mod v102_to_v103;
34mod v103_to_v104;
35mod v104_to_v105;
36mod v105_to_v107;
37mod v10_to_v11;
38mod v11_to_v12;
39mod v12_to_v13;
40mod v13_to_v14;
41mod v14_to_v101;
42mod v5_to_v7;
43mod v7;
44mod v7_to_v8;
45mod v8_to_v10;
46
47struct MigrationDb {
48 db: Database,
49 next_version: u32,
50}
51
52impl MigrationDb {
53 async fn new(name: &str, next_version: u32) -> Result<Self> {
57 info!("IndexeddbCryptoStore migrate data before v{next_version} starting");
58 Ok(Self { db: Database::open(name).await?, next_version })
59 }
60}
61
62impl Deref for MigrationDb {
63 type Target = Database;
64
65 fn deref(&self) -> &Self::Target {
66 &self.db
67 }
68}
69
70impl Drop for MigrationDb {
71 fn drop(&mut self) {
72 let version = self.next_version;
73 info!("IndexeddbCryptoStore migrate data before v{version} finished");
74 self.db.as_sys().close();
75 }
76}
77
78const MAX_SUPPORTED_SCHEMA_VERSION: u32 = 199;
111
112pub async fn open_and_upgrade_db(
115 name: &str,
116 serializer: &SafeEncodeSerializer,
117) -> Result<Database, IndexeddbCryptoStoreError> {
118 let old_version = db_version(name).await?;
131
132 if old_version > MAX_SUPPORTED_SCHEMA_VERSION {
136 return Err(IndexeddbCryptoStoreError::SchemaTooNewError {
137 max_supported_version: MAX_SUPPORTED_SCHEMA_VERSION,
138 current_version: old_version,
139 });
140 }
141
142 if old_version < 5 {
143 v0_to_v5::schema_add(name).await?;
144 }
145
146 if old_version < 6 {
147 v5_to_v7::schema_add(name).await?;
148 }
149 if old_version < 7 {
150 v5_to_v7::data_migrate(name, serializer).await?;
151 v5_to_v7::schema_delete(name).await?;
152 }
153
154 if old_version < 8 {
155 v7_to_v8::data_migrate(name, serializer).await?;
156 v7_to_v8::schema_bump(name).await?;
157 }
158
159 if old_version < 9 {
160 v8_to_v10::schema_add(name).await?;
161 }
162 if old_version < 10 {
163 v8_to_v10::data_migrate(name, serializer).await?;
164 v8_to_v10::schema_delete(name).await?;
165 }
166
167 if old_version < 11 {
168 v10_to_v11::data_migrate(name, serializer).await?;
169 v10_to_v11::schema_bump(name).await?;
170 }
171
172 if old_version < 12 {
173 v11_to_v12::schema_add(name).await?;
174 }
175
176 if old_version < 13 {
177 v12_to_v13::schema_add(name).await?;
178 }
179
180 if old_version < 14 {
181 v13_to_v14::data_migrate(name, serializer).await?;
182 v13_to_v14::schema_bump(name).await?;
183 }
184
185 if old_version < 100 {
186 v14_to_v101::schema_add(name).await?;
187 }
188
189 if old_version < 101 {
190 v14_to_v101::data_migrate(name, serializer).await?;
191 v14_to_v101::schema_delete(name).await?;
192 }
193
194 if old_version < 102 {
195 v101_to_v102::schema_add(name).await?;
196 }
197
198 if old_version < 103 {
199 v102_to_v103::schema_add(name).await?;
200 }
201
202 if old_version < 104 {
203 v103_to_v104::schema_add(name).await?;
204 }
205
206 if old_version < 105 {
207 v104_to_v105::data_migrate(name, serializer).await?;
208 v104_to_v105::schema_bump(name).await?;
209 }
210
211 if old_version < 106 {
212 v105_to_v107::schema_add(name).await?;
213 }
214
215 if old_version < 107 {
216 v105_to_v107::data_migrate(name, serializer).await?;
217 v105_to_v107::schema_delete(name).await?;
218 }
219
220 Ok(Database::open(name).await?)
229}
230
231async fn db_version(name: &str) -> Result<u32, IndexeddbCryptoStoreError> {
232 let db = Database::open(name).await?;
233 let old_version = db.version() as u32;
234 db.close();
235 Ok(old_version)
236}
237
238type OldVersion = u32;
239
240async fn do_schema_upgrade<F>(name: &str, version: u32, f: F) -> Result<(), OpenDbError>
252where
253 F: Fn(&Transaction<'_>, OldVersion) -> Result<(), Error> + 'static,
254{
255 info!("IndexeddbCryptoStore upgrade schema -> v{version} starting");
256 let db = Database::open(name)
257 .with_version(version)
258 .with_on_upgrade_needed(move |evt, tx| {
259 let old_version = evt.old_version() as u32;
263
264 f(tx, old_version)
266 })
267 .await?;
268 db.close();
269 info!("IndexeddbCryptoStore upgrade schema -> v{version} complete");
270 Ok(())
271}
272
273fn add_nonunique_index<'a>(
274 object_store: &'a ObjectStore<'a>,
275 name: &str,
276 key_path: &str,
277) -> Result<Index<'a>, Error> {
278 object_store.create_index(name, key_path.into()).with_unique(false).build()
279}
280
281fn add_unique_index<'a>(
282 object_store: &'a ObjectStore<'a>,
283 name: &str,
284 key_path: &str,
285) -> Result<Index<'a>, Error> {
286 object_store.create_index(name, key_path.into()).with_unique(true).build()
287}
288
289#[cfg(all(test, target_family = "wasm"))]
290mod tests {
291 use std::{cell::Cell, future::Future, rc::Rc, sync::Arc};
292
293 use assert_matches::assert_matches;
294 use gloo_utils::format::JsValueSerdeExt;
295 use indexed_db_futures::{
296 database::VersionChangeEvent, prelude::*, transaction::TransactionMode,
297 };
298 use matrix_sdk_common::{
299 deserialized_responses::WithheldCode, js_tracing::make_tracing_subscriber,
300 };
301 use matrix_sdk_crypto::{
302 olm::{InboundGroupSession, SenderData, SessionKey},
303 store::{CryptoStore, types::RoomKeyWithheldEntry},
304 types::{EventEncryptionAlgorithm, events::room_key_withheld::RoomKeyWithheldContent},
305 vodozemac::{Curve25519PublicKey, Curve25519SecretKey, Ed25519PublicKey, Ed25519SecretKey},
306 };
307 use matrix_sdk_store_encryption::StoreCipher;
308 use matrix_sdk_test::async_test;
309 use ruma::{OwnedRoomId, RoomId, owned_device_id, owned_user_id, room_id};
310 use serde::Serialize;
311 use tracing_subscriber::util::SubscriberInitExt;
312 use wasm_bindgen::JsValue;
313 use web_sys::console;
314
315 use super::{v0_to_v5, v7::InboundGroupSessionIndexedDbObject2};
316 use crate::{
317 IndexeddbCryptoStore,
318 crypto_store::{InboundGroupSessionIndexedDbObject, keys, migrations::*},
319 };
320
321 wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser);
322
323 const EXPECTED_SCHEMA_VERSION: u32 = 107;
325
326 const NUM_RECORDS_FOR_PERF: usize = 2_000;
328
329 #[async_test]
331 async fn test_count_lots_of_sessions_v8() {
332 let cipher = Arc::new(StoreCipher::new().unwrap());
333 let serializer = SafeEncodeSerializer::new(Some(cipher.clone()));
334 let session_key = create_session_key();
337
338 let mut objects = Vec::with_capacity(NUM_RECORDS_FOR_PERF);
340 for i in 0..NUM_RECORDS_FOR_PERF {
341 objects.push(
342 create_inbound_group_sessions2_record(i, &session_key, &cipher, &serializer).await,
343 );
344 }
345
346 let db_prefix = "count_lots_of_sessions_v8";
348 let db = create_db(db_prefix).await;
349 let transaction = create_transaction(&db, db_prefix).await;
350 let store = create_store(&transaction, db_prefix).await;
351
352 measure_performance("Inserting", "v8", NUM_RECORDS_FOR_PERF, || async {
354 for (key, session_js) in objects.iter() {
355 store
356 .add(session_js)
357 .with_key(key)
358 .without_key_type()
359 .build()
360 .unwrap()
361 .await
362 .unwrap();
363 }
364 })
365 .await;
366
367 measure_performance("Counting", "v8", NUM_RECORDS_FOR_PERF, || async {
369 store.count().await.unwrap();
370 })
371 .await;
372 }
373
374 #[async_test]
376 async fn test_count_lots_of_sessions_v10() {
377 let serializer = SafeEncodeSerializer::new(Some(Arc::new(StoreCipher::new().unwrap())));
378
379 let session_key = create_session_key();
382
383 let mut objects = Vec::with_capacity(NUM_RECORDS_FOR_PERF);
385 for i in 0..NUM_RECORDS_FOR_PERF {
386 objects.push(create_inbound_group_sessions3_record(i, &session_key, &serializer).await);
387 }
388
389 let db_prefix = "count_lots_of_sessions_v8";
391 let db = create_db(db_prefix).await;
392 let transaction = create_transaction(&db, db_prefix).await;
393 let store = create_store(&transaction, db_prefix).await;
394
395 measure_performance("Inserting", "v10", NUM_RECORDS_FOR_PERF, || async {
397 for (key, session_js) in objects.iter() {
398 store
399 .add(session_js)
400 .with_key(key)
401 .without_key_type()
402 .build()
403 .unwrap()
404 .await
405 .unwrap();
406 }
407 })
408 .await;
409
410 measure_performance("Counting", "v10", NUM_RECORDS_FOR_PERF, || async {
412 store.count().await.unwrap();
413 })
414 .await;
415 }
416
417 async fn create_db(db_prefix: &str) -> Database {
418 let db_name = format!("{db_prefix}::matrix-sdk-crypto");
419 let store_name = format!("{db_prefix}_store");
420 Database::open(&db_name)
421 .with_version(1u32)
422 .with_on_upgrade_needed(
423 move |_: VersionChangeEvent, tx: &Transaction<'_>| -> Result<(), Error> {
424 tx.db().create_object_store(&store_name).build()?;
425 Ok(())
426 },
427 )
428 .build()
429 .unwrap()
430 .await
431 .unwrap()
432 }
433
434 async fn create_transaction<'a>(db: &'a Database, db_prefix: &str) -> Transaction<'a> {
435 let store_name = format!("{db_prefix}_store");
436 db.transaction(&store_name).with_mode(TransactionMode::Readwrite).build().unwrap()
437 }
438
439 async fn create_store<'a>(
440 transaction: &'a Transaction<'a>,
441 db_prefix: &str,
442 ) -> ObjectStore<'a> {
443 let store_name = format!("{db_prefix}_store");
444 transaction.object_store(&store_name).unwrap()
445 }
446
447 fn create_session_key() -> SessionKey {
448 SessionKey::from_base64(
449 "\
450 AgAAAADBy9+YIYTIqBjFT67nyi31gIOypZQl8day2hkhRDCZaHoG+cZh4tZLQIAZimJail0\
451 0zq4DVJVljO6cZ2t8kIto/QVk+7p20Fcf2nvqZyL2ZCda2Ei7VsqWZHTM/gqa2IU9+ktkwz\
452 +KFhENnHvDhG9f+hjsAPZd5mTTpdO+tVcqtdWhX4dymaJ/2UpAAjuPXQW+nXhQWQhXgXOUa\
453 JCYurJtvbCbqZGeDMmVIoqukBs2KugNJ6j5WlTPoeFnMl6Guy9uH2iWWxGg8ZgT2xspqVl5\
454 CwujjC+m7Dh1toVkvu+bAw\
455 ",
456 )
457 .unwrap()
458 }
459
460 async fn create_inbound_group_sessions2_record(
461 i: usize,
462 session_key: &SessionKey,
463 cipher: &Arc<StoreCipher>,
464 serializer: &SafeEncodeSerializer,
465 ) -> (JsValue, JsValue) {
466 let session = create_inbound_group_session(i, session_key);
467 let pickled_session = session.pickle().await;
468 let session_dbo = InboundGroupSessionIndexedDbObject2 {
469 pickled_session: cipher.encrypt_value(&pickled_session).unwrap(),
470 needs_backup: false,
471 };
472 let session_js: JsValue = serde_wasm_bindgen::to_value(&session_dbo).unwrap();
473
474 let key = serializer.encode_key(
475 old_keys::INBOUND_GROUP_SESSIONS_V2,
476 (&session.room_id, session.session_id()),
477 );
478
479 (key, session_js)
480 }
481
482 async fn create_inbound_group_sessions3_record(
483 i: usize,
484 session_key: &SessionKey,
485 serializer: &SafeEncodeSerializer,
486 ) -> (JsValue, JsValue) {
487 let session = create_inbound_group_session(i, session_key);
488 let pickled_session = session.pickle().await;
489
490 let session_dbo = InboundGroupSessionIndexedDbObject {
491 pickled_session: serializer.maybe_encrypt_value(pickled_session).unwrap(),
492 session_id: None,
493 needs_backup: false,
494 backed_up_to: -1,
495 sender_key: None,
496 sender_data_type: None,
497 };
498 let session_js: JsValue = serde_wasm_bindgen::to_value(&session_dbo).unwrap();
499
500 let key = serializer.encode_key(
501 old_keys::INBOUND_GROUP_SESSIONS_V2,
502 (&session.room_id, session.session_id()),
503 );
504
505 (key, session_js)
506 }
507
508 async fn measure_performance<Fut, R>(
509 name: &str,
510 schema: &str,
511 num_records: usize,
512 f: impl Fn() -> Fut,
513 ) -> R
514 where
515 Fut: Future<Output = R>,
516 {
517 let window = web_sys::window().expect("should have a window in this context");
518 let performance = window.performance().expect("performance should be available");
519 let start = performance.now();
520
521 let ret = f().await;
522
523 let elapsed = performance.now() - start;
524 console::log_1(
525 &format!("{name} {num_records} records with {schema} schema took {elapsed:.2}ms.")
526 .into(),
527 );
528
529 ret
530 }
531
532 fn create_inbound_group_session(i: usize, session_key: &SessionKey) -> InboundGroupSession {
534 let sender_key = Curve25519PublicKey::from_bytes([
535 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
536 24, 25, 26, 27, 28, 29, 30, 31,
537 ]);
538 let signing_key = Ed25519PublicKey::from_slice(&[
539 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
540 24, 25, 26, 27, 28, 29, 30, 31,
541 ])
542 .unwrap();
543 let room_id: OwnedRoomId = format!("!a{i}:b.co").try_into().unwrap();
544 let encryption_algorithm = EventEncryptionAlgorithm::MegolmV1AesSha2;
545 let history_visibility = None;
546
547 InboundGroupSession::new(
548 sender_key,
549 signing_key,
550 &room_id,
551 session_key,
552 SenderData::unknown(),
553 None,
554 encryption_algorithm,
555 history_visibility,
556 false,
557 )
558 .unwrap()
559 }
560
561 #[async_test]
564 async fn test_v8_v10_v12_migration_unencrypted() {
565 test_v8_v10_v12_migration_with_cipher("test_v8_migration_unencrypted", None).await
566 }
567
568 #[async_test]
571 async fn test_v8_v10_v12_migration_encrypted() {
572 let cipher = StoreCipher::new().unwrap();
573 test_v8_v10_v12_migration_with_cipher(
574 "test_v8_migration_encrypted",
575 Some(Arc::new(cipher)),
576 )
577 .await;
578 }
579
580 async fn test_v8_v10_v12_migration_with_cipher(
583 db_prefix: &str,
584 store_cipher: Option<Arc<StoreCipher>>,
585 ) {
586 let _ = make_tracing_subscriber(None).try_init();
587 let db_name = format!("{db_prefix:0}::matrix-sdk-crypto");
588
589 let _ = Database::delete_by_name(&db_name);
591
592 let room_id = room_id!("!test:localhost");
594 let (backed_up_session, not_backed_up_session) = create_sessions(&room_id);
595 populate_v5_db(
596 &db_name,
597 store_cipher.clone(),
598 &[&backed_up_session, ¬_backed_up_session],
599 )
600 .await;
601
602 let store =
604 IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
605
606 let fetched_backed_up_session = store
608 .get_inbound_group_session(room_id, backed_up_session.session_id())
609 .await
610 .unwrap()
611 .unwrap();
612 assert_eq!(fetched_backed_up_session.session_id(), backed_up_session.session_id());
613
614 let fetched_not_backed_up_session = store
615 .get_inbound_group_session(room_id, not_backed_up_session.session_id())
616 .await
617 .unwrap()
618 .unwrap();
619 assert_eq!(fetched_not_backed_up_session.session_id(), not_backed_up_session.session_id());
620
621 assert!(fetched_backed_up_session.backed_up());
623 assert!(!fetched_not_backed_up_session.backed_up());
624
625 assert_matches_v10_schema(&db_name, &store, &fetched_backed_up_session).await;
627
628 assert_matches_v12_schema(&db_name, &store, &fetched_backed_up_session).await;
631 }
632
633 async fn assert_matches_v10_schema(
634 db_name: &str,
635 store: &IndexeddbCryptoStore,
636 fetched_backed_up_session: &InboundGroupSession,
637 ) {
638 let db = Database::open(&db_name).build().unwrap().await.unwrap();
639 assert!(db.version() >= 10.0);
640 let transaction = db.transaction("inbound_group_sessions3").build().unwrap();
641 let raw_store = transaction.object_store("inbound_group_sessions3").unwrap();
642 let key = store.serializer.encode_key(
643 keys::INBOUND_GROUP_SESSIONS_V3,
644 (fetched_backed_up_session.room_id(), fetched_backed_up_session.session_id()),
645 );
646 let idb_object: InboundGroupSessionIndexedDbObject =
647 serde_wasm_bindgen::from_value(raw_store.get(&key).await.unwrap().unwrap()).unwrap();
648
649 assert_eq!(idb_object.backed_up_to, -1);
650 assert!(raw_store.index_names().find(|idx| idx == "backed_up_to").is_some());
651
652 transaction.commit().await.unwrap();
653 db.close();
654 }
655
656 async fn assert_matches_v12_schema(
657 db_name: &str,
658 store: &IndexeddbCryptoStore,
659 session: &InboundGroupSession,
660 ) {
661 let db = Database::open(&db_name).build().unwrap().await.unwrap();
662 assert!(db.version() >= 12.0);
663 let transaction = db.transaction("inbound_group_sessions3").build().unwrap();
664 let raw_store = transaction.object_store("inbound_group_sessions3").unwrap();
665 let key = store
666 .serializer
667 .encode_key(keys::INBOUND_GROUP_SESSIONS_V3, (session.room_id(), session.session_id()));
668 let idb_object: InboundGroupSessionIndexedDbObject =
669 serde_wasm_bindgen::from_value(raw_store.get(&key).await.unwrap().unwrap()).unwrap();
670
671 assert_eq!(
672 idb_object.session_id,
673 Some(
674 store
675 .serializer
676 .encode_key_as_string(keys::INBOUND_GROUP_SESSIONS_V3, session.session_id())
677 )
678 );
679 assert_eq!(
680 idb_object.sender_key,
681 Some(store.serializer.encode_key_as_string(
682 keys::INBOUND_GROUP_SESSIONS_V3,
683 session.sender_key().to_base64()
684 ))
685 );
686 assert_eq!(idb_object.sender_data_type, Some(session.sender_data_type() as u8));
687 assert!(
688 raw_store
689 .index_names()
690 .find(|idx| idx == "inbound_group_session_sender_key_sender_data_type_idx")
691 .is_some()
692 );
693
694 transaction.commit().await.unwrap();
695 db.close();
696 }
697
698 fn create_sessions(room_id: &RoomId) -> (InboundGroupSession, InboundGroupSession) {
699 let curve_key = Curve25519PublicKey::from(&Curve25519SecretKey::new());
700 let ed_key = Ed25519SecretKey::new().public_key();
701
702 let backed_up_session = InboundGroupSession::new(
703 curve_key,
704 ed_key,
705 room_id,
706 &SessionKey::from_base64(
707 "AgAAAABTyn3CR8mzAxhsHH88td5DrRqfipJCnNbZeMrfzhON6O1Cyr9ewx/sDFLO6\
708 +NvyW92yGvMub7nuAEQb+SgnZLm7nwvuVvJgSZKpoJMVliwg8iY9TXKFT286oBtT2\
709 /8idy6TcpKax4foSHdMYlZXu5zOsGDdd9eYnYHpUEyDT0utuiaakZM3XBMNLEVDj9\
710 Ps929j1FGgne1bDeFVoty2UAOQK8s/0JJigbKSu6wQ/SzaCYpE/LD4Egk2Nxs1JE2\
711 33ii9J8RGPYOp7QWl0kTEc8mAlqZL7mKppo9AwgtmYweAg",
712 )
713 .unwrap(),
714 SenderData::legacy(),
715 None,
716 EventEncryptionAlgorithm::MegolmV1AesSha2,
717 None,
718 false,
719 )
720 .unwrap();
721 backed_up_session.mark_as_backed_up();
722
723 let not_backed_up_session = InboundGroupSession::new(
724 curve_key,
725 ed_key,
726 room_id,
727 &SessionKey::from_base64(
728 "AgAAAACO1PjBdqucFUcNFU6JgXYAi7KMeeUqUibaLm6CkHJcMiDTFWq/K5SFAukJc\
729 WjeyOpnZr4vpezRlbvNaQpNPMub2Cs2u14fHj9OpKFD7c4hFS4j94q4pTLZly3qEV\
730 BIjWdOpcIVfN7QVGVIxYiI6KHEddCHrNCo9fc8GUdfzrMnmUooQr/m4ZAkRdErzUH\
731 uUAlUBwOKcPi7Cs/KrMw/sHCRDkTntHZ3BOrzJsAVbHUgq+8/Sqy3YE+CX6uEnig+\
732 1NWjZD9f1vvXnSKKDdHj1927WFMFZ/yYc24607zEVUaODQ",
733 )
734 .unwrap(),
735 SenderData::legacy(),
736 None,
737 EventEncryptionAlgorithm::MegolmV1AesSha2,
738 None,
739 false,
740 )
741 .unwrap();
742
743 (backed_up_session, not_backed_up_session)
744 }
745
746 async fn populate_v5_db(
747 db_name: &str,
748 store_cipher: Option<Arc<StoreCipher>>,
749 session_entries: &[&InboundGroupSession],
750 ) {
751 let db = create_v5_db(&db_name).await.unwrap();
755
756 let serializer = SafeEncodeSerializer::new(store_cipher.clone());
757
758 let txn = db
759 .transaction(old_keys::INBOUND_GROUP_SESSIONS_V1)
760 .with_mode(TransactionMode::Readwrite)
761 .build()
762 .unwrap();
763 let sessions = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V1).unwrap();
764 for session in session_entries {
765 let room_id = session.room_id();
766 let session_id = session.session_id();
767 let key =
768 serializer.encode_key(old_keys::INBOUND_GROUP_SESSIONS_V1, (room_id, session_id));
769 let pickle = session.pickle().await;
770
771 let serialized_session = serialize_value_as_legacy(&store_cipher, &pickle);
774 sessions.put(&serialized_session).with_key(key).build().unwrap();
775 }
776 txn.commit().await.unwrap();
777
778 db.close();
781 }
782
783 #[async_test]
786 async fn test_v10_v11_migration_unencrypted() {
787 test_v10_v11_migration_with_cipher("test_v10_migration_unencrypted", None).await
788 }
789
790 #[async_test]
793 async fn test_v10_v11_migration_encrypted() {
794 let cipher = StoreCipher::new().unwrap();
795 test_v10_v11_migration_with_cipher("test_v10_migration_encrypted", Some(Arc::new(cipher)))
796 .await;
797 }
798
799 async fn test_v10_v11_migration_with_cipher(
802 db_prefix: &str,
803 store_cipher: Option<Arc<StoreCipher>>,
804 ) {
805 let _ = make_tracing_subscriber(None).try_init();
806 let db_name = format!("{db_prefix:0}::matrix-sdk-crypto");
807
808 let _ = Database::delete_by_name(&db_name).unwrap().await.unwrap();
810
811 let db = create_v5_db(&db_name).await.unwrap();
813
814 let txn = db
815 .transaction(keys::BACKUP_KEYS)
816 .with_mode(TransactionMode::Readwrite)
817 .build()
818 .unwrap();
819 let store = txn.object_store(keys::BACKUP_KEYS).unwrap();
820 store
821 .put(&serialize_value_as_legacy(&store_cipher, &"1".to_owned()))
822 .with_key(JsValue::from_str(old_keys::BACKUP_KEY_V1))
823 .build()
824 .unwrap();
825 txn.commit().await.unwrap();
826 db.close();
827
828 let store =
830 IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
831
832 let backup_data = store.load_backup_keys().await.unwrap();
834 assert_eq!(backup_data.backup_version, Some("1".to_owned()));
835 }
836
837 #[async_test]
840 async fn test_v14_v101_migration_unencrypted() {
841 test_v14_v101_migration_with_cipher("test_v101_migration_unencrypted", None).await
842 }
843
844 #[async_test]
847 async fn test_v14_v101_migration_encrypted() {
848 let cipher = StoreCipher::new().unwrap();
849 test_v14_v101_migration_with_cipher(
850 "test_v101_migration_encrypted",
851 Some(Arc::new(cipher)),
852 )
853 .await;
854 }
855
856 async fn test_v14_v101_migration_with_cipher(
859 db_prefix: &str,
860 store_cipher: Option<Arc<StoreCipher>>,
861 ) {
862 let serializer = SafeEncodeSerializer::new(store_cipher.clone());
863
864 let _ = make_tracing_subscriber(None).try_init();
865 let db_name = format!("{db_prefix:0}::matrix-sdk-crypto");
866
867 let _ = Database::delete_by_name(&db_name).unwrap().await.unwrap();
869
870 let room_id = room_id!("!test:example.com");
871 let session_id = "12345";
872
873 {
875 let db = create_v5_db(&db_name).await.unwrap();
876
877 let txn = db
878 .transaction(old_keys::DIRECT_WITHHELD_INFO)
879 .with_mode(TransactionMode::Readwrite)
880 .build()
881 .unwrap();
882 let store = txn.object_store(old_keys::DIRECT_WITHHELD_INFO).unwrap();
883
884 let sender_key =
885 Curve25519PublicKey::from_base64("9n7mdWKOjr9c4NTlG6zV8dbFtNK79q9vZADoh7nMUwA")
886 .unwrap();
887
888 let withheld_entry = RoomKeyWithheldEntry {
889 sender: owned_user_id!("@alice:example.com"),
890 content: RoomKeyWithheldContent::new(
891 EventEncryptionAlgorithm::MegolmV1AesSha2,
892 WithheldCode::Blacklisted,
893 room_id.to_owned(),
894 session_id.to_owned(),
895 sender_key,
896 owned_device_id!("ABC"),
897 ),
898 };
899
900 let key = serializer.encode_key(old_keys::DIRECT_WITHHELD_INFO, (room_id, session_id));
901 let value = serializer.serialize_value(&withheld_entry).unwrap();
902 store.add(value).with_key(key).build().unwrap();
903 txn.commit().await.unwrap();
904 db.close();
905 }
906
907 let store =
909 IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
910
911 let withheld_entry = store
913 .get_withheld_info(room_id, session_id)
914 .await
915 .unwrap()
916 .expect("Should find a withheld entry in migrated data");
917 assert_eq!(withheld_entry.content.withheld_code(), WithheldCode::Blacklisted)
918 }
919
920 #[async_test]
923 async fn test_v105_v107_migration_unencrypted() {
924 test_v105_v107_migration_with_cipher("test_v107_migration_unencrypted", None).await
925 }
926
927 #[async_test]
930 async fn test_v105_v107_migration_encrypted() {
931 let cipher = StoreCipher::new().unwrap();
932 test_v105_v107_migration_with_cipher(
933 "test_v107_migration_encrypted",
934 Some(Arc::new(cipher)),
935 )
936 .await;
937 }
938
939 async fn test_v105_v107_migration_with_cipher(
942 db_prefix: &str,
943 store_cipher: Option<Arc<StoreCipher>>,
944 ) {
945 use std::ops::Deref;
946
947 use matrix_sdk_crypto::{
948 GossipRequest, GossippedSecret, SecretInfo,
949 types::events::{
950 olm_v1::{DecryptedSecretSendEvent, OlmV1Keys},
951 secret_send::SecretSendContent,
952 },
953 };
954 use ruma::{TransactionId, events::secret::request::SecretName};
955
956 let serializer = SafeEncodeSerializer::new(store_cipher.clone());
957
958 let _ = make_tracing_subscriber(None).try_init();
959 let db_name = format!("{db_prefix:0}::matrix-sdk-crypto");
960
961 let _ = Database::delete_by_name(&db_name).unwrap().await.unwrap();
963
964 {
966 let db = create_v5_db(&db_name).await.unwrap();
967 let txn = db
968 .transaction(old_keys::SECRETS_INBOX_V1)
969 .with_mode(TransactionMode::Readwrite)
970 .build()
971 .unwrap();
972 let store = txn.object_store(old_keys::SECRETS_INBOX_V1).unwrap();
973
974 let gossipped_secret = GossippedSecret {
975 secret_name: SecretName::CrossSigningMasterKey,
976 gossip_request: GossipRequest {
977 request_recipient: owned_user_id!("@alice:example.com"),
978 request_id: TransactionId::new(),
979 info: SecretInfo::SecretRequest(SecretName::CrossSigningMasterKey),
980 sent_out: true,
981 },
982 event: DecryptedSecretSendEvent {
983 sender: owned_user_id!("@alice:example.com"),
984 recipient: owned_user_id!("@alice:example.com"),
985 keys: OlmV1Keys { ed25519: Ed25519SecretKey::new().public_key() },
986 recipient_keys: OlmV1Keys { ed25519: Ed25519SecretKey::new().public_key() },
987 sender_device_keys: None,
988 content: SecretSendContent::new(
989 "abc".into(),
990 "It is a secret to everybody".to_owned(),
991 ),
992 },
993 };
994
995 let key = serializer.encode_key(
996 old_keys::SECRETS_INBOX_V1,
997 (
998 gossipped_secret.secret_name.to_string(),
999 gossipped_secret.gossip_request.request_id.to_string(),
1000 ),
1001 );
1002 let value = serializer.serialize_value(&gossipped_secret).unwrap();
1003 store.add(value).with_key(key).build().unwrap();
1004 txn.commit().await.unwrap();
1005 db.close();
1006 }
1007
1008 let store =
1010 IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
1011
1012 let secrets =
1014 store.get_secrets_from_inbox(&SecretName::CrossSigningMasterKey).await.unwrap();
1015 assert_eq!(secrets.len(), 1);
1016 assert_eq!(secrets[0].deref(), "It is a secret to everybody");
1017 }
1018
1019 async fn create_v5_db(name: &str) -> std::result::Result<Database, OpenDbError> {
1020 v0_to_v5::schema_add(name).await?;
1021 Database::open(name).with_version(5u32).build()?.await
1022 }
1023
1024 #[async_test]
1027 async fn test_can_open_max_supported_schema_version() {
1028 let _ = make_tracing_subscriber(None).try_init();
1029
1030 let db_prefix = "test_can_open_max_supported_schema_version";
1031 create_future_schema_db(db_prefix, MAX_SUPPORTED_SCHEMA_VERSION).await;
1033
1034 IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, None).await.unwrap();
1036 }
1037
1038 #[async_test]
1041 async fn test_can_not_open_too_new_db() {
1042 let _ = make_tracing_subscriber(None).try_init();
1043
1044 let db_prefix = "test_can_not_open_too_new_db";
1045 create_future_schema_db(db_prefix, MAX_SUPPORTED_SCHEMA_VERSION + 1).await;
1047
1048 let result = IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, None).await;
1050 assert_matches!(
1051 result,
1052 Err(IndexeddbCryptoStoreError::SchemaTooNewError {
1053 max_supported_version,
1054 current_version
1055 }) => {
1056 assert_eq!(max_supported_version, MAX_SUPPORTED_SCHEMA_VERSION);
1057 assert_eq!(current_version, MAX_SUPPORTED_SCHEMA_VERSION + 1);
1058 }
1059 );
1060 }
1061
1062 async fn create_future_schema_db(db_prefix: &str, version: u32) {
1065 let db_name = format!("{db_prefix}::matrix-sdk-crypto");
1066
1067 let _ = Database::delete_by_name(&db_name);
1069
1070 IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, None).await.unwrap();
1072
1073 let old_version: Rc<Cell<Option<u32>>> = Rc::new(Cell::new(None));
1076 let old_version2 = old_version.clone();
1077
1078 let db = Database::open(&db_name)
1079 .with_version(version)
1080 .with_on_upgrade_needed(move |evt: VersionChangeEvent, _: &Transaction<'_>| {
1081 old_version2.set(Some(evt.old_version() as u32));
1082 Ok(())
1083 })
1084 .build()
1085 .unwrap()
1086 .await
1087 .unwrap();
1088
1089 assert_eq!(
1090 old_version.get(),
1091 Some(EXPECTED_SCHEMA_VERSION),
1092 "Existing store had unexpected version number"
1093 );
1094 db.close();
1095 }
1096
1097 fn serialize_value_as_legacy<T: Serialize>(
1103 store_cipher: &Option<Arc<StoreCipher>>,
1104 value: &T,
1105 ) -> JsValue {
1106 if let Some(cipher) = &store_cipher {
1107 let data = serde_json::to_vec(&value).unwrap();
1109 let encrypted = cipher.encrypt_value_data(data).unwrap();
1111 let value = serde_json::to_vec(&encrypted).unwrap();
1113 JsValue::from_serde(&value).unwrap()
1115 } else {
1116 JsValue::from_serde(&value).unwrap()
1117 }
1118 }
1119}