174 lines
7.7 KiB
Rust
174 lines
7.7 KiB
Rust
// 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"));
|
|
}
|