325 lines
16 KiB
Rust
325 lines
16 KiB
Rust
// file: crates/ksp-app-wallet-desk/tests/desktop_security.rs
|
|
// version: 14
|
|
|
|
//! Static desktop security contracts for the Wallet Desk pre.002 shell.
|
|
|
|
fn app_root() -> std::path::PathBuf {
|
|
return std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
}
|
|
|
|
fn read_text(path: &std::path::Path) -> String {
|
|
let source = std::fs::read_to_string(path);
|
|
assert!(source.is_ok(), "unable to read {}", path.display());
|
|
return match source {
|
|
std::result::Result::Ok(value) => value,
|
|
std::result::Result::Err(_) => String::new(),
|
|
};
|
|
}
|
|
|
|
#[test]
|
|
fn initial_capability_surface_is_core_plus_tracing_only() {
|
|
let root = app_root();
|
|
let capability = read_text(root.join("capabilities/default.json").as_path());
|
|
assert!(capability.contains("\"core:default\""));
|
|
assert!(capability.contains("\"tracing:default\""));
|
|
assert!(!capability.contains("dialog:"));
|
|
assert!(!capability.contains("fs:"));
|
|
}
|
|
|
|
#[test]
|
|
fn frontend_shell_uses_no_browser_native_dialog_or_secret_storage() {
|
|
let root = app_root();
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let html = read_text(root.join("frontend/main.html").as_path());
|
|
for forbidden in ["window.alert", "window.confirm", "window.prompt", "localStorage", "sessionStorage"] {
|
|
assert!(!main.contains(forbidden), "forbidden frontend primitive {forbidden}");
|
|
assert!(!html.contains(forbidden), "forbidden frontend primitive {forbidden}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn locked_inventory_frontend_never_receives_protected_wallet_identity_fields() {
|
|
let root = app_root();
|
|
let inventory = read_text(root.join("src/wallet_inventory.rs").as_path());
|
|
assert!(!inventory.contains("pubkey:"));
|
|
assert!(!inventory.contains("alias:"));
|
|
assert!(!inventory.contains("notes:"));
|
|
assert!(!inventory.contains("password:"));
|
|
}
|
|
|
|
#[test]
|
|
fn create_passwords_are_request_only_and_protected_projection_is_purged() {
|
|
let root = app_root();
|
|
let session = read_text(root.join("src/wallet_session.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let authorized = session.split("pub(crate) struct WalletAuthorizedDto").nth(1);
|
|
assert!(authorized.is_some());
|
|
let authorized = authorized.unwrap_or_default().split("}\n").next().unwrap_or_default();
|
|
let authorized_fields = authorized.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
|
|
assert!(!authorized_fields.contains("password"));
|
|
let request_start = session.find("pub(crate) struct WalletCreateRequestDto");
|
|
assert!(request_start.is_some());
|
|
let request_start = request_start.unwrap_or_default();
|
|
assert!(session[request_start..].contains("owner_password"));
|
|
assert!(session[request_start..].contains("view_password"));
|
|
assert!(main.contains("clearAuthorizedProjection"));
|
|
assert!(main.contains("clearCreateFormSensitiveInputs"));
|
|
assert!(!main.contains("frontendDebug(\"main\", request.ownerPassword"));
|
|
assert!(!main.contains("frontendTrace(\"main\", request.viewPassword"));
|
|
}
|
|
|
|
#[test]
|
|
fn unlock_passwords_and_config_secret_names_never_cross_rust_to_frontend() {
|
|
let root = app_root();
|
|
let session = read_text(root.join("src/wallet_session.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let html = read_text(root.join("frontend/main.html").as_path());
|
|
let authorized = session.split("pub(crate) struct WalletAuthorizedDto").nth(1);
|
|
assert!(authorized.is_some());
|
|
let authorized = authorized.unwrap_or_default().split("}\n").next().unwrap_or_default();
|
|
let authorized_fields = authorized.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
|
|
assert!(!authorized_fields.contains("password"));
|
|
let unlock_request = session.find("pub(crate) struct WalletUnlockRequestDto");
|
|
assert!(unlock_request.is_some());
|
|
assert!(session[unlock_request.unwrap_or_default()..].contains("password: String"));
|
|
assert!(main.contains("clearUnlockSensitiveInputs"));
|
|
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
|
|
assert!(!html.contains("KSP_SECRET_WALLET_PASS_"));
|
|
assert!(!main.contains("variableName"));
|
|
assert!(!main.contains("effectiveSource"));
|
|
}
|
|
|
|
#[test]
|
|
fn wallet_selection_does_not_auto_unlock_configured_secrets() {
|
|
let root = app_root();
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let selection_start = main.find("async function selectWallet");
|
|
let selection_end = main.find("function bindWalletTableSelection");
|
|
assert!(selection_start.is_some());
|
|
assert!(selection_end.is_some());
|
|
let selection = &main[selection_start.unwrap_or_default()..selection_end.unwrap_or(main.len())];
|
|
assert!(selection.contains("select_wallet"));
|
|
assert!(!selection.contains("unlock_wallet_view_configured"));
|
|
assert!(!selection.contains("unlock_wallet_owner_configured"));
|
|
}
|
|
|
|
#[test]
|
|
fn balance_refresh_never_accepts_pubkey_or_endpoint_url_from_frontend() {
|
|
let root = app_root();
|
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let command_start = tauri.find("async fn refresh_wallet_balance");
|
|
assert!(command_start.is_some());
|
|
let command = &tauri[command_start.unwrap_or_default()..];
|
|
let command = command.split("#[tauri::command]").next().unwrap_or_default();
|
|
assert!(!command.contains("request:"));
|
|
assert!(!command.contains("pubkey"));
|
|
let refresh_start = main.find("async function refreshWalletBalance");
|
|
let refresh_end = main.find("function bindBalanceActions");
|
|
assert!(refresh_start.is_some());
|
|
assert!(refresh_end.is_some());
|
|
let refresh = &main[refresh_start.unwrap_or_default()..refresh_end.unwrap_or(main.len())];
|
|
assert!(refresh.contains("refresh_wallet_balance"));
|
|
assert!(!refresh.contains("pubkey"));
|
|
assert!(!main.contains("transportUrl"));
|
|
assert!(!main.contains("endpointUrl"));
|
|
assert!(state.contains("wallet.pubkey()"));
|
|
}
|
|
|
|
#[test]
|
|
fn import_picker_path_and_transfer_secret_never_cross_into_frontend() {
|
|
let root = app_root();
|
|
let package = read_text(root.join("package.json").as_path());
|
|
let capability = read_text(root.join("capabilities/default.json").as_path());
|
|
let import = read_text(root.join("src/wallet_import.rs").as_path());
|
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
assert!(!package.contains("@tauri-apps/plugin-dialog"));
|
|
assert!(!capability.contains("dialog:"));
|
|
assert!(!capability.contains("fs:"));
|
|
assert!(!main.contains("@tauri-apps/plugin-dialog"));
|
|
assert!(!main.contains("sourcePath"));
|
|
assert!(!main.contains("source_path"));
|
|
assert!(import.contains("zeroize::Zeroizing"));
|
|
let inspection = import.split("pub(crate) struct WalletTransferInspectionDto").nth(1);
|
|
assert!(inspection.is_some());
|
|
let inspection = inspection.unwrap_or_default().split("}\n").next().unwrap_or_default();
|
|
let inspection_fields = inspection.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
|
|
assert!(!inspection_fields.contains("path"));
|
|
assert!(!inspection_fields.contains("password"));
|
|
let request = import.split("pub(crate) struct WalletImportRequestDto").nth(1);
|
|
assert!(request.is_some());
|
|
let request = request.unwrap_or_default().split("}\n").next().unwrap_or_default();
|
|
let request_fields = request.lines().filter(|line| return line.trim_start().starts_with("pub(crate) ")).collect::<std::vec::Vec<_>>().join("\n");
|
|
assert!(request_fields.contains("owner_password"));
|
|
assert!(request_fields.contains("view_password"));
|
|
assert!(!request_fields.contains("path"));
|
|
let command_start = tauri.find("async fn inspect_import_source");
|
|
assert!(command_start.is_some());
|
|
let command = &tauri[command_start.unwrap_or_default()..];
|
|
let command = command.split("#[tauri::command]").next().unwrap_or_default();
|
|
assert!(command.contains("WalletImportSourceRequestDto"));
|
|
assert!(!command.contains("source_path"));
|
|
}
|
|
|
|
#[test]
|
|
fn owner_metadata_values_are_request_only_and_state_conflict_forces_reauthorization() {
|
|
let root = app_root();
|
|
let metadata = read_text(root.join("src/wallet_metadata.rs").as_path());
|
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
assert!(!metadata.contains("derive(Clone"));
|
|
assert!(!metadata.contains("derive(Debug"));
|
|
assert!(state.contains("recover_owner_state_conflict"));
|
|
assert!(state.contains("inspect_locked_wallet_file"));
|
|
assert!(state.contains("ERROR_CODE_STATE_CONFLICT"));
|
|
assert!(main.contains("isWalletStateConflict"));
|
|
assert!(main.contains("select_wallet"));
|
|
assert!(main.contains("clearAuthorizedProjection"));
|
|
assert!(!main.contains("frontendDebug(\"main\", request.text"));
|
|
assert!(!main.contains("frontendTrace(\"main\", request.text"));
|
|
assert!(!main.contains("frontendDebug(\"main\", request.alias"));
|
|
assert!(!main.contains("window.confirm"));
|
|
assert!(main.contains("deleteNoteModal = new Modal(modalElement)"));
|
|
}
|
|
|
|
#[test]
|
|
fn rotation_passwords_are_request_only_confirmed_frontend_side_and_never_logged() {
|
|
let root = app_root();
|
|
let security = read_text(root.join("src/wallet_security.rs").as_path());
|
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
assert!(!security.contains("derive(Clone"));
|
|
assert!(!security.contains("derive(Debug"));
|
|
let request = security.split("pub(crate) struct WalletPasswordRotationRequestDto").nth(1);
|
|
assert!(request.is_some());
|
|
let fields = request
|
|
.unwrap_or_default()
|
|
.split("}\n")
|
|
.next()
|
|
.unwrap_or_default()
|
|
.lines()
|
|
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
|
.collect::<std::vec::Vec<_>>()
|
|
.join("\n");
|
|
assert_eq!(fields.matches("password").count(), 1);
|
|
assert!(!fields.contains("confirm"));
|
|
assert!(state.contains("OwnerPassword::new(request.password)"));
|
|
assert!(state.contains("ViewPassword::new(request.password)"));
|
|
assert!(state.contains("view.rotate_view_password"));
|
|
assert!(state.contains("owner.rotate_view_password"));
|
|
assert!(main.contains("password.value !== confirmation.value"));
|
|
assert!(main.contains("clearRotationSensitiveInputs"));
|
|
assert!(main.contains("viewAuthorized"));
|
|
assert!(main.contains("recoverRotationStateConflict"));
|
|
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
|
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
|
assert!(!main.contains("KSP_SECRET_WALLET_PASS_"));
|
|
}
|
|
|
|
#[test]
|
|
fn strong_view_administration_is_owner_only_uses_bootstrap_modals_and_returns_no_secret() {
|
|
let root = app_root();
|
|
let security = read_text(root.join("src/wallet_security.rs").as_path());
|
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let status = security.split("pub(crate) struct WalletViewSecurityStatusDto").nth(1);
|
|
assert!(status.is_some());
|
|
let fields = status
|
|
.unwrap_or_default()
|
|
.split("}\n")
|
|
.next()
|
|
.unwrap_or_default()
|
|
.lines()
|
|
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
|
.collect::<std::vec::Vec<_>>()
|
|
.join("\n");
|
|
assert!(!fields.contains("password"));
|
|
assert!(!fields.contains("pubkey"));
|
|
assert!(!fields.contains("alias"));
|
|
assert!(!fields.contains("note"));
|
|
assert!(state.contains("begin_owner_operation(\"view_disable\")"));
|
|
assert!(state.contains("begin_owner_operation(\"view_recreate\")"));
|
|
assert!(!state.contains("begin_view_operation(\"view_disable\")"));
|
|
assert!(!state.contains("begin_view_operation(\"view_recreate\")"));
|
|
let disable = tauri.find("async fn disable_wallet_view");
|
|
assert!(disable.is_some());
|
|
let disable = &tauri[disable.unwrap_or_default()..];
|
|
let disable = disable.split("#[tauri::command]").next().unwrap_or_default();
|
|
assert!(!disable.contains("request:"));
|
|
let recreate = tauri.find("async fn recreate_wallet_view");
|
|
assert!(recreate.is_some());
|
|
let recreate = &tauri[recreate.unwrap_or_default()..];
|
|
let recreate = recreate.split("#[tauri::command]").next().unwrap_or_default();
|
|
assert!(recreate.contains("WalletPasswordRotationRequestDto"));
|
|
assert!(main.contains("new Modal(disableModalElement)"));
|
|
assert!(main.contains("new Modal(recreateModalElement)"));
|
|
assert!(main.contains("clearStrongViewSensitiveInputs"));
|
|
assert!(main.contains("hidden.bs.modal"));
|
|
assert!(!main.contains("window.confirm"));
|
|
assert!(!main.contains("frontendDebug(\"main\", request.password"));
|
|
assert!(!main.contains("frontendTrace(\"main\", request.password"));
|
|
}
|
|
|
|
#[test]
|
|
fn owner_export_never_projects_key_material_or_full_destination_path_to_frontend() {
|
|
let root = app_root();
|
|
let export = read_text(root.join("src/wallet_export.rs").as_path());
|
|
let state = read_text(root.join("src/app_state.rs").as_path());
|
|
let tauri = read_text(root.join("src/tauri.rs").as_path());
|
|
let main = read_text(root.join("frontend/ts/main.ts").as_path());
|
|
let request = export.split("pub(crate) struct WalletExportRequestDto").nth(1);
|
|
assert!(request.is_some());
|
|
let request_fields = request
|
|
.unwrap_or_default()
|
|
.split("}\n")
|
|
.next()
|
|
.unwrap_or_default()
|
|
.lines()
|
|
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
|
.collect::<std::vec::Vec<_>>()
|
|
.join("\n");
|
|
assert!(request_fields.contains("format"));
|
|
assert!(!export.contains("#[derive(Clone, serde::Deserialize"));
|
|
assert!(!export.contains("#[derive(Debug, serde::Deserialize"));
|
|
assert!(!request_fields.contains("path"));
|
|
assert!(!request_fields.contains("password"));
|
|
assert!(!request_fields.contains("secret"));
|
|
let result = export.split("pub(crate) struct WalletExportResultDto").nth(1);
|
|
assert!(result.is_some());
|
|
let result_fields = result
|
|
.unwrap_or_default()
|
|
.split("}\n")
|
|
.next()
|
|
.unwrap_or_default()
|
|
.lines()
|
|
.filter(|line| return line.trim_start().starts_with("pub(crate) "))
|
|
.collect::<std::vec::Vec<_>>()
|
|
.join("\n");
|
|
assert!(result_fields.contains("destination_name"));
|
|
assert!(result_fields.contains("format"));
|
|
assert!(result_fields.contains("wallet_id"));
|
|
assert!(!result_fields.contains("path"));
|
|
assert!(!result_fields.contains("pubkey"));
|
|
assert!(!result_fields.contains("password"));
|
|
assert!(!result_fields.contains("secret"));
|
|
assert!(!result_fields.contains("content"));
|
|
assert!(!result_fields.contains("bytes"));
|
|
assert!(state.contains("owner.export_transfer_file"));
|
|
assert!(!state.contains("owner.export_transfer(format"));
|
|
let command_start = tauri.find("async fn export_wallet_transfer");
|
|
assert!(command_start.is_some());
|
|
let command = &tauri[command_start.unwrap_or_default()..];
|
|
let command = command.split("#[tauri::command]").next().unwrap_or_default();
|
|
assert!(command.contains("WalletExportRequestDto"));
|
|
assert!(command.contains("WalletExportResultDto"));
|
|
assert!(!main.contains("navigator.clipboard"));
|
|
assert!(!main.contains("destinationPath"));
|
|
assert!(!main.contains("destination_path"));
|
|
assert!(!main.contains("keypairBytes"));
|
|
assert!(!main.contains("secretKey"));
|
|
assert!(main.contains("exportWalletModal = new Modal(modalElement)"));
|
|
}
|