use std::fmt;
#[cfg(windows)]
#[derive(Debug)]
pub enum BiometricChallengeError {
NoWindowHandle,
NotEnrolled,
Canceled,
NotConfigured,
Failed(String),
}
#[cfg(windows)]
impl fmt::Display for BiometricChallengeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NoWindowHandle => {
f.write_str("no console window handle (ConPTY / headless context)")
}
Self::NotEnrolled => f.write_str("no biometric device enrolled for Windows Hello"),
Self::Canceled => f.write_str("Windows Hello verification canceled by user"),
Self::NotConfigured => {
f.write_str("Windows Hello is not configured for the current user")
}
Self::Failed(msg) => write!(f, "Windows Hello verification failed: {msg}"),
}
}
}
#[cfg(windows)]
pub fn request_windows_hello_verification(reason: &str) -> Result<bool, BiometricChallengeError> {
use windows::{
Security::Credentials::UI::UserConsentVerificationResult,
Win32::System::Console::GetConsoleWindow,
Win32::System::WinRT::IUserConsentVerifierInterop,
};
use windows_future::IAsyncOperation;
let hwnd = unsafe { GetConsoleWindow() };
if hwnd.0.is_null() {
tracing::debug!("windows_hello: GetConsoleWindow returned NULL — challenge unavailable");
return Err(BiometricChallengeError::NoWindowHandle);
}
let interop: IUserConsentVerifierInterop = windows::core::imp::load_factory::<
windows::Security::Credentials::UI::UserConsentVerifier,
IUserConsentVerifierInterop,
>()
.map_err(|e| {
tracing::debug!("windows_hello: failed to load factory: {e}");
BiometricChallengeError::Failed(e.to_string())
})?;
let reason_hstring = windows::core::HSTRING::from(reason);
let operation: IAsyncOperation<UserConsentVerificationResult> = unsafe {
interop
.RequestVerificationForWindowAsync(hwnd, &reason_hstring)
.map_err(|e| {
tracing::debug!("windows_hello: RequestVerificationForWindowAsync error: {e}");
BiometricChallengeError::Failed(e.to_string())
})?
};
let result = operation.get().map_err(|e| {
tracing::debug!("windows_hello: async operation .get() error: {e}");
BiometricChallengeError::Failed(e.to_string())
})?;
tracing::debug!("windows_hello: UserConsentVerificationResult = {result:?}");
match result {
UserConsentVerificationResult::Verified => Ok(true),
UserConsentVerificationResult::DeviceNotPresent => {
Err(BiometricChallengeError::NotEnrolled)
}
UserConsentVerificationResult::Canceled => Err(BiometricChallengeError::Canceled),
UserConsentVerificationResult::NotConfiguredForUser => {
Err(BiometricChallengeError::NotConfigured)
}
other => Err(BiometricChallengeError::Failed(format!(
"unexpected result: {other:?}"
))),
}
}
#[cfg(test)]
mod tests {
#[cfg(windows)]
#[test]
fn no_window_handle_display_is_descriptive() {
use super::BiometricChallengeError;
let msg = BiometricChallengeError::NoWindowHandle.to_string();
assert!(
msg.contains("ConPTY") || msg.contains("headless"),
"expected description to mention ConPTY or headless, got: {msg}"
);
}
#[cfg(windows)]
#[test]
fn device_not_present_display_mentions_biometric_device() {
use super::BiometricChallengeError;
let msg = BiometricChallengeError::NotEnrolled.to_string();
assert!(
msg.contains("biometric") || msg.contains("enrolled"),
"expected description to mention biometric or enrolled, got: {msg}"
);
}
#[cfg(windows)]
#[test]
fn canceled_display_mentions_canceled() {
use super::BiometricChallengeError;
let msg = BiometricChallengeError::Canceled.to_string();
assert!(
msg.contains("cancel"),
"expected description to mention 'cancel', got: {msg}"
);
}
#[cfg(windows)]
#[test]
fn failed_display_includes_inner_message() {
use super::BiometricChallengeError;
let msg = BiometricChallengeError::Failed("OS error 42".to_string()).to_string();
assert!(
msg.contains("OS error 42"),
"expected inner message in display, got: {msg}"
);
}
#[cfg(windows)]
#[test]
fn challenge_unavailable_variants_are_not_hard_errors() {
use super::BiometricChallengeError;
for err in [
BiometricChallengeError::NoWindowHandle,
BiometricChallengeError::NotEnrolled,
BiometricChallengeError::NotConfigured,
] {
let is_challenge_unavailable = matches!(
err,
BiometricChallengeError::NoWindowHandle
| BiometricChallengeError::NotEnrolled
| BiometricChallengeError::NotConfigured
);
assert!(
is_challenge_unavailable,
"{err} must be a challenge-unavailable case (proceed to keyring), not a hard error"
);
}
}
#[cfg(windows)]
#[test]
fn canceled_and_failed_are_not_fallback_cases() {
use super::BiometricChallengeError;
let canceled = BiometricChallengeError::Canceled;
let failed = BiometricChallengeError::Failed("detail".to_string());
assert!(
!matches!(
canceled,
BiometricChallengeError::NoWindowHandle
| BiometricChallengeError::NotEnrolled
| BiometricChallengeError::NotConfigured
),
"Canceled must not be a fallback case"
);
assert!(
!matches!(
failed,
BiometricChallengeError::NoWindowHandle
| BiometricChallengeError::NotEnrolled
| BiometricChallengeError::NotConfigured
),
"Failed must not be a fallback case"
);
}
#[cfg(windows)]
#[test]
#[ignore = "requires Windows Hello hardware and an interactive desktop session — run manually"]
fn live_windows_hello_challenge_returns_verified() {
use super::request_windows_hello_verification;
let result = request_windows_hello_verification("tsafe test — verify your identity");
assert!(
matches!(result, Ok(true)),
"expected Verified, got: {result:?}"
);
}
}