diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs index df3c3226c..e26f7716d 100644 --- a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/mod.rs @@ -22,6 +22,8 @@ use crate::{ IndexeddbCryptoStoreError, }; +mod v8_to_v10; + mod old_keys { /// Old format of the `inbound_group_sessions` store which lacked indexes or /// a sensible structure @@ -416,188 +418,6 @@ async fn prepare_data_for_v8(name: &str, serializer: &IndexeddbSerializer) -> Re Ok(()) } -/// Migration code that moves from inbound_group_sessions2 to -/// inbound_group_sessions3, shrinking the values stored in each record. -/// -/// The migration 8->9 creates the new store inbound_group_sessions3. -/// Then we move the data into the new store. -/// The migration 9->10 deletes the old store inbound_group_sessions2. -mod v8_to_v10 { - use indexed_db_futures::{ - idb_object_store::IdbObjectStore, - request::{IdbOpenDbRequestLike, OpenDbRequest}, - IdbDatabase, IdbIndex, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent, - }; - use matrix_sdk_crypto::olm::InboundGroupSession; - use tracing::{debug, info}; - use wasm_bindgen::JsValue; - use web_sys::{DomException, IdbIndexParameters, IdbTransactionMode}; - - use super::Result; - use crate::{ - crypto_store::{ - indexeddb_serializer::IndexeddbSerializer, keys, migrations::old_keys, - InboundGroupSessionIndexedDbObject, - }, - IndexeddbCryptoStoreError, - }; - - /// The objects we store in the inbound_group_sessions2 indexeddb object - /// store (in schemas v7 and v8) - #[derive(Debug, serde::Serialize, serde::Deserialize)] - pub struct InboundGroupSessionIndexedDbObject2 { - /// (Possibly encrypted) serialisation of a - /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`] - /// structure. - pub pickled_session: Vec, - - /// Whether the session data has yet to be backed up. - /// - /// Since we only need to be able to find entries where this is `true`, - /// we skip serialization in cases where it is `false`. That has - /// the effect of omitting it from the indexeddb index. - /// - /// We also use a custom serializer because bools can't be used as keys - /// in indexeddb. - #[serde( - default, - skip_serializing_if = "std::ops::Not::not", - with = "crate::serialize_bool_for_indexeddb" - )] - pub needs_backup: bool, - } - - fn add_nonunique_index<'a>( - object_store: &'a IdbObjectStore<'a>, - name: &str, - key_path: &str, - ) -> Result, DomException> { - let mut params = IdbIndexParameters::new(); - params.unique(false); - object_store.create_index_with_params(name, &IdbKeyPath::str(key_path), ¶ms) - } - - async fn do_schema_upgrade(name: &str, version: u32, f: F) -> Result<(), DomException> - where - F: Fn(&IdbDatabase) -> Result<(), JsValue> + 'static, - { - info!("IndexeddbCryptoStore upgrade schema -> v{version} starting"); - let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?; - - db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| f(evt.db()))); - - let db = db_req.await?; - db.close(); - info!("IndexeddbCryptoStore upgrade schema -> v{version} complete"); - Ok(()) - } - - pub(crate) async fn upgrade_scheme_to_v9_create_inbound_group_sessions3( - name: &str, - ) -> Result<(), DomException> { - do_schema_upgrade(name, 9, |db| { - let object_store = db.create_object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; - - add_nonunique_index( - &object_store, - keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX, - "needs_backup", - )?; - - // See https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076 - // for the plan concerning this property and index. At time of writing, it is - // unused, and needs_backup is still used. - add_nonunique_index( - &object_store, - keys::INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX, - "backed_up_to", - )?; - - Ok(()) - }) - .await - } - - pub(crate) async fn migrate_data_before_v10_populate_inbound_group_sessions3( - name: &str, - serializer: &IndexeddbSerializer, - ) -> Result<()> { - info!("IndexeddbCryptoStore migrate data before v10 starting"); - - let db = IdbDatabase::open(name)?.await?; - let txn = db.transaction_on_multi_with_mode( - &[old_keys::INBOUND_GROUP_SESSIONS_V2, keys::INBOUND_GROUP_SESSIONS_V3], - IdbTransactionMode::Readwrite, - )?; - - let inbound_group_sessions2 = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?; - let inbound_group_sessions3 = txn.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; - - let row_count = inbound_group_sessions2.count()?.await?; - info!(row_count, "Shrinking inbound_group_session records"); - - // Iterate through all rows - if let Some(cursor) = inbound_group_sessions2.open_cursor()?.await? { - let mut idx = 0; - loop { - idx += 1; - - if idx % 100 == 0 { - debug!("Migrating session {idx} of {row_count}"); - } - - // Deserialize the session from the old store - let old_value: InboundGroupSessionIndexedDbObject2 = - serde_wasm_bindgen::from_value(cursor.value())?; - - let session = InboundGroupSession::from_pickle( - serializer.deserialize_value_from_bytes(&old_value.pickled_session)?, - ) - .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?; - - // Calculate its key in the new table - let new_key = serializer.encode_key( - keys::INBOUND_GROUP_SESSIONS_V3, - (&session.room_id, session.session_id()), - ); - - // Serialize the session in the new format - // This is much the same as [`IndexeddbStore::serialize_inbound_group_session`]. - let new_value = InboundGroupSessionIndexedDbObject::new( - serializer.maybe_encrypt_value(session.pickle().await)?, - !session.backed_up(), - ); - - // Write it to the new store - inbound_group_sessions3 - .add_key_val(&new_key, &serde_wasm_bindgen::to_value(&new_value)?)?; - - // Continue to the next record, or stop if we're done - if !cursor.continue_cursor()?.await? { - debug!("Migrated {idx} sessions."); - break; - } - } - } - - txn.await.into_result()?; - db.close(); - info!("IndexeddbCryptoStore upgrade data before v10 finished"); - - Ok(()) - } - - pub(crate) async fn upgrade_scheme_to_v10_delete_inbound_group_sessions2( - name: &str, - ) -> Result<(), DomException> { - do_schema_upgrade(name, 10, |db| { - db.delete_object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?; - Ok(()) - }) - .await - } -} - #[cfg(all(test, target_arch = "wasm32"))] mod tests { use std::{future::Future, sync::Arc}; diff --git a/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs new file mode 100644 index 000000000..6dd5647dd --- /dev/null +++ b/crates/matrix-sdk-indexeddb/src/crypto_store/migrations/v8_to_v10.rs @@ -0,0 +1,194 @@ +// Copyright 2024 The Matrix.org Foundation C.I.C. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! Migration code that moves from inbound_group_sessions2 to +//! inbound_group_sessions3, shrinking the values stored in each record. +//! +//! The migration 8->9 creates the new store inbound_group_sessions3. +//! Then we move the data into the new store. +//! The migration 9->10 deletes the old store inbound_group_sessions2. + +use indexed_db_futures::{ + idb_object_store::IdbObjectStore, + request::{IdbOpenDbRequestLike, OpenDbRequest}, + IdbDatabase, IdbIndex, IdbKeyPath, IdbQuerySource, IdbVersionChangeEvent, +}; +use matrix_sdk_crypto::olm::InboundGroupSession; +use tracing::{debug, info}; +use wasm_bindgen::JsValue; +use web_sys::{DomException, IdbIndexParameters, IdbTransactionMode}; + +use super::Result; +use crate::{ + crypto_store::{ + indexeddb_serializer::IndexeddbSerializer, keys, migrations::old_keys, + InboundGroupSessionIndexedDbObject, + }, + IndexeddbCryptoStoreError, +}; + +/// The objects we store in the inbound_group_sessions2 indexeddb object +/// store (in schemas v7 and v8) +#[derive(Debug, serde::Serialize, serde::Deserialize)] +pub struct InboundGroupSessionIndexedDbObject2 { + /// (Possibly encrypted) serialisation of a + /// [`matrix_sdk_crypto::olm::group_sessions::PickledInboundGroupSession`] + /// structure. + pub pickled_session: Vec, + + /// Whether the session data has yet to be backed up. + /// + /// Since we only need to be able to find entries where this is `true`, + /// we skip serialization in cases where it is `false`. That has + /// the effect of omitting it from the indexeddb index. + /// + /// We also use a custom serializer because bools can't be used as keys + /// in indexeddb. + #[serde( + default, + skip_serializing_if = "std::ops::Not::not", + with = "crate::serialize_bool_for_indexeddb" + )] + pub needs_backup: bool, +} + +fn add_nonunique_index<'a>( + object_store: &'a IdbObjectStore<'a>, + name: &str, + key_path: &str, +) -> Result, DomException> { + let mut params = IdbIndexParameters::new(); + params.unique(false); + object_store.create_index_with_params(name, &IdbKeyPath::str(key_path), ¶ms) +} + +async fn do_schema_upgrade(name: &str, version: u32, f: F) -> Result<(), DomException> +where + F: Fn(&IdbDatabase) -> Result<(), JsValue> + 'static, +{ + info!("IndexeddbCryptoStore upgrade schema -> v{version} starting"); + let mut db_req: OpenDbRequest = IdbDatabase::open_u32(name, version)?; + + db_req.set_on_upgrade_needed(Some(move |evt: &IdbVersionChangeEvent| f(evt.db()))); + + let db = db_req.await?; + db.close(); + info!("IndexeddbCryptoStore upgrade schema -> v{version} complete"); + Ok(()) +} + +pub(crate) async fn upgrade_scheme_to_v9_create_inbound_group_sessions3( + name: &str, +) -> Result<(), DomException> { + do_schema_upgrade(name, 9, |db| { + let object_store = db.create_object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; + + add_nonunique_index( + &object_store, + keys::INBOUND_GROUP_SESSIONS_BACKUP_INDEX, + "needs_backup", + )?; + + // See https://github.com/element-hq/element-web/issues/26892#issuecomment-1906336076 + // for the plan concerning this property and index. At time of writing, it is + // unused, and needs_backup is still used. + add_nonunique_index( + &object_store, + keys::INBOUND_GROUP_SESSIONS_BACKED_UP_TO_INDEX, + "backed_up_to", + )?; + + Ok(()) + }) + .await +} + +pub(crate) async fn migrate_data_before_v10_populate_inbound_group_sessions3( + name: &str, + serializer: &IndexeddbSerializer, +) -> Result<()> { + info!("IndexeddbCryptoStore migrate data before v10 starting"); + + let db = IdbDatabase::open(name)?.await?; + let txn = db.transaction_on_multi_with_mode( + &[old_keys::INBOUND_GROUP_SESSIONS_V2, keys::INBOUND_GROUP_SESSIONS_V3], + IdbTransactionMode::Readwrite, + )?; + + let inbound_group_sessions2 = txn.object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?; + let inbound_group_sessions3 = txn.object_store(keys::INBOUND_GROUP_SESSIONS_V3)?; + + let row_count = inbound_group_sessions2.count()?.await?; + info!(row_count, "Shrinking inbound_group_session records"); + + // Iterate through all rows + if let Some(cursor) = inbound_group_sessions2.open_cursor()?.await? { + let mut idx = 0; + loop { + idx += 1; + + if idx % 100 == 0 { + debug!("Migrating session {idx} of {row_count}"); + } + + // Deserialize the session from the old store + let old_value: InboundGroupSessionIndexedDbObject2 = + serde_wasm_bindgen::from_value(cursor.value())?; + + let session = InboundGroupSession::from_pickle( + serializer.deserialize_value_from_bytes(&old_value.pickled_session)?, + ) + .map_err(|e| IndexeddbCryptoStoreError::CryptoStoreError(e.into()))?; + + // Calculate its key in the new table + let new_key = serializer.encode_key( + keys::INBOUND_GROUP_SESSIONS_V3, + (&session.room_id, session.session_id()), + ); + + // Serialize the session in the new format + // This is much the same as [`IndexeddbStore::serialize_inbound_group_session`]. + let new_value = InboundGroupSessionIndexedDbObject::new( + serializer.maybe_encrypt_value(session.pickle().await)?, + !session.backed_up(), + ); + + // Write it to the new store + inbound_group_sessions3 + .add_key_val(&new_key, &serde_wasm_bindgen::to_value(&new_value)?)?; + + // Continue to the next record, or stop if we're done + if !cursor.continue_cursor()?.await? { + debug!("Migrated {idx} sessions."); + break; + } + } + } + + txn.await.into_result()?; + db.close(); + info!("IndexeddbCryptoStore upgrade data before v10 finished"); + + Ok(()) +} + +pub(crate) async fn upgrade_scheme_to_v10_delete_inbound_group_sessions2( + name: &str, +) -> Result<(), DomException> { + do_schema_upgrade(name, 10, |db| { + db.delete_object_store(old_keys::INBOUND_GROUP_SESSIONS_V2)?; + Ok(()) + }) + .await +}