1use std::sync::Mutex;
2
3use bitwarden_core::Client;
4use bitwarden_crypto::CryptoError;
5use bitwarden_vault::{CipherError, CipherView, EncryptionContext};
6use itertools::Itertools;
7use log::error;
8use passkey::{
9 authenticator::{Authenticator, DiscoverabilitySupport, StoreInfo, UIHint, UserCheck},
10 types::{
11 Passkey,
12 ctap2::{self, Ctap2Error, StatusCode, VendorError},
13 },
14};
15use thiserror::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).await,
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_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).await,
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 ) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
259 let key_store = self.client.internal.get_key_store();
260 let result = self.credential_store.find_credentials(None, rp_id).await?;
261
262 let mut ctx = key_store.context();
263 result
264 .into_iter()
265 .map(
266 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
267 Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?)
268 },
269 )
270 .flatten_ok()
271 .collect()
272 }
273
274 pub async fn credentials_for_autofill(
277 &mut self,
278 ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
279 let all_credentials = self.credential_store.all_credentials().await?;
280
281 all_credentials
282 .into_iter()
283 .map(
284 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
285 Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
286 },
287 )
288 .flatten_ok()
289 .collect()
290 }
291
292 pub(super) fn get_authenticator(
293 &self,
294 create_credential: bool,
295 ) -> Authenticator<CredentialStoreImpl<'_>, UserValidationMethodImpl<'_>> {
296 Authenticator::new(
297 AAGUID,
298 CredentialStoreImpl {
299 authenticator: self,
300 create_credential,
301 },
302 UserValidationMethodImpl {
303 authenticator: self,
304 },
305 )
306 }
307
308 async fn convert_requested_uv(&self, uv: UV) -> bool {
309 let verification_enabled = self.user_interface.is_verification_enabled().await;
310 match (uv, verification_enabled) {
311 (UV::Preferred, true) => true,
312 (UV::Preferred, false) => false,
313 (UV::Required, _) => true,
314 (UV::Discouraged, _) => false,
315 }
316 }
317
318 pub(super) fn get_selected_credential(
319 &self,
320 ) -> Result<SelectedCredential, GetSelectedCredentialError> {
321 let key_store = self.client.internal.get_key_store();
322
323 let cipher = self
324 .selected_cipher
325 .lock()
326 .expect("Mutex is not poisoned")
327 .clone()
328 .ok_or(GetSelectedCredentialError::NoSelectedCredential)?;
329
330 let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?;
331
332 let credential = creds
333 .first()
334 .ok_or(GetSelectedCredentialError::NoCredentialFound)?
335 .clone();
336
337 Ok(SelectedCredential { cipher, credential })
338 }
339}
340
341pub(super) struct CredentialStoreImpl<'a> {
342 authenticator: &'a Fido2Authenticator<'a>,
343 create_credential: bool,
344}
345pub(super) struct UserValidationMethodImpl<'a> {
346 authenticator: &'a Fido2Authenticator<'a>,
347}
348
349#[async_trait::async_trait]
350impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> {
351 type PasskeyItem = CipherViewContainer;
352 async fn find_credentials(
353 &self,
354 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
355 rp_id: &str,
356 ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
357 #[derive(Debug, Error)]
358 enum InnerError {
359 #[error(transparent)]
360 Cipher(#[from] CipherError),
361 #[error(transparent)]
362 Crypto(#[from] CryptoError),
363 #[error(transparent)]
364 Fido2Callback(#[from] Fido2CallbackError),
365 }
366
367 async fn inner(
369 this: &CredentialStoreImpl<'_>,
370 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
371 rp_id: &str,
372 ) -> Result<Vec<CipherViewContainer>, InnerError> {
373 let ids: Option<Vec<Vec<u8>>> =
374 ids.map(|ids| ids.iter().map(|id| id.id.clone().into()).collect());
375
376 let ciphers = this
377 .authenticator
378 .credential_store
379 .find_credentials(ids, rp_id.to_string())
380 .await?;
381
382 let creds: Vec<_> = ciphers
384 .into_iter()
385 .filter(|c| {
386 c.login
387 .as_ref()
388 .and_then(|l| l.fido2_credentials.as_ref())
389 .is_some()
390 })
391 .collect();
392
393 let key_store = this.authenticator.client.internal.get_key_store();
394
395 if this.create_credential {
397 Ok(creds
398 .into_iter()
399 .map(|c| CipherViewContainer::new(c, &mut key_store.context()))
400 .collect::<Result<_, _>>()?)
401 } else {
402 let picked = this
403 .authenticator
404 .user_interface
405 .pick_credential_for_authentication(creds)
406 .await?;
407
408 this.authenticator
410 .selected_cipher
411 .lock()
412 .expect("Mutex is not poisoned")
413 .replace(picked.clone());
414
415 Ok(vec![CipherViewContainer::new(
416 picked,
417 &mut key_store.context(),
418 )?])
419 }
420 }
421
422 inner(self, ids, rp_id).await.map_err(|e| {
423 error!("Error finding credentials: {e:?}");
424 VendorError::try_from(0xF0)
425 .expect("Valid vendor error code")
426 .into()
427 })
428 }
429
430 async fn save_credential(
431 &mut self,
432 cred: Passkey,
433 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
434 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
435 options: passkey::types::ctap2::get_assertion::Options,
436 ) -> Result<(), StatusCode> {
437 #[derive(Debug, Error)]
438 enum InnerError {
439 #[error("Client User Id has not been set")]
440 MissingUserId,
441 #[error(transparent)]
442 FillCredential(#[from] FillCredentialError),
443 #[error(transparent)]
444 Cipher(#[from] CipherError),
445 #[error(transparent)]
446 Crypto(#[from] CryptoError),
447 #[error(transparent)]
448 Fido2Callback(#[from] Fido2CallbackError),
449
450 #[error("No selected credential available")]
451 NoSelectedCredential,
452 }
453
454 async fn inner(
456 this: &mut CredentialStoreImpl<'_>,
457 cred: Passkey,
458 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
459 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
460 options: passkey::types::ctap2::get_assertion::Options,
461 ) -> Result<(), InnerError> {
462 let user_id = this
463 .authenticator
464 .client
465 .internal
466 .get_user_id()
467 .ok_or(InnerError::MissingUserId)?;
468 let cred = try_from_credential_full(cred, user, rp, options)?;
469
470 let mut selected: CipherView = this
472 .authenticator
473 .selected_cipher
474 .lock()
475 .expect("Mutex is not poisoned")
476 .clone()
477 .ok_or(InnerError::NoSelectedCredential)?;
478
479 let key_store = this.authenticator.client.internal.get_key_store();
480
481 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
482
483 this.authenticator
485 .selected_cipher
486 .lock()
487 .expect("Mutex is not poisoned")
488 .replace(selected.clone());
489
490 let encrypted = key_store.encrypt(selected)?;
492
493 this.authenticator
494 .credential_store
495 .save_credential(EncryptionContext {
496 cipher: encrypted,
497 encrypted_for: user_id,
498 })
499 .await?;
500
501 Ok(())
502 }
503
504 inner(self, cred, user, rp, options).await.map_err(|e| {
505 error!("Error saving credential: {e:?}");
506 VendorError::try_from(0xF1)
507 .expect("Valid vendor error code")
508 .into()
509 })
510 }
511
512 async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
513 #[derive(Debug, Error)]
514 enum InnerError {
515 #[error("Client User Id has not been set")]
516 MissingUserId,
517 #[error(transparent)]
518 InvalidGuid(#[from] InvalidGuidError),
519 #[error("Credential ID does not match selected credential")]
520 CredentialIdMismatch,
521 #[error(transparent)]
522 FillCredential(#[from] FillCredentialError),
523 #[error(transparent)]
524 Cipher(#[from] CipherError),
525 #[error(transparent)]
526 Crypto(#[from] CryptoError),
527 #[error(transparent)]
528 Fido2Callback(#[from] Fido2CallbackError),
529 #[error(transparent)]
530 GetSelectedCredential(#[from] GetSelectedCredentialError),
531 }
532
533 async fn inner(
535 this: &mut CredentialStoreImpl<'_>,
536 cred: Passkey,
537 ) -> Result<(), InnerError> {
538 let user_id = this
539 .authenticator
540 .client
541 .internal
542 .get_user_id()
543 .ok_or(InnerError::MissingUserId)?;
544 let selected = this.authenticator.get_selected_credential()?;
546
547 let new_id: &Vec<u8> = &cred.credential_id;
549 let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
550 if new_id != &selected_id {
551 return Err(InnerError::CredentialIdMismatch);
552 }
553
554 let cred = fill_with_credential(&selected.credential, cred)?;
555
556 let key_store = this.authenticator.client.internal.get_key_store();
557
558 let mut selected = selected.cipher;
559 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
560
561 this.authenticator
563 .selected_cipher
564 .lock()
565 .expect("Mutex is not poisoned")
566 .replace(selected.clone());
567
568 let encrypted = key_store.encrypt(selected)?;
570
571 this.authenticator
572 .credential_store
573 .save_credential(EncryptionContext {
574 cipher: encrypted,
575 encrypted_for: user_id,
576 })
577 .await?;
578
579 Ok(())
580 }
581
582 inner(self, cred).await.map_err(|e| {
583 error!("Error updating credential: {e:?}");
584 VendorError::try_from(0xF2)
585 .expect("Valid vendor error code")
586 .into()
587 })
588 }
589
590 async fn get_info(&self) -> StoreInfo {
591 StoreInfo {
592 discoverability: DiscoverabilitySupport::Full,
593 }
594 }
595}
596
597#[async_trait::async_trait]
598impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
599 type PasskeyItem = CipherViewContainer;
600
601 async fn check_user<'a>(
602 &self,
603 hint: UIHint<'a, Self::PasskeyItem>,
604 presence: bool,
605 _verification: bool,
606 ) -> Result<UserCheck, Ctap2Error> {
607 let verification = self
608 .authenticator
609 .requested_uv
610 .lock()
611 .expect("Mutex is not poisoned")
612 .ok_or(Ctap2Error::UserVerificationInvalid)?;
613
614 let options = CheckUserOptions {
615 require_presence: presence,
616 require_verification: verification.into(),
617 };
618
619 let result = match hint {
620 UIHint::RequestNewCredential(user, rp) => {
621 let new_credential = try_from_credential_new_view(user, rp)
622 .map_err(|_| Ctap2Error::InvalidCredential)?;
623
624 let (cipher_view, user_check) = self
625 .authenticator
626 .user_interface
627 .check_user_and_pick_credential_for_creation(options, new_credential)
628 .await
629 .map_err(|_| Ctap2Error::OperationDenied)?;
630
631 self.authenticator
632 .selected_cipher
633 .lock()
634 .expect("Mutex is not poisoned")
635 .replace(cipher_view);
636
637 Ok(user_check)
638 }
639 _ => {
640 self.authenticator
641 .user_interface
642 .check_user(options, map_ui_hint(hint))
643 .await
644 }
645 };
646
647 let result = result.map_err(|e| {
648 error!("Error checking user: {e:?}");
649 Ctap2Error::UserVerificationInvalid
650 })?;
651
652 Ok(UserCheck {
653 presence: result.user_present,
654 verification: result.user_verified,
655 })
656 }
657
658 async fn is_presence_enabled(&self) -> bool {
659 true
660 }
661
662 async fn is_verification_enabled(&self) -> Option<bool> {
663 Some(
664 self.authenticator
665 .user_interface
666 .is_verification_enabled()
667 .await,
668 )
669 }
670}
671
672fn map_ui_hint(hint: UIHint<'_, CipherViewContainer>) -> UIHint<'_, CipherView> {
673 use UIHint::*;
674 match hint {
675 InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
676 InformNoCredentialsFound => InformNoCredentialsFound,
677 RequestNewCredential(u, r) => RequestNewCredential(u, r),
678 RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
679 }
680}