Feat: Windows-Hello Testing (DevTools Only)

This commit is contained in:
Youwes09
2026-05-01 00:09:49 -05:00
parent 0cd799f450
commit 1801fecdbb
6 changed files with 200 additions and 2 deletions
+65
View File
@@ -2111,6 +2111,7 @@ dependencies = [
"tokio", "tokio",
"urlencoding", "urlencoding",
"walkdir", "walkdir",
"windows 0.58.0",
] ]
[[package]] [[package]]
@@ -5472,6 +5473,16 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6"
dependencies = [
"windows-core 0.58.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.3"
@@ -5506,6 +5517,19 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-core"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99"
dependencies = [
"windows-implement 0.58.0",
"windows-interface 0.58.0",
"windows-result 0.2.0",
"windows-strings 0.1.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-core" name = "windows-core"
version = "0.61.2" version = "0.61.2"
@@ -5554,6 +5578,17 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "windows-implement"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "windows-implement" name = "windows-implement"
version = "0.60.2" version = "0.60.2"
@@ -5576,6 +5611,17 @@ dependencies = [
"syn 2.0.117", "syn 2.0.117",
] ]
[[package]]
name = "windows-interface"
version = "0.58.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]] [[package]]
name = "windows-interface" name = "windows-interface"
version = "0.59.3" version = "0.59.3"
@@ -5629,6 +5675,15 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.3.4" version = "0.3.4"
@@ -5647,6 +5702,16 @@ dependencies = [
"windows-link 0.2.1", "windows-link 0.2.1",
] ]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-strings" name = "windows-strings"
version = "0.4.2" version = "0.4.2"
+6
View File
@@ -32,6 +32,12 @@ urlencoding = "2"
tokio = { version = "1", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
reqwest = { version = "0.12", features = ["blocking"] } reqwest = { version = "0.12", features = ["blocking"] }
[target.'cfg(target_os = "windows")'.dependencies]
windows = { version = "0.58", features = [
"Security_Credentials_UI",
"Win32_UI_WindowsAndMessaging",
] }
[profile.release] [profile.release]
codegen-units = 1 codegen-units = 1
lto = true lto = true
+93
View File
@@ -0,0 +1,93 @@
#[cfg(target_os = "windows")]
mod windows_hello {
use windows::{
core::HSTRING,
Security::Credentials::UI::{UserConsentVerificationResult, UserConsentVerifier},
Win32::UI::WindowsAndMessaging::{
BringWindowToTop, FindWindowW, IsIconic, SetForegroundWindow, ShowWindow, SW_RESTORE,
},
};
fn to_wide(s: &str) -> Vec<u16> {
use std::os::windows::ffi::OsStrExt;
std::ffi::OsStr::new(s)
.encode_wide()
.chain(std::iter::once(0))
.collect()
}
fn try_focus_hello_dialog() -> bool {
let cls = to_wide("Credential Dialog Xaml Host");
unsafe {
let Ok(hwnd) = FindWindowW(
windows::core::PCWSTR(cls.as_ptr()),
windows::core::PCWSTR::null(),
) else {
return false;
};
if IsIconic(hwnd).as_bool() {
let _ = ShowWindow(hwnd, SW_RESTORE);
}
let _ = BringWindowToTop(hwnd);
let _ = SetForegroundWindow(hwnd);
true
}
}
fn nudge_focus(retries: u32, delay_ms: u64) {
std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
for _ in 0..retries {
if try_focus_hello_dialog() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
}
});
}
pub fn authenticate(reason: &str) -> Result<(), String> {
nudge_focus(5, 250);
let result = UserConsentVerifier::RequestVerificationAsync(&HSTRING::from(reason))
.and_then(|op| {
nudge_focus(5, 250);
op.get()
})
.map_err(|e| format!("internalError:{e:?}"))?;
match result {
UserConsentVerificationResult::Verified => Ok(()),
UserConsentVerificationResult::Canceled => Err("userCancel".into()),
UserConsentVerificationResult::RetriesExhausted => Err("biometryLockout".into()),
UserConsentVerificationResult::DeviceBusy => Err("systemCancel".into()),
UserConsentVerificationResult::DeviceNotPresent => Err("biometryNotAvailable".into()),
UserConsentVerificationResult::DisabledByPolicy => Err("biometryNotAvailable".into()),
UserConsentVerificationResult::NotConfiguredForUser => Err("biometryNotEnrolled".into()),
_ => Err("authenticationFailed".into()),
}
}
pub fn is_available() -> bool {
use windows::Security::Credentials::UI::UserConsentVerifierAvailability;
UserConsentVerifier::CheckAvailabilityAsync()
.and_then(|op| op.get())
.map(|a| a == UserConsentVerifierAvailability::Available)
.unwrap_or(false)
}
}
#[tauri::command]
pub fn windows_hello_authenticate(_reason: String) -> Result<(), String> {
#[cfg(target_os = "windows")]
return windows_hello::authenticate(&reason);
#[cfg(not(target_os = "windows"))]
Err("notSupported".into())
}
#[tauri::command]
pub fn windows_hello_available() -> bool {
#[cfg(target_os = "windows")]
return windows_hello::is_available();
#[cfg(not(target_os = "windows"))]
false
}
+1
View File
@@ -1,4 +1,5 @@
pub mod backup; pub mod backup;
pub mod biometric;
pub mod server; pub mod server;
pub mod storage; pub mod storage;
pub mod system; pub mod system;
+2
View File
@@ -41,6 +41,8 @@ pub fn run() {
commands::backup::get_auto_backup_dir, commands::backup::get_auto_backup_dir,
commands::updater::list_releases, commands::updater::list_releases,
commands::updater::download_and_install_update, commands::updater::download_and_install_update,
commands::biometric::windows_hello_authenticate,
commands::biometric::windows_hello_available,
]) ])
.setup(|_app| Ok(())) .setup(|_app| Ok(()))
.on_window_event(|window, event| { .on_window_event(|window, event| {
@@ -2,6 +2,7 @@
import ThreeDCard from "@shared/manga/ThreeDCard.svelte"; import ThreeDCard from "@shared/manga/ThreeDCard.svelte";
import { store, addToast } from "@store/state.svelte"; import { store, addToast } from "@store/state.svelte";
import { cache } from "@core/cache/index"; import { cache } from "@core/cache/index";
import { invoke } from "@tauri-apps/api/core";
interface PerfSnapshot { cacheEntries: number; cacheKeys: string[]; oldestEntryMs: number | null; newestEntryMs: number | null; } interface PerfSnapshot { cacheEntries: number; cacheKeys: string[]; oldestEntryMs: number | null; newestEntryMs: number | null; }
@@ -9,10 +10,13 @@
let splashTriggered = $state(false); let splashTriggered = $state(false);
let expOpen = $state(false); let expOpen = $state(false);
let appVersion = $state("…"); let appVersion = $state("…");
let helloAvailable = $state<boolean | null>(null);
let helloBusy = $state(false);
$effect(() => { $effect(() => {
import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {}); import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {});
refreshPerfMetrics(); refreshPerfMetrics();
invoke<boolean>("windows_hello_available").then(v => helloAvailable = v).catch(() => helloAvailable = false);
}); });
function refreshPerfMetrics() { function refreshPerfMetrics() {
@@ -49,6 +53,18 @@
setTimeout(() => splashTriggered = false, 200); setTimeout(() => splashTriggered = false, 200);
(window as any).__mokuShowSplash?.(); (window as any).__mokuShowSplash?.();
} }
async function testWindowsHello() {
helloBusy = true;
try {
await invoke("windows_hello_authenticate", { reason: "Moku devtools test" });
addToast({ kind: "success", title: "Windows Hello", body: "Verified successfully" });
} catch (e: any) {
addToast({ kind: "error", title: "Windows Hello", body: String(e) });
} finally {
helloBusy = false;
}
}
</script> </script>
<div class="s-panel"> <div class="s-panel">
@@ -81,6 +97,21 @@
</div> </div>
</div> </div>
<div class="s-section">
<p class="s-section-title">Biometrics</p>
<div class="s-section-body">
<div class="s-row">
<div class="s-row-info">
<span class="s-label">Windows Hello</span>
<span class="s-desc">Available: {helloAvailable === null ? "…" : helloAvailable ? "yes" : "no"}</span>
</div>
<button class="s-btn" disabled={!helloAvailable || helloBusy} onclick={testWindowsHello}>
{helloBusy ? "…" : "Test"}
</button>
</div>
</div>
</div>
<div class="s-section"> <div class="s-section">
<button class="s-collapsible-trigger" onclick={() => expOpen = !expOpen} aria-expanded={expOpen}> <button class="s-collapsible-trigger" onclick={() => expOpen = !expOpen} aria-expanded={expOpen}>
<span class="s-label">Experimental</span> <span class="s-label">Experimental</span>