1use std::sync::Mutex;
2
3use bitwarden_core::Client;
4use bitwarden_crypto::CryptoError;
5use bitwarden_vault::{CipherError, CipherView, EncryptionContext};
6use itertools::Itertools;
7use passkey::{
8 authenticator::{Authenticator, DiscoverabilitySupport, StoreInfo, UiHint, UserCheck},
9 types::{
10 Passkey,
11 ctap2::{self, Ctap2Error, StatusCode, VendorError},
12 },
13};
14use thiserror::Error;
15use tracing::error;
16
17use super::{
18 AAGUID, CheckUserOptions, CipherViewContainer, Fido2CredentialStore, Fido2UserInterface,
19 SelectedCredential, UnknownEnumError, try_from_credential_new_view, types::*,
20};
21use crate::{
22 Fido2CallbackError, FillCredentialError, InvalidGuidError, fill_with_credential,
23 string_to_guid_bytes, try_from_credential_full,
24};
25
26#[derive(Debug, Error)]
27pub enum GetSelectedCredentialError {
28 #[error("No selected credential available")]
29 NoSelectedCredential,
30 #[error("No fido2 credentials found")]
31 NoCredentialFound,
32
33 #[error(transparent)]
34 Crypto(#[from] CryptoError),
35}
36
37#[allow(missing_docs)]
38#[derive(Debug, Error)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
40pub enum MakeCredentialError {
41 #[error(transparent)]
42 PublicKeyCredentialParameters(#[from] PublicKeyCredentialParametersError),
43 #[error(transparent)]
44 UnknownEnum(#[from] UnknownEnumError),
45 #[error(transparent)]
46 Serde(#[from] serde_json::Error),
47 #[error("Missing attested_credential_data")]
48 MissingAttestedCredentialData,
49 #[error("make_credential error: {0}")]
50 Other(String),
51}
52
53#[allow(missing_docs)]
54#[derive(Debug, Error)]
55#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
56pub enum GetAssertionError {
57 #[error(transparent)]
58 UnknownEnum(#[from] UnknownEnumError),
59 #[error(transparent)]
60 Serde(#[from] serde_json::Error),
61 #[error(transparent)]
62 GetSelectedCredential(#[from] GetSelectedCredentialError),
63 #[error(transparent)]
64 InvalidGuid(#[from] InvalidGuidError),
65 #[error("missing user")]
66 MissingUser,
67 #[error("get_assertion error: {0}")]
68 Other(String),
69}
70
71#[allow(missing_docs)]
72#[derive(Debug, Error)]
73#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
74pub enum SilentlyDiscoverCredentialsError {
75 #[error(transparent)]
76 Cipher(#[from] CipherError),
77 #[error(transparent)]
78 InvalidGuid(#[from] InvalidGuidError),
79 #[error(transparent)]
80 Fido2Callback(#[from] Fido2CallbackError),
81 #[error(transparent)]
82 FromCipherView(#[from] Fido2CredentialAutofillViewError),
83}
84
85#[allow(missing_docs)]
86#[derive(Debug, Error)]
87#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
88pub enum CredentialsForAutofillError {
89 #[error(transparent)]
90 Cipher(#[from] CipherError),
91 #[error(transparent)]
92 InvalidGuid(#[from] InvalidGuidError),
93 #[error(transparent)]
94 Fido2Callback(#[from] Fido2CallbackError),
95 #[error(transparent)]
96 FromCipherView(#[from] Fido2CredentialAutofillViewError),
97}
98
99#[allow(missing_docs)]
100pub struct Fido2Authenticator<'a> {
101 pub client: &'a Client,
102 pub user_interface: &'a dyn Fido2UserInterface,
103 pub credential_store: &'a dyn Fido2CredentialStore,
104
105 pub(crate) selected_cipher: Mutex<Option<CipherView>>,
106 pub(crate) requested_uv: Mutex<Option<UV>>,
107}
108
109impl<'a> Fido2Authenticator<'a> {
110 #[allow(missing_docs)]
111 pub fn new(
112 client: &'a Client,
113 user_interface: &'a dyn Fido2UserInterface,
114 credential_store: &'a dyn Fido2CredentialStore,
115 ) -> Fido2Authenticator<'a> {
116 Fido2Authenticator {
117 client,
118 user_interface,
119 credential_store,
120 selected_cipher: Mutex::new(None),
121 requested_uv: Mutex::new(None),
122 }
123 }
124
125 #[allow(missing_docs)]
126 pub async fn make_credential(
127 &mut self,
128 request: MakeCredentialRequest,
129 ) -> Result<MakeCredentialResult, MakeCredentialError> {
130 self.requested_uv
132 .get_mut()
133 .expect("Mutex is not poisoned")
134 .replace(request.options.uv);
135
136 let mut authenticator = self.get_authenticator(true);
137
138 let response = authenticator
139 .make_credential(ctap2::make_credential::Request {
140 client_data_hash: request.client_data_hash.into(),
141 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity {
142 id: request.rp.id,
143 name: request.rp.name,
144 },
145 user: passkey::types::webauthn::PublicKeyCredentialUserEntity {
146 id: request.user.id.into(),
147 display_name: request.user.display_name,
148 name: request.user.name,
149 },
150 pub_key_cred_params: request
151 .pub_key_cred_params
152 .into_iter()
153 .map(TryInto::try_into)
154 .collect::<Result<_, _>>()?,
155 exclude_list: request
156 .exclude_list
157 .map(|x| x.into_iter().map(TryInto::try_into).collect())
158 .transpose()?,
159 extensions: request
160 .extensions
161 .map(|e| serde_json::from_str(&e))
162 .transpose()?,
163 options: passkey::types::ctap2::make_credential::Options {
164 rk: request.options.rk,
165 up: true,
166 uv: self.convert_requested_uv(request.options.uv),
167 },
168 pin_auth: None,
169 pin_protocol: None,
170 })
171 .await;
172
173 let response = match response {
174 Ok(x) => x,
175 Err(e) => return Err(MakeCredentialError::Other(format!("{e:?}"))),
176 };
177
178 let attestation_object = response.as_webauthn_bytes().to_vec();
179 let authenticator_data = response.auth_data.to_vec();
180 let attested_credential_data = response
181 .auth_data
182 .attested_credential_data
183 .ok_or(MakeCredentialError::MissingAttestedCredentialData)?;
184 let credential_id = attested_credential_data.credential_id().to_vec();
185
186 Ok(MakeCredentialResult {
187 authenticator_data,
188 attestation_object,
189 credential_id,
190 })
191 }
192
193 #[allow(missing_docs)]
194 pub async fn get_assertion(
195 &mut self,
196 request: GetAssertionRequest,
197 ) -> Result<GetAssertionResult, GetAssertionError> {
198 self.requested_uv
200 .get_mut()
201 .expect("Mutex is not poisoned")
202 .replace(request.options.uv);
203
204 let mut authenticator = self.get_authenticator(false);
205
206 let response = authenticator
207 .get_assertion(ctap2::get_assertion::Request {
208 rp_id: request.rp_id,
209 client_data_hash: request.client_data_hash.into(),
210 allow_list: request
211 .allow_list
212 .map(|l| {
213 l.into_iter()
214 .map(TryInto::try_into)
215 .collect::<Result<Vec<_>, _>>()
216 })
217 .transpose()?,
218 extensions: request
219 .extensions
220 .map(|e| serde_json::from_str(&e))
221 .transpose()?,
222 options: passkey::types::ctap2::make_credential::Options {
223 rk: request.options.rk,
224 up: true,
225 uv: self.convert_requested_uv(request.options.uv),
226 },
227 pin_auth: None,
228 pin_protocol: None,
229 })
230 .await;
231
232 let response = match response {
233 Ok(x) => x,
234 Err(e) => return Err(GetAssertionError::Other(format!("{e:?}"))),
235 };
236
237 let selected_credential = self.get_selected_credential()?;
238 let authenticator_data = response.auth_data.to_vec();
239 let credential_id = string_to_guid_bytes(&selected_credential.credential.credential_id)?;
240
241 Ok(GetAssertionResult {
242 credential_id,
243 authenticator_data,
244 signature: response.signature.into(),
245 user_handle: response
246 .user
247 .ok_or(GetAssertionError::MissingUser)?
248 .id
249 .into(),
250 selected_credential,
251 })
252 }
253
254 #[allow(missing_docs)]
255 pub async fn silently_discover_credentials(
256 &mut self,
257 rp_id: String,
258 user_handle: Option<Vec<u8>>,
259 ) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
260 let key_store = self.client.internal.get_key_store();
261 let result = self
262 .credential_store
263 .find_credentials(None, rp_id, user_handle)
264 .await?;
265
266 let mut ctx = key_store.context();
267 result
268 .into_iter()
269 .map(
270 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
271 Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?)
272 },
273 )
274 .flatten_ok()
275 .collect()
276 }
277
278 pub async fn credentials_for_autofill(
281 &mut self,
282 ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
283 let all_credentials = self.credential_store.all_credentials().await?;
284
285 all_credentials
286 .into_iter()
287 .map(
288 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
289 Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
290 },
291 )
292 .flatten_ok()
293 .collect()
294 }
295
296 pub(super) fn get_authenticator(
297 &self,
298 create_credential: bool,
299 ) -> Authenticator<CredentialStoreImpl<'_>, UserValidationMethodImpl<'_>> {
300 Authenticator::new(
301 AAGUID,
302 CredentialStoreImpl {
303 authenticator: self,
304 create_credential,
305 },
306 UserValidationMethodImpl {
307 authenticator: self,
308 },
309 )
310 }
311
312 fn convert_requested_uv(&self, uv: UV) -> bool {
313 let verification_enabled = self.user_interface.is_verification_enabled();
314 match (uv, verification_enabled) {
315 (UV::Preferred, true) => true,
316 (UV::Preferred, false) => false,
317 (UV::Required, _) => true,
318 (UV::Discouraged, _) => false,
319 }
320 }
321
322 pub(super) fn get_selected_credential(
323 &self,
324 ) -> Result<SelectedCredential, GetSelectedCredentialError> {
325 let key_store = self.client.internal.get_key_store();
326
327 let cipher = self
328 .selected_cipher
329 .lock()
330 .expect("Mutex is not poisoned")
331 .clone()
332 .ok_or(GetSelectedCredentialError::NoSelectedCredential)?;
333
334 let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?;
335
336 let credential = creds
337 .first()
338 .ok_or(GetSelectedCredentialError::NoCredentialFound)?
339 .clone();
340
341 Ok(SelectedCredential { cipher, credential })
342 }
343}
344
345pub(super) struct CredentialStoreImpl<'a> {
346 authenticator: &'a Fido2Authenticator<'a>,
347 create_credential: bool,
348}
349pub(super) struct UserValidationMethodImpl<'a> {
350 authenticator: &'a Fido2Authenticator<'a>,
351}
352
353#[async_trait::async_trait]
354impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> {
355 type PasskeyItem = CipherViewContainer;
356 async fn find_credentials(
357 &self,
358 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
359 rp_id: &str,
360 user_handle: Option<&[u8]>,
361 ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
362 #[derive(Debug, Error)]
363 enum InnerError {
364 #[error(transparent)]
365 Cipher(#[from] CipherError),
366 #[error(transparent)]
367 Crypto(#[from] CryptoError),
368 #[error(transparent)]
369 Fido2Callback(#[from] Fido2CallbackError),
370 }
371
372 async fn inner(
374 this: &CredentialStoreImpl<'_>,
375 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
376 rp_id: &str,
377 user_handle: Option<&[u8]>,
378 ) -> Result<Vec<CipherViewContainer>, InnerError> {
379 let ids: Option<Vec<Vec<u8>>> =
380 ids.map(|ids| ids.iter().map(|id| id.id.clone().into()).collect());
381
382 let ciphers = this
383 .authenticator
384 .credential_store
385 .find_credentials(ids, rp_id.to_string(), user_handle.map(|h| h.to_vec()))
386 .await?;
387
388 let creds: Vec<_> = ciphers
390 .into_iter()
391 .filter(|c| {
392 c.login
393 .as_ref()
394 .and_then(|l| l.fido2_credentials.as_ref())
395 .is_some()
396 })
397 .collect();
398
399 let key_store = this.authenticator.client.internal.get_key_store();
400
401 if this.create_credential {
403 Ok(creds
404 .into_iter()
405 .map(|c| CipherViewContainer::new(c, &mut key_store.context()))
406 .collect::<Result<_, _>>()?)
407 } else {
408 let picked = this
409 .authenticator
410 .user_interface
411 .pick_credential_for_authentication(creds)
412 .await?;
413
414 this.authenticator
416 .selected_cipher
417 .lock()
418 .expect("Mutex is not poisoned")
419 .replace(picked.clone());
420
421 Ok(vec![CipherViewContainer::new(
422 picked,
423 &mut key_store.context(),
424 )?])
425 }
426 }
427
428 inner(self, ids, rp_id, user_handle).await.map_err(|error| {
429 error!(%error, "Error finding credentials.");
430 VendorError::try_from(0xF0)
431 .expect("Valid vendor error code")
432 .into()
433 })
434 }
435
436 async fn save_credential(
437 &mut self,
438 cred: Passkey,
439 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
440 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
441 options: passkey::types::ctap2::get_assertion::Options,
442 ) -> Result<(), StatusCode> {
443 #[derive(Debug, Error)]
444 enum InnerError {
445 #[error("Client User Id has not been set")]
446 MissingUserId,
447 #[error(transparent)]
448 FillCredential(#[from] FillCredentialError),
449 #[error(transparent)]
450 Cipher(#[from] CipherError),
451 #[error(transparent)]
452 Crypto(#[from] CryptoError),
453 #[error(transparent)]
454 Fido2Callback(#[from] Fido2CallbackError),
455
456 #[error("No selected credential available")]
457 NoSelectedCredential,
458 }
459
460 async fn inner(
462 this: &mut CredentialStoreImpl<'_>,
463 cred: Passkey,
464 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
465 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
466 options: passkey::types::ctap2::get_assertion::Options,
467 ) -> Result<(), InnerError> {
468 let user_id = this
469 .authenticator
470 .client
471 .internal
472 .get_user_id()
473 .ok_or(InnerError::MissingUserId)?;
474 let cred = try_from_credential_full(cred, user, rp, options)?;
475
476 let mut selected: CipherView = this
478 .authenticator
479 .selected_cipher
480 .lock()
481 .expect("Mutex is not poisoned")
482 .clone()
483 .ok_or(InnerError::NoSelectedCredential)?;
484
485 let key_store = this.authenticator.client.internal.get_key_store();
486
487 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
488
489 this.authenticator
491 .selected_cipher
492 .lock()
493 .expect("Mutex is not poisoned")
494 .replace(selected.clone());
495
496 let encrypted = key_store.encrypt(selected)?;
498
499 this.authenticator
500 .credential_store
501 .save_credential(EncryptionContext {
502 cipher: encrypted,
503 encrypted_for: user_id,
504 })
505 .await?;
506
507 Ok(())
508 }
509
510 inner(self, cred, user, rp, options).await.map_err(|error| {
511 error!(%error, "Error saving credential.");
512 VendorError::try_from(0xF1)
513 .expect("Valid vendor error code")
514 .into()
515 })
516 }
517
518 async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
519 #[derive(Debug, Error)]
520 enum InnerError {
521 #[error("Client User Id has not been set")]
522 MissingUserId,
523 #[error(transparent)]
524 InvalidGuid(#[from] InvalidGuidError),
525 #[error("Credential ID does not match selected credential")]
526 CredentialIdMismatch,
527 #[error(transparent)]
528 FillCredential(#[from] FillCredentialError),
529 #[error(transparent)]
530 Cipher(#[from] CipherError),
531 #[error(transparent)]
532 Crypto(#[from] CryptoError),
533 #[error(transparent)]
534 Fido2Callback(#[from] Fido2CallbackError),
535 #[error(transparent)]
536 GetSelectedCredential(#[from] GetSelectedCredentialError),
537 }
538
539 async fn inner(
541 this: &mut CredentialStoreImpl<'_>,
542 cred: Passkey,
543 ) -> Result<(), InnerError> {
544 let user_id = this
545 .authenticator
546 .client
547 .internal
548 .get_user_id()
549 .ok_or(InnerError::MissingUserId)?;
550 let selected = this.authenticator.get_selected_credential()?;
552
553 let new_id: &Vec<u8> = &cred.credential_id;
555 let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
556 if new_id != &selected_id {
557 return Err(InnerError::CredentialIdMismatch);
558 }
559
560 let cred = fill_with_credential(&selected.credential, cred)?;
561
562 let key_store = this.authenticator.client.internal.get_key_store();
563
564 let mut selected = selected.cipher;
565 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
566
567 this.authenticator
569 .selected_cipher
570 .lock()
571 .expect("Mutex is not poisoned")
572 .replace(selected.clone());
573
574 let encrypted = key_store.encrypt(selected)?;
576
577 this.authenticator
578 .credential_store
579 .save_credential(EncryptionContext {
580 cipher: encrypted,
581 encrypted_for: user_id,
582 })
583 .await?;
584
585 Ok(())
586 }
587
588 inner(self, cred).await.map_err(|error| {
589 error!(%error, "Error updating credential.");
590 VendorError::try_from(0xF2)
591 .expect("Valid vendor error code")
592 .into()
593 })
594 }
595
596 async fn get_info(&self) -> StoreInfo {
597 StoreInfo {
598 discoverability: DiscoverabilitySupport::Full,
599 }
600 }
601}
602
603#[async_trait::async_trait]
604impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
605 type PasskeyItem = CipherViewContainer;
606
607 async fn check_user<'a>(
608 &self,
609 hint: UiHint<'a, Self::PasskeyItem>,
610 presence: bool,
611 _verification: bool,
612 ) -> Result<UserCheck, Ctap2Error> {
613 let verification = self
614 .authenticator
615 .requested_uv
616 .lock()
617 .expect("Mutex is not poisoned")
618 .ok_or(Ctap2Error::UserVerificationInvalid)?;
619
620 let options = CheckUserOptions {
621 require_presence: presence,
622 require_verification: verification.into(),
623 };
624
625 let result = match hint {
626 UiHint::RequestNewCredential(user, rp) => {
627 let new_credential = try_from_credential_new_view(user, rp)
628 .map_err(|_| Ctap2Error::InvalidCredential)?;
629
630 let (cipher_view, user_check) = self
631 .authenticator
632 .user_interface
633 .check_user_and_pick_credential_for_creation(options, new_credential)
634 .await
635 .map_err(|_| Ctap2Error::OperationDenied)?;
636
637 self.authenticator
638 .selected_cipher
639 .lock()
640 .expect("Mutex is not poisoned")
641 .replace(cipher_view);
642
643 Ok(user_check)
644 }
645 _ => {
646 self.authenticator
647 .user_interface
648 .check_user(options, map_ui_hint(hint))
649 .await
650 }
651 };
652
653 let result = result.map_err(|error| {
654 error!(%error, "Error checking user.");
655 Ctap2Error::UserVerificationInvalid
656 })?;
657
658 Ok(UserCheck {
659 presence: result.user_present,
660 verification: result.user_verified,
661 })
662 }
663
664 fn is_presence_enabled(&self) -> bool {
665 true
666 }
667
668 fn is_verification_enabled(&self) -> Option<bool> {
669 Some(self.authenticator.user_interface.is_verification_enabled())
670 }
671}
672
673fn map_ui_hint(hint: UiHint<'_, CipherViewContainer>) -> UiHint<'_, CipherView> {
674 use UiHint::*;
675 match hint {
676 InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
677 InformNoCredentialsFound => InformNoCredentialsFound,
678 RequestNewCredential(u, r) => RequestNewCredential(u, r),
679 RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
680 }
681}