v0.2.6-pre.016

This commit is contained in:
2026-08-22 08:18:38 +02:00
parent 946d88322b
commit 92a2c4fff3
43 changed files with 2813 additions and 280 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/constants.rs
// version: 8
// version: 9
//! Wallet-owned constants.
@@ -91,6 +91,14 @@ pub const KSPWALLET_V1_XCHACHA_NONCE_BYTES: usize = 24;
pub const KSPWALLET_V2_AEAD_TAG_BYTES: usize = KSPWALLET_V1_AEAD_TAG_BYTES;
/// Argon2 version serialized by V2 key slots.
pub const KSPWALLET_V2_ARGON2_VERSION: u32 = KSPWALLET_V1_ARGON2_VERSION;
/// Default Argon2id iteration count for newly created V2 key slots.
pub const KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS: u32 = KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS;
/// Default Argon2id memory cost for newly created V2 key slots.
pub const KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB: u32 = KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB;
/// Default Argon2id parallelism for newly created V2 key slots.
pub const KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM: u32 = KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM;
/// Default KDF salt size generated independently for every newly created V2 key slot.
pub const KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES: usize = KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES;
/// Byte length of an Ed25519 public key used as V2 Wallet format authority.
pub const KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES: usize = KSPWALLET_V1_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of an Ed25519 detached V2 state signature.
@@ -123,6 +131,8 @@ pub const KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNE
pub const KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-OWNER-SLOT";
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
pub const KSPWALLET_V2_SECRET_AAD_DOMAIN: &[u8] = b"KSPWALLET-V2-AAD-SECRET";
/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile.
pub const KSPWALLET_V2_SECRET_PLAINTEXT_BYTES: usize = KSPWALLET_V1_SECRET_PLAINTEXT_BYTES;
/// Byte length of every V2 key-slot identifier.
pub const KSPWALLET_V2_SLOT_ID_BYTES: usize = KSPWALLET_V1_SLOT_ID_BYTES;
/// Domain separator for the V2 OWNER state-signature transcript.

View File

@@ -1,12 +1,12 @@
// file: crates/ksp-wallet-lib/src/crypto.rs
// version: 4
// version: 5
//! In-memory cryptographic primitives for native `.kspwallet` V1.
//! In-memory cryptographic primitives shared by native `.kspwallet` V1/V2 runtime.
use chacha20poly1305::KeyInit; // rust-rules: trait-import
use chacha20poly1305::aead::Aead; // rust-rules: trait-import
/// Exact V1 content-key and password-derived-key size in bytes.
/// Exact content-key and password-derived-key size in bytes for the current V1/V2 profiles.
pub(crate) const SECRET_KEY_BYTES: usize = 32;
/// Owned 32-byte secret key with redacted diagnostics and drop-time zeroization.
@@ -68,6 +68,11 @@ pub(crate) fn derive_password_key(password: &[u8], kdf: &crate::WalletKdfParamet
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Derives one V2 password wrapping key from serialized Argon2id parameters.
pub(crate) fn derive_password_key_v2(password: &[u8], kdf: &crate::WalletKdfParametersV2) -> ksp_core_lib::Result<crate::SecretKeyV1> {
return derive_argon2id(password, kdf.salt(), kdf.memory_kib(), kdf.iterations(), kdf.parallelism());
}
/// Wraps one 32-byte content key with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) fn wrap_key(
wrapping_key: &crate::SecretKeyV1,

View File

@@ -0,0 +1,111 @@
// file: crates/ksp-wallet-lib/src/format.rs
// version: 2
//! Native `.kspwallet` version selection and bounded dispatch detection.
/// Explicit native `.kspwallet` wire format understood by this Wallet release.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WalletFormat {
/// Stable historical JSON/Base64url format.
V1,
/// Canonical binary KSP format.
V2,
}
impl WalletFormat {
/// Returns the serialized native format version.
#[must_use]
pub const fn version(self) -> u32 {
return match self {
Self::V1 => crate::KSPWALLET_FORMAT_VERSION_V1,
Self::V2 => crate::KSPWALLET_FORMAT_VERSION_V2,
};
}
}
/// Default format selected by the non-versioned native Wallet creation/import APIs.
///
/// This is intentionally independent from [`LATEST_SUPPORTED_WALLET_FORMAT`]. Adding a future format does not implicitly move this default.
pub const DEFAULT_WALLET_FORMAT: WalletFormat = WalletFormat::V2;
/// Highest native Wallet format understood by this release.
pub const LATEST_SUPPORTED_WALLET_FORMAT: WalletFormat = WalletFormat::V2;
/// Creates a new in-memory native Wallet using [`DEFAULT_WALLET_FORMAT`].
pub async fn create_wallet(
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
return match DEFAULT_WALLET_FORMAT {
WalletFormat::V1 => crate::create_wallet_v1(owner_password, view_password, metadata).await,
WalletFormat::V2 => crate::create_wallet_v2(owner_password, view_password, metadata).await,
};
}
/// Opens VIEW from any currently supported native Wallet wire format.
pub async fn open_wallet_view(source: &[u8], password: crate::ViewPassword) -> ksp_core_lib::Result<crate::WalletView> {
return match detect_wallet_format(source) {
std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_view_v1(source, password).await,
std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_view_v2(source, password).await,
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Opens OWNER from any currently supported native Wallet wire format.
pub async fn open_wallet_owner(source: &[u8], password: crate::OwnerPassword) -> ksp_core_lib::Result<crate::WalletOwner> {
return match detect_wallet_format(source) {
std::result::Result::Ok(WalletFormat::V1) => crate::open_wallet_owner_v1(source, password).await,
std::result::Result::Ok(WalletFormat::V2) => crate::open_wallet_owner_v2(source, password).await,
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Inspects the authenticated locked projection of any currently supported native Wallet wire format.
pub fn inspect_locked_wallet(source: &[u8]) -> ksp_core_lib::Result<crate::LockedWalletInfo> {
return match detect_wallet_format(source) {
std::result::Result::Ok(WalletFormat::V1) => crate::inspect_locked_wallet_v1(source),
std::result::Result::Ok(WalletFormat::V2) => crate::inspect_locked_wallet_v2(source),
std::result::Result::Err(error) => std::result::Result::Err(error),
};
}
/// Detects the native wire format without decrypting or authenticating the document.
///
/// Detection only selects the strict parser. The selected V1/V2 parser remains authoritative for all structural and cryptographic validation.
pub fn detect_wallet_format(source: &[u8]) -> ksp_core_lib::Result<WalletFormat> {
if source.len() > crate::KSPWALLET_MAX_FILE_BYTES {
return std::result::Result::Err(format_error("Wallet document exceeds the maximum size"));
}
if source.starts_with(crate::KSPWALLET_MAGIC.as_bytes()) {
let version_start = crate::KSPWALLET_MAGIC.len();
let version_end = version_start + 2;
let version_bytes = match source.get(version_start..version_end) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(format_error("Wallet binary header is truncated")),
};
let version = u16::from_be_bytes([version_bytes[0], version_bytes[1]]);
return match u32::from(version) {
crate::KSPWALLET_FORMAT_VERSION_V2 => std::result::Result::Ok(WalletFormat::V2),
other => std::result::Result::Err(version_error(other)),
};
}
let first_non_whitespace = source.iter().copied().find(|byte| return !byte.is_ascii_whitespace());
if first_non_whitespace == std::option::Option::Some(b'{') {
return std::result::Result::Ok(WalletFormat::V1);
}
return std::result::Result::Err(format_error("Wallet document does not match a supported native framing"));
}
fn format_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
}
fn version_error(version: u32) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_VERSION_UNSUPPORTED, "Wallet format version is not supported")
.with_context("format_version", version.to_string());
}
#[cfg(test)]
#[path = "../unit_tests/format.rs"]
mod tests;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/lib.rs
// version: 12
// version: 13
#![warn(missing_docs)]
#![deny(unreachable_pub)]
@@ -16,7 +16,8 @@
//! replacement. `0.2.5-pre.008` adds bounded Solana CLI JSON and canonical full-keypair Base58 import/export adapters with safe inspection and no-clobber
//! native import/export publication. `0.2.5-pre.009` adds adversarial security/compliance canaries and records the final dependency/interop audit before
//! documentation closure. `0.2.6-pre.015` defines the strict binary `.kspwallet` V2 wire and its bounded canonical codec without yet changing the public
//! persistence default or Wallet Desk dispatch. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! persistence default or Wallet Desk dispatch. `0.2.6-pre.016` adds complete V2 create/open/administration, stable version-neutral dispatch, explicit V1/V2
//! APIs and moves the default native creation/import format to V2 without coupling that default to the latest supported version. Public keys are consumed exclusively through the [`ksp_core_lib::Pubkey`] re-export owned by KSP Core, and behavioral
//! observability uses only
//! `ksp-logging-lib` with the explicit crate target defined in `src/constants.rs`.
@@ -24,16 +25,19 @@ mod capability;
mod constants;
mod crypto;
mod error;
mod format;
mod metadata;
mod owner;
mod password;
mod payload;
mod persistence;
mod runtime;
mod transcript;
mod transcript_v2;
mod transfer;
mod view;
mod wallet;
mod wallet_v2;
mod wire;
mod wire_v2;
@@ -131,6 +135,14 @@ pub use self::constants::KSPWALLET_V1_XCHACHA_NONCE_BYTES;
pub use self::constants::KSPWALLET_V2_AEAD_TAG_BYTES;
/// Argon2 version serialized by `.kspwallet` V2 key slots.
pub use self::constants::KSPWALLET_V2_ARGON2_VERSION;
/// Default Argon2id iteration count for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS;
/// Default Argon2id memory cost for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB;
/// Default Argon2id parallelism for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM;
/// Default KDF salt size generated for newly created V2 slots.
pub use self::constants::KSPWALLET_V2_DEFAULT_KDF_SALT_BYTES;
/// Byte length of the V2 Ed25519 format-authority public key.
pub use self::constants::KSPWALLET_V2_ED25519_PUBLIC_KEY_BYTES;
/// Byte length of the V2 Ed25519 detached state signature.
@@ -163,6 +175,8 @@ pub use self::constants::KSPWALLET_V2_OWNER_CONTROL_AAD_DOMAIN;
pub use self::constants::KSPWALLET_V2_OWNER_SLOT_AAD_DOMAIN;
/// Domain separator for V2 OWNER-only secret compartment AEAD AAD.
pub use self::constants::KSPWALLET_V2_SECRET_AAD_DOMAIN;
/// Exact Solana keypair plaintext size retained by the initial V2 secret payload profile.
pub use self::constants::KSPWALLET_V2_SECRET_PLAINTEXT_BYTES;
/// Byte length of every V2 key-slot identifier.
pub use self::constants::KSPWALLET_V2_SLOT_ID_BYTES;
/// Domain separator for the V2 OWNER state-signature transcript.
@@ -205,9 +219,27 @@ pub use self::error::ERROR_CODE_STATE_CONFLICT;
pub use self::error::ERROR_CODE_TRANSFER_FORMAT_UNSUPPORTED;
/// Error code used when a VIEW unlock attempt fails without exposing a finer cryptographic oracle.
pub use self::error::ERROR_CODE_VIEW_UNLOCK_FAILED;
/// Default native Wallet format selected by non-versioned create/import APIs.
pub use self::format::DEFAULT_WALLET_FORMAT;
/// Highest native Wallet format supported by this release.
pub use self::format::LATEST_SUPPORTED_WALLET_FORMAT;
/// Explicit native Wallet wire-format selector.
pub use self::format::WalletFormat;
/// Creates a new in-memory native Wallet using the explicit default format.
pub use self::format::create_wallet;
/// Detects V1 JSON versus V2 binary framing before strict parser dispatch.
pub use self::format::detect_wallet_format;
/// Inspects any supported in-memory native Wallet document.
pub use self::format::inspect_locked_wallet;
/// Opens OWNER from any supported in-memory native Wallet document.
pub use self::format::open_wallet_owner;
/// Opens VIEW from any supported in-memory native Wallet document.
pub use self::format::open_wallet_view;
/// Minimal non-secret information available while a native Wallet remains locked.
pub use self::metadata::LockedWalletInfo;
/// Protected initial metadata supplied to native Wallet creation.
/// Protected initial metadata supplied to version-neutral native Wallet creation.
pub type WalletCreateMetadata = self::metadata::WalletCreateMetadataV1;
/// Protected initial metadata payload profile retained for explicit V1 compatibility.
pub use self::metadata::WalletCreateMetadataV1;
/// Safe metadata projection produced after VIEW or OWNER authorization.
pub use self::metadata::WalletInfo;
@@ -219,22 +251,46 @@ pub use self::owner::WalletOwner;
pub use self::password::OwnerPassword;
/// Owned VIEW password material with redacted diagnostics and drop-time zeroization.
pub use self::password::ViewPassword;
/// Creates and no-clobber persists a native Wallet using the explicit default format.
pub use self::persistence::create_wallet_file;
/// Creates and no-clobber persists a new native `.kspwallet` V1 file.
pub use self::persistence::create_wallet_file_v1;
/// Creates and no-clobber persists a new native `.kspwallet` V2 binary file.
pub use self::persistence::create_wallet_file_v2;
/// Reads and verifies a locked native Wallet using V1/V2 auto-detection.
pub use self::persistence::inspect_locked_wallet_file;
/// Reads and verifies a locked native `.kspwallet` V1 file.
pub use self::persistence::inspect_locked_wallet_file_v1;
/// Reads and verifies a locked native `.kspwallet` V2 binary file.
pub use self::persistence::inspect_locked_wallet_file_v2;
/// Opens a native Wallet with OWNER capability using V1/V2 auto-detection.
pub use self::persistence::open_wallet_owner_file;
/// Opens a native `.kspwallet` V1 file with OWNER capability.
pub use self::persistence::open_wallet_owner_file_v1;
/// Opens a native `.kspwallet` V2 binary file with OWNER capability.
pub use self::persistence::open_wallet_owner_file_v2;
/// Opens a native Wallet with VIEW capability using V1/V2 auto-detection.
pub use self::persistence::open_wallet_view_file;
/// Opens a native `.kspwallet` V1 file with VIEW capability.
pub use self::persistence::open_wallet_view_file_v1;
/// Opens a native `.kspwallet` V2 binary file with VIEW capability.
pub use self::persistence::open_wallet_view_file_v2;
/// Explicit secret-transfer format supported by Wallet.
pub use self::transfer::WalletTransferFormat;
/// Safe public identity projection of one validated secret-transfer source.
pub use self::transfer::WalletTransferInspection;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet using the explicit default format.
pub use self::transfer::import_wallet_transfer;
/// Imports one bounded external transfer file into a new no-clobber native Wallet using the explicit default format.
pub use self::transfer::import_wallet_transfer_file;
/// Imports one bounded external transfer file into a new no-clobber native Wallet V1.
pub use self::transfer::import_wallet_transfer_file_v1;
/// Imports one bounded external transfer file into a new no-clobber native Wallet V2.
pub use self::transfer::import_wallet_transfer_file_v2;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V1.
pub use self::transfer::import_wallet_transfer_v1;
/// Imports one in-memory transfer payload into a new no-clobber native Wallet V2.
pub use self::transfer::import_wallet_transfer_v2;
/// Validates one in-memory transfer payload and exposes only its derived public identity.
pub use self::transfer::inspect_wallet_transfer;
/// Validates one bounded external transfer file and exposes only its derived public identity.
@@ -249,6 +305,14 @@ pub use self::wallet::inspect_locked_wallet_v1;
pub use self::wallet::open_wallet_owner_v1;
/// Opens the VIEW capability from a native Wallet V1 document.
pub use self::wallet::open_wallet_view_v1;
/// Creates a new in-memory native Wallet V2.
pub use self::wallet_v2::create_wallet_v2;
/// Parses and verifies locked native Wallet V2 state without unlocking protected metadata.
pub use self::wallet_v2::inspect_locked_wallet_v2;
/// Opens the OWNER capability from a native Wallet V2 document.
pub use self::wallet_v2::open_wallet_owner_v2;
/// Opens the VIEW capability from a native Wallet V2 document.
pub use self::wallet_v2::open_wallet_view_v2;
/// Strict semantic representation of one parsed native `.kspwallet` V1 envelope.
pub use self::wire::KspWalletEnvelopeV1;
/// Authenticated-encryption algorithm fixed by native Wallet V1.
@@ -308,6 +372,8 @@ pub(crate) use self::crypto::SecretKeyV1;
pub(crate) use self::crypto::decrypt_bytes;
/// Derives one V1 password wrapping key from serialized Argon2id parameters.
pub(crate) use self::crypto::derive_password_key;
/// Derives one V2 password wrapping key from serialized Argon2id parameters.
pub(crate) use self::crypto::derive_password_key_v2;
/// Encrypts bounded plaintext bytes with XChaCha20-Poly1305 and caller-provided domain-separated AAD.
pub(crate) use self::crypto::encrypt_bytes;
/// Generates a fresh fixed-size byte array from the operating-system CSPRNG.
@@ -328,7 +394,9 @@ pub(crate) use self::payload::decode_owner_control_payload;
pub(crate) use self::payload::encode_initial_metadata_payload;
/// Encodes owner control payload.
pub(crate) use self::payload::encode_owner_control_payload;
/// Internal no-clobber native persistence path shared by transfer adapters.
/// Internal no-clobber native persistence path shared by version-neutral transfer adapters.
pub(crate) use self::persistence::persist_new_wallet_content;
/// Internal V1 no-clobber native persistence path retained for explicit compatibility adapters.
pub(crate) use self::persistence::persist_new_wallet_content_v1;
/// Persists new wallet fault before publish.
#[cfg(test)]
@@ -341,9 +409,15 @@ pub(crate) use self::persistence::persist_new_wallet_for_test;
pub(crate) use self::persistence::replace_wallet_fault_before_publish;
/// Replaces wallet file v1.
pub(crate) use self::persistence::replace_wallet_file_v1;
/// Replaces wallet file v2.
pub(crate) use self::persistence::replace_wallet_file_v2;
/// Replaces wallet for test.
#[cfg(test)]
pub(crate) use self::persistence::replace_wallet_for_test;
/// Version-neutral OWNER runtime state shared by public Wallet handles.
pub(crate) use self::runtime::OwnerState;
/// Version-neutral VIEW runtime state shared by public Wallet handles.
pub(crate) use self::runtime::ViewState;
/// Internal deterministic compartment-AAD codec shared by Wallet crypto layers.
pub(crate) use self::transcript::compartment_aad;
/// Internal deterministic key-slot-AAD codec shared by Wallet crypto layers.
@@ -357,12 +431,20 @@ pub(crate) use self::transcript_v2::slot_aad_v2;
/// Internal deterministic V2 OWNER-state transcript codec shared by Wallet crypto layers.
pub(crate) use self::transcript_v2::state_transcript_v2;
/// Internal no-clobber transfer-file writer used only by OWNER export.
pub(crate) use self::transfer::write_wallet_transfer_file_v1;
pub(crate) use self::transfer::write_wallet_transfer_file;
/// Crate-internal `OwnerStateV1` state shared across the owning crate.
pub(crate) use self::wallet::OwnerStateV1;
/// Crate-internal `ViewStateV1` state shared across the owning crate.
pub(crate) use self::wallet::ViewStateV1;
/// Internal imported-keypair creation path shared by transfer adapters.
/// Internal imported-keypair V1 creation path shared by transfer adapters.
pub(crate) use self::wallet::create_wallet_v1_from_keypair;
/// Verifies state signature.
/// Verifies V1 state signature.
pub(crate) use self::wallet::verify_state_signature;
/// Crate-internal `OwnerStateV2` state shared across the owning crate.
pub(crate) use self::wallet_v2::OwnerStateV2;
/// Crate-internal `ViewStateV2` state shared across the owning crate.
pub(crate) use self::wallet_v2::ViewStateV2;
/// Internal imported-keypair V2 creation path shared by transfer adapters.
pub(crate) use self::wallet_v2::create_wallet_v2_from_keypair;
/// Verifies V2 state signature.
pub(crate) use self::wallet_v2::verify_state_signature_v2;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/metadata.rs
// version: 4
// version: 5
/// Protected metadata requested when creating a new native Wallet.
///
@@ -78,12 +78,13 @@ pub struct WalletInfo {
impl WalletInfo {
/// Creates a new `WalletInfo` value.
pub(crate) fn new(
format_version: u32,
capability: crate::WalletCapability,
pubkey: ksp_core_lib::Pubkey,
alias: std::option::Option<std::string::String>,
notes: std::vec::Vec<crate::WalletNote>,
) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, capability, pubkey, alias, notes };
return Self { format_version, capability, pubkey, alias, notes };
}
/// Returns the native Wallet format version parsed for this projection.
@@ -139,8 +140,8 @@ pub struct LockedWalletInfo {
impl LockedWalletInfo {
/// Creates a new `LockedWalletInfo` value.
pub(crate) const fn new(view_enabled: bool) -> Self {
return Self { format_version: crate::KSPWALLET_FORMAT_VERSION_V1, view_enabled };
pub(crate) const fn new(format_version: u32, view_enabled: bool) -> Self {
return Self { format_version, view_enabled };
}
/// Returns the native Wallet format version.

View File

@@ -1,21 +1,27 @@
// file: crates/ksp-wallet-lib/src/owner.rs
// version: 6
// version: 7
/// Authorized OWNER capability handle.
///
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by native Wallet V1. Secret key
/// OWNER exposes protected metadata, Solana message signing and the authenticated administration operations defined by the authenticated native Wallet format. Secret key
/// material remains encapsulated and is never exposed through a general-purpose getter.
pub struct WalletOwner {
info: crate::WalletInfo,
state: crate::OwnerStateV1,
state: crate::OwnerState,
}
impl WalletOwner {
/// Builds `WalletOwner` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerStateV1) -> Self {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::OwnerState) -> Self {
return Self { info, state };
}
/// Returns the native Wallet format version backing this authorized handle.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.info.format_version();
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
@@ -84,7 +90,7 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let result = crate::write_wallet_transfer_file_v1(destination.as_ref().to_path_buf(), encoded, format).await;
let result = crate::write_wallet_transfer_file(destination.as_ref().to_path_buf(), encoded, format).await;
if result.is_ok() {
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -181,11 +187,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_owner_password",
@@ -207,11 +215,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
@@ -229,11 +239,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_disable_view",
@@ -253,11 +265,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination.as_ref().to_path_buf(), self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_strong_view_state(envelope, metadata_key);
if let std::result::Result::Err(error) = self.state.apply_strong_view_state(envelope, metadata_key) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_recreate_view",
@@ -267,9 +281,16 @@ impl WalletOwner {
return std::result::Result::Ok(());
}
/// Serializes the complete locked `.kspwallet` V1 document without exposing any unlocked secret material.
/// Serializes the complete locked native Wallet in its current V1 or V2 wire format.
pub fn to_native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.native_bytes();
}
/// Serializes a V1 handle as its historical JSON document.
///
/// V2 handles return a format error instead of being converted implicitly.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
return self.state.json_bytes_v1();
}
async fn persist_metadata_payload(
@@ -282,11 +303,13 @@ impl WalletOwner {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = persist_staged(destination, self.state.envelope(), &envelope).await;
let persist_result = self.state.persist_staged(destination, &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
self.info = info;
ksp_logging_lib::debug!(target: crate::TRACING_TARGET, operation = operation, capability = "owner", "wallet protected metadata updated");
return std::result::Result::Ok(());
@@ -299,18 +322,6 @@ impl std::fmt::Debug for WalletOwner {
}
}
async fn persist_staged(
destination: std::path::PathBuf,
expected_current: &crate::KspWalletEnvelopeV1,
envelope: &crate::KspWalletEnvelopeV1,
) -> ksp_core_lib::Result<()> {
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return crate::replace_wallet_file_v1(destination, expected_current.clone(), serialized).await;
}
#[cfg(test)]
#[path = "../unit_tests/administration.rs"]
mod tests;

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/src/payload.rs
// version: 5
// version: 6
//! Plaintext payload codecs protected inside native `.kspwallet` V1 compartments.
//! Plaintext payload codecs shared by the initial native `.kspwallet` V1/V2 protected compartments.
use base64::Engine; // rust-rules: trait-import
use std::str::FromStr; // rust-rules: trait-import
@@ -119,8 +119,8 @@ impl crate::MetadataPayloadV1 {
}
/// Consumes this value and returns info.
pub(crate) fn into_info(self, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(capability, self.pubkey, self.alias, self.notes);
pub(crate) fn into_info(self, format_version: u32, capability: crate::WalletCapability) -> crate::WalletInfo {
return crate::WalletInfo::new(format_version, capability, self.pubkey, self.alias, self.notes);
}
}

View File

@@ -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(&current) {
return std::result::Result::Err(error);
}
if &current != 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<()>,

View File

@@ -0,0 +1,233 @@
// file: crates/ksp-wallet-lib/src/runtime.rs
// version: 1
//! Version-neutral unlocked Wallet state dispatch used by stable OWNER/VIEW handles.
/// One staged native envelope produced by an authenticated administration operation.
pub(crate) enum StagedEnvelope {
V1(crate::KspWalletEnvelopeV1),
V2(crate::KspWalletEnvelopeV2),
}
/// Version-neutral OWNER runtime state.
pub(crate) enum OwnerState {
V1(crate::OwnerStateV1),
V2(crate::OwnerStateV2),
}
impl OwnerState {
/// Returns the native format version owned by this OWNER state.
pub(crate) const fn format_version(&self) -> u32 {
return match self {
Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1,
Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2,
};
}
/// Exports the Solana identity through the requested external transfer format.
pub(crate) fn export_transfer(&self, format: crate::WalletTransferFormat) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.export_transfer(format),
Self::V2(state) => state.export_transfer(format),
};
}
/// Signs one message with the OWNER-authorized Solana identity.
pub(crate) fn sign_message(&self, message: &[u8]) -> ksp_core_lib::Result<[u8; crate::KSPWALLET_SOLANA_SIGNATURE_BYTES]> {
return match self {
Self::V1(state) => state.sign_message(message),
Self::V2(state) => state.sign_message(message),
};
}
/// Stages an authenticated metadata replacement without publishing it.
pub(crate) fn stage_metadata_payload(&self, payload: crate::MetadataPayloadV1) -> ksp_core_lib::Result<(StagedEnvelope, crate::WalletInfo)> {
return match self {
Self::V1(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V1(envelope), info)),
Self::V2(state) => state.stage_metadata_payload(payload).map(|(envelope, info)| (StagedEnvelope::V2(envelope), info)),
};
}
/// Stages an OWNER credential rotation in the state native format.
pub(crate) async fn stage_owner_password_rotation(&self, password: crate::OwnerPassword) -> ksp_core_lib::Result<StagedEnvelope> {
return match self {
Self::V1(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V1),
Self::V2(state) => state.stage_owner_password_rotation(password).await.map(StagedEnvelope::V2),
};
}
/// Stages a VIEW credential rotation in the state native format.
pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<StagedEnvelope> {
return match self {
Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1),
Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2),
};
}
/// Stages strong VIEW disable while retaining OWNER authority.
pub(crate) fn stage_disable_view(&self) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> {
return match self {
Self::V1(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)),
Self::V2(state) => state.stage_disable_view().map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)),
};
}
/// Stages strong VIEW recreation under a new credential.
pub(crate) async fn stage_recreate_view(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<(StagedEnvelope, crate::SecretKeyV1)> {
return match self {
Self::V1(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V1(envelope), key)),
Self::V2(state) => state.stage_recreate_view(password).await.map(|(envelope, key)| (StagedEnvelope::V2(envelope), key)),
};
}
/// Atomically publishes a staged envelope after format-specific state-conflict verification.
pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(staged)) => {
let serialized = match staged.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await
},
(Self::V2(state), StagedEnvelope::V2(staged)) => {
let serialized = match staged.to_binary_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Replaces the in-memory authenticated envelope after successful persistence.
pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
(Self::V2(state), StagedEnvelope::V2(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Replaces the envelope and metadata key after a strong VIEW administration operation.
pub(crate) fn apply_strong_view_state(&mut self, envelope: StagedEnvelope, metadata_key: crate::SecretKeyV1) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(value)) => {
state.apply_strong_view_state(value, metadata_key);
std::result::Result::Ok(())
},
(Self::V2(state), StagedEnvelope::V2(value)) => {
state.apply_strong_view_state(value, metadata_key);
std::result::Result::Ok(())
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Serializes the authenticated state in its native V1 or V2 wire format.
pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(state) => state.envelope().to_binary_bytes(),
};
}
/// Serializes only V1 state as JSON and rejects V2 instead of converting formats.
pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(_) => std::result::Result::Err(v1_serialization_error()),
};
}
}
/// Version-neutral VIEW runtime state.
pub(crate) enum ViewState {
V1(crate::ViewStateV1),
V2(crate::ViewStateV2),
}
impl ViewState {
/// Returns the native format version owned by this VIEW state.
pub(crate) const fn format_version(&self) -> u32 {
return match self {
Self::V1(_) => crate::KSPWALLET_FORMAT_VERSION_V1,
Self::V2(_) => crate::KSPWALLET_FORMAT_VERSION_V2,
};
}
/// Stages a VIEW credential rotation in the state native format.
pub(crate) async fn stage_view_password_rotation(&self, password: crate::ViewPassword) -> ksp_core_lib::Result<StagedEnvelope> {
return match self {
Self::V1(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V1),
Self::V2(state) => state.stage_view_password_rotation(password).await.map(StagedEnvelope::V2),
};
}
/// Atomically publishes a staged envelope after format-specific state-conflict verification.
pub(crate) async fn persist_staged(&self, destination: std::path::PathBuf, envelope: &StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(staged)) => {
let serialized = match staged.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v1(destination, state.envelope().clone(), serialized).await
},
(Self::V2(state), StagedEnvelope::V2(staged)) => {
let serialized = match staged.to_binary_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
crate::replace_wallet_file_v2(destination, state.envelope().clone(), serialized).await
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Replaces the in-memory authenticated envelope after successful persistence.
pub(crate) fn apply_envelope(&mut self, envelope: StagedEnvelope) -> ksp_core_lib::Result<()> {
return match (self, envelope) {
(Self::V1(state), StagedEnvelope::V1(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
(Self::V2(state), StagedEnvelope::V2(value)) => {
state.apply_envelope(value);
std::result::Result::Ok(())
},
_ => std::result::Result::Err(version_state_error()),
};
}
/// Serializes the authenticated state in its native V1 or V2 wire format.
pub(crate) fn native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(state) => state.envelope().to_binary_bytes(),
};
}
/// Serializes only V1 state as JSON and rejects V2 instead of converting formats.
pub(crate) fn json_bytes_v1(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return match self {
Self::V1(state) => state.envelope().to_json_bytes(),
Self::V2(_) => std::result::Result::Err(v1_serialization_error()),
};
}
}
fn version_state_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet runtime state and staged format are inconsistent");
}
fn v1_serialization_error() -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, "Wallet is not a V1 JSON document");
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/transfer.rs
// version: 3
// version: 4
//! Explicit OWNER-only Solana keypair import/export adapters.
@@ -9,7 +9,7 @@ use std::io::Write; // rust-rules: trait-import
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
/// Explicit secret-transfer formats supported by Wallet `0.2.5`.
/// Explicit secret-transfer formats supported by Wallet.
#[non_exhaustive]
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WalletTransferFormat {
@@ -106,6 +106,55 @@ pub async fn inspect_wallet_transfer_file(
return inspect_wallet_transfer(bytes.as_slice(), format);
}
/// Imports one in-memory Solana keypair transfer payload into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`].
pub async fn import_wallet_transfer(
destination: impl std::convert::AsRef<std::path::Path>,
source: &[u8],
format: WalletTransferFormat,
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 => import_wallet_transfer_v1(destination, source, format, owner_password, view_password, metadata).await,
crate::WalletFormat::V2 => import_wallet_transfer_v2(destination, source, format, owner_password, view_password, metadata).await,
};
}
/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V2 binary file.
pub async fn import_wallet_transfer_v2(
destination: impl std::convert::AsRef<std::path::Path>,
source: &[u8],
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let keypair = match decode_transfer_keypair(source, format) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner = match crate::create_wallet_v2_from_keypair(owner_password, view_password, metadata, keypair).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) = crate::persist_new_wallet_content(destination.as_ref().to_path_buf(), serialized).await {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_import_transfer",
format_version = crate::KSPWALLET_FORMAT_VERSION_V2,
transfer_format = format.code(),
"external Solana keypair imported into a new native wallet"
);
return std::result::Result::Ok(owner);
}
/// Imports one in-memory Solana keypair transfer payload into a new native `.kspwallet` V1 file.
///
/// Import always creates a fresh native Wallet envelope around the validated immutable Solana keypair and publishes with the same no-clobber semantics as
@@ -143,6 +192,38 @@ pub async fn import_wallet_transfer_v1(
return std::result::Result::Ok(owner);
}
/// Imports one bounded external transfer file into a new native Wallet using [`crate::DEFAULT_WALLET_FORMAT`].
pub async fn import_wallet_transfer_file(
destination: impl std::convert::AsRef<std::path::Path>,
source: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await {
std::result::Result::Ok(value) => zeroize::Zeroizing::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return import_wallet_transfer(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Imports one bounded external transfer file into a new native `.kspwallet` V2 binary file.
pub async fn import_wallet_transfer_file_v2(
destination: impl std::convert::AsRef<std::path::Path>,
source: impl std::convert::AsRef<std::path::Path>,
format: WalletTransferFormat,
owner_password: crate::OwnerPassword,
view_password: std::option::Option<crate::ViewPassword>,
metadata: crate::WalletCreateMetadata,
) -> ksp_core_lib::Result<crate::WalletOwner> {
let bytes = match read_transfer_file_async(source.as_ref().to_path_buf(), format).await {
std::result::Result::Ok(value) => zeroize::Zeroizing::new(value),
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
return import_wallet_transfer_v2(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Imports one bounded external transfer file into a new native `.kspwallet` V1 file.
///
/// The source file is never modified and the destination remains no-clobber.
@@ -161,8 +242,8 @@ pub async fn import_wallet_transfer_file_v1(
return import_wallet_transfer_v1(destination, bytes.as_slice(), format, owner_password, view_password, metadata).await;
}
/// Executes the crate-internal write wallet transfer file v1 operation for the owning module.
pub(crate) async fn write_wallet_transfer_file_v1(
/// Writes one bounded OWNER export using no-clobber external transfer semantics.
pub(crate) async fn write_wallet_transfer_file(
destination: std::path::PathBuf,
content: std::vec::Vec<u8>,
format: WalletTransferFormat,

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/view.rs
// version: 4
// version: 5
/// Authorized VIEW capability handle.
///
@@ -7,15 +7,21 @@
/// Solana secret, OWNER administration material or metadata-write authority.
pub struct WalletView {
info: crate::WalletInfo,
state: crate::ViewStateV1,
state: crate::ViewState,
}
impl WalletView {
/// Builds `WalletView` from unlocked.
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewStateV1) -> Self {
pub(crate) fn from_unlocked(info: crate::WalletInfo, state: crate::ViewState) -> Self {
return Self { info, state };
}
/// Returns the native Wallet format version backing this authorized handle.
#[must_use]
pub const fn format_version(&self) -> u32 {
return self.info.format_version();
}
/// Returns the authorization capability represented by this handle.
#[must_use]
pub const fn capability(&self) -> crate::WalletCapability {
@@ -65,15 +71,13 @@ impl WalletView {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let serialized = match envelope.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let persist_result = crate::replace_wallet_file_v1(destination.as_ref().to_path_buf(), self.state.envelope().clone(), serialized).await;
let persist_result = self.state.persist_staged(destination.as_ref().to_path_buf(), &envelope).await;
if let std::result::Result::Err(error) = persist_result {
return std::result::Result::Err(error);
}
self.state.apply_envelope(envelope);
if let std::result::Result::Err(error) = self.state.apply_envelope(envelope) {
return std::result::Result::Err(error);
}
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
operation = "wallet_rotate_view_password",
@@ -83,9 +87,16 @@ impl WalletView {
return std::result::Result::Ok(());
}
/// Serializes the unchanged locked `.kspwallet` V1 document without exposing the metadata content key.
/// Serializes the unchanged locked native Wallet in its current V1 or V2 wire format.
pub fn to_native_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.native_bytes();
}
/// Serializes a V1 handle as its historical JSON document.
///
/// V2 handles return a format error instead of being converted implicitly.
pub fn to_json_bytes(&self) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
return self.state.envelope().to_json_bytes();
return self.state.json_bytes_v1();
}
}

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/wallet.rs
// version: 8
// version: 9
//! In-memory native Wallet V1 create/open orchestration.
@@ -143,7 +143,7 @@ impl OwnerStateV1 {
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok((envelope, payload.into_info(crate::WalletCapability::Owner)));
return std::result::Result::Ok((envelope, payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner)));
}
/// Executes the crate-internal stage owner password rotation operation for `OwnerStateV1`.
@@ -497,7 +497,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let info = metadata_payload.into_info(crate::WalletCapability::View);
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::View);
let state = ViewStateV1::new(envelope, metadata_key);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -506,7 +506,7 @@ pub async fn open_wallet_view_v1(source: &[u8], password: crate::ViewPassword) -
capability = "view",
"native wallet VIEW capability opened"
);
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, state));
return std::result::Result::Ok(crate::WalletView::from_unlocked(info, crate::ViewState::V1(state)));
}
/// Opens the OWNER capability from a native `.kspwallet` V1 JSON document.
@@ -602,7 +602,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
if &secret_pubkey != metadata_payload.pubkey() {
return std::result::Result::Err(key_material_error());
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner);
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -611,7 +611,7 @@ pub async fn open_wallet_owner_v1(source: &[u8], password: crate::OwnerPassword)
capability = "owner",
"native wallet OWNER capability opened"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state)));
}
/// Parses and verifies the OWNER-authenticated locked state without unlocking metadata or secret material.
@@ -624,7 +624,7 @@ pub fn inspect_locked_wallet_v1(source: &[u8]) -> ksp_core_lib::Result<crate::Lo
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
return std::result::Result::Ok(crate::LockedWalletInfo::new(envelope.view_descriptor().enabled()));
return std::result::Result::Ok(crate::LockedWalletInfo::new(crate::KSPWALLET_FORMAT_VERSION_V1, envelope.view_descriptor().enabled()));
}
/// Executes the crate-internal create wallet v1 from keypair operation for the owning module.
@@ -821,7 +821,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
if let std::result::Result::Err(error) = verify_result {
return std::result::Result::Err(error);
}
let info = metadata_payload.into_info(crate::WalletCapability::Owner);
let info = metadata_payload.into_info(crate::KSPWALLET_FORMAT_VERSION_V1, crate::WalletCapability::Owner);
let state = OwnerStateV1::new(envelope, owner_root, metadata_key, secret_key, admin_signing_key, solana_keypair);
ksp_logging_lib::debug!(
target: crate::TRACING_TARGET,
@@ -830,7 +830,7 @@ pub(crate) async fn create_wallet_v1_from_keypair(
view_enabled = state.envelope().view_descriptor().enabled(),
"native wallet created in memory"
);
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, state));
return std::result::Result::Ok(crate::WalletOwner::from_unlocked(info, crate::OwnerState::V1(state)));
}
/// Verifies state signature.

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/src/wire_v2.rs
// version: 2
// version: 3
//! Strict native `.kspwallet` V2 binary wire envelope.
@@ -120,6 +120,17 @@ impl WalletKdfParametersV2 {
return Self { algorithm: WalletKdfAlgorithmV2::Argon2id, version, memory_kib, iterations, parallelism, salt };
}
/// Creates one V2 KDF value using the release-calibrated default creation profile.
pub(crate) fn new_creation(salt: std::vec::Vec<u8>) -> Self {
return Self::new_internal(
crate::KSPWALLET_V2_ARGON2_VERSION,
crate::KSPWALLET_V2_DEFAULT_ARGON2_MEMORY_KIB,
crate::KSPWALLET_V2_DEFAULT_ARGON2_ITERATIONS,
crate::KSPWALLET_V2_DEFAULT_ARGON2_PARALLELISM,
salt,
);
}
/// Returns the KDF algorithm.
#[must_use]
pub const fn algorithm(&self) -> WalletKdfAlgorithmV2 {