// file: ks-wallet/tests/native_password.rs // version: 1 //! External password-protected native wallet lifecycle tests. fn make_directory_private(path: &std::path::Path) { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o700)).unwrap_or_else( |error| panic!("wallet fixture directory permissions must be set: {error}"), ); } } fn make_file_private(path: &std::path::Path) { #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)) .unwrap_or_else(|error| panic!("wallet fixture permissions must be set: {error}")); } } fn password(value: &str) -> ks_wallet::WalletPassword { return ks_wallet::WalletPassword::new(value.to_owned()) .unwrap_or_else(|error| panic!("test password must be accepted: {error}")); } #[tokio::test] async fn native_wallet_password_lifecycle_preserves_keypair_and_supports_external_open() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); make_directory_private(directory.path()); let manager = ks_wallet::WalletManager::new(directory.path()) .unwrap_or_else(|error| panic!("manager must be created: {error}")); let alias = ks_wallet::WalletAlias::parse("protected") .unwrap_or_else(|error| panic!("alias must be valid: {error}")); let created = manager .create(alias.clone(), password("initial-password")) .await .unwrap_or_else(|error| panic!("native wallet creation must succeed: {error}")); let public_key = created.public_key(); let signature = created .sign_message(b"ks-wallet password lifecycle") .unwrap_or_else(|error| panic!("unlocked wallet must sign: {error}")); assert!(!signature.is_empty()); created.lock(); let handle = manager .lookup(&alias) .await .unwrap_or_else(|error| panic!("lookup must succeed: {error}")) .unwrap_or_else(|| panic!("created wallet must be discoverable")); assert_eq!(handle.public_key(), public_key); let external_directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("external directory must exist: {error}")); let external_path = external_directory.path().join("protected.kswallet"); std::fs::copy(directory.path().join("protected.kswallet"), &external_path) .unwrap_or_else(|error| panic!("external fixture copy must succeed: {error}")); make_file_private(&external_path); let external_handle = manager .inspect_file(&external_path) .await .unwrap_or_else(|error| panic!("external inspection must succeed: {error}")); let external = manager .unlock_file(&external_handle, password("initial-password")) .await .unwrap_or_else(|error| panic!("external wallet unlock must succeed: {error}")); assert_eq!(external.public_key(), public_key); external.lock(); let changed = manager .change_password(&alias, password("initial-password"), password("replacement-password")) .await .unwrap_or_else(|error| panic!("password change must succeed: {error}")); assert_eq!(changed.public_key(), public_key); let old_password_error = manager .unlock(&alias, password("initial-password")) .await .err() .unwrap_or_else(|| panic!("old password must no longer unlock the wallet")); assert_eq!(old_password_error.code(), "wallet_native_authentication_failed"); assert!(!old_password_error.to_string().contains("initial-password")); let reopened = manager .unlock(&alias, password("replacement-password")) .await .unwrap_or_else(|error| panic!("replacement password must unlock: {error}")); assert_eq!(reopened.public_key(), public_key); reopened.lock(); let bytes = std::fs::read(directory.path().join("protected.kswallet")) .unwrap_or_else(|error| panic!("native wallet must be readable for canary check: {error}")); assert!( !bytes .windows("initial-password".len()) .any(|window| return window == b"initial-password") ); assert!( !bytes .windows("replacement-password".len()) .any(|window| return window == b"replacement-password") ); } #[tokio::test] async fn native_creation_is_private_and_refuses_overwrite() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); make_directory_private(directory.path()); let manager = ks_wallet::WalletManager::new(directory.path()) .unwrap_or_else(|error| panic!("manager must be created: {error}")); let alias = ks_wallet::WalletAlias::parse("no-overwrite") .unwrap_or_else(|error| panic!("alias must be valid: {error}")); let created = manager .create(alias.clone(), password("first-password")) .await .unwrap_or_else(|error| panic!("native wallet creation must succeed: {error}")); let public_key = created.public_key(); created.lock(); let path = directory.path().join("no-overwrite.kswallet"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; // rust-rules: trait-import let metadata = std::fs::metadata(&path) .unwrap_or_else(|error| panic!("native wallet metadata must be readable: {error}")); assert_eq!(metadata.permissions().mode() & 0o777, 0o600); } let error = manager .create(alias.clone(), password("second-password")) .await .err() .unwrap_or_else(|| panic!("duplicate native wallet creation must fail")); assert_eq!(error.code(), "wallet_native_already_exists"); let handle = manager .lookup(&alias) .await .unwrap_or_else(|error| panic!("existing native wallet lookup must succeed: {error}")) .unwrap_or_else(|| panic!("existing native wallet must remain discoverable")); assert_eq!(handle.public_key(), public_key); } #[tokio::test] async fn authenticated_header_tampering_fails_before_signing_capability() { let directory = tempfile::tempdir() .unwrap_or_else(|error| panic!("temporary directory must exist: {error}")); make_directory_private(directory.path()); let manager = ks_wallet::WalletManager::new(directory.path()) .unwrap_or_else(|error| panic!("manager must be created: {error}")); let alias = ks_wallet::WalletAlias::parse("tamper") .unwrap_or_else(|error| panic!("alias must be valid: {error}")); let created = manager .create(alias.clone(), password("tamper-password")) .await .unwrap_or_else(|error| panic!("native wallet creation must succeed: {error}")); created.lock(); let path = directory.path().join("tamper.kswallet"); let mut bytes = std::fs::read(&path) .unwrap_or_else(|error| panic!("native wallet fixture must be readable: {error}")); bytes[40] ^= 0x01; std::fs::write(&path, bytes) .unwrap_or_else(|error| panic!("tampered fixture must be writable: {error}")); make_file_private(&path); let handle = manager .lookup(&alias) .await .unwrap_or_else(|error| panic!("structural lookup must succeed: {error}")) .unwrap_or_else(|| panic!("tampered wallet must remain structurally discoverable")); let error = manager .unlock_file(&handle, password("tamper-password")) .await .err() .unwrap_or_else(|| panic!("authenticated header tampering must fail")); assert_eq!(error.code(), "wallet_native_authentication_failed"); assert!(!error.to_string().contains("tamper-password")); }