167 lines
8.4 KiB
Rust
167 lines
8.4 KiB
Rust
// file: crates/ksp-wallet-lib/src/migration.rs
|
|
// version: 1
|
|
|
|
//! Explicit authenticated native Wallet migration operations.
|
|
|
|
/// Converts one authenticated V1 document snapshot into a fresh V2 binary Wallet in memory.
|
|
///
|
|
/// The OWNER password authenticates the V1 source and is reused for the V2 OWNER credential. Because V1 and V2 slot AAD are domain-separated, an enabled
|
|
/// V1 VIEW slot cannot be copied byte-for-byte: the caller must provide the target VIEW password used to rewrap the migrated V2 metadata key. That value
|
|
/// may be the current VIEW password or a replacement chosen under OWNER authority. A disabled source VIEW must remain disabled during pure format migration.
|
|
/// No source bytes are modified by this function.
|
|
pub async fn migrate_wallet_v1_to_v2(
|
|
source: &[u8],
|
|
owner_password: crate::OwnerPassword,
|
|
target_view_password: std::option::Option<crate::ViewPassword>,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let source_envelope = match crate::KspWalletEnvelopeV1::parse_json(source) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let source_owner_password = owner_password.duplicate_for_internal_use();
|
|
let source_owner = match crate::open_wallet_owner_v1(source, source_owner_password).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = validate_view_migration_shape(source_envelope.view_descriptor().enabled(), target_view_password.is_some()) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
let metadata_payload = crate::MetadataPayloadV1::from_info(source_owner.info());
|
|
let solana_keypair = match source_owner.clone_v1_solana_keypair_for_migration() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let mut migrated =
|
|
match crate::create_wallet_v2_from_keypair(owner_password, target_view_password, crate::WalletCreateMetadata::default(), solana_keypair).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = migrated.replace_metadata_for_migration(metadata_payload) {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if migrated.pubkey() != source_owner.pubkey() {
|
|
return std::result::Result::Err(migration_error("Migrated Wallet public identity changed unexpectedly"));
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_migrate_v1_to_v2",
|
|
source_format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
|
target_format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
|
view_enabled = source_envelope.view_descriptor().enabled(),
|
|
"authenticated native wallet migrated in memory"
|
|
);
|
|
return std::result::Result::Ok(migrated);
|
|
}
|
|
|
|
/// Migrates one V1 file snapshot into a new no-clobber V2 destination while preserving the V1 source file.
|
|
///
|
|
/// The source is read through the bounded Wallet persistence boundary. Existing destinations are never overwritten. This operation never performs an
|
|
/// implicit migration when a Wallet is merely opened.
|
|
pub async fn migrate_wallet_file_v1_to_v2(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
destination: impl std::convert::AsRef<std::path::Path>,
|
|
owner_password: crate::OwnerPassword,
|
|
target_view_password: std::option::Option<crate::ViewPassword>,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
let source_path = source.as_ref().to_path_buf();
|
|
let destination_path = destination.as_ref().to_path_buf();
|
|
let source_bytes = match crate::read_wallet_file_async(source_path).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let migrated = match migrate_wallet_v1_to_v2(source_bytes.as_slice(), owner_password, target_view_password).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let serialized = match migrated.to_native_bytes() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = crate::persist_new_wallet_content(destination_path, serialized).await {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_migrate_v1_to_v2_file",
|
|
source_format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
|
target_format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
|
publication = "no_clobber",
|
|
"authenticated native wallet migration published to a new destination"
|
|
);
|
|
return std::result::Result::Ok(migrated);
|
|
}
|
|
|
|
/// Atomically replaces one authenticated V1 file with its V2 migration when the source state is still current.
|
|
///
|
|
/// The V1 envelope observed before OWNER authentication is retained as the expected state. Publication uses the existing state-conflict protected atomic
|
|
/// replacement boundary, so concurrent or stale changes abort rather than overwriting a newer Wallet. Failure before publication leaves the V1 bytes intact.
|
|
pub async fn migrate_wallet_file_v1_to_v2_in_place(
|
|
source: impl std::convert::AsRef<std::path::Path>,
|
|
owner_password: crate::OwnerPassword,
|
|
target_view_password: std::option::Option<crate::ViewPassword>,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
|
return migrate_wallet_file_v1_to_v2_in_place_with_hook(source.as_ref().to_path_buf(), owner_password, target_view_password, || {
|
|
return std::result::Result::Ok(());
|
|
})
|
|
.await;
|
|
}
|
|
|
|
async fn migrate_wallet_file_v1_to_v2_in_place_with_hook<F>(
|
|
source_path: std::path::PathBuf,
|
|
owner_password: crate::OwnerPassword,
|
|
target_view_password: std::option::Option<crate::ViewPassword>,
|
|
before_publish: F,
|
|
) -> ksp_core_lib::Result<crate::WalletOwner>
|
|
where
|
|
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
|
|
{
|
|
let source_bytes = match crate::read_wallet_file_async(source_path.clone()).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let expected_v1 = match crate::KspWalletEnvelopeV1::parse_json(source_bytes.as_slice()) {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let migrated = match migrate_wallet_v1_to_v2(source_bytes.as_slice(), owner_password, target_view_password).await {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
let serialized = match migrated.to_native_bytes() {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
|
};
|
|
if let std::result::Result::Err(error) = before_publish() {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
if let std::result::Result::Err(error) = crate::replace_wallet_file_v1(source_path, expected_v1, serialized).await {
|
|
return std::result::Result::Err(error);
|
|
}
|
|
ksp_logging_lib::debug!(
|
|
target: crate::TRACING_TARGET,
|
|
operation = "wallet_migrate_v1_to_v2_file",
|
|
source_format_version = crate::KSPWALLET_FORMAT_VERSION_V1,
|
|
target_format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
|
publication = "atomic_replace",
|
|
"authenticated native wallet migration replaced the current V1 source"
|
|
);
|
|
return std::result::Result::Ok(migrated);
|
|
}
|
|
|
|
fn validate_view_migration_shape(source_view_enabled: bool, target_view_present: bool) -> ksp_core_lib::Result<()> {
|
|
if source_view_enabled != target_view_present {
|
|
return std::result::Result::Err(migration_error(
|
|
"V1 -> V2 migration must preserve whether VIEW capability is enabled; provide a target VIEW password exactly when the source VIEW is enabled",
|
|
));
|
|
}
|
|
return std::result::Result::Ok(());
|
|
}
|
|
|
|
fn migration_error(message: &'static str) -> ksp_core_lib::Error {
|
|
return ksp_core_lib::Error::new(crate::ERROR_CODE_MIGRATION_INVALID, message);
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "../unit_tests/migration.rs"]
|
|
mod tests;
|