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("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 key_store = self.client.internal.get_key_store();
265 let result = self
266 .credential_store
267 .find_credentials(None, rp_id, user_handle)
268 .await?;
269
270 let mut ctx = key_store.context();
271 result
272 .into_iter()
273 .map(
274 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, SilentlyDiscoverCredentialsError> {
275 Ok(Fido2CredentialAutofillView::from_cipher_view(&cipher, &mut ctx)?)
276 },
277 )
278 .flatten_ok()
279 .collect()
280 }
281
282 pub async fn credentials_for_autofill(
285 &mut self,
286 ) -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
287 let all_credentials = self.credential_store.all_credentials().await?;
288
289 all_credentials
290 .into_iter()
291 .map(
292 |cipher| -> Result<Vec<Fido2CredentialAutofillView>, CredentialsForAutofillError> {
293 Ok(Fido2CredentialAutofillView::from_cipher_list_view(&cipher)?)
294 },
295 )
296 .flatten_ok()
297 .collect()
298 }
299
300 pub(super) fn get_authenticator(
301 &self,
302 create_credential: bool,
303 ) -> Authenticator<CredentialStoreImpl<'_>, UserValidationMethodImpl<'_>> {
304 Authenticator::new(
305 AAGUID,
306 CredentialStoreImpl {
307 authenticator: self,
308 create_credential,
309 },
310 UserValidationMethodImpl {
311 authenticator: self,
312 },
313 )
314 }
315
316 fn convert_requested_uv(&self, uv: UV) -> bool {
317 let verification_enabled = self.user_interface.is_verification_enabled();
318 match (uv, verification_enabled) {
319 (UV::Preferred, true) => true,
320 (UV::Preferred, false) => false,
321 (UV::Required, _) => true,
322 (UV::Discouraged, _) => false,
323 }
324 }
325
326 pub(super) fn get_selected_credential(
327 &self,
328 ) -> Result<SelectedCredential, GetSelectedCredentialError> {
329 let key_store = self.client.internal.get_key_store();
330
331 let cipher = self
332 .selected_cipher
333 .lock()
334 .expect("Mutex is not poisoned")
335 .clone()
336 .ok_or(GetSelectedCredentialError::NoSelectedCredential)?;
337
338 let creds = cipher.decrypt_fido2_credentials(&mut key_store.context())?;
339
340 let credential = creds
341 .first()
342 .ok_or(GetSelectedCredentialError::NoCredentialFound)?
343 .clone();
344
345 Ok(SelectedCredential { cipher, credential })
346 }
347}
348
349pub(super) struct CredentialStoreImpl<'a> {
350 authenticator: &'a Fido2Authenticator<'a>,
351 create_credential: bool,
352}
353pub(super) struct UserValidationMethodImpl<'a> {
354 authenticator: &'a Fido2Authenticator<'a>,
355}
356
357#[async_trait::async_trait]
358impl passkey::authenticator::CredentialStore for CredentialStoreImpl<'_> {
359 type PasskeyItem = CipherViewContainer;
360 async fn find_credentials(
361 &self,
362 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
363 rp_id: &str,
364 user_handle: Option<&[u8]>,
365 ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
366 #[derive(Debug, Error)]
367 enum InnerError {
368 #[error(transparent)]
369 Cipher(#[from] CipherError),
370 #[error(transparent)]
371 Crypto(#[from] CryptoError),
372 #[error(transparent)]
373 Fido2Callback(#[from] Fido2CallbackError),
374 }
375
376 async fn inner(
378 this: &CredentialStoreImpl<'_>,
379 ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
380 rp_id: &str,
381 user_handle: Option<&[u8]>,
382 ) -> Result<Vec<CipherViewContainer>, InnerError> {
383 let ids: Option<Vec<Vec<u8>>> =
384 ids.map(|ids| ids.iter().map(|id| id.id.clone().into()).collect());
385
386 let ciphers = this
387 .authenticator
388 .credential_store
389 .find_credentials(ids, rp_id.to_string(), user_handle.map(|h| h.to_vec()))
390 .await?;
391
392 let creds: Vec<_> = ciphers
394 .into_iter()
395 .filter(|c| {
396 c.login
397 .as_ref()
398 .and_then(|l| l.fido2_credentials.as_ref())
399 .is_some()
400 })
401 .collect();
402
403 let key_store = this.authenticator.client.internal.get_key_store();
404
405 if this.create_credential {
407 Ok(creds
408 .into_iter()
409 .map(|c| CipherViewContainer::new(c, &mut key_store.context()))
410 .collect::<Result<_, _>>()?)
411 } else {
412 let picked = this
413 .authenticator
414 .user_interface
415 .pick_credential_for_authentication(creds)
416 .await?;
417
418 this.authenticator
420 .selected_cipher
421 .lock()
422 .expect("Mutex is not poisoned")
423 .replace(picked.clone());
424
425 Ok(vec![CipherViewContainer::new(
426 picked,
427 &mut key_store.context(),
428 )?])
429 }
430 }
431
432 inner(self, ids, rp_id, user_handle).await.map_err(|error| {
433 error!(%error, "Error finding credentials.");
434 VendorError::try_from(0xF0)
435 .expect("Valid vendor error code")
436 .into()
437 })
438 }
439
440 async fn save_credential(
441 &mut self,
442 cred: Passkey,
443 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
444 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
445 options: passkey::types::ctap2::get_assertion::Options,
446 ) -> Result<(), StatusCode> {
447 #[derive(Debug, Error)]
448 enum InnerError {
449 #[error("Client User Id has not been set")]
450 MissingUserId,
451 #[error(transparent)]
452 FillCredential(#[from] FillCredentialError),
453 #[error(transparent)]
454 Cipher(#[from] CipherError),
455 #[error(transparent)]
456 Crypto(#[from] CryptoError),
457 #[error(transparent)]
458 Fido2Callback(#[from] Fido2CallbackError),
459
460 #[error("No selected credential available")]
461 NoSelectedCredential,
462 }
463
464 async fn inner(
466 this: &mut CredentialStoreImpl<'_>,
467 cred: Passkey,
468 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
469 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
470 options: passkey::types::ctap2::get_assertion::Options,
471 ) -> Result<(), InnerError> {
472 let user_id = this
473 .authenticator
474 .client
475 .internal
476 .get_user_id()
477 .ok_or(InnerError::MissingUserId)?;
478 let cred = try_from_credential_full(cred, user, rp, options)?;
479
480 let mut selected: CipherView = this
482 .authenticator
483 .selected_cipher
484 .lock()
485 .expect("Mutex is not poisoned")
486 .clone()
487 .ok_or(InnerError::NoSelectedCredential)?;
488
489 let key_store = this.authenticator.client.internal.get_key_store();
490
491 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
492
493 this.authenticator
495 .selected_cipher
496 .lock()
497 .expect("Mutex is not poisoned")
498 .replace(selected.clone());
499
500 let encrypted = key_store.encrypt(selected)?;
502
503 this.authenticator
504 .credential_store
505 .save_credential(EncryptionContext {
506 cipher: encrypted,
507 encrypted_for: user_id,
508 })
509 .await?;
510
511 Ok(())
512 }
513
514 inner(self, cred, user, rp, options).await.map_err(|error| {
515 error!(%error, "Error saving credential.");
516 VendorError::try_from(0xF1)
517 .expect("Valid vendor error code")
518 .into()
519 })
520 }
521
522 async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
523 #[derive(Debug, Error)]
524 enum InnerError {
525 #[error("Client User Id has not been set")]
526 MissingUserId,
527 #[error(transparent)]
528 InvalidGuid(#[from] InvalidGuidError),
529 #[error("Credential ID does not match selected credential")]
530 CredentialIdMismatch,
531 #[error(transparent)]
532 FillCredential(#[from] FillCredentialError),
533 #[error(transparent)]
534 Cipher(#[from] CipherError),
535 #[error(transparent)]
536 Crypto(#[from] CryptoError),
537 #[error(transparent)]
538 Fido2Callback(#[from] Fido2CallbackError),
539 #[error(transparent)]
540 GetSelectedCredential(#[from] GetSelectedCredentialError),
541 }
542
543 async fn inner(
545 this: &mut CredentialStoreImpl<'_>,
546 cred: Passkey,
547 ) -> Result<(), InnerError> {
548 let user_id = this
549 .authenticator
550 .client
551 .internal
552 .get_user_id()
553 .ok_or(InnerError::MissingUserId)?;
554 let selected = this.authenticator.get_selected_credential()?;
556
557 let new_id: &Vec<u8> = &cred.credential_id;
559 let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
560 if new_id != &selected_id {
561 return Err(InnerError::CredentialIdMismatch);
562 }
563
564 let cred = fill_with_credential(&selected.credential, cred)?;
565
566 let key_store = this.authenticator.client.internal.get_key_store();
567
568 let mut selected = selected.cipher;
569 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
570
571 this.authenticator
573 .selected_cipher
574 .lock()
575 .expect("Mutex is not poisoned")
576 .replace(selected.clone());
577
578 let encrypted = key_store.encrypt(selected)?;
580
581 this.authenticator
582 .credential_store
583 .save_credential(EncryptionContext {
584 cipher: encrypted,
585 encrypted_for: user_id,
586 })
587 .await?;
588
589 Ok(())
590 }
591
592 inner(self, cred).await.map_err(|error| {
593 error!(%error, "Error updating credential.");
594 VendorError::try_from(0xF2)
595 .expect("Valid vendor error code")
596 .into()
597 })
598 }
599
600 async fn get_info(&self) -> StoreInfo {
601 StoreInfo {
602 discoverability: DiscoverabilitySupport::Full,
603 }
604 }
605}
606
607#[async_trait::async_trait]
608impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
609 type PasskeyItem = CipherViewContainer;
610
611 async fn check_user<'a>(
612 &self,
613 hint: UiHint<'a, Self::PasskeyItem>,
614 presence: bool,
615 _verification: bool,
616 ) -> Result<UserCheck, Ctap2Error> {
617 let verification = self
618 .authenticator
619 .requested_uv
620 .lock()
621 .expect("Mutex is not poisoned")
622 .ok_or(Ctap2Error::UserVerificationInvalid)?;
623
624 let options = CheckUserOptions {
625 require_presence: presence,
626 require_verification: verification.into(),
627 };
628
629 let result = match hint {
630 UiHint::RequestNewCredential(user, rp) => {
631 let new_credential = try_from_credential_new_view(user, rp)
632 .map_err(|_| Ctap2Error::InvalidCredential)?;
633
634 let (cipher_view, user_check) = self
635 .authenticator
636 .user_interface
637 .check_user_and_pick_credential_for_creation(options, new_credential)
638 .await
639 .map_err(|_| Ctap2Error::OperationDenied)?;
640
641 self.authenticator
642 .selected_cipher
643 .lock()
644 .expect("Mutex is not poisoned")
645 .replace(cipher_view);
646
647 Ok(user_check)
648 }
649 _ => {
650 self.authenticator
651 .user_interface
652 .check_user(options, map_ui_hint(hint))
653 .await
654 }
655 };
656
657 let result = result.map_err(|error| {
658 error!(%error, "Error checking user.");
659 Ctap2Error::UserVerificationInvalid
660 })?;
661
662 Ok(UserCheck {
663 presence: result.user_present,
664 verification: result.user_verified,
665 })
666 }
667
668 fn is_presence_enabled(&self) -> bool {
669 true
670 }
671
672 fn is_verification_enabled(&self) -> Option<bool> {
673 Some(self.authenticator.user_interface.is_verification_enabled())
674 }
675}
676
677fn map_ui_hint(hint: UiHint<'_, CipherViewContainer>) -> UiHint<'_, CipherView> {
678 use UiHint::*;
679 match hint {
680 InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
681 InformNoCredentialsFound => InformNoCredentialsFound,
682 RequestNewCredential(u, r) => RequestNewCredential(u, r),
683 RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
684 }
685}
686
687#[cfg(test)]
688mod tests {
689 use async_trait::async_trait;
690 use bitwarden_core::{
691 Client,
692 key_management::{KeyIds, SymmetricKeyId},
693 };
694 use bitwarden_crypto::{KeyStoreContext, PrimitiveEncryptable, SymmetricCryptoKey};
695 use bitwarden_encoding::B64Url;
696 use bitwarden_vault::{
697 CipherListView, CipherRepromptType, CipherType, CipherView, EncryptionContext,
698 Fido2Credential, Fido2CredentialNewView, LoginView,
699 };
700 use passkey::authenticator::UiHint;
701
702 use super::Fido2Authenticator;
703 use crate::{
704 CheckUserOptions, CheckUserResult, Fido2CallbackError, Fido2CredentialStore,
705 Fido2UserInterface, GetAssertionExtensionsInput, GetAssertionPrfInput, PrfInputValues,
706 guid_bytes_to_string,
707 types::{GetAssertionRequest, Options, UV},
708 };
709
710 struct MockUserInterface;
711
712 #[async_trait]
713 impl Fido2UserInterface for MockUserInterface {
714 async fn check_user<'a>(
715 &self,
716 _options: CheckUserOptions,
717 _hint: UiHint<'a, CipherView>,
718 ) -> Result<CheckUserResult, Fido2CallbackError> {
719 Ok(CheckUserResult {
720 user_present: true,
721 user_verified: true,
722 })
723 }
724
725 async fn pick_credential_for_authentication(
726 &self,
727 available_credentials: Vec<CipherView>,
728 ) -> Result<CipherView, Fido2CallbackError> {
729 available_credentials
730 .into_iter()
731 .next()
732 .ok_or(Fido2CallbackError::Unknown("no credentials".to_string()))
733 }
734
735 async fn check_user_and_pick_credential_for_creation(
736 &self,
737 _options: CheckUserOptions,
738 _new_credential: Fido2CredentialNewView,
739 ) -> Result<(CipherView, CheckUserResult), Fido2CallbackError> {
740 unimplemented!("not needed for this test")
741 }
742
743 fn is_verification_enabled(&self) -> bool {
744 true
745 }
746 }
747
748 struct MockCredentialStore {
749 cipher: CipherView,
750 }
751
752 #[async_trait]
753 impl Fido2CredentialStore for MockCredentialStore {
754 async fn find_credentials(
755 &self,
756 _ids: Option<Vec<Vec<u8>>>,
757 _rp_id: String,
758 _user_handle: Option<Vec<u8>>,
759 ) -> Result<Vec<CipherView>, Fido2CallbackError> {
760 Ok(vec![self.cipher.clone()])
761 }
762
763 async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError> {
764 Ok(vec![])
765 }
766
767 async fn save_credential(
768 &self,
769 _cred: EncryptionContext,
770 ) -> Result<(), Fido2CallbackError> {
771 Ok(())
772 }
773 }
774
775 static TEST_FIDO_CREDENTIAL_ID: &str = "a36f3d35-5dae-4d07-8b24-f89e11082090";
776 static TEST_FIDO_RP_ID: &str = "example.com";
777 static TEST_FIDO_USER_HANDLE: &str = "YWJjZA";
778 static TEST_FIDO_P256_KEY: &[u8] = &[
780 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d,
781 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x6d, 0x30,
782 0x6b, 0x02, 0x01, 0x01, 0x04, 0x20, 0x06, 0x76, 0x5e, 0x85, 0xe0, 0x7f, 0xef, 0x43, 0xaa,
783 0x17, 0xe0, 0x7a, 0xd7, 0x85, 0x63, 0x01, 0x80, 0x70, 0x8c, 0x6c, 0x61, 0x43, 0x7d, 0xc3,
784 0xb1, 0xe6, 0xf9, 0x09, 0x24, 0xeb, 0x1f, 0xf5, 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0x35,
785 0x9a, 0x52, 0xf3, 0x82, 0x44, 0x66, 0x5f, 0x3f, 0xe2, 0xc4, 0x0b, 0x1c, 0x16, 0x34, 0xc5,
786 0x60, 0x07, 0x3a, 0x25, 0xfe, 0x7e, 0x7f, 0x7f, 0xda, 0xd4, 0x1c, 0x36, 0x90, 0x00, 0xee,
787 0xb1, 0x8e, 0x92, 0xb3, 0xac, 0x91, 0x7f, 0xb1, 0x8c, 0xa4, 0x85, 0xe7, 0x03, 0x07, 0xd1,
788 0xf5, 0x5b, 0xd3, 0x7b, 0xc3, 0x56, 0x11, 0xdf, 0xbc, 0x7a, 0x97, 0x70, 0x32, 0x4b, 0x3c,
789 0x84, 0x05, 0x71,
790 ];
791
792 fn create_test_cipher(ctx: &mut KeyStoreContext<KeyIds>) -> CipherView {
793 let key = SymmetricKeyId::User;
794 let key_value = B64Url::from(TEST_FIDO_P256_KEY).to_string();
795
796 let fido2_credential = Fido2Credential {
797 credential_id: TEST_FIDO_CREDENTIAL_ID.encrypt(ctx, key).unwrap(),
798 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
799 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
800 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
801 key_value: key_value.encrypt(ctx, key).unwrap(),
802 rp_id: TEST_FIDO_RP_ID.encrypt(ctx, key).unwrap(),
803 user_handle: Some(TEST_FIDO_USER_HANDLE.encrypt(ctx, key).unwrap()),
804 user_name: None,
805 counter: "0".to_string().encrypt(ctx, key).unwrap(),
806 rp_name: None,
807 user_display_name: None,
808 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
809 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
810 };
811
812 CipherView {
813 id: Some("c2c7e624-dcfd-4f23-af41-b177014ffcb5".parse().unwrap()),
814 organization_id: None,
815 folder_id: None,
816 collection_ids: vec![],
817 key: None,
818 name: "Test Login".to_string(),
819 notes: None,
820 r#type: CipherType::Login,
821 login: Some(LoginView {
822 username: None,
823 password: None,
824 password_revision_date: None,
825 uris: None,
826 totp: None,
827 autofill_on_page_load: None,
828 fido2_credentials: Some(vec![fido2_credential]),
829 }),
830 identity: None,
831 card: None,
832 secure_note: None,
833 ssh_key: None,
834 favorite: false,
835 reprompt: CipherRepromptType::None,
836 organization_use_totp: false,
837 edit: true,
838 permissions: None,
839 view_password: true,
840 local_data: None,
841 attachments: None,
842 attachment_decryption_failures: None,
843 fields: None,
844 password_history: None,
845 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
846 deleted_date: None,
847 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
848 archived_date: None,
849 }
850 }
851
852 #[tokio::test]
857 async fn test_prf_is_not_evaluated() {
858 let client = Client::new(None);
859 let user_key: SymmetricCryptoKey =
860 "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q=="
861 .to_string()
862 .try_into()
863 .unwrap();
864
865 #[allow(deprecated)]
866 client
867 .internal
868 .get_key_store()
869 .context_mut()
870 .set_symmetric_key(SymmetricKeyId::User, user_key)
871 .unwrap();
872
873 let cipher = {
874 let mut ctx = client.internal.get_key_store().context();
875 create_test_cipher(&mut ctx)
876 };
877
878 let user_interface = MockUserInterface;
879 let credential_store = MockCredentialStore { cipher };
880 let mut authenticator =
881 Fido2Authenticator::new(&client, &user_interface, &credential_store);
882
883 let request = GetAssertionRequest {
884 rp_id: "example.com".to_string(),
885 client_data_hash: vec![0u8; 32],
886 allow_list: None,
887 options: Options {
888 rk: false,
889 uv: UV::Preferred,
890 },
891 extensions: Some(GetAssertionExtensionsInput {
892 prf: Some(GetAssertionPrfInput {
893 eval: Some(PrfInputValues {
894 first: vec![1u8; 32],
895 second: None,
896 }),
897 eval_by_credential: None,
898 }),
899 }),
900 };
901
902 let result = authenticator.get_assertion(request).await.unwrap();
903 assert_eq!(
904 TEST_FIDO_CREDENTIAL_ID,
905 guid_bytes_to_string(&result.credential_id).unwrap()
906 );
907 assert!(
908 result.extensions.prf.is_none(),
909 "PRF should not be evaluated"
910 );
911 }
912}