Files
khadhroony-solana-project/crates/ksp-wallet-lib/unit_tests/security.rs
2026-08-20 09:24:10 +02:00

87 lines
4.7 KiB
Rust

// file: crates/ksp-wallet-lib/unit_tests/security.rs
// version: 2
//! Adversarial security canaries for native `.kspwallet` V1.
use base64::Engine; // rust-rules: derive-import
const FULL_VECTOR: &[u8] = include_bytes!("../tests/fixtures/kspwallet_v1_full_vector.json");
#[test]
fn owner_signed_regions_reject_canonical_tampering_before_unlock() {
for pointer in [
"/key_slots/0/kdf/salt",
"/key_slots/0/wrap/ciphertext",
"/owner_control/ciphertext",
"/metadata/ciphertext",
"/secret/ciphertext",
"/state_signature/signature",
] {
let tampered = tamper_base64url_field(FULL_VECTOR, pointer);
let error = crate::inspect_locked_wallet_v1(tampered.as_slice()).expect_err("OWNER-signed canonical tampering must be rejected");
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED, "unexpected error code for tampered field {pointer}");
}
}
#[test]
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);
}
#[test]
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);
}
#[test]
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);
assert!(!view_error.to_string().contains(view_canary));
assert!(!format!("{view_error:?}").contains(view_canary));
}
fn tamper_base64url_field(source: &[u8], pointer: &str) -> std::vec::Vec<u8> {
let mut value: serde_json::Value = serde_json::from_slice(source).expect("security fixture must be valid JSON");
let target = value.pointer_mut(pointer).expect("security fixture pointer must exist");
let encoded = target.as_str().expect("security fixture target must be a Base64url string");
let mut decoded = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(encoded.as_bytes()).expect("security fixture Base64url must decode");
let first = decoded.first_mut().expect("security fixture Base64url field must not be empty");
*first ^= 0x01;
*target = serde_json::Value::String(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(decoded.as_slice()));
zeroize::Zeroize::zeroize(decoded.as_mut_slice());
return serde_json::to_vec(&value).expect("tampered security fixture must serialize");
}
fn runtime() -> tokio::runtime::Runtime {
return tokio::runtime::Builder::new_current_thread().build().expect("security test runtime must be creatable");
}