diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4ed22b9..60925f5 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2111,6 +2111,7 @@ dependencies = [ "tokio", "urlencoding", "walkdir", + "windows 0.58.0", ] [[package]] @@ -5472,6 +5473,16 @@ dependencies = [ "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]] name = "windows" version = "0.61.3" @@ -5506,6 +5517,19 @@ dependencies = [ "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]] name = "windows-core" version = "0.61.2" @@ -5554,6 +5578,17 @@ dependencies = [ "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]] name = "windows-implement" version = "0.60.2" @@ -5576,6 +5611,17 @@ dependencies = [ "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]] name = "windows-interface" version = "0.59.3" @@ -5629,6 +5675,15 @@ dependencies = [ "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]] name = "windows-result" version = "0.3.4" @@ -5647,6 +5702,16 @@ dependencies = [ "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]] name = "windows-strings" version = "0.4.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 45bc6b3..21bcf8d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -32,6 +32,12 @@ urlencoding = "2" tokio = { version = "1", features = ["rt-multi-thread"] } 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] codegen-units = 1 lto = true diff --git a/src-tauri/src/commands/biometric.rs b/src-tauri/src/commands/biometric.rs new file mode 100644 index 0000000..d3723a7 --- /dev/null +++ b/src-tauri/src/commands/biometric.rs @@ -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 { + 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 +} \ No newline at end of file diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index ece0d93..e95d774 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -1,5 +1,6 @@ pub mod backup; +pub mod biometric; pub mod server; pub mod storage; pub mod system; -pub mod updater; +pub mod updater; \ No newline at end of file diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 78dad53..48c95ae 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -41,6 +41,8 @@ pub fn run() { commands::backup::get_auto_backup_dir, commands::updater::list_releases, commands::updater::download_and_install_update, + commands::biometric::windows_hello_authenticate, + commands::biometric::windows_hello_available, ]) .setup(|_app| Ok(())) .on_window_event(|window, event| { diff --git a/src/features/settings/sections/DevtoolsSettings.svelte b/src/features/settings/sections/DevtoolsSettings.svelte index abb704e..6e6fb36 100644 --- a/src/features/settings/sections/DevtoolsSettings.svelte +++ b/src/features/settings/sections/DevtoolsSettings.svelte @@ -2,17 +2,21 @@ import ThreeDCard from "@shared/manga/ThreeDCard.svelte"; import { store, addToast } from "@store/state.svelte"; 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; } - let perfSnapshot = $state(null); + let perfSnapshot = $state(null); let splashTriggered = $state(false); let expOpen = $state(false); let appVersion = $state("…"); + let helloAvailable = $state(null); + let helloBusy = $state(false); $effect(() => { import("@tauri-apps/api/app").then(m => m.getVersion()).then(v => appVersion = v).catch(() => {}); refreshPerfMetrics(); + invoke("windows_hello_available").then(v => helloAvailable = v).catch(() => helloAvailable = false); }); function refreshPerfMetrics() { @@ -49,6 +53,18 @@ setTimeout(() => splashTriggered = false, 200); (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; + } + }
@@ -81,6 +97,21 @@
+
+

Biometrics

+
+
+
+ Windows Hello + Available: {helloAvailable === null ? "…" : helloAvailable ? "yes" : "no"} +
+ +
+
+
+