v0.2.6-pre.016
This commit is contained in:
@@ -1,11 +1,54 @@
|
||||
// file: crates/ksp-wallet-lib/src/persistence.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
//! Async-first native Wallet V1 filesystem persistence.
|
||||
//! Async-first native Wallet V1/V2 filesystem persistence and version-neutral dispatch.
|
||||
|
||||
use std::io::Read; // rust-rules: trait-import
|
||||
use std::io::Write; // rust-rules: trait-import
|
||||
|
||||
/// Creates a new native `.kspwallet` using [`crate::DEFAULT_WALLET_FORMAT`].
|
||||
///
|
||||
/// The default is explicitly V2 in this release and does not track future `LATEST_SUPPORTED_WALLET_FORMAT` values automatically.
|
||||
pub async fn create_wallet_file(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
return match crate::DEFAULT_WALLET_FORMAT {
|
||||
crate::WalletFormat::V1 => create_wallet_file_v1(destination, owner_password, view_password, metadata).await,
|
||||
crate::WalletFormat::V2 => create_wallet_file_v2(destination, owner_password, view_password, metadata).await,
|
||||
};
|
||||
}
|
||||
|
||||
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
|
||||
pub async fn create_wallet_file_v2(
|
||||
destination: impl std::convert::AsRef<std::path::Path>,
|
||||
owner_password: crate::OwnerPassword,
|
||||
view_password: std::option::Option<crate::ViewPassword>,
|
||||
metadata: crate::WalletCreateMetadata,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let destination = destination.as_ref().to_path_buf();
|
||||
let owner = match crate::create_wallet_v2(owner_password, view_password, metadata).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let serialized = match owner.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) = persist_new_wallet_async(destination, serialized).await {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
ksp_logging_lib::debug!(
|
||||
target: crate::TRACING_TARGET,
|
||||
operation = "wallet_create_file",
|
||||
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
|
||||
"native wallet persisted with no-clobber semantics"
|
||||
);
|
||||
return std::result::Result::Ok(owner);
|
||||
}
|
||||
|
||||
/// Creates a new native `.kspwallet` V1 at `destination` without overwriting an existing path.
|
||||
///
|
||||
/// The complete encrypted document is created in memory first, written and synchronized through a temporary file in the destination directory, then
|
||||
@@ -40,6 +83,30 @@ pub async fn create_wallet_file_v1(
|
||||
return std::result::Result::Ok(owner);
|
||||
}
|
||||
|
||||
/// Opens a supported native `.kspwallet` file with VIEW capability after bounded V1/V2 detection.
|
||||
pub async fn open_wallet_view_file(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_view(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
|
||||
pub async fn open_wallet_view_file_v2(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::ViewPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletView> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_view_v2(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V1 from `source` with VIEW capability.
|
||||
///
|
||||
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and VIEW KDF flow.
|
||||
@@ -56,6 +123,30 @@ pub async fn open_wallet_view_file_v1(
|
||||
return crate::open_wallet_view_v1(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a supported native `.kspwallet` file with OWNER capability after bounded V1/V2 detection.
|
||||
pub async fn open_wallet_owner_file(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::OwnerPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_owner(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
|
||||
pub async fn open_wallet_owner_file_v2(
|
||||
source: impl std::convert::AsRef<std::path::Path>,
|
||||
password: crate::OwnerPassword,
|
||||
) -> ksp_core_lib::Result<crate::WalletOwner> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::open_wallet_owner_v2(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Opens a native `.kspwallet` V1 from `source` with OWNER capability.
|
||||
///
|
||||
/// The file is read through the bounded async persistence boundary before the normal strict parser, OWNER state-signature verification and OWNER KDF flow.
|
||||
@@ -72,6 +163,24 @@ pub async fn open_wallet_owner_file_v1(
|
||||
return crate::open_wallet_owner_v1(bytes.as_slice(), password).await;
|
||||
}
|
||||
|
||||
/// Reads and verifies the locked projection of a supported native `.kspwallet` file after bounded V1/V2 detection.
|
||||
pub async fn inspect_locked_wallet_file(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::inspect_locked_wallet(bytes.as_slice());
|
||||
}
|
||||
|
||||
/// Reads and verifies the locked projection of a native `.kspwallet` V2 binary file without running a password KDF.
|
||||
pub async fn inspect_locked_wallet_file_v2(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let bytes = match read_wallet_file_async(source.as_ref().to_path_buf()).await {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
return crate::inspect_locked_wallet_v2(bytes.as_slice());
|
||||
}
|
||||
|
||||
/// Reads and verifies the locked projection of a native `.kspwallet` V1 from `source` without running a password KDF.
|
||||
pub async fn inspect_locked_wallet_file_v1(source: impl std::convert::AsRef<std::path::Path>) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
|
||||
let source = source.as_ref().to_path_buf();
|
||||
@@ -88,6 +197,11 @@ pub(crate) async fn persist_new_wallet_content_v1(destination: std::path::PathBu
|
||||
return persist_new_wallet_async(destination, content).await;
|
||||
}
|
||||
|
||||
/// Persists one already serialized native Wallet document with no-clobber semantics.
|
||||
pub(crate) async fn persist_new_wallet_content(destination: std::path::PathBuf, content: std::vec::Vec<u8>) -> ksp_core_lib::Result<()> {
|
||||
return persist_new_wallet_async(destination, content).await;
|
||||
}
|
||||
|
||||
/// Replaces wallet file v1.
|
||||
pub(crate) async fn replace_wallet_file_v1(
|
||||
destination: std::path::PathBuf,
|
||||
@@ -103,6 +217,21 @@ pub(crate) async fn replace_wallet_file_v1(
|
||||
};
|
||||
}
|
||||
|
||||
/// Replaces one authenticated V2 Wallet file only if the current V2 state still matches the caller's expected state.
|
||||
pub(crate) async fn replace_wallet_file_v2(
|
||||
destination: std::path::PathBuf,
|
||||
expected_current: crate::KspWalletEnvelopeV2,
|
||||
content: std::vec::Vec<u8>,
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let task = tokio::task::spawn_blocking(move || {
|
||||
return replace_wallet_file_v2_checked_blocking(destination.as_path(), &expected_current, content.as_slice());
|
||||
});
|
||||
return match task.await {
|
||||
std::result::Result::Ok(result) => result,
|
||||
std::result::Result::Err(error) => std::result::Result::Err(blocking_atomic_error("replace_task", error)),
|
||||
};
|
||||
}
|
||||
|
||||
async fn read_wallet_file_async(source: std::path::PathBuf) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
|
||||
let task = tokio::task::spawn_blocking(move || return read_wallet_file_blocking(source.as_path()));
|
||||
return match task.await {
|
||||
@@ -162,6 +291,18 @@ fn replace_wallet_file_checked_blocking(
|
||||
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state(destination, expected_current));
|
||||
}
|
||||
|
||||
fn replace_wallet_file_v2_checked_blocking(
|
||||
destination: &std::path::Path,
|
||||
expected_current: &crate::KspWalletEnvelopeV2,
|
||||
content: &[u8],
|
||||
) -> ksp_core_lib::Result<()> {
|
||||
let current_check = verify_expected_wallet_state_v2(destination, expected_current);
|
||||
if let std::result::Result::Err(error) = current_check {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
return replace_wallet_file_with_hook(destination, content, || return verify_expected_wallet_state_v2(destination, expected_current));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn replace_wallet_file_blocking(destination: &std::path::Path, content: &[u8]) -> ksp_core_lib::Result<()> {
|
||||
return replace_wallet_file_with_hook(destination, content, || return std::result::Result::Ok(()));
|
||||
@@ -186,6 +327,24 @@ fn verify_expected_wallet_state(destination: &std::path::Path, expected_current:
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn verify_expected_wallet_state_v2(destination: &std::path::Path, expected_current: &crate::KspWalletEnvelopeV2) -> ksp_core_lib::Result<()> {
|
||||
let current_bytes = match read_wallet_file_blocking(destination) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let current = match crate::KspWalletEnvelopeV2::parse_binary(current_bytes.as_slice()) {
|
||||
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::verify_state_signature_v2(¤t) {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
if ¤t != expected_current {
|
||||
return std::result::Result::Err(state_conflict_error());
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn replace_wallet_file_with_hook<F>(destination: &std::path::Path, content: &[u8], before_publish: F) -> ksp_core_lib::Result<()>
|
||||
where
|
||||
F: std::ops::FnOnce() -> ksp_core_lib::Result<()>,
|
||||
|
||||
Reference in New Issue
Block a user