1use crate::error::{Error, Result};
5use crate::types::BackendKind;
6
7#[derive(Debug, Clone)]
9pub struct AuthCapabilities {
10 pub biometric_available: bool,
12 pub password_available: bool,
14 pub presence_caching: bool,
16 pub authenticator_name: Option<String>,
18}
19
20pub struct AuthHandle {
23 backend_kind: BackendKind,
24 #[cfg(target_os = "windows")]
29 hello_gate: crate::internal::windows::hello_gate::HelloGate,
30}
31
32impl std::fmt::Debug for AuthHandle {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("AuthHandle")
35 .field("backend_kind", &self.backend_kind)
36 .finish_non_exhaustive()
37 }
38}
39
40impl AuthHandle {
41 pub(crate) fn new(backend_kind: BackendKind) -> Self {
42 Self {
43 backend_kind,
44 #[cfg(target_os = "windows")]
45 hello_gate: crate::internal::windows::hello_gate::HelloGate::new(),
46 }
47 }
48
49 pub fn capabilities(&self) -> AuthCapabilities {
52 platform_auth_capabilities()
53 }
54
55 #[allow(clippy::needless_return, unreachable_code)]
71 pub fn request_presence(&self, reason: &str) -> Result<()> {
72 #[cfg(target_os = "macos")]
73 {
74 return crate::internal::apple::evaluate_presence(reason).map_err(|e| {
75 use crate::internal::core::Error as CE;
76 match e {
77 CE::NotAvailable => Error::PresenceNotAvailable,
78 CE::UserCancelled { label } => Error::UserCancelled { label },
79 other => Error::from(other),
80 }
81 });
82 }
83 #[cfg(target_os = "windows")]
84 {
85 return self
86 .hello_gate
87 .ensure_verified("__standalone_presence__", reason, std::time::Duration::ZERO)
88 .map_err(Error::from);
89 }
90 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
91 {
92 let _ = reason;
93 return Err(Error::PresenceNotAvailable);
94 }
95 }
96
97 #[allow(clippy::needless_return, unreachable_code)]
109 pub fn evict_presence_cache(&self) {
110 #[cfg(target_os = "macos")]
111 {
112 crate::internal::apple::evict_all_contexts();
113 return;
114 }
115 #[cfg(target_os = "windows")]
116 {
117 self.hello_gate.invalidate_all();
118 return;
119 }
120 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
121 {}
122 }
123
124 pub fn backend_kind(&self) -> BackendKind {
126 self.backend_kind
127 }
128}
129
130#[allow(clippy::needless_return, unreachable_code)]
132pub fn platform_auth_capabilities() -> AuthCapabilities {
133 #[cfg(target_os = "macos")]
134 return AuthCapabilities {
135 biometric_available: crate::internal::apple::touch_id_available(),
136 password_available: true,
137 presence_caching: true,
138 authenticator_name: Some("Touch ID".into()),
139 };
140
141 #[cfg(target_os = "windows")]
142 return AuthCapabilities {
143 biometric_available: crate::internal::windows::hello_gate::is_available(),
145 password_available: true,
146 presence_caching: false,
147 authenticator_name: Some("Windows Hello".into()),
148 };
149
150 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
151 AuthCapabilities {
152 biometric_available: false,
153 password_available: false,
154 presence_caching: false,
155 authenticator_name: None,
156 }
157}
158
159#[cfg(test)]
160#[allow(clippy::unwrap_used)]
161mod tests {
162 use super::*;
163 use crate::config::EnclaveConfig;
164 use crate::factory::create_auth;
165
166 #[test]
167 #[cfg(not(target_os = "windows"))]
168 fn request_presence_never_panics() {
169 if platform_auth_capabilities().biometric_available {
176 return;
177 }
178 let config = EnclaveConfig::new("testapp", "key");
179 let handle = create_auth(&config).unwrap();
180 drop(handle.request_presence("test reason"));
181 }
182
183 #[test]
184 fn evict_presence_cache_never_panics() {
185 let config = EnclaveConfig::new("testapp", "key");
186 let handle = create_auth(&config).unwrap();
187 handle.evict_presence_cache(); }
189
190 #[test]
191 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
192 fn request_presence_returns_not_available_on_linux() {
193 let config = EnclaveConfig::new("testapp", "key");
194 let handle = create_auth(&config).unwrap();
195 let result = handle.request_presence("test");
196 assert!(
197 matches!(result, Err(Error::PresenceNotAvailable)),
198 "Linux must return PresenceNotAvailable, got {result:?}"
199 );
200 }
201
202 #[test]
203 #[cfg(not(any(target_os = "macos", target_os = "windows")))]
204 fn evict_presence_cache_is_noop_on_linux() {
205 let config = EnclaveConfig::new("testapp", "key");
206 let handle = create_auth(&config).unwrap();
207 handle.evict_presence_cache(); handle.evict_presence_cache();
210 }
211
212 #[test]
213 fn platform_capabilities_does_not_panic() {
214 let caps = platform_auth_capabilities();
215 let _ = caps.biometric_available;
216 let _ = caps.password_available;
217 let _ = caps.presence_caching;
218 }
219
220 #[test]
221 #[cfg(not(target_os = "windows"))]
222 fn request_presence_returns_not_available_when_no_biometric() {
223 if platform_auth_capabilities().biometric_available {
226 return;
227 }
228 let config = EnclaveConfig::new("testapp", "key");
229 let handle = create_auth(&config).unwrap();
230 let result = handle.request_presence("ci test");
231 assert!(
232 matches!(result, Err(Error::PresenceNotAvailable)),
233 "platform without biometric must return PresenceNotAvailable, got {result:?}"
234 );
235 }
236}