308 lines
16 KiB
Rust
308 lines
16 KiB
Rust
// file: crates/ksp-wallet-lib/unit_tests/wallet.rs
|
|
// 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");
|
|
|
|
#[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(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,
|
|
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(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,
|
|
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(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")),
|
|
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 = crate::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 crate::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(crate::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(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")),
|
|
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 crate::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(crate::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);
|
|
}
|