#![allow(
dead_code,
unused_imports,
unused_qualifications,
unreachable_patterns,
unsafe_code
)]
use std::collections::HashMap;
use std::sync::Mutex;
use std::time::{Duration, Instant};
use crate::internal::core::{Error, Result};
use windows::core::HSTRING;
use windows::Security::Credentials::UI::{UserConsentVerificationResult, UserConsentVerifier};
#[derive(Default)]
pub struct HelloGate {
entries: Mutex<HashMap<String, Instant>>,
}
impl std::fmt::Debug for HelloGate {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HelloGate").finish_non_exhaustive()
}
}
impl HelloGate {
pub fn new() -> Self {
Self::default()
}
pub fn ensure_verified(&self, scope: &str, reason: &str, ttl: Duration) -> Result<()> {
if self.is_fresh(scope, ttl) {
return Ok(());
}
prompt_user_consent(reason)?;
self.mark_verified(scope);
Ok(())
}
pub fn invalidate_all(&self) {
if let Ok(mut entries) = self.entries.lock() {
entries.clear();
}
}
fn is_fresh(&self, scope: &str, ttl: Duration) -> bool {
if ttl.is_zero() {
return false;
}
let Ok(entries) = self.entries.lock() else {
return false;
};
entries
.get(scope)
.map(|t| t.elapsed() < ttl)
.unwrap_or(false)
}
fn mark_verified(&self, scope: &str) {
let Ok(mut entries) = self.entries.lock() else {
return;
};
entries.insert(scope.to_string(), Instant::now());
}
}
pub fn is_available() -> bool {
use windows::Security::Credentials::UI::UserConsentVerifierAvailability;
let async_op = match UserConsentVerifier::CheckAvailabilityAsync() {
Ok(op) => op,
Err(_) => return false,
};
let result = match async_op.get() {
Ok(r) => r,
Err(_) => return false,
};
matches!(result, UserConsentVerifierAvailability::Available)
}
fn prompt_user_consent(reason: &str) -> Result<()> {
let reason_h = HSTRING::from(reason);
let async_op = UserConsentVerifier::RequestVerificationAsync(&reason_h).map_err(|e| {
Error::KeyOperation {
operation: "hello_request_verification".into(),
detail: format!("UserConsentVerifier::RequestVerificationAsync: {e}"),
}
})?;
let result = async_op.get().map_err(|e| Error::KeyOperation {
operation: "hello_await_result".into(),
detail: format!("UserConsentVerifier async wait: {e}"),
})?;
match result {
UserConsentVerificationResult::Verified => Ok(()),
UserConsentVerificationResult::DeviceNotPresent
| UserConsentVerificationResult::NotConfiguredForUser
| UserConsentVerificationResult::DisabledByPolicy => {
fallback_to_password_gate(reason, result)
}
UserConsentVerificationResult::DeviceBusy => Err(Error::KeyOperation {
operation: "hello_request_verification".into(),
detail: "Windows Hello device is busy; try again".into(),
}),
UserConsentVerificationResult::RetriesExhausted => Err(Error::KeyOperation {
operation: "hello_request_verification".into(),
detail: "Windows Hello retries exhausted; user could not be verified".into(),
}),
UserConsentVerificationResult::Canceled => Err(Error::KeyOperation {
operation: "hello_request_verification".into(),
detail: "User cancelled Windows Hello verification".into(),
}),
other => Err(Error::KeyOperation {
operation: "hello_request_verification".into(),
detail: format!("UserConsentVerifier returned unexpected result {other:?}"),
}),
}
}
fn fallback_to_password_gate(
reason: &str,
hello_result: UserConsentVerificationResult,
) -> Result<()> {
use super::password_gate::{verify_current_user, PresenceOutcome};
match verify_current_user(reason) {
PresenceOutcome::Verified => Ok(()),
PresenceOutcome::Denied(detail) => Err(Error::KeyOperation {
operation: "password_request_verification".into(),
detail,
}),
PresenceOutcome::Unavailable(detail) => {
tracing::warn!(
hello_result = ?hello_result,
reason = %detail,
"Windows Hello is not enrolled and the password presence gate is \
unavailable; proceeding without a presence prompt (credentials \
remain TPM-encrypted)"
);
Ok(())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cache_hit_within_ttl_returns_ok_without_prompt() {
let gate = HelloGate::new();
gate.mark_verified("test-scope");
let result = gate.ensure_verified("test-scope", "should-not-fire", Duration::from_secs(60));
assert!(result.is_ok());
}
#[test]
fn zero_ttl_disables_cache() {
let gate = HelloGate::new();
gate.mark_verified("test-scope");
assert!(!gate.is_fresh("test-scope", Duration::ZERO));
}
#[test]
fn invalidate_all_clears_entries() {
let gate = HelloGate::new();
gate.mark_verified("scope-a");
gate.mark_verified("scope-b");
assert!(gate.is_fresh("scope-a", Duration::from_secs(60)));
gate.invalidate_all();
assert!(!gate.is_fresh("scope-a", Duration::from_secs(60)));
assert!(!gate.is_fresh("scope-b", Duration::from_secs(60)));
}
#[test]
fn cache_scopes_are_independent() {
let gate = HelloGate::new();
gate.mark_verified("scope-a");
assert!(gate.is_fresh("scope-a", Duration::from_secs(60)));
assert!(!gate.is_fresh("scope-b", Duration::from_secs(60)));
}
#[test]
fn doc_comment_classifies_as_soft_gate() {
let gate = HelloGate::new();
gate.invalidate_all();
let _is_fresh: bool = gate.is_fresh("any", Duration::ZERO);
}
}