diff --git a/keystore/src/connection/platform/wasm/migration.rs b/keystore/src/connection/platform/wasm/migration.rs new file mode 100644 index 0000000000..f9c119a4b4 --- /dev/null +++ b/keystore/src/connection/platform/wasm/migration.rs @@ -0,0 +1,196 @@ +use crate::connection::platform::wasm::version_number; +use crate::connection::storage::{WasmEncryptedStorage, WasmStorageWrapper}; +use crate::connection::KeystoreDatabaseConnection; +use crate::entities::{ + E2eiAcmeCA, E2eiCrl, E2eiEnrollment, E2eiIntermediateCert, E2eiRefreshToken, Entity, EntityBase, MlsCredential, + MlsEncryptionKeyPair, MlsEpochEncryptionKeyPair, MlsHpkePrivateKey, MlsKeyPackage, MlsPendingMessage, MlsPskBundle, + MlsSignatureKeyPair, PersistedMlsGroup, PersistedMlsPendingGroup, ProteusIdentity, ProteusPrekey, ProteusSession, +}; +use crate::{CryptoKeystoreError, CryptoKeystoreResult}; +use idb::builder::{DatabaseBuilder, IndexBuilder, ObjectStoreBuilder}; +use idb::KeyPath; +use keystore_v_1_0_0::connection::KeystoreDatabaseConnection as KeystoreDatabaseConnectionV1_0_0; +use keystore_v_1_0_0::entities::{ + Entity as EntityV1_0_0, EntityFindParams as EntityFindParamsV1_0_0, MlsCredential as MlsCredentialV1_0_0, +}; +use keystore_v_1_0_0::Connection as ConnectionV1_0_0; +const VERSION_NUMBER_V1_0_2: u32 = version_number(1, 0, 2, 0); + +/// This is called from a while loop. The `from` argument represents the version the migration is performed from. +/// The function will return the version number of the DB resulting from the migration. +/// +/// To add a new migration, adjust the previous bottom match arm to return the current version, +/// add a new match arm below that matches on that version, perform the migration workload +/// and finally return `final_target`. +pub(crate) async fn migrate(from: u32, final_target: u32, name: &str, key: &str) -> CryptoKeystoreResult { + match from { + // The latest version that results from a migration must always map to "final_target" + // to ensure convergence of the while loop this is called from. + 0..=VERSION_NUMBER_V1_0_2 => { + migrate_to_post_v_1_0_2(name, key).await?; + Ok(final_target) + } + _ => Err(CryptoKeystoreError::MigrationNotSupported(from)), + } +} + +/// Migrates from any old version to post 1.0.2 (unclear right now what number this will be). +/// Assumption: the entire storage fits into memory +async fn migrate_to_post_v_1_0_2(name: &str, key: &str) -> CryptoKeystoreResult<()> { + let old_storage = keystore_v_1_0_0::Connection::open_with_key(name, key).await?; + + // Get all "old" records and convert them + // ! Assumption: the entire storage fits into memory + let mut credentials = MlsCredential::convert_from_v_1_2_0_or_earlier(&old_storage).await?; + old_storage.close().await?; + + // Now store all converted records in the new storage. + // This will overwrite all previous entities in the DB. + // Cannot use public API here because we would end in a never-ending loop + let new_idb = get_builder(name, VERSION_NUMBER_V1_0_2).build().await?; + let new_wrapper = WasmStorageWrapper::Persistent(new_idb); + let mut new_storage = WasmEncryptedStorage::new(key, new_wrapper); + + new_storage + .save(MlsCredential::COLLECTION_NAME, &mut credentials) + .await?; + + new_storage.close()?; + Ok(()) +} + +fn get_builder(name: &str, version: u32) -> DatabaseBuilder { + let idb_builder = DatabaseBuilder::new(&name) + .version(version) + .add_object_store( + ObjectStoreBuilder::new(MlsCredential::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id"))) + .add_index(IndexBuilder::new("credential".into(), KeyPath::new_single("credential")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsSignatureKeyPair::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new( + "signature_scheme".into(), + KeyPath::new_single("signature_scheme"), + )) + .add_index(IndexBuilder::new("signature_pk".into(), KeyPath::new_single("pk"))), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsHpkePrivateKey::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("pk".into(), KeyPath::new_single("pk")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsEncryptionKeyPair::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("pk".into(), KeyPath::new_single("pk")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsEpochEncryptionKeyPair::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsPskBundle::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("psk_id".into(), KeyPath::new_single("psk_id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsKeyPackage::COLLECTION_NAME) + .auto_increment(false) + .add_index( + IndexBuilder::new("keypackage_ref".into(), KeyPath::new_single("keypackage_ref")).unique(true), + ), + ) + .add_object_store( + ObjectStoreBuilder::new(PersistedMlsGroup::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(PersistedMlsPendingGroup::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(MlsPendingMessage::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id"))), + ) + .add_object_store( + ObjectStoreBuilder::new(E2eiEnrollment::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(E2eiRefreshToken::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(E2eiAcmeCA::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(E2eiIntermediateCert::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("ski_aki_pair".into(), KeyPath::new_single("ski_aki_pair")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(E2eiCrl::COLLECTION_NAME) + .auto_increment(false) + .add_index( + IndexBuilder::new("distribution_point".into(), KeyPath::new_single("distribution_point")) + .unique(true), + ), + ) + .add_object_store( + ObjectStoreBuilder::new(ProteusPrekey::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(ProteusIdentity::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("pk".into(), KeyPath::new_single("pk")).unique(true)), + ) + .add_object_store( + ObjectStoreBuilder::new(ProteusSession::COLLECTION_NAME) + .auto_increment(false) + .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), + ); + #[cfg(feature = "idb-regression-test")] + let idb_builder = idb_builder.add_object_store(ObjectStoreBuilder::new("regression_check").auto_increment(false)); + idb_builder +} + +pub trait WasmMigrationExt: Entity +where + Self: 'static, +{ + type EntityTypeV1_0_0: EntityV1_0_0; + + async fn convert_from_v_1_2_0_or_earlier(connection: &ConnectionV1_0_0) -> CryptoKeystoreResult> { + // We can use the v1 keystore because it didn't change between v1.0.0 and v1.0.2. + // Further, v1.0.0 migrates automatically from any earlier version. + let converted_records = connection + .find_all::(EntityFindParamsV1_0_0::default()) + .await? + .iter() + .map(|old_record| { + let serialized = postcard::to_stdvec(old_record)?; + postcard::from_bytes::(&serialized).map_err(Into::into) + }) + .collect::>>() + .into_iter() + .collect::, CryptoKeystoreError>>()?; + Ok(converted_records) + } +} + +impl WasmMigrationExt for MlsCredential { + type EntityTypeV1_0_0 = MlsCredentialV1_0_0; +} diff --git a/keystore/src/connection/platform/wasm/mod.rs b/keystore/src/connection/platform/wasm/mod.rs index b38ab151da..56f4babdb5 100644 --- a/keystore/src/connection/platform/wasm/mod.rs +++ b/keystore/src/connection/platform/wasm/mod.rs @@ -14,15 +14,17 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see http://www.gnu.org/licenses/. +use crate::connection::platform::wasm::migration::migrate; use crate::{ connection::{DatabaseConnection, DatabaseConnectionRequirements}, CryptoKeystoreError, CryptoKeystoreResult, }; -use idb::builder::{DatabaseBuilder, IndexBuilder, ObjectStoreBuilder}; -use idb::{Factory, KeyPath}; +use idb::Factory; use std::sync::OnceLock; +mod migration; pub mod storage; + use self::storage::{WasmEncryptedStorage, WasmStorageWrapper}; #[derive(Debug)] @@ -107,113 +109,30 @@ impl DatabaseConnection for WasmConnection { } } - let idb_builder = DatabaseBuilder::new(&name) - .version(version) - .add_object_store( - ObjectStoreBuilder::new("mls_credentials") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id"))) - .add_index(IndexBuilder::new("credential".into(), KeyPath::new_single("credential")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_signature_keypairs") - .auto_increment(false) - .add_index(IndexBuilder::new( - "signature_scheme".into(), - KeyPath::new_single("signature_scheme"), - )) - .add_index(IndexBuilder::new("signature_pk".into(), KeyPath::new_single("pk"))), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_hpke_private_keys") - .auto_increment(false) - .add_index(IndexBuilder::new("pk".into(), KeyPath::new_single("pk")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_encryption_keypairs") - .auto_increment(false) - .add_index(IndexBuilder::new("pk".into(), KeyPath::new_single("pk")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_epoch_encryption_keypairs") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_psk_bundles") - .auto_increment(false) - .add_index(IndexBuilder::new("psk_id".into(), KeyPath::new_single("psk_id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_keypackages") - .auto_increment(false) - .add_index( - IndexBuilder::new("keypackage_ref".into(), KeyPath::new_single("keypackage_ref")).unique(true), - ), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_groups") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_pending_groups") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("mls_pending_messages") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id"))), - ) - .add_object_store( - ObjectStoreBuilder::new("e2ei_enrollment") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("e2ei_refresh_token") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("e2ei_acme_ca") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("e2ei_intermediate_certs") - .auto_increment(false) - .add_index( - IndexBuilder::new("ski_aki_pair".into(), KeyPath::new_single("ski_aki_pair")).unique(true), - ), - ) - .add_object_store(ObjectStoreBuilder::new("e2ei_crls").auto_increment(false).add_index( - IndexBuilder::new("distribution_point".into(), KeyPath::new_single("distribution_point")).unique(true), - )) - .add_object_store( - ObjectStoreBuilder::new("proteus_prekeys") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("proteus_identities") - .auto_increment(false) - .add_index(IndexBuilder::new("pk".into(), KeyPath::new_single("pk")).unique(true)), - ) - .add_object_store( - ObjectStoreBuilder::new("proteus_sessions") - .auto_increment(false) - .add_index(IndexBuilder::new("id".into(), KeyPath::new_single("id")).unique(true)), - ); - - #[cfg(feature = "idb-regression-test")] - let idb_builder = - idb_builder.add_object_store(ObjectStoreBuilder::new("regression_check").auto_increment(false)); - - let idb = idb_builder.build().await?; + let factory = Factory::new()?; + + let open_existing = factory.open(&name, None)?; + let existing_db = open_existing.await?; + let mut migrated_version = existing_db.version()?; + + let idb = if migrated_version == version { + // Migration is not needed, just return existing db + existing_db + } else { + // Migration is needed + existing_db.close(); + + while migrated_version < version { + migrated_version = migrate(migrated_version, version, &name, key).await?; + } + + let open_request = factory.open(&name, Some(version))?; + let migrated_db = open_request.await?; + migrated_db + }; let storage = WasmStorageWrapper::Persistent(idb); + let conn = WasmEncryptedStorage::new(key, storage); Ok(Self { name, conn }) diff --git a/keystore/src/error.rs b/keystore/src/error.rs index d58eb53f75..dcee0ecd94 100644 --- a/keystore/src/error.rs +++ b/keystore/src/error.rs @@ -153,6 +153,12 @@ pub enum CryptoKeystoreError { #[cfg(target_family = "wasm")] #[error(transparent)] IdbError(#[from] idb::Error), + #[cfg(target_family = "wasm")] + #[error(transparent)] + CryptoKeystoreErrorV1_0_0(#[from] keystore_v_1_0_0::CryptoKeystoreError), + #[cfg(target_family = "wasm")] + #[error("Migration from the version {0} is not supported")] + MigrationNotSupported(u32), } #[cfg(target_family = "wasm")]