v0.2.5-pre.010.fix.001

This commit is contained in:
2026-08-20 11:17:12 +02:00
parent 5bf9651038
commit c0b131bf6f
97 changed files with 2277 additions and 655 deletions

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/administration.rs
// version: 1
// version: 2
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
@@ -10,7 +10,7 @@ fn runtime() -> tokio::runtime::Runtime {
fn published_vector(name: &str) -> (tempfile::TempDir, std::path::PathBuf) {
let directory = tempfile::Builder::new().prefix("ksp-wallet-admin-").tempdir().expect("Wallet administration test directory must be creatable");
let path = directory.path().join(name);
crate::persistence::persist_new_wallet_for_test(path.as_path(), FULL_VECTOR).expect("full Wallet vector must publish for administration test");
crate::persist_new_wallet_for_test(path.as_path(), FULL_VECTOR).expect("full Wallet vector must publish for administration test");
return (directory, path);
}
@@ -26,14 +26,11 @@ fn owner_signing_is_deterministic_and_owner_rotation_preserves_solana_identity()
let signature_before = owner.sign(message).expect("OWNER must sign");
let signature_repeat = owner.sign(message).expect("OWNER repeated signature must succeed");
assert_eq!(signature_before, signature_repeat);
runtime
.block_on(owner.rotate_owner_password(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre007-new-owner-password"))))
.expect("OWNER password rotation must persist");
let old = runtime.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
assert_eq!(old.expect_err("old OWNER password must stop unlocking current file").code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre007-new-owner-password"))))
.expect("new OWNER password must unlock current file");
@@ -51,21 +48,17 @@ fn view_self_rotation_changes_only_the_current_view_credential() {
let pubkey = *view.pubkey();
let alias = view.alias().map(std::string::String::from);
let notes = view.notes().to_vec();
runtime
.block_on(view.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-new-view-password"))))
.expect("VIEW self-rotation must persist");
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("old VIEW password must stop unlocking current file").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let reopened = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-new-view-password"))))
.expect("new VIEW password must unlock current file");
assert_eq!(*reopened.pubkey(), pubkey);
assert_eq!(reopened.alias(), alias.as_deref());
assert_eq!(reopened.notes(), notes.as_slice());
let owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("VIEW self-rotation must not change OWNER credential");
@@ -80,11 +73,9 @@ fn owner_rotates_view_without_old_view_password() {
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
runtime
.block_on(owner.rotate_view_password(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-owner-set-view-password"))))
.expect("OWNER must rotate VIEW without old VIEW password");
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("old VIEW password must stop unlocking current file").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let reopened = runtime
@@ -101,7 +92,6 @@ fn owner_metadata_administration_is_visible_to_view_but_does_not_change_pubkey()
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let pubkey = *owner.pubkey();
runtime
.block_on(owner.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("pre007-updated-alias"))))
.expect("OWNER alias update must persist");
@@ -109,14 +99,12 @@ fn owner_metadata_administration_is_visible_to_view_but_does_not_change_pubkey()
runtime
.block_on(owner.update_note(path.as_path(), note_id.as_str(), std::string::String::from("pre007-updated-note")))
.expect("OWNER note update must persist");
let view = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must read OWNER-updated metadata");
assert_eq!(*view.pubkey(), pubkey);
assert_eq!(view.alias(), std::option::Option::Some("pre007-updated-alias"));
assert!(view.notes().iter().any(|note| return note.id() == note_id.as_str() && note.text() == "pre007-updated-note"));
runtime.block_on(owner.delete_note(path.as_path(), note_id.as_str())).expect("OWNER note delete must persist");
let view_after_delete = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
@@ -139,14 +127,12 @@ fn strong_view_disable_and_recreate_change_current_view_authority_without_changi
let before = crate::KspWalletEnvelopeV1::parse_json(before_bytes.as_slice()).expect("current Wallet must parse");
let old_slot_id = *before.view_descriptor().slot_id().expect("fixture VIEW descriptor must have slot ID");
let old_metadata_ciphertext = before.metadata().ciphertext().to_vec();
runtime.block_on(owner.disable_view(path.as_path())).expect("OWNER strong VIEW disable must persist");
let locked_disabled = runtime.block_on(crate::inspect_locked_wallet_file_v1(path.as_path())).expect("disabled Wallet must inspect");
assert!(!locked_disabled.view_enabled());
let disabled_view =
runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(disabled_view.expect_err("disabled VIEW must not open").code(), crate::ERROR_CODE_CAPABILITY_INSUFFICIENT);
runtime
.block_on(owner.recreate_view(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre007-recreated-view-password"))))
.expect("OWNER strong VIEW recreate must persist");
@@ -155,7 +141,6 @@ fn strong_view_disable_and_recreate_change_current_view_authority_without_changi
let new_slot_id = *recreated.view_descriptor().slot_id().expect("recreated VIEW descriptor must have slot ID");
assert_ne!(new_slot_id, old_slot_id);
assert_ne!(recreated.metadata().ciphertext(), old_metadata_ciphertext.as_slice());
let old = runtime.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(old.expect_err("historical VIEW password must not unlock recreated current state").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let recreated_view = runtime
@@ -173,7 +158,6 @@ fn owner_note_mutation_reports_missing_identifier_without_persisting() {
let mut owner = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("OWNER fixture must open");
let result = runtime.block_on(owner.update_note(path.as_path(), "AAAAAAAAAAAAAAAAAAAAAA", std::string::String::from("must-not-persist")));
assert_eq!(result.expect_err("missing note identifier must be rejected").code(), crate::ERROR_CODE_NOTE_NOT_FOUND);
assert_eq!(std::fs::read(path.as_path()).expect("Wallet bytes must remain readable"), before);
@@ -189,13 +173,11 @@ fn stale_owner_handle_cannot_overwrite_a_newer_authenticated_wallet_state() {
let mut stale = runtime
.block_on(crate::open_wallet_owner_file_v1(path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("second OWNER handle must open on the same state");
runtime
.block_on(first.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("first-writer"))))
.expect("first OWNER mutation must persist");
let stale_result = runtime.block_on(stale.update_alias(path.as_path(), std::option::Option::Some(std::string::String::from("stale-writer"))));
assert_eq!(stale_result.expect_err("stale OWNER handle must not overwrite newer state").code(), crate::ERROR_CODE_STATE_CONFLICT);
let current = runtime
.block_on(crate::open_wallet_view_file_v1(path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("VIEW must open the first writer's state");
@@ -213,14 +195,12 @@ fn owner_handle_cannot_replace_a_different_authenticated_wallet_state() {
let mut second = runtime
.block_on(crate::open_wallet_owner_file_v1(second_path.as_path(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))))
.expect("second OWNER handle must open");
runtime
.block_on(second.update_alias(second_path.as_path(), std::option::Option::Some(std::string::String::from("different-authenticated-state"))))
.expect("second Wallet must diverge before wrong-target canary");
let wrong_target =
runtime.block_on(first.update_alias(second_path.as_path(), std::option::Option::Some(std::string::String::from("must-not-overwrite-second-wallet"))));
assert_eq!(wrong_target.expect_err("OWNER handle must not replace a different authenticated Wallet state").code(), crate::ERROR_CODE_STATE_CONFLICT);
let unchanged_first = runtime
.block_on(crate::open_wallet_view_file_v1(first_path.as_path(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))))
.expect("first Wallet must remain unchanged");

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/crypto.rs
// version: 2
// version: 3
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
const VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_crypto_vectors.json");
@@ -62,14 +62,12 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let derived_result = super::derive_argon2id(vector.password_utf8.as_bytes(), salt.as_slice(), vector.memory_kib, vector.iterations, vector.parallelism);
let derived = match derived_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(derived.as_bytes(), &expected_derived);
let content = crate::SecretKeyV1::from_bytes(content_key);
let wrapped_result = crate::wrap_key(&derived, &content, &nonce, aad.as_slice());
let wrapped = match wrapped_result {
@@ -77,7 +75,6 @@ fn deterministic_argon2id_and_xchacha_wrap_vector_matches_external_canary() -> k
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(wrapped, expected_wrapped);
let unwrapped_result = crate::unwrap_key(&derived, &nonce, aad.as_slice(), wrapped.as_slice());
let unwrapped = match unwrapped_result {
std::result::Result::Ok(value) => value,

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/payload.rs
// version: 2
// version: 3
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
#[test]
fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result<()> {

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/persistence.rs
// version: 3
// version: 4
fn temp_directory() -> std::io::Result<tempfile::TempDir> {
return tempfile::tempdir();
@@ -11,9 +11,8 @@ fn no_clobber_create_keeps_the_first_published_document() {
let destination = directory.path().join("wallet.kspwallet");
let first = b"first-wallet";
let second = b"second-wallet";
crate::persistence::persist_new_wallet_for_test(destination.as_path(), first).expect("first no-clobber publication must succeed");
let second_result = crate::persistence::persist_new_wallet_for_test(destination.as_path(), second);
crate::persist_new_wallet_for_test(destination.as_path(), first).expect("first no-clobber publication must succeed");
let second_result = crate::persist_new_wallet_for_test(destination.as_path(), second);
let error = second_result.expect_err("second publication must not overwrite an existing wallet");
assert_eq!(error.code(), crate::ERROR_CODE_DESTINATION_EXISTS);
let persisted = std::fs::read(destination.as_path()).expect("published wallet must remain readable");
@@ -24,11 +23,10 @@ fn no_clobber_create_keeps_the_first_published_document() {
fn injected_failure_before_publish_leaves_no_destination_or_partial_wallet() {
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
let destination = directory.path().join("wallet.kspwallet");
let result = crate::persistence::persist_new_wallet_fault_before_publish(destination.as_path(), b"candidate-wallet");
let result = crate::persist_new_wallet_fault_before_publish(destination.as_path(), b"candidate-wallet");
let error = result.expect_err("fault injection must abort before publication");
assert_eq!(error.code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
assert!(!destination.exists());
let entries = std::fs::read_dir(directory.path()).expect("Wallet persistence test directory must remain readable");
let count = entries.count();
assert_eq!(count, 0, "temporary artifacts should be cleaned on ordinary error unwinding");
@@ -40,17 +38,15 @@ fn concurrent_no_clobber_publish_has_exactly_one_winner() {
let destination = directory.path().join("wallet.kspwallet");
let barrier = std::sync::Arc::new(std::sync::Barrier::new(8));
let mut handles = std::vec::Vec::new();
for index in 0_u8..8 {
let destination = destination.clone();
let barrier = std::sync::Arc::clone(&barrier);
handles.push(std::thread::spawn(move || {
barrier.wait();
let content = [index; 32];
return crate::persistence::persist_new_wallet_for_test(destination.as_path(), content.as_slice());
return crate::persist_new_wallet_for_test(destination.as_path(), content.as_slice());
}));
}
let mut success_count = 0_usize;
let mut exists_count = 0_usize;
let mut unexpected_code = std::option::Option::None;
@@ -74,7 +70,6 @@ fn bounded_reader_rejects_oversized_wallet_before_parser_allocation() {
let destination = directory.path().join("oversized.kspwallet");
let oversized = std::vec![b'x'; crate::KSPWALLET_MAX_FILE_BYTES + 1];
std::fs::write(destination.as_path(), oversized).expect("oversized fixture must be writable");
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
let result = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path()));
let error = result.expect_err("oversized Wallet file must be rejected before parsing");
@@ -91,7 +86,6 @@ fn public_file_create_and_locked_inspect_round_trip_without_revealing_identity()
.block_on(crate::create_wallet_file_v1(destination.as_path(), owner_password, std::option::Option::None, crate::WalletCreateMetadataV1::default()))
.expect("native Wallet file creation must succeed");
let locked = runtime.block_on(crate::inspect_locked_wallet_file_v1(destination.as_path())).expect("persisted Wallet must inspect successfully");
assert!(!locked.view_enabled());
assert_eq!(created.capability(), crate::WalletCapability::Owner);
assert!(destination.exists());
@@ -110,8 +104,7 @@ fn persisted_full_vector_opens_view_and_owner_through_file_apis() {
let directory = temp_directory().expect("Wallet persistence test directory must be creatable");
let destination = directory.path().join("vector.kspwallet");
let vector = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
crate::persistence::persist_new_wallet_for_test(destination.as_path(), vector).expect("full vector must publish through no-clobber persistence");
crate::persist_new_wallet_for_test(destination.as_path(), vector).expect("full vector must publish through no-clobber persistence");
let runtime = tokio::runtime::Builder::new_current_thread().build().expect("Wallet persistence test runtime must build");
let view_password = crate::ViewPassword::new(std::string::String::from("pre005-view-password"));
let view = runtime
@@ -121,7 +114,6 @@ fn persisted_full_vector_opens_view_and_owner_through_file_apis() {
let owner = runtime
.block_on(crate::open_wallet_owner_file_v1(destination.as_path(), owner_password))
.expect("persisted full vector must open through OWNER file API");
assert_eq!(view.capability(), crate::WalletCapability::View);
assert_eq!(owner.capability(), crate::WalletCapability::Owner);
assert_eq!(view.pubkey(), owner.pubkey());
@@ -136,8 +128,7 @@ fn replacement_fault_before_publish_preserves_the_previous_wallet_bytes() {
let original = b"ORIGINAL-WALLET-CANARY";
let replacement = b"REPLACEMENT-WALLET-CANARY";
std::fs::write(destination.as_path(), original).expect("replacement test original must be writable");
let result = crate::persistence::replace_wallet_fault_before_publish(destination.as_path(), replacement);
let result = crate::replace_wallet_fault_before_publish(destination.as_path(), replacement);
assert_eq!(result.expect_err("injected replacement fault must fail").code(), crate::ERROR_CODE_ATOMIC_PERSISTENCE_FAILED);
assert_eq!(std::fs::read(destination.as_path()).expect("original Wallet bytes must remain readable"), original);
}
@@ -146,10 +137,9 @@ fn replacement_fault_before_publish_preserves_the_previous_wallet_bytes() {
fn replacement_requires_an_existing_regular_file_and_replaces_complete_content() {
let directory = temp_directory().expect("Wallet replacement test directory must be creatable");
let destination = directory.path().join("replace.kspwallet");
let missing = crate::persistence::replace_wallet_for_test(destination.as_path(), b"replacement");
let missing = crate::replace_wallet_for_test(destination.as_path(), b"replacement");
assert_eq!(missing.expect_err("replacement must not create a missing Wallet destination").code(), crate::ERROR_CODE_IO_FAILED);
std::fs::write(destination.as_path(), b"old").expect("replacement test original must be writable");
crate::persistence::replace_wallet_for_test(destination.as_path(), b"new-complete-wallet").expect("existing Wallet replacement must succeed");
crate::replace_wallet_for_test(destination.as_path(), b"new-complete-wallet").expect("existing Wallet replacement must succeed");
assert_eq!(std::fs::read(destination.as_path()).expect("replaced Wallet must be readable"), b"new-complete-wallet");
}

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/security.rs
// version: 2
// version: 3
//! Adversarial security canaries for native `.kspwallet` V1.
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
@@ -27,10 +27,8 @@ fn owner_signed_regions_reject_canonical_tampering_before_unlock() {
fn owner_signature_verification_precedes_owner_and_view_password_kdf() {
let tampered = tamper_base64url_field(FULL_VECTOR, "/metadata/ciphertext");
let runtime = runtime();
let owner = runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::new())));
assert_eq!(owner.expect_err("tampered state must fail before an empty OWNER password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::new())));
assert_eq!(view.expect_err("tampered state must fail before an empty VIEW password reaches Argon2").code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
}
@@ -40,12 +38,10 @@ fn view_credential_tampering_does_not_forge_owner_state_and_cannot_unlock_view()
let tampered = tamper_base64url_field(FULL_VECTOR, "/key_slots/1/wrap/ciphertext");
let locked = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect("VIEW wrapping credentials are intentionally outside the OWNER state transcript");
assert!(locked.view_enabled());
let runtime = runtime();
let owner =
runtime.block_on(crate::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(std::string::String::from("pre005-owner-password"))));
assert!(owner.is_ok(), "VIEW credential tampering must not invalidate OWNER unlock");
let view = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(std::string::String::from("pre005-view-password"))));
assert_eq!(view.expect_err("tampered VIEW wrapping must not unlock metadata").code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
}
@@ -55,13 +51,11 @@ fn unlock_failures_do_not_echo_password_material() {
let owner_canary = "OWNER-PASSWORD-LEAK-CANARY";
let view_canary = "VIEW-PASSWORD-LEAK-CANARY";
let runtime = runtime();
let owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from(owner_canary))));
let owner_error = owner.expect_err("wrong OWNER password must fail");
assert_eq!(owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
assert!(!owner_error.to_string().contains(owner_canary));
assert!(!format!("{owner_error:?}").contains(owner_canary));
let view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from(view_canary))));
let view_error = view.expect_err("wrong VIEW password must fail");
assert_eq!(view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);

View File

@@ -1,7 +1,7 @@
// file: crates/ksp-wallet-lib/unit_tests/transcript.rs
// version: 2
// version: 3
use base64::Engine; // rust-rules: derive-import
use base64::Engine; // rust-rules: trait-import
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");

View File

@@ -1,9 +1,9 @@
// file: crates/ksp-wallet-lib/unit_tests/transfer.rs
// version: 2
// version: 3
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt; // rust-rules: derive-import
use zeroize::Zeroize; // rust-rules: derive-import
use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import
use zeroize::Zeroize; // rust-rules: trait-import
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("Wallet transfer test runtime must build");
@@ -41,7 +41,6 @@ fn in_memory_inspection_accepts_cli_json_and_canonical_base58_without_exposing_s
crate::inspect_wallet_transfer(json.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("Solana CLI JSON inspection must succeed");
let base58_inspection =
crate::inspect_wallet_transfer(base58.as_bytes(), crate::WalletTransferFormat::SolanaKeypairBase58).expect("canonical Base58 inspection must succeed");
assert_eq!(json_inspection.pubkey(), base58_inspection.pubkey());
assert_eq!(json_inspection.format(), crate::WalletTransferFormat::SolanaCliJson);
assert_eq!(base58_inspection.format(), crate::WalletTransferFormat::SolanaKeypairBase58);
@@ -63,7 +62,6 @@ fn external_seed7_transfer_canary_matches_known_solana_keypair_encodings() {
assert_eq!(actual_keypair_bytes, expected_keypair_bytes);
let mut actual_base58 = keypair.to_base58_string();
assert_eq!(actual_base58, expected_base58);
let mut json = serde_json::to_vec(expected_keypair_bytes.as_slice()).expect("external keypair canary must encode as JSON");
let json_inspection =
crate::inspect_wallet_transfer(json.as_slice(), crate::WalletTransferFormat::SolanaCliJson).expect("external Solana CLI JSON canary must inspect");
@@ -71,7 +69,6 @@ fn external_seed7_transfer_canary_matches_known_solana_keypair_encodings() {
.expect("external Base58 canary must inspect");
assert_eq!(json_inspection.pubkey().to_string(), expected_pubkey);
assert_eq!(base58_inspection.pubkey().to_string(), expected_pubkey);
json.zeroize();
actual_base58.zeroize();
actual_keypair_bytes.zeroize();
@@ -84,7 +81,6 @@ fn transfer_decoders_reject_short_inconsistent_or_noncanonical_sources() {
crate::inspect_wallet_transfer(short_json, crate::WalletTransferFormat::SolanaCliJson).expect_err("short Solana CLI JSON must fail").code(),
crate::ERROR_CODE_KEY_MATERIAL_INVALID
);
let keypair = test_keypair();
let mut bytes = keypair.to_bytes();
bytes[63] ^= 0x01;
@@ -97,7 +93,6 @@ fn transfer_decoders_reject_short_inconsistent_or_noncanonical_sources() {
);
bytes.zeroize();
inconsistent_json.zeroize();
assert_eq!(
crate::inspect_wallet_transfer(b"not valid base58 !!!", crate::WalletTransferFormat::SolanaKeypairBase58)
.expect_err("invalid Base58 must fail")
@@ -136,7 +131,6 @@ fn cli_json_import_creates_new_no_clobber_wallet_with_imported_identity_and_meta
.expect("Solana CLI JSON import must succeed");
assert_eq!(owner.pubkey(), expected.pubkey());
assert_eq!(owner.alias(), std::option::Option::Some("imported-wallet"));
let second_password = crate::OwnerPassword::new(std::string::String::from("pre008-second-owner-password"));
let second = runtime.block_on(crate::import_wallet_transfer_v1(
destination.as_path(),
@@ -172,7 +166,6 @@ fn transfer_file_import_is_non_destructive_and_bounded() {
))
.expect("transfer-file import must succeed");
assert_eq!(std::fs::read(source_path.as_path()).expect("source must survive import"), original);
let oversized_path = directory.path().join("oversized.json");
std::fs::write(oversized_path.as_path(), std::vec![b'1'; crate::KSPWALLET_TRANSFER_MAX_SOLANA_CLI_JSON_BYTES + 1])
.expect("oversized transfer fixture must be writable");
@@ -202,13 +195,11 @@ fn owner_exports_cli_json_and_base58_that_roundtrip_to_the_same_imported_keypair
crate::WalletCreateMetadataV1::default(),
))
.expect("Base58 import must succeed");
let mut exported_json = owner.export_transfer(crate::WalletTransferFormat::SolanaCliJson).expect("OWNER JSON export must succeed");
let mut decoded_json: std::vec::Vec<u8> = serde_json::from_slice(exported_json.as_slice()).expect("OWNER JSON export must be valid JSON");
assert_eq!(decoded_json.as_slice(), expected_bytes.as_slice());
let mut exported_base58 = owner.export_transfer(crate::WalletTransferFormat::SolanaKeypairBase58).expect("OWNER Base58 export must succeed");
assert_eq!(exported_base58.as_slice(), source_base58.as_bytes());
exported_json.zeroize();
decoded_json.zeroize();
exported_base58.zeroize();
@@ -233,7 +224,6 @@ fn owner_transfer_file_export_is_no_clobber() {
let second = runtime.block_on(owner.export_transfer_file(export_path.as_path(), crate::WalletTransferFormat::SolanaCliJson));
assert_eq!(second.expect_err("OWNER transfer-file export must not overwrite").code(), crate::ERROR_CODE_DESTINATION_EXISTS);
assert_eq!(std::fs::read(export_path.as_path()).expect("first export must remain intact"), first);
#[cfg(unix)]
{
let mode = std::fs::metadata(export_path.as_path()).expect("export metadata must be readable").permissions().mode() & 0o777;

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
// version: 3
// version: 4
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
const FULL_VECTOR_META: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector_meta.json");
@@ -25,7 +25,6 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let view_result = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(vector.view_password_utf8)));
let view = match view_result {
std::result::Result::Ok(value) => value,
@@ -35,7 +34,6 @@ fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_
assert_eq!(view.pubkey().to_string(), vector.expected_pubkey);
assert_eq!(view.alias(), std::option::Option::Some(vector.expected_alias.as_str()));
assert_note_texts(view.notes(), vector.expected_notes.as_slice());
let owner_result = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.owner_password_utf8)));
let owner = match owner_result {
std::result::Result::Ok(value) => value,
@@ -58,21 +56,18 @@ fn wrong_passwords_do_not_cross_unlock_capabilities() -> ksp_core_lib::Result<()
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wrong_view = runtime.block_on(crate::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(std::string::String::from("wrong-view-password"))));
let wrong_view_error = match wrong_view {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("wrong VIEW password unexpectedly unlocked Wallet")),
std::result::Result::Err(error) => error,
};
assert_eq!(wrong_view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
let wrong_owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(std::string::String::from("wrong-owner-password"))));
let wrong_owner_error = match wrong_owner {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("wrong OWNER password unexpectedly unlocked Wallet")),
std::result::Result::Err(error) => error,
};
assert_eq!(wrong_owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
let view_as_owner = runtime.block_on(crate::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.view_password_utf8)));
let view_as_owner_error = match view_as_owner {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("VIEW password unexpectedly unlocked OWNER")),
@@ -112,7 +107,6 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(inspected.view_enabled());
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -121,7 +115,6 @@ fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core
if let std::result::Result::Err(error) = owner_result {
return std::result::Result::Err(error);
}
let view_result = runtime.block_on(crate::open_wallet_view_v1(tampered.as_slice(), crate::ViewPassword::new(vector.view_password_utf8)));
let view_error = match view_result {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("modified VIEW wrapping unexpectedly unlocked metadata")),
@@ -197,7 +190,6 @@ fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_
std::option::Option::None => return std::result::Result::Err(test_error("created Wallet is missing initial note")),
};
assert_eq!(created_note.text(), note.as_str());
let locked_bytes = match owner.to_json_bytes() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -209,7 +201,6 @@ fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_
assert!(!locked_text.contains(alias.as_str()));
assert!(!locked_text.contains(note.as_str()));
assert!(!locked_text.contains(owner.pubkey().to_string().as_str()));
let envelope = match crate::KspWalletEnvelopeV1::parse_json(locked_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),

View File

@@ -1,5 +1,5 @@
// file: crates/ksp-wallet-lib/unit_tests/wire.rs
// version: 3
// version: 4
const FIXTURE: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_wire_only.json");
@@ -16,7 +16,6 @@ fn strict_v1_fixture_parses_and_round_trips_semantically() -> ksp_core_lib::Resu
assert_eq!(parsed.owner_control().payload_version(), 1);
assert_eq!(parsed.metadata().payload_version(), 1);
assert_eq!(parsed.secret().payload_version(), 1);
let serialized = match parsed.to_json_bytes() {
std::result::Result::Ok(serialized) => serialized,
std::result::Result::Err(error) => return std::result::Result::Err(error),
@@ -89,7 +88,6 @@ fn zero_or_pathological_kdf_parameters_are_rejected_before_crypto() {
std::result::Result::Ok(_) => return,
};
assert_eq!(zero_error.code(), crate::ERROR_CODE_CRYPTO_PARAMETERS_INVALID);
let high_source = std::string::String::from_utf8_lossy(FIXTURE).replacen("\"memory_kib\": 65536", "\"memory_kib\": 1048577", 1);
let high_result = crate::KspWalletEnvelopeV1::parse_json(high_source.as_bytes());
assert!(high_result.is_err());