v0.2.5-pre.005
This commit is contained in:
32
crates/ksp-wallet-lib/unit_tests/payload.rs
Normal file
32
crates/ksp-wallet-lib/unit_tests/payload.rs
Normal file
@@ -0,0 +1,32 @@
|
||||
// file: crates/ksp-wallet-lib/unit_tests/payload.rs
|
||||
// version: 1
|
||||
|
||||
use base64::Engine as _;
|
||||
|
||||
#[test]
|
||||
fn metadata_payload_rejects_duplicate_note_identifiers() -> ksp_core_lib::Result<()> {
|
||||
let duplicate_id = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode([7_u8; crate::KSPWALLET_V1_NOTE_ID_BYTES]);
|
||||
let payload = serde_json::json!({
|
||||
"pubkey": "11111111111111111111111111111111",
|
||||
"alias": null,
|
||||
"notes": [
|
||||
{"id": duplicate_id.clone(), "text": "first"},
|
||||
{"id": duplicate_id, "text": "second"}
|
||||
]
|
||||
});
|
||||
let encoded = match serde_json::to_vec(&payload) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(test_error("test metadata payload could not be serialized")),
|
||||
};
|
||||
let result = super::decode_metadata_payload(encoded.as_slice());
|
||||
let error = match result {
|
||||
std::result::Result::Ok(_) => return std::result::Result::Err(test_error("duplicate note identifiers unexpectedly parsed")),
|
||||
std::result::Result::Err(error) => error,
|
||||
};
|
||||
assert_eq!(error.code(), crate::ERROR_CODE_FORMAT_INVALID);
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn test_error(message: &'static str) -> ksp_core_lib::Error {
|
||||
return ksp_core_lib::Error::new(crate::ERROR_CODE_FORMAT_INVALID, message);
|
||||
}
|
||||
316
crates/ksp-wallet-lib/unit_tests/wallet.rs
Normal file
316
crates/ksp-wallet-lib/unit_tests/wallet.rs
Normal file
@@ -0,0 +1,316 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user