1use std::sync::Mutex;
2
3use bitwarden_core::Client;
4use bitwarden_crypto::CryptoError;
5use bitwarden_vault::{CipherError, CipherView, EncryptError, VaultClientExt};
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("Missing attested_credential_data")]
46 MissingAttestedCredentialData,
47 #[error("make_credential error: {0}")]
48 Other(String),
49}
50
51#[allow(missing_docs)]
52#[derive(Debug, Error)]
53#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
54pub enum GetAssertionError {
55 #[error(transparent)]
56 UnknownEnum(#[from] UnknownEnumError),
57 #[error(transparent)]
58 GetSelectedCredential(#[from] GetSelectedCredentialError),
59 #[error(transparent)]
60 InvalidGuid(#[from] InvalidGuidError),
61 #[error("missing user")]
62 MissingUser,
63 #[error("get_assertion error: {0}")]
64 Other(String),
65}
66
67#[allow(missing_docs)]
68#[derive(Debug, Error)]
69#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
70pub enum SilentlyDiscoverCredentialsError {
71 #[error(transparent)]
72 Cipher(#[from] CipherError),
73 #[error(transparent)]
74 InvalidGuid(#[from] InvalidGuidError),
75 #[error(transparent)]
76 Fido2Callback(#[from] Fido2CallbackError),
77 #[error(transparent)]
78 FromCipherView(#[from] Fido2CredentialAutofillViewError),
79}
80
81#[allow(missing_docs)]
82#[derive(Debug, Error)]
83#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
84pub enum CredentialsForAutofillError {
85 #[error(transparent)]
86 Cipher(#[from] CipherError),
87 #[error(transparent)]
88 InvalidGuid(#[from] InvalidGuidError),
89 #[error(transparent)]
90 Fido2Callback(#[from] Fido2CallbackError),
91 #[error(transparent)]
92 FromCipherView(#[from] Fido2CredentialAutofillViewError),
93}
94
95#[allow(missing_docs)]
96pub struct Fido2Authenticator<'a> {
97 pub client: &'a Client,
98 pub user_interface: &'a dyn Fido2UserInterface,
99 pub credential_store: &'a dyn Fido2CredentialStore,
100
101 pub(crate) selected_cipher: Mutex<Option<CipherView>>,
102 pub(crate) requested_uv: Mutex<Option<UV>>,
103}
104
105impl<'a> Fido2Authenticator<'a> {
106 #[allow(missing_docs)]
107 pub fn new(
108 client: &'a Client,
109 user_interface: &'a dyn Fido2UserInterface,
110 credential_store: &'a dyn Fido2CredentialStore,
111 ) -> Fido2Authenticator<'a> {
112 Fido2Authenticator {
113 client,
114 user_interface,
115 credential_store,
116 selected_cipher: Mutex::new(None),
117 requested_uv: Mutex::new(None),
118 }
119 }
120
121 #[allow(missing_docs)]
122 pub async fn make_credential(
123 &mut self,
124 request: MakeCredentialRequest,
125 ) -> Result<MakeCredentialResult, MakeCredentialError> {
126 self.requested_uv
128 .get_mut()
129 .expect("Mutex is not poisoned")
130 .replace(request.options.uv);
131
132 let mut authenticator = self.get_authenticator(true);
133
134 let response = authenticator
135 .make_credential(ctap2::make_credential::Request {
136 client_data_hash: request.client_data_hash.into(),
137 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity {
138 id: request.rp.id,
139 name: request.rp.name,
140 },
141 user: passkey::types::webauthn::PublicKeyCredentialUserEntity {
142 id: request.user.id.into(),
143 display_name: request.user.display_name,
144 name: request.user.name,
145 },
146 pub_key_cred_params: request
147 .pub_key_cred_params
148 .into_iter()
149 .map(TryInto::try_into)
150 .collect::<Result<_, _>>()?,
151 exclude_list: request
152 .exclude_list
153 .map(|x| x.into_iter().map(TryInto::try_into).collect())
154 .transpose()?,
155 extensions: request
159 .extensions
160 .map(passkey::types::ctap2::make_credential::ExtensionInputs::from),
161 options: passkey::types::ctap2::make_credential::Options {
162 rk: request.options.rk,
163 up: true,
164 uv: self.convert_requested_uv(request.options.uv),
165 },
166 pin_auth: None,
167 pin_protocol: None,
168 })
169 .await;
170
171 let response = match response {
172 Ok(x) => x,
173 Err(e) => return Err(MakeCredentialError::Other(format!("{e:?}"))),
174 };
175
176 let attestation_object = response.as_webauthn_bytes().to_vec();
177 let authenticator_data = response.auth_data.to_vec();
178 let attested_credential_data = response
179 .auth_data
180 .attested_credential_data
181 .ok_or(MakeCredentialError::MissingAttestedCredentialData)?;
182 let credential_id = attested_credential_data.credential_id().to_vec();
183 let extensions = response.unsigned_extension_outputs.into();
184
185 Ok(MakeCredentialResult {
186 authenticator_data,
187 attestation_object,
188 credential_id,
189 extensions,
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
222 .extensions
223 .map(passkey::types::ctap2::get_assertion::ExtensionInputs::from),
224 options: passkey::types::ctap2::make_credential::Options {
225 rk: request.options.rk,
226 up: true,
227 uv: self.convert_requested_uv(request.options.uv),
228 },
229 pin_auth: None,
230 pin_protocol: None,
231 })
232 .await;
233
234 let response = match response {
235 Ok(x) => x,
236 Err(e) => return Err(GetAssertionError::Other(format!("{e:?}"))),
237 };
238
239 let selected_credential = self.get_selected_credential()?;
240 let authenticator_data = response.auth_data.to_vec();
241 let credential_id = string_to_guid_bytes(&selected_credential.credential.credential_id)?;
242 let extensions = response.unsigned_extension_outputs.into();
243
244 Ok(GetAssertionResult {
245 credential_id,
246 authenticator_data,
247 signature: response.signature.into(),
248 user_handle: response
249 .user
250 .ok_or(GetAssertionError::MissingUser)?
251 .id
252 .into(),
253 selected_credential,
254 extensions,
255 })
256 }
257
258 #[allow(missing_docs)]
259 pub async fn silently_discover_credentials(
260 &mut self,
261 rp_id: String,
262 user_handle: Option<Vec<u8>>,
263 ) -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
264 let result = self
265 .credential_store
266 .find_credentials(None, rp_id, user_handle)
267 .await?;
268
269 result
270 .into_iter()
271 .map(
272 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
273 Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher)?)
274 },
275 )
276 .flatten_ok()
277 .collect()
278 }
279
280 pub async fn credentials_for_autofill(
283 &mut self,
284 ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
285 let all_credentials = self.credential_store.all_credentials().await?;
286
287 all_credentials
288 .into_iter()
289 .map(
290 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
291 Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
292 },
293 )
294 .flatten_ok()
295 .collect()
296 }
297
298 pub(super) fn get_authenticator(
299 &self,
300 create_credential: bool,
301 ) -> Authenticator<CredentialStoreImpl<'_>, UserValidationMethodImpl<'_>> {
302 Authenticator::new(
303 AAGUID,
304 CredentialStoreImpl {
305 authenticator: self,
306 create_credential,
307 },
308 UserValidationMethodImpl {
309 authenticator: self,
310 },
311 )
312 }
313
314 fn convert_requested_uv(&self, uv: UV) -> bool {
315 let verification_enabled = self.user_interface.is_verification_enabled();
316 match (uv, verification_enabled) {
317 (UV::Preferred, true) => true,
318 (UV::Preferred, false) => false,
319 (UV::Required, _) => true,
320 (UV::Discouraged, _) => false,
321 }
322 }
323
324 pub(super) fn get_selected_credential(
325 &self,
326 ) -> Result<SelectedCredential, GetSelectedCredentialError> {
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.get_fido2_credentials();
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 if this.create_credential {
401 Ok(creds
402 .into_iter()
403 .map(CipherViewContainer::new)
404 .collect::<Result<_, _>>()?)
405 } else {
406 let picked = this
407 .authenticator
408 .user_interface
409 .pick_credential_for_authentication(creds)
410 .await?;
411
412 this.authenticator
414 .selected_cipher
415 .lock()
416 .expect("Mutex is not poisoned")
417 .replace(picked.clone());
418
419 Ok(vec![CipherViewContainer::new(picked)?])
420 }
421 }
422
423 inner(self, ids, rp_id, user_handle).await.map_err(|error| {
424 error!(%error, "Error finding credentials.");
425 VendorError::try_from(0xF0)
426 .expect("Valid vendor error code")
427 .into()
428 })
429 }
430
431 async fn save_credential(
432 &mut self,
433 cred: Passkey,
434 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
435 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
436 options: passkey::types::ctap2::get_assertion::Options,
437 ) -> Result<(), StatusCode> {
438 #[derive(Debug, Error)]
439 enum InnerError {
440 #[error(transparent)]
441 FillCredential(#[from] FillCredentialError),
442 #[error(transparent)]
443 Cipher(#[from] CipherError),
444 #[error(transparent)]
445 Crypto(#[from] CryptoError),
446 #[error(transparent)]
447 Encrypt(#[from] EncryptError),
448 #[error(transparent)]
449 Fido2Callback(#[from] Fido2CallbackError),
450
451 #[error("No selected credential available")]
452 NoSelectedCredential,
453 }
454
455 async fn inner(
457 this: &mut CredentialStoreImpl<'_>,
458 cred: Passkey,
459 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
460 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
461 options: passkey::types::ctap2::get_assertion::Options,
462 ) -> Result<(), InnerError> {
463 let cred = try_from_credential_full(cred, user, rp, options)?;
464
465 let mut selected: CipherView = this
467 .authenticator
468 .selected_cipher
469 .lock()
470 .expect("Mutex is not poisoned")
471 .clone()
472 .ok_or(InnerError::NoSelectedCredential)?;
473
474 selected.set_new_fido2_credentials(vec![cred])?;
475
476 this.authenticator
478 .selected_cipher
479 .lock()
480 .expect("Mutex is not poisoned")
481 .replace(selected.clone());
482
483 let encryption_context = this
485 .authenticator
486 .client
487 .vault()
488 .ciphers()
489 .encrypt(selected)
490 .await?;
491
492 this.authenticator
493 .credential_store
494 .save_credential(encryption_context)
495 .await?;
496
497 Ok(())
498 }
499
500 inner(self, cred, user, rp, options).await.map_err(|error| {
501 error!(%error, "Error saving credential.");
502 VendorError::try_from(0xF1)
503 .expect("Valid vendor error code")
504 .into()
505 })
506 }
507
508 async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
509 #[derive(Debug, Error)]
510 enum InnerError {
511 #[error(transparent)]
512 InvalidGuid(#[from] InvalidGuidError),
513 #[error("Credential ID does not match selected credential")]
514 CredentialIdMismatch,
515 #[error(transparent)]
516 FillCredential(#[from] FillCredentialError),
517 #[error(transparent)]
518 Cipher(#[from] CipherError),
519 #[error(transparent)]
520 Crypto(#[from] CryptoError),
521 #[error(transparent)]
522 Encrypt(#[from] EncryptError),
523 #[error(transparent)]
524 Fido2Callback(#[from] Fido2CallbackError),
525 #[error(transparent)]
526 GetSelectedCredential(#[from] GetSelectedCredentialError),
527 }
528
529 async fn inner(
531 this: &mut CredentialStoreImpl<'_>,
532 cred: Passkey,
533 ) -> Result<(), InnerError> {
534 let selected = this.authenticator.get_selected_credential()?;
536
537 let new_id: &Vec<u8> = &cred.credential_id;
539 let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
540 if new_id != &selected_id {
541 return Err(InnerError::CredentialIdMismatch);
542 }
543
544 let cred = fill_with_credential(&selected.credential, cred)?;
545
546 let mut selected = selected.cipher;
547 selected.set_new_fido2_credentials(vec![cred])?;
548
549 this.authenticator
551 .selected_cipher
552 .lock()
553 .expect("Mutex is not poisoned")
554 .replace(selected.clone());
555
556 let encryption_context = this
558 .authenticator
559 .client
560 .vault()
561 .ciphers()
562 .encrypt(selected)
563 .await?;
564
565 this.authenticator
566 .credential_store
567 .save_credential(encryption_context)
568 .await?;
569
570 Ok(())
571 }
572
573 inner(self, cred).await.map_err(|error| {
574 error!(%error, "Error updating credential.");
575 VendorError::try_from(0xF2)
576 .expect("Valid vendor error code")
577 .into()
578 })
579 }
580
581 async fn get_info(&self) -> StoreInfo {
582 StoreInfo {
583 discoverability: DiscoverabilitySupport::Full,
584 }
585 }
586}
587
588#[async_trait::async_trait]
589impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
590 type PasskeyItem = CipherViewContainer;
591
592 async fn check_user<'a>(
593 &self,
594 hint: UiHint<'a, Self::PasskeyItem>,
595 presence: bool,
596 _verification: bool,
597 ) -> Result<UserCheck, Ctap2Error> {
598 let verification = self
599 .authenticator
600 .requested_uv
601 .lock()
602 .expect("Mutex is not poisoned")
603 .ok_or(Ctap2Error::UserVerificationInvalid)?;
604
605 let options = CheckUserOptions {
606 require_presence: presence,
607 require_verification: verification.into(),
608 };
609
610 let result = match hint {
611 UiHint::RequestNewCredential(user, rp) => {
612 let new_credential = try_from_credential_new_view(user, rp)
613 .map_err(|_| Ctap2Error::InvalidCredential)?;
614
615 let (cipher_view, user_check) = self
616 .authenticator
617 .user_interface
618 .check_user_and_pick_credential_for_creation(options, new_credential)
619 .await
620 .map_err(|_| Ctap2Error::OperationDenied)?;
621
622 self.authenticator
623 .selected_cipher
624 .lock()
625 .expect("Mutex is not poisoned")
626 .replace(cipher_view);
627
628 Ok(user_check)
629 }
630 _ => {
631 self.authenticator
632 .user_interface
633 .check_user(options, map_ui_hint(hint))
634 .await
635 }
636 };
637
638 let result = result.map_err(|error| {
639 error!(%error, "Error checking user.");
640 Ctap2Error::UserVerificationInvalid
641 })?;
642
643 Ok(UserCheck {
644 presence: result.user_present,
645 verification: result.user_verified,
646 })
647 }
648
649 fn is_presence_enabled(&self) -> bool {
650 true
651 }
652
653 fn is_verification_enabled(&self) -> Option<bool> {
654 Some(self.authenticator.user_interface.is_verification_enabled())
655 }
656}
657
658fn map_ui_hint(hint: UiHint<'_, CipherViewContainer>) -> UiHint<'_, CipherView> {
659 use UiHint::*;
660 match hint {
661 InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
662 InformNoCredentialsFound => InformNoCredentialsFound,
663 RequestNewCredential(u, r) => RequestNewCredential(u, r),
664 RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
665 }
666}
667
668#[cfg(test)]
669mod tests {
670 use async_trait::async_trait;
671 use bitwarden_core::{Client, key_management::SymmetricKeySlotId};
672 use bitwarden_crypto::SymmetricCryptoKey;
673 use bitwarden_encoding::B64Url;
674 use bitwarden_vault::{
675 CipherListView, CipherRepromptType, CipherType, CipherView, EncryptionContext,
676 Fido2CredentialNewView, Fido2CredentialView, LoginView,
677 };
678 use passkey::authenticator::UiHint;
679
680 use super::Fido2Authenticator;
681 use crate::{
682 CheckUserOptions, CheckUserResult, Fido2CallbackError, Fido2CredentialStore,
683 Fido2UserInterface, GetAssertionExtensionsInput, GetAssertionPrfInput, PrfInputValues,
684 guid_bytes_to_string,
685 types::{GetAssertionRequest, Options, UV},
686 };
687
688 struct MockUserInterface;
689
690 #[async_trait]
691 impl Fido2UserInterface for MockUserInterface {
692 async fn check_user<'a>(
693 &self,
694 _options: CheckUserOptions,
695 _hint: UiHint<'a, CipherView>,
696 ) -> Result<CheckUserResult, Fido2CallbackError> {
697 Ok(CheckUserResult {
698 user_present: true,
699 user_verified: true,
700 })
701 }
702
703 async fn pick_credential_for_authentication(
704 &self,
705 available_credentials: Vec<CipherView>,
706 ) -> Result<CipherView, Fido2CallbackError> {
707 available_credentials
708 .into_iter()
709 .next()
710 .ok_or(Fido2CallbackError::Unknown("no credentials".to_string()))
711 }
712
713 async fn check_user_and_pick_credential_for_creation(
714 &self,
715 _options: CheckUserOptions,
716 _new_credential: Fido2CredentialNewView,
717 ) -> Result<(CipherView, CheckUserResult), Fido2CallbackError> {
718 unimplemented!("not needed for this test")
719 }
720
721 fn is_verification_enabled(&self) -> bool {
722 true
723 }
724 }
725
726 struct MockCredentialStore {
727 cipher: CipherView,
728 }
729
730 #[async_trait]
731 impl Fido2CredentialStore for MockCredentialStore {
732 async fn find_credentials(
733 &self,
734 _ids: Option<Vec<Vec<u8>>>,
735 _rp_id: String,
736 _user_handle: Option<Vec<u8>>,
737 ) -> Result<Vec<CipherView>, Fido2CallbackError> {
738 Ok(vec![self.cipher.clone()])
739 }
740
741 async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError> {
742 Ok(vec![])
743 }
744
745 async fn save_credential(
746 &self,
747 _cred: EncryptionContext,
748 ) -> Result<(), Fido2CallbackError> {
749 Ok(())
750 }
751 }
752
753 static TEST_FIDO_CREDENTIAL_ID: &str = "a36f3d35-5dae-4d07-8b24-f89e11082090";
754 static TEST_FIDO_RP_ID: &str = "example.com";
755 static TEST_FIDO_USER_HANDLE: &str = "YWJjZA";
756 static TEST_FIDO_P256_KEY: &[u8] = &[
758 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d,
759 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x6d, 0x30,
760 0x6b, 0x02, 0x01, 0x01, 0x04, 0x20, 0x06, 0x76, 0x5e, 0x85, 0xe0, 0x7f, 0xef, 0x43, 0xaa,
761 0x17, 0xe0, 0x7a, 0xd7, 0x85, 0x63, 0x01, 0x80, 0x70, 0x8c, 0x6c, 0x61, 0x43, 0x7d, 0xc3,
762 0xb1, 0xe6, 0xf9, 0x09, 0x24, 0xeb, 0x1f, 0xf5, 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0x35,
763 0x9a, 0x52, 0xf3, 0x82, 0x44, 0x66, 0x5f, 0x3f, 0xe2, 0xc4, 0x0b, 0x1c, 0x16, 0x34, 0xc5,
764 0x60, 0x07, 0x3a, 0x25, 0xfe, 0x7e, 0x7f, 0x7f, 0xda, 0xd4, 0x1c, 0x36, 0x90, 0x00, 0xee,
765 0xb1, 0x8e, 0x92, 0xb3, 0xac, 0x91, 0x7f, 0xb1, 0x8c, 0xa4, 0x85, 0xe7, 0x03, 0x07, 0xd1,
766 0xf5, 0x5b, 0xd3, 0x7b, 0xc3, 0x56, 0x11, 0xdf, 0xbc, 0x7a, 0x97, 0x70, 0x32, 0x4b, 0x3c,
767 0x84, 0x05, 0x71,
768 ];
769
770 fn create_test_cipher() -> CipherView {
771 let key_value = B64Url::from(TEST_FIDO_P256_KEY).to_string();
772
773 let fido2_credential = Fido2CredentialView {
774 credential_id: TEST_FIDO_CREDENTIAL_ID.to_string(),
775 key_type: "public-key".to_string(),
776 key_algorithm: "ECDSA".to_string(),
777 key_curve: "P-256".to_string(),
778 key_value,
779 rp_id: TEST_FIDO_RP_ID.to_string(),
780 user_handle: Some(TEST_FIDO_USER_HANDLE.to_string()),
781 user_name: None,
782 counter: "0".to_string(),
783 rp_name: None,
784 user_display_name: None,
785 discoverable: "true".to_string(),
786 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
787 };
788
789 CipherView {
790 partial: false,
791 id: Some("c2c7e624-dcfd-4f23-af41-b177014ffcb5".parse().unwrap()),
792 organization_id: None,
793 folder_id: None,
794 collection_ids: vec![],
795 key: None,
796 name: "Test Login".to_string(),
797 notes: None,
798 r#type: CipherType::Login,
799 login: Some(LoginView {
800 username: None,
801 password: None,
802 password_revision_date: None,
803 uris: None,
804 totp: None,
805 autofill_on_page_load: None,
806 fido2_credentials: Some(vec![fido2_credential]),
807 }),
808 identity: None,
809 card: None,
810 secure_note: None,
811 ssh_key: None,
812 bank_account: None,
813 passport: None,
814 drivers_license: None,
815 favorite: false,
816 reprompt: CipherRepromptType::None,
817 organization_use_totp: false,
818 edit: true,
819 permissions: None,
820 view_password: true,
821 local_data: None,
822 attachments: None,
823 attachment_decryption_failures: None,
824 fields: None,
825 password_history: None,
826 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
827 deleted_date: None,
828 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
829 archived_date: None,
830 }
831 }
832
833 #[tokio::test]
838 async fn test_prf_is_not_evaluated() {
839 let client = Client::new(None);
840 let user_key: SymmetricCryptoKey =
841 "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q=="
842 .to_string()
843 .try_into()
844 .unwrap();
845
846 #[allow(deprecated)]
847 client
848 .internal
849 .get_key_store()
850 .context_mut()
851 .set_symmetric_key(SymmetricKeySlotId::User, user_key)
852 .unwrap();
853
854 let cipher = create_test_cipher();
855
856 let user_interface = MockUserInterface;
857 let credential_store = MockCredentialStore { cipher };
858 let mut authenticator =
859 Fido2Authenticator::new(&client, &user_interface, &credential_store);
860
861 let request = GetAssertionRequest {
862 rp_id: "example.com".to_string(),
863 client_data_hash: vec![0u8; 32],
864 allow_list: None,
865 options: Options {
866 rk: false,
867 uv: UV::Preferred,
868 },
869 extensions: Some(GetAssertionExtensionsInput {
870 prf: Some(GetAssertionPrfInput {
871 eval: Some(PrfInputValues {
872 first: vec![1u8; 32],
873 second: None,
874 }),
875 eval_by_credential: None,
876 }),
877 }),
878 };
879
880 let result = authenticator.get_assertion(request).await.unwrap();
881 assert_eq!(
882 TEST_FIDO_CREDENTIAL_ID,
883 guid_bytes_to_string(&result.credential_id).unwrap()
884 );
885 assert!(
886 result.extensions.prf.is_none(),
887 "PRF should not be evaluated"
888 );
889 }
890}