// file: crates/ksp-wallet-lib/unit_tests/wallet.rs // version: 3 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, } #[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 { let parsed = serde_json::from_slice::(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 { 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> { let parsed = serde_json::from_slice::(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> { let parsed = serde_json::from_slice::(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> { 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); }