Skip to main content

matrix_sdk_indexeddb/crypto_store/migrations/
mod.rs

1// Copyright 2023, 2026 The Matrix.org Foundation C.I.C.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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    /// Create an Indexed DB wrapper that manages a database migration,
54    /// logging messages before and after the migration, and automatically
55    /// closing the DB when this object is dropped.
56    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
78/// The latest version of the schema we can support. If we encounter a database
79/// version with a higher schema version, we will return an error.
80///
81/// A note on how this works.
82///
83/// Normally, when you open an indexeddb database, you tell it the "schema
84/// version" that you know about. If the existing database is older than
85/// that, it lets you run a migration. If the existing database is newer, then
86/// it assumes that there have been incompatible schema changes and complains
87/// with an error ("The requested version (10) is less than the existing version
88/// (11)").
89///
90/// The problem with this is that, if someone upgrades their installed
91/// application, then realises it was a terrible mistake and tries to roll
92/// back, then suddenly every user's session is completely hosed. (They see
93/// an "unable to restore session" dialog.) Often, schema updates aren't
94/// actually backwards-incompatible — for example, existing code will work just
95/// fine if someone adds a new store or a new index — so this approach is too
96/// heavy-handed.
97///
98/// The solution we take here is to say "any schema changes up to
99/// [`MAX_SUPPORTED_SCHEMA_VERSION`] will be backwards-compatible". If, at some
100/// point, we do make a breaking change, we will give that schema version a
101/// higher number. Then, rather than using the implicit version check that comes
102/// with `indexedDB.open(name, version)`, we explicitly check the version
103/// ourselves.
104///
105/// It is expected that we will use version numbers that are multiples of 100 to
106/// represent breaking changes — for example, version 100 is a breaking change,
107/// as is version 200, but versions 101-199 are all backwards compatible with
108/// version 100. In other words, if you divide by 100, you get something
109/// approaching semver: version 200 is major version 2, minor version 0.
110const MAX_SUPPORTED_SCHEMA_VERSION: u32 = 199;
111
112/// Open the indexeddb with the given name, upgrading it to the latest version
113/// of the schema if necessary.
114pub async fn open_and_upgrade_db(
115    name: &str,
116    serializer: &SafeEncodeSerializer,
117) -> Result<Database, IndexeddbCryptoStoreError> {
118    // Move the DB version up from where it is to the latest version.
119    //
120    // Schema changes need to be separate from data migrations, so we often
121    // have a pattern of:
122    //
123    // 1. schema_add - create new object stores, indices etc.
124    // 2. data_migrate - move data from the old stores to the new ones
125    // 3. schema_delete - delete any now-unused stores etc.
126    //
127    // Migrations like these require the schema version to be bumped twice,
128    // because of the separate "add" and "delete" stages.
129
130    let old_version = db_version(name).await?;
131
132    // If the database version is too new, bail out. We assume that schema updates
133    // all the way up to `MAX_SUPPORTED_SCHEMA_VERSION` will be
134    // backwards-compatible.
135    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    // If you add more migrations here, you'll need to update
221    // `tests::EXPECTED_SCHEMA_VERSION`.
222
223    // NOTE: IF YOU MAKE A BREAKING CHANGE TO THE SCHEMA, BUMP THE SCHEMA VERSION TO
224    // SOMETHING HIGHER THAN `MAX_SUPPORTED_SCHEMA_VERSION`! (And then bump
225    // `MAX_SUPPORTED_SCHEMA_VERSION` itself to the next multiple of 10).
226
227    // Open and return the DB (we know it's at the latest version)
228    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
240/// Run a database schema upgrade operation
241///
242/// # Arguments
243///
244/// * `name` - name of the indexeddb database to be upgraded.
245/// * `version` - version we are upgrading to.
246/// * `f` - closure which will be called if the database is below the version
247///   given. It will be called with three arguments `(db, txn, oldver)`, where:
248///   * `db` - the [`Database`]
249///   * `txn` - the database transaction: a [`Transaction`]
250///   * `oldver` - the version number before the upgrade.
251async 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            // Even if the web-sys bindings expose the version as a f64, the IndexedDB API
260            // works with an unsigned integer.
261            // See <https://github.com/rustwasm/wasm-bindgen/issues/1149>
262            let old_version = evt.old_version() as u32;
263
264            // Run the upgrade code we were supplied
265            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    /// The schema version we expect after we open the store.
324    const EXPECTED_SCHEMA_VERSION: u32 = 107;
325
326    /// Adjust this to test do a more comprehensive perf test
327    const NUM_RECORDS_FOR_PERF: usize = 2_000;
328
329    /// Make lots of sessions and see how long it takes to count them in v8
330    #[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        // Session keys are slow to create, so make one upfront and use it for every
335        // session
336        let session_key = create_session_key();
337
338        // Create lots of InboundGroupSessionIndexedDbObject2 objects
339        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        // Create a DB with an inbound_group_sessions2 store
347        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        // Check how long it takes to insert these records
353        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        // Check how long it takes to count these records
368        measure_performance("Counting", "v8", NUM_RECORDS_FOR_PERF, || async {
369            store.count().await.unwrap();
370        })
371        .await;
372    }
373
374    /// Make lots of sessions and see how long it takes to count them in v10
375    #[async_test]
376    async fn test_count_lots_of_sessions_v10() {
377        let serializer = SafeEncodeSerializer::new(Some(Arc::new(StoreCipher::new().unwrap())));
378
379        // Session keys are slow to create, so make one upfront and use it for every
380        // session
381        let session_key = create_session_key();
382
383        // Create lots of InboundGroupSessionIndexedDbObject objects
384        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        // Create a DB with an inbound_group_sessions3 store
390        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        // Check how long it takes to insert these records
396        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        // Check how long it takes to count these records
411        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    /// Create an example InboundGroupSession of known size
533    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    /// Test migrating `inbound_group_sessions` data from store v5 to latest,
562    /// on a store with encryption disabled.
563    #[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    /// Test migrating `inbound_group_sessions` data from store v5 to store v8,
569    /// on a store with encryption enabled.
570    #[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    /// Helper function for `test_v8_v10_v12_migration_{un,}encrypted`: test
581    /// migrating `inbound_group_sessions` data from store v5 to store v12.
582    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        // delete the db in case it was used in a previous run
590        let _ = Database::delete_by_name(&db_name);
591
592        // Given a DB with data in it as it was at v5
593        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, &not_backed_up_session],
599        )
600        .await;
601
602        // When I open a store based on that DB, triggering an upgrade
603        let store =
604            IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
605
606        // Then I can find the sessions using their keys and their info is correct
607        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        // For v8: the backed_up info is preserved
622        assert!(fetched_backed_up_session.backed_up());
623        assert!(!fetched_not_backed_up_session.backed_up());
624
625        // For v10: they have the backed_up_to property and it is indexed
626        assert_matches_v10_schema(&db_name, &store, &fetched_backed_up_session).await;
627
628        // For v12: they have the session_id, sender_key and sender_data_type properties
629        // and they are indexed
630        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        // Schema V7 migrated the inbound group sessions to a new format.
752        // To test, first create a database and populate it with the *old* style of
753        // entry.
754        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            // Serialize the session with the old style of serialization, since that's what
772            // we used at the time.
773            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        // now close our DB, reopen it properly, and check that we can still read our
779        // data.
780        db.close();
781    }
782
783    /// Test migrating `backup_keys` data from store v10 to latest,
784    /// on a store with encryption disabled.
785    #[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    /// Test migrating `backup_keys` data from store v10 to latest,
791    /// on a store with encryption enabled.
792    #[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    /// Helper function for `test_v10_v11_migration_{un,}encrypted`: test
800    /// migrating `backup_keys` data from store v10 to store v11.
801    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        // delete the db in case it was used in a previous run
809        let _ = Database::delete_by_name(&db_name).unwrap().await.unwrap();
810
811        // Given a DB with data in it as it was at v5
812        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        // When I open a store based on that DB, triggering an upgrade
829        let store =
830            IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
831
832        // Then I can read the backup settings
833        let backup_data = store.load_backup_keys().await.unwrap();
834        assert_eq!(backup_data.backup_version, Some("1".to_owned()));
835    }
836
837    /// Test migrating `withheld_sessions` data from store v14 to latest,
838    /// on a store with encryption disabled.
839    #[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    /// Test migrating `withheld_sessions` data from store v14 to latest,
845    /// on a store with encryption enabled.
846    #[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    /// Helper function for `test_v14_v101_migration_{un,}encrypted`: test
857    /// migrating `withheld_sessions` data from store v14 to store v101.
858    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        // delete the db in case it was used in a previous run
868        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        // Given a DB with data in it as it was at v5
874        {
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        // When I open a store based on that DB, triggering an upgrade
908        let store =
909            IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
910
911        // Then I can read the withheld session settings
912        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    /// Test migrating `secrets_inbox` data from store v105 to latest,
921    /// on a store with encryption disabled.
922    #[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    /// Test migrating `secrets_inbox` data from store v105 to latest,
928    /// on a store with encryption enabled.
929    #[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    /// Helper function for `test_v105_v107_migration_{un,}encrypted`: test
940    /// migrating `secrets_inbox` data from store v105 to store v107.
941    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        // delete the db in case it was used in a previous run
962        let _ = Database::delete_by_name(&db_name).unwrap().await.unwrap();
963
964        // Given a DB with data in it as it was at v5
965        {
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        // When I open a store based on that DB, triggering an upgrade
1009        let store =
1010            IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, store_cipher).await.unwrap();
1011
1012        // Then I can read the secrets inbox
1013        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    /// Opening a db that has been upgraded to MAX_SUPPORTED_SCHEMA_VERSION
1025    /// should be ok
1026    #[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 a database at MAX_SUPPORTED_SCHEMA_VERSION
1032        create_future_schema_db(db_prefix, MAX_SUPPORTED_SCHEMA_VERSION).await;
1033
1034        // Now, try opening it again
1035        IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, None).await.unwrap();
1036    }
1037
1038    /// Opening a db that has been upgraded beyond MAX_SUPPORTED_SCHEMA_VERSION
1039    /// should throw an error
1040    #[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 a database at MAX_SUPPORTED_SCHEMA_VERSION+1
1046        create_future_schema_db(db_prefix, MAX_SUPPORTED_SCHEMA_VERSION + 1).await;
1047
1048        // Now, try opening it again
1049        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    // Create a database, and increase its schema version to the given version
1063    // number.
1064    async fn create_future_schema_db(db_prefix: &str, version: u32) {
1065        let db_name = format!("{db_prefix}::matrix-sdk-crypto");
1066
1067        // delete the db in case it was used in a previous run
1068        let _ = Database::delete_by_name(&db_name);
1069
1070        // Open, and close, the store at the regular version.
1071        IndexeddbCryptoStore::open_with_store_cipher(&db_prefix, None).await.unwrap();
1072
1073        // Now upgrade to the given version, keeping a record of the previous version so
1074        // that we can double-check it.
1075        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    /// Emulate the old behaviour of [`IndexeddbSerializer::serialize_value`].
1098    ///
1099    /// We used to use an inefficient format for serializing objects in the
1100    /// indexeddb store. This replicates that old behaviour, for testing
1101    /// purposes.
1102    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            // Old-style serialization/encryption. First JSON-serialize into a byte array...
1108            let data = serde_json::to_vec(&value).unwrap();
1109            // ... then encrypt...
1110            let encrypted = cipher.encrypt_value_data(data).unwrap();
1111            // ... then JSON-serialize into another byte array ...
1112            let value = serde_json::to_vec(&encrypted).unwrap();
1113            // and finally, turn it into a javascript array.
1114            JsValue::from_serde(&value).unwrap()
1115        } else {
1116            JsValue::from_serde(&value).unwrap()
1117        }
1118    }
1119}