Skip to content

Commit 953d01a

Browse files
authored
Merge pull request #3 from Choochmeque/windows-support
Windows support
2 parents 36b6a1f + f0e861f commit 953d01a

File tree

5 files changed

+215
-9
lines changed

5 files changed

+215
-9
lines changed

Cargo.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "tauri-plugin-biometry"
33
version = "0.1.1"
4-
authors = [ "You" ]
4+
authors = ["You"]
55
description = "A Tauri v2 plugin for biometric authentication (Touch ID, Face ID, fingerprint) on iOS and Android"
66
edition = "2021"
77
rust-version = "1.77.2"
@@ -18,5 +18,8 @@ serde_repr = "0.1"
1818
thiserror = "2"
1919
log = "0.4"
2020

21+
[target.'cfg(target_os = "windows")'.dependencies]
22+
windows = { version = "0.58", features = ["Foundation", "Security_Credentials_UI", "Win32_Foundation", "Win32_UI_WindowsAndMessaging"] }
23+
2124
[build-dependencies]
2225
tauri-plugin = { version = "2.4.0", features = ["build"] }

guest-js/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@ import { invoke } from "@tauri-apps/api/core";
77
export enum BiometryType {
88
/** No biometry available */
99
None = 0,
10+
/** Automatic biometry (e.g., Face ID, Touch ID) */
11+
Auto = 1,
1012
/** Apple Touch ID or Android fingerprint authentication */
11-
TouchID = 1,
13+
TouchID = 2,
1214
/** Apple Face ID or Android face authentication */
13-
FaceID = 2,
15+
FaceID = 3,
1416
/** Android iris authentication (Samsung devices) */
15-
Iris = 3,
17+
Iris = 4,
1618
}
1719

1820
/**

src/lib.rs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,25 @@ use tauri::{
55

66
pub use models::*;
77

8-
#[cfg(desktop)]
8+
#[cfg(all(desktop, not(target_os = "windows")))]
99
mod desktop;
1010
#[cfg(mobile)]
1111
mod mobile;
12+
#[cfg(target_os = "windows")]
13+
mod windows;
1214

1315
mod commands;
1416
mod error;
1517
mod models;
1618

1719
pub use error::{Error, Result};
1820

19-
#[cfg(desktop)]
21+
#[cfg(all(desktop, not(target_os = "windows")))]
2022
use desktop::Biometry;
2123
#[cfg(mobile)]
2224
use mobile::Biometry;
25+
#[cfg(target_os = "windows")]
26+
use windows::Biometry;
2327

2428
/// Extensions to [`tauri::App`], [`tauri::AppHandle`], [`tauri::WebviewWindow`], [`tauri::Webview`] and [`tauri::Window`] to access the biometry APIs.
2529
pub trait BiometryExt<R: Runtime> {
@@ -46,8 +50,10 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
4650
.setup(|app, api| {
4751
#[cfg(mobile)]
4852
let biometry = mobile::init(app, api)?;
49-
#[cfg(desktop)]
53+
#[cfg(all(desktop, not(target_os = "windows")))]
5054
let biometry = desktop::init(app, api)?;
55+
#[cfg(target_os = "windows")]
56+
let biometry = windows::init(app, api)?;
5157
app.manage(biometry);
5258
Ok(())
5359
})

src/models.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,9 @@ pub struct AuthenticatePayload {
2828
#[repr(u8)]
2929
pub enum BiometryType {
3030
None = 0,
31-
TouchID = 1,
32-
FaceID = 2,
31+
Auto = 1,
32+
TouchID = 2,
33+
FaceID = 3,
3334
}
3435

3536
#[derive(Debug, Clone, Deserialize, Serialize)]

src/windows.rs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
use serde::de::DeserializeOwned;
2+
use tauri::{plugin::PluginApi, AppHandle, Runtime};
3+
4+
use crate::models::*;
5+
6+
use windows::{
7+
core::*,
8+
Security::Credentials::UI::{
9+
UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability,
10+
},
11+
Win32::UI::WindowsAndMessaging::{
12+
BringWindowToTop, FindWindowW, IsIconic, SetForegroundWindow, ShowWindow, SW_RESTORE,
13+
},
14+
};
15+
16+
pub fn init<R: Runtime, C: DeserializeOwned>(
17+
app: &AppHandle<R>,
18+
_api: PluginApi<R, C>,
19+
) -> crate::Result<Biometry<R>> {
20+
Ok(Biometry(app.clone()))
21+
}
22+
23+
#[inline]
24+
fn to_wide(s: &str) -> Vec<u16> {
25+
use std::os::windows::ffi::OsStrExt;
26+
std::ffi::OsStr::new(s)
27+
.encode_wide()
28+
.chain(std::iter::once(0))
29+
.collect()
30+
}
31+
32+
/// Try to find and foreground the Windows Hello credential dialog.
33+
fn try_focus_hello_dialog_once() -> bool {
34+
// Common class name for the PIN/Hello dialog host
35+
let cls = to_wide("Credential Dialog Xaml Host");
36+
unsafe {
37+
let hwnd = FindWindowW(
38+
windows::core::PCWSTR(cls.as_ptr()),
39+
windows::core::PCWSTR::null(),
40+
);
41+
if let Ok(hwnd) = hwnd {
42+
if IsIconic(hwnd).as_bool() {
43+
let _ = ShowWindow(hwnd, SW_RESTORE);
44+
}
45+
let _ = BringWindowToTop(hwnd);
46+
let _ = SetForegroundWindow(hwnd);
47+
return true;
48+
}
49+
}
50+
false
51+
}
52+
53+
/// Focus the Hello dialog by retrying a few times in a helper thread.
54+
fn nudge_hello_dialog_focus_async(retries: u32, delay_ms: u64) {
55+
std::thread::spawn(move || {
56+
// Small initial delay gives the dialog time to appear
57+
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
58+
for _ in 0..retries {
59+
if try_focus_hello_dialog_once() {
60+
break;
61+
}
62+
std::thread::sleep(std::time::Duration::from_millis(delay_ms));
63+
}
64+
});
65+
}
66+
67+
/// Access to the biometry APIs.
68+
pub struct Biometry<R: Runtime>(AppHandle<R>);
69+
70+
impl<R: Runtime> Biometry<R> {
71+
pub fn status(&self) -> crate::Result<Status> {
72+
let availability = UserConsentVerifier::CheckAvailabilityAsync()
73+
.and_then(|async_op| async_op.get())
74+
.map_err(|e| {
75+
crate::Error::from(std::io::Error::other(
76+
format!("Failed to check biometry availability: {:?}", e),
77+
))
78+
})?;
79+
80+
let (is_available, biometry_type, error, error_code) = match availability {
81+
UserConsentVerifierAvailability::Available => (true, BiometryType::Auto, None, None),
82+
UserConsentVerifierAvailability::DeviceNotPresent => (
83+
false,
84+
BiometryType::None,
85+
Some("No biometric device found".to_string()),
86+
Some("biometryNotAvailable".to_string()),
87+
),
88+
UserConsentVerifierAvailability::NotConfiguredForUser => (
89+
false,
90+
BiometryType::None,
91+
Some("Biometric authentication not configured".to_string()),
92+
Some("biometryNotEnrolled".to_string()),
93+
),
94+
UserConsentVerifierAvailability::DisabledByPolicy => (
95+
false,
96+
BiometryType::None,
97+
Some("Biometric authentication disabled by policy".to_string()),
98+
Some("biometryNotAvailable".to_string()),
99+
),
100+
UserConsentVerifierAvailability::DeviceBusy => (
101+
false,
102+
BiometryType::None,
103+
Some("Biometric device is busy".to_string()),
104+
Some("systemCancel".to_string()),
105+
),
106+
_ => (
107+
false,
108+
BiometryType::None,
109+
Some("Unknown availability status".to_string()),
110+
Some("biometryNotAvailable".to_string()),
111+
),
112+
};
113+
114+
Ok(Status {
115+
is_available,
116+
biometry_type,
117+
error,
118+
error_code,
119+
})
120+
}
121+
122+
pub fn authenticate(&self, reason: String, _options: AuthOptions) -> crate::Result<()> {
123+
let result = UserConsentVerifier::RequestVerificationAsync(&HSTRING::from(reason))
124+
.and_then(|async_op| {
125+
nudge_hello_dialog_focus_async(5, 250);
126+
async_op.get()
127+
})
128+
.map_err(|e| {
129+
crate::Error::from(std::io::Error::other(
130+
format!("Failed to request user verification: {:?}", e),
131+
))
132+
})?;
133+
134+
match result {
135+
UserConsentVerificationResult::Verified => Ok(()),
136+
UserConsentVerificationResult::DeviceBusy => Err(crate::Error::from(
137+
std::io::Error::new(std::io::ErrorKind::ResourceBusy, "Device is busy"),
138+
)),
139+
UserConsentVerificationResult::DeviceNotPresent => Err(crate::Error::from(
140+
std::io::Error::new(std::io::ErrorKind::NotFound, "No biometric device found"),
141+
)),
142+
UserConsentVerificationResult::DisabledByPolicy => {
143+
Err(crate::Error::from(std::io::Error::new(
144+
std::io::ErrorKind::PermissionDenied,
145+
"Biometric authentication is disabled by policy",
146+
)))
147+
}
148+
UserConsentVerificationResult::NotConfiguredForUser => {
149+
Err(crate::Error::from(std::io::Error::other(
150+
"Biometric authentication is not configured for the user",
151+
)))
152+
}
153+
UserConsentVerificationResult::Canceled => {
154+
Err(crate::Error::from(std::io::Error::new(
155+
std::io::ErrorKind::Interrupted,
156+
"Authentication was canceled by the user",
157+
)))
158+
}
159+
UserConsentVerificationResult::RetriesExhausted => {
160+
Err(crate::Error::from(std::io::Error::new(
161+
std::io::ErrorKind::PermissionDenied,
162+
"Too many failed authentication attempts",
163+
)))
164+
}
165+
_ => Err(crate::Error::from(std::io::Error::other(
166+
"Authentication failed",
167+
))),
168+
}
169+
}
170+
171+
pub fn has_data(&self, _options: DataOptions) -> crate::Result<bool> {
172+
Err(crate::Error::from(std::io::Error::other(
173+
"Has data is not supported on windows platform",
174+
)))
175+
}
176+
177+
pub fn get_data(&self, _options: GetDataOptions) -> crate::Result<DataResponse> {
178+
Err(crate::Error::from(std::io::Error::other(
179+
"Get data is not supported on windows platform",
180+
)))
181+
}
182+
183+
pub fn set_data(&self, _options: SetDataOptions) -> crate::Result<()> {
184+
Err(crate::Error::from(std::io::Error::other(
185+
"Set data is not supported on windows platform",
186+
)))
187+
}
188+
189+
pub fn remove_data(&self, _options: RemoveDataOptions) -> crate::Result<()> {
190+
Err(crate::Error::from(std::io::Error::other(
191+
"Remove data is not supported on windows platform",
192+
)))
193+
}
194+
}

0 commit comments

Comments
 (0)