Skip to content

Commit

Permalink
feat: implement idb migration for one entity [WPB-10144]
Browse files Browse the repository at this point in the history
  • Loading branch information
SimonThormeyer committed Aug 29, 2024
1 parent 0283de6 commit e38ab3b
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 107 deletions.
196 changes: 196 additions & 0 deletions keystore/src/connection/platform/wasm/migration.rs
Original file line number Diff line number Diff line change
@@ -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<u32> {
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<ConnectionType = KeystoreDatabaseConnection>
where
Self: 'static,
{
type EntityTypeV1_0_0: EntityV1_0_0<ConnectionType = KeystoreDatabaseConnectionV1_0_0>;

async fn convert_from_v_1_2_0_or_earlier(connection: &ConnectionV1_0_0) -> CryptoKeystoreResult<Vec<Self>> {
// 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::<Self::EntityTypeV1_0_0>(EntityFindParamsV1_0_0::default())
.await?
.iter()
.map(|old_record| {
let serialized = postcard::to_stdvec(old_record)?;
postcard::from_bytes::<Self>(&serialized).map_err(Into::into)
})
.collect::<Vec<Result<Self, CryptoKeystoreError>>>()
.into_iter()
.collect::<Result<Vec<Self>, CryptoKeystoreError>>()?;
Ok(converted_records)
}
}

impl WasmMigrationExt for MlsCredential {
type EntityTypeV1_0_0 = MlsCredentialV1_0_0;
}
133 changes: 26 additions & 107 deletions keystore/src/connection/platform/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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 })
Expand Down
6 changes: 6 additions & 0 deletions keystore/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down

0 comments on commit e38ab3b

Please sign in to comment.