1#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
3use std::{
4 ops::{Deref, DerefMut},
5 time::Duration,
6};
7
8#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
9use async_trait::async_trait;
10#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
11use unicode_normalization::UnicodeNormalization;
12#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
13use webauthn_rs_proto::UserVerificationPolicy;
14
15use crate::ui::UiCallback;
16#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
17use crate::{
18 error::{CtapError, WebauthnCError},
19 transport::Token,
20};
21
22#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
23use super::{
24 commands::{
25 BioEnrollmentRequestTrait, BioEnrollmentResponse, BioSubCommand, Modality, Permissions,
26 TemplateInfo,
27 },
28 ctap20::AuthSession,
29 Ctap20Authenticator,
30};
31
32pub trait BiometricAuthenticatorInfo<U: UiCallback>: Sync + Send {
34 #[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
35 type RequestType: BioEnrollmentRequestTrait;
37
38 fn biometrics(&self) -> Option<bool>;
47
48 #[inline]
50 fn supports_biometrics(&self) -> bool {
51 self.biometrics().is_some()
52 }
53
54 #[inline]
57 fn configured_biometrics(&self) -> bool {
58 self.biometrics().unwrap_or_default()
59 }
60}
61
62#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
63#[async_trait]
65trait BiometricAuthenticatorSupport<T, U, R>
66where
67 T: BiometricAuthenticatorInfo<U, RequestType = R>,
68 U: UiCallback,
69 R: BioEnrollmentRequestTrait,
70{
71 async fn bio(
72 &mut self,
73 sub_command: BioSubCommand,
74 ) -> Result<BioEnrollmentResponse, WebauthnCError>;
75
76 async fn bio_with_session(
78 &mut self,
79 sub_command: BioSubCommand,
80 auth_session: &AuthSession,
81 ) -> Result<BioEnrollmentResponse, WebauthnCError>;
82}
83
84#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
85#[async_trait]
86impl<'a, K, T, U, R> BiometricAuthenticatorSupport<T, U, R> for T
87where
88 K: Token,
89 T: BiometricAuthenticatorInfo<U, RequestType = R>
90 + Deref<Target = Ctap20Authenticator<'a, K, U>>
91 + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
92 U: UiCallback + 'a,
93 R: BioEnrollmentRequestTrait,
94{
95 async fn bio(
96 &mut self,
97 sub_command: BioSubCommand,
98 ) -> Result<BioEnrollmentResponse, WebauthnCError> {
99 let (pin_uv_auth_proto, pin_uv_auth_param) = self
100 .get_pin_uv_auth_token(
101 sub_command.prf().as_slice(),
102 Permissions::BIO_ENROLLMENT,
103 None,
104 UserVerificationPolicy::Required,
105 )
106 .await?
107 .into_pin_uv_params();
108
109 let ui = self.ui_callback;
110 let r = self
111 .token
112 .transmit(
113 T::RequestType::new(sub_command, pin_uv_auth_proto, pin_uv_auth_param),
114 ui,
115 )
116 .await?;
117 self.refresh_info().await?;
118 Ok(r)
119 }
120
121 async fn bio_with_session(
122 &mut self,
123 sub_command: BioSubCommand,
124 auth_session: &AuthSession,
125 ) -> Result<BioEnrollmentResponse, WebauthnCError> {
126 let client_data_hash = sub_command.prf();
127
128 let (pin_uv_protocol, pin_uv_auth_param) = match auth_session {
129 AuthSession::InterfaceToken(iface, pin_token) => {
130 let mut pin_uv_auth_param =
131 iface.authenticate(pin_token, client_data_hash.as_slice())?;
132 pin_uv_auth_param.truncate(16);
133
134 (Some(iface.get_pin_uv_protocol()), Some(pin_uv_auth_param))
135 }
136
137 _ => (None, None),
138 };
139
140 let ui = self.ui_callback;
141 self.token
142 .transmit(
143 T::RequestType::new(sub_command, pin_uv_protocol, pin_uv_auth_param),
144 ui,
145 )
146 .await
147 }
148}
149
150#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
151#[async_trait]
157pub trait BiometricAuthenticator {
158 async fn check_fingerprint_support(&mut self) -> Result<(), WebauthnCError>;
163
164 async fn check_friendly_name(
170 &mut self,
171 friendly_name: String,
172 ) -> Result<String, WebauthnCError>;
173
174 async fn get_fingerprint_sensor_info(
179 &mut self,
180 ) -> Result<BioEnrollmentResponse, WebauthnCError>;
181
182 async fn list_fingerprints(&mut self) -> Result<Vec<TemplateInfo>, WebauthnCError>;
189
190 async fn enroll_fingerprint(
200 &mut self,
201 timeout: Duration,
202 friendly_name: Option<String>,
203 ) -> Result<Vec<u8>, WebauthnCError>;
204
205 async fn rename_fingerprint(
207 &mut self,
208 id: Vec<u8>,
209 friendly_name: String,
210 ) -> Result<(), WebauthnCError>;
211
212 async fn remove_fingerprint(&mut self, id: Vec<u8>) -> Result<(), WebauthnCError>;
214
215 async fn remove_fingerprints(&mut self, ids: Vec<Vec<u8>>) -> Result<(), WebauthnCError>;
221}
222
223#[cfg(any(all(doc, not(doctest)), feature = "ctap2-management"))]
224#[async_trait]
230impl<'a, K, T, U, R> BiometricAuthenticator for T
231where
232 K: Token,
233 T: BiometricAuthenticatorInfo<U, RequestType = R>
234 + Deref<Target = Ctap20Authenticator<'a, K, U>>
235 + DerefMut<Target = Ctap20Authenticator<'a, K, U>>,
236 U: UiCallback + 'a,
237 R: BioEnrollmentRequestTrait,
238{
239 async fn check_fingerprint_support(&mut self) -> Result<(), WebauthnCError> {
240 if !self.supports_biometrics() {
241 return Err(WebauthnCError::NotSupported);
242 }
243
244 let ui = self.ui_callback;
245 let r = self.token.transmit(R::GET_MODALITY, ui).await?;
246 if r.modality != Some(Modality::Fingerprint) {
247 return Err(WebauthnCError::NotSupported);
248 }
249
250 Ok(())
251 }
252
253 async fn check_friendly_name(
254 &mut self,
255 friendly_name: String,
256 ) -> Result<String, WebauthnCError> {
257 let ui = self.ui_callback;
258 let r = self
259 .token
260 .transmit(R::GET_FINGERPRINT_SENSOR_INFO, ui)
261 .await?;
262
263 let friendly_name = friendly_name.nfc().collect::<String>();
265 if friendly_name.len() > r.get_max_template_friendly_name() {
266 return Err(WebauthnCError::FriendlyNameTooLong);
267 }
268
269 Ok(friendly_name)
270 }
271
272 async fn get_fingerprint_sensor_info(
273 &mut self,
274 ) -> Result<BioEnrollmentResponse, WebauthnCError> {
275 self.check_fingerprint_support().await?;
276 let ui = self.ui_callback;
277 self.token
278 .transmit(R::GET_FINGERPRINT_SENSOR_INFO, ui)
279 .await
280 }
281
282 async fn enroll_fingerprint(
283 &mut self,
284 timeout: Duration,
285 friendly_name: Option<String>,
286 ) -> Result<Vec<u8>, WebauthnCError> {
287 self.check_fingerprint_support().await?;
288 let friendly_name = match friendly_name {
289 Some(n) => Some(self.check_friendly_name(n).await?),
290 None => None,
291 };
292
293 let session = self
294 .get_pin_uv_auth_session(
295 Permissions::BIO_ENROLLMENT,
296 None,
297 UserVerificationPolicy::Required,
298 )
299 .await?;
300
301 let mut r = self
302 .bio_with_session(BioSubCommand::FingerprintEnrollBegin(timeout), &session)
303 .await?;
304
305 trace!("began enrollment: {:?}", r);
306 let id = r.template_id.ok_or(WebauthnCError::MissingRequiredField)?;
307
308 let mut remaining_samples = r
309 .remaining_samples
310 .ok_or(WebauthnCError::MissingRequiredField)?;
311 while remaining_samples > 0 {
312 self.ui_callback
313 .fingerprint_enrollment_feedback(remaining_samples, r.last_enroll_sample_status);
314
315 r = self
316 .bio_with_session(
317 BioSubCommand::FingerprintEnrollCaptureNextSample(id.clone(), timeout),
318 &session,
319 )
320 .await?;
321
322 remaining_samples = r
323 .remaining_samples
324 .ok_or(WebauthnCError::MissingRequiredField)?;
325 }
326
327 if friendly_name.is_some() {
329 self.bio_with_session(
330 BioSubCommand::FingerprintSetFriendlyName(TemplateInfo {
331 id: id.clone(),
332 friendly_name,
333 }),
334 &session,
335 )
336 .await?;
337 }
338
339 self.refresh_info().await?;
341
342 Ok(id)
343 }
344
345 async fn list_fingerprints(&mut self) -> Result<Vec<TemplateInfo>, WebauthnCError> {
346 self.check_fingerprint_support().await?;
347 if !self.configured_biometrics() {
348 trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
354 return Ok(vec![]);
355 }
356
357 let templates = self
359 .bio(BioSubCommand::FingerprintEnumerateEnrollments)
360 .await;
361
362 match templates {
363 Ok(templates) => Ok(templates.template_infos),
364 Err(e) => {
365 if let WebauthnCError::Ctap(e) = &e {
366 if matches!(e, CtapError::Ctap2InvalidOption) {
367 return Ok(vec![]);
371 }
372 }
373
374 Err(e)
375 }
376 }
377 }
378
379 async fn rename_fingerprint(
380 &mut self,
381 id: Vec<u8>,
382 friendly_name: String,
383 ) -> Result<(), WebauthnCError> {
384 self.check_fingerprint_support().await?;
385 if !self.configured_biometrics() {
386 trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
389 return Err(CtapError::Ctap2InvalidOption.into());
390 }
391
392 let friendly_name = Some(self.check_friendly_name(friendly_name).await?);
393 self.bio(BioSubCommand::FingerprintSetFriendlyName(TemplateInfo {
394 id,
395 friendly_name,
396 }))
397 .await?;
398 Ok(())
399 }
400
401 async fn remove_fingerprint(&mut self, id: Vec<u8>) -> Result<(), WebauthnCError> {
402 self.check_fingerprint_support().await?;
403 if !self.configured_biometrics() {
404 trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
407 return Err(CtapError::Ctap2InvalidOption.into());
408 }
409
410 self.bio(BioSubCommand::FingerprintRemoveEnrollment(id))
411 .await?;
412
413 self.refresh_info().await?;
416 Ok(())
417 }
418
419 async fn remove_fingerprints(&mut self, ids: Vec<Vec<u8>>) -> Result<(), WebauthnCError> {
420 self.check_fingerprint_support().await?;
421 if !self.configured_biometrics() {
422 trace!("Fingerprint authentication is supported but not configured, ie: there no enrolled fingerprints, skipping request.");
425 return Err(CtapError::Ctap2InvalidOption.into());
426 }
427
428 let session = self
429 .get_pin_uv_auth_session(
430 Permissions::BIO_ENROLLMENT,
431 None,
432 UserVerificationPolicy::Required,
433 )
434 .await?;
435
436 for id in ids {
437 self.bio_with_session(BioSubCommand::FingerprintRemoveEnrollment(id), &session)
438 .await?;
439 }
440
441 self.refresh_info().await?;
443 Ok(())
444 }
445}