Files
khadhroony-solana-project/crates/ksp-wallet-lib/unit_tests/wallet.rs
2026-08-19 13:09:16 +02:00

317 lines
16 KiB
Rust

// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
// version: 2
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");
#[derive(serde::Deserialize)]
struct FullVectorMeta {
warning: std::string::String,
owner_password_utf8: std::string::String,
view_password_utf8: std::string::String,
expected_pubkey: std::string::String,
expected_alias: std::string::String,
expected_notes: std::vec::Vec<std::string::String>,
}
#[test]
fn externally_generated_full_vector_opens_view_and_owner_independently() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert!(vector.warning.contains("TEST ONLY"));
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let view_result = runtime.block_on(super::open_wallet_view_v1(FULL_VECTOR, crate::ViewPassword::new(vector.view_password_utf8)));
let view = match view_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(view.capability(), crate::WalletCapability::View);
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(super::open_wallet_owner_v1(FULL_VECTOR, crate::OwnerPassword::new(vector.owner_password_utf8)));
let owner = match owner_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(owner.capability(), crate::WalletCapability::Owner);
assert_eq!(owner.pubkey(), view.pubkey());
assert_eq!(owner.alias(), view.alias());
assert_eq!(owner.notes(), view.notes());
return std::result::Result::Ok(());
}
#[test]
fn wrong_passwords_do_not_cross_unlock_capabilities() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let wrong_view = runtime.block_on(super::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(super::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(super::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")),
std::result::Result::Err(error) => error,
};
assert_eq!(view_as_owner_error.code(), crate::ERROR_CODE_OWNER_UNLOCK_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn owner_signed_metadata_tampering_is_rejected_before_unlock() -> ksp_core_lib::Result<()> {
let tampered = match replace_string_field(FULL_VECTOR, &["metadata", "ciphertext"], "AAAAAAAAAAAAAAAAAAAAAA") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let inspect_result = super::inspect_locked_wallet_v1(tampered.as_slice());
let error = match inspect_result {
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("tampered OWNER-signed metadata unexpectedly verified")),
std::result::Result::Err(error) => error,
};
assert_eq!(error.code(), crate::ERROR_CODE_AUTHENTICATION_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn view_wrap_can_change_without_breaking_owner_authenticated_state() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let tampered = match replace_view_wrap_ciphertext(FULL_VECTOR, "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let inspected = match super::inspect_locked_wallet_v1(tampered.as_slice()) {
std::result::Result::Ok(value) => value,
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),
};
let owner_result = runtime.block_on(super::open_wallet_owner_v1(tampered.as_slice(), crate::OwnerPassword::new(vector.owner_password_utf8)));
if let std::result::Result::Err(error) = owner_result {
return std::result::Result::Err(error);
}
let view_result = runtime.block_on(super::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")),
std::result::Result::Err(error) => error,
};
assert_eq!(view_error.code(), crate::ERROR_CODE_VIEW_UNLOCK_FAILED);
return std::result::Result::Ok(());
}
#[test]
fn locked_full_vector_contains_no_authorized_identity_or_metadata_plaintext() -> ksp_core_lib::Result<()> {
let vector = match full_vector_meta() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let document_result = std::str::from_utf8(FULL_VECTOR);
let document = match document_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("full Wallet vector is not UTF-8")),
};
assert!(!document.contains(vector.expected_pubkey.as_str()));
assert!(!document.contains(vector.expected_alias.as_str()));
for note in vector.expected_notes {
assert!(!document.contains(note.as_str()));
}
let locked = match super::inspect_locked_wallet_v1(FULL_VECTOR) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(locked.format_version(), crate::KSPWALLET_FORMAT_VERSION_V1);
assert!(locked.view_enabled());
return std::result::Result::Ok(());
}
#[test]
fn owner_and_view_slots_use_independent_kdf_material() -> ksp_core_lib::Result<()> {
let envelope = match crate::KspWalletEnvelopeV1::parse_json(FULL_VECTOR) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let view_slot = match envelope.view_slot() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("full Wallet vector is missing VIEW slot")),
};
assert_ne!(envelope.owner_slot().slot_id(), view_slot.slot_id());
assert_ne!(envelope.owner_slot().kdf().salt(), view_slot.kdf().salt());
assert_ne!(envelope.owner_slot().wrap().nonce(), view_slot.wrap().nonce());
return std::result::Result::Ok(());
}
#[test]
fn create_uses_calibrated_defaults_and_keeps_locked_projection_private() -> ksp_core_lib::Result<()> {
let alias = std::string::String::from("CREATE-ALIAS-PLAINTEXT-CANARY");
let note = std::string::String::from("CREATE-NOTE-PLAINTEXT-CANARY");
let metadata = crate::WalletCreateMetadataV1::new(std::option::Option::Some(alias.clone()), std::vec![note.clone()]);
let runtime = match test_runtime() {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
let owner_result = runtime.block_on(super::create_wallet_v1(
crate::OwnerPassword::new(std::string::String::from("pre005-create-owner-password")),
std::option::Option::None,
metadata,
));
let owner = match owner_result {
std::result::Result::Ok(value) => value,
std::result::Result::Err(error) => return std::result::Result::Err(error),
};
assert_eq!(owner.alias(), std::option::Option::Some(alias.as_str()));
assert_eq!(owner.notes().len(), 1);
let created_note = match owner.notes().first() {
std::option::Option::Some(value) => value,
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),
};
let locked_text = match std::str::from_utf8(locked_bytes.as_slice()) {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("created Wallet JSON is not UTF-8")),
};
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),
};
assert_eq!(envelope.owner_slot().kdf().memory_kib(), crate::KSPWALLET_V1_DEFAULT_ARGON2_MEMORY_KIB);
assert_eq!(envelope.owner_slot().kdf().iterations(), crate::KSPWALLET_V1_DEFAULT_ARGON2_ITERATIONS);
assert_eq!(envelope.owner_slot().kdf().parallelism(), crate::KSPWALLET_V1_DEFAULT_ARGON2_PARALLELISM);
assert_eq!(envelope.owner_slot().kdf().salt().len(), crate::KSPWALLET_V1_DEFAULT_KDF_SALT_BYTES);
assert!(!envelope.view_descriptor().enabled());
assert!(envelope.view_slot().is_none());
return std::result::Result::Ok(());
}
fn full_vector_meta() -> ksp_core_lib::Result<FullVectorMeta> {
let parsed = serde_json::from_slice::<FullVectorMeta>(FULL_VECTOR_META);
return match parsed {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(test_error("full Wallet vector metadata JSON is invalid")),
};
}
fn test_runtime() -> ksp_core_lib::Result<tokio::runtime::Runtime> {
let built = tokio::runtime::Builder::new_current_thread().build();
return match built {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(error) => std::result::Result::Err(test_error("Wallet unit-test Tokio runtime could not be built").with_source(error)),
};
}
fn assert_note_texts(notes: &[crate::WalletNote], expected: &[std::string::String]) {
assert_eq!(notes.len(), expected.len());
for (note, expected_text) in notes.iter().zip(expected.iter()) {
assert_eq!(note.text(), expected_text.as_str());
}
}
fn replace_string_field(source: &[u8], path: &[&str], replacement: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let parsed = serde_json::from_slice::<serde_json::Value>(source);
let mut value = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("Wallet test JSON cannot be parsed")),
};
let mut current = &mut value;
for key in path {
let object = match current.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test JSON path does not reference an object")),
};
current = match object.get_mut(*key) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test JSON path is missing")),
};
}
*current = serde_json::Value::String(std::string::String::from(replacement));
return serialize_test_json(&value);
}
fn replace_view_wrap_ciphertext(source: &[u8], replacement: &str) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let parsed = serde_json::from_slice::<serde_json::Value>(source);
let mut value = match parsed {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::result::Result::Err(test_error("Wallet test JSON cannot be parsed")),
};
let object = match value.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test JSON root is not an object")),
};
let slots = match object.get_mut("key_slots") {
std::option::Option::Some(value) => match value.as_array_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test key_slots is not an array")),
},
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test key_slots is missing")),
};
let view_slot = match slots.get_mut(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW slot is missing")),
};
let view_object = match view_slot.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW slot is not an object")),
};
let wrap = match view_object.get_mut("wrap") {
std::option::Option::Some(value) => match value.as_object_mut() {
std::option::Option::Some(value) => value,
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW wrap is not an object")),
},
std::option::Option::None => return std::result::Result::Err(test_error("Wallet test VIEW wrap is missing")),
};
wrap.insert(std::string::String::from("ciphertext"), serde_json::Value::String(std::string::String::from(replacement)));
return serialize_test_json(&value);
}
fn serialize_test_json(value: &serde_json::Value) -> ksp_core_lib::Result<std::vec::Vec<u8>> {
let serialized = serde_json::to_vec(value);
return match serialized {
std::result::Result::Ok(value) => std::result::Result::Ok(value),
std::result::Result::Err(_) => std::result::Result::Err(test_error("Wallet test JSON cannot be serialized")),
};
}
fn test_error(message: &'static str) -> ksp_core_lib::Error {
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
}