Files
khadhroony-bot3/ks-wallet/tests/native_password.rs
2026-08-11 15:15:03 +02:00

213 lines
9.8 KiB
Rust

// file: ks-wallet/tests/native_password.rs
// version: 2
//! 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 concurrent_native_creation_has_one_winner_and_preserves_valid_destination() {
let directory = tempfile::tempdir()
.unwrap_or_else(|error| panic!("temporary directory must exist: {error}"));
make_directory_private(directory.path());
let left_manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("left manager must be created: {error}"));
let right_manager = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("right manager must be created: {error}"));
let alias = ks_wallet::WalletAlias::parse("concurrent-create")
.unwrap_or_else(|error| panic!("alias must be valid: {error}"));
let left_alias = alias.clone();
let right_alias = alias.clone();
let left = left_manager.create(left_alias, password("left-password"));
let right = right_manager.create(right_alias, password("right-password"));
let (left_result, right_result) = tokio::join!(left, right);
let (winner, loser_error) = match (left_result, right_result) {
(std::result::Result::Ok(wallet), std::result::Result::Err(error)) => (wallet, error),
(std::result::Result::Err(error), std::result::Result::Ok(wallet)) => (wallet, error),
(std::result::Result::Ok(_), std::result::Result::Ok(_)) => {
panic!("concurrent creation must not publish two wallets for one alias");
},
(std::result::Result::Err(_), std::result::Result::Err(_)) => {
panic!("exactly one concurrent creation must succeed");
},
};
assert_eq!(loser_error.code(), "wallet_native_already_exists");
let public_key = winner.public_key();
winner.lock();
let verifier = ks_wallet::WalletManager::new(directory.path())
.unwrap_or_else(|error| panic!("verification manager must be created: {error}"));
let handle = verifier
.lookup(&alias)
.await
.unwrap_or_else(|error| panic!("published wallet lookup must succeed: {error}"))
.unwrap_or_else(|| panic!("published wallet must exist"));
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"));
}