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 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(transparent)]
450 FillCredential(#[from] FillCredentialError),
451 #[error(transparent)]
452 Cipher(#[from] CipherError),
453 #[error(transparent)]
454 Crypto(#[from] CryptoError),
455 #[error(transparent)]
456 Encrypt(#[from] EncryptError),
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 cred = try_from_credential_full(cred, user, rp, options)?;
473
474 let mut selected: CipherView = this
476 .authenticator
477 .selected_cipher
478 .lock()
479 .expect("Mutex is not poisoned")
480 .clone()
481 .ok_or(InnerError::NoSelectedCredential)?;
482
483 let key_store = this.authenticator.client.internal.get_key_store();
484
485 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
486
487 this.authenticator
489 .selected_cipher
490 .lock()
491 .expect("Mutex is not poisoned")
492 .replace(selected.clone());
493
494 let encryption_context = this
496 .authenticator
497 .client
498 .vault()
499 .ciphers()
500 .encrypt(selected)
501 .await?;
502
503 this.authenticator
504 .credential_store
505 .save_credential(encryption_context)
506 .await?;
507
508 Ok(())
509 }
510
511 inner(self, cred, user, rp, options).await.map_err(|error| {
512 error!(%error, "Error saving credential.");
513 VendorError::try_from(0xF1)
514 .expect("Valid vendor error code")
515 .into()
516 })
517 }
518
519 async fn update_credential(&mut self, cred: Passkey) -> Result<(), StatusCode> {
520 #[derive(Debug, Error)]
521 enum InnerError {
522 #[error(transparent)]
523 InvalidGuid(#[from] InvalidGuidError),
524 #[error("Credential ID does not match selected credential")]
525 CredentialIdMismatch,
526 #[error(transparent)]
527 FillCredential(#[from] FillCredentialError),
528 #[error(transparent)]
529 Cipher(#[from] CipherError),
530 #[error(transparent)]
531 Crypto(#[from] CryptoError),
532 #[error(transparent)]
533 Encrypt(#[from] EncryptError),
534 #[error(transparent)]
535 Fido2Callback(#[from] Fido2CallbackError),
536 #[error(transparent)]
537 GetSelectedCredential(#[from] GetSelectedCredentialError),
538 }
539
540 async fn inner(
542 this: &mut CredentialStoreImpl<'_>,
543 cred: Passkey,
544 ) -> Result<(), InnerError> {
545 let selected = this.authenticator.get_selected_credential()?;
547
548 let new_id: &Vec<u8> = &cred.credential_id;
550 let selected_id = string_to_guid_bytes(&selected.credential.credential_id)?;
551 if new_id != &selected_id {
552 return Err(InnerError::CredentialIdMismatch);
553 }
554
555 let cred = fill_with_credential(&selected.credential, cred)?;
556
557 let key_store = this.authenticator.client.internal.get_key_store();
558
559 let mut selected = selected.cipher;
560 selected.set_new_fido2_credentials(&mut key_store.context(), vec![cred])?;
561
562 this.authenticator
564 .selected_cipher
565 .lock()
566 .expect("Mutex is not poisoned")
567 .replace(selected.clone());
568
569 let encryption_context = this
571 .authenticator
572 .client
573 .vault()
574 .ciphers()
575 .encrypt(selected)
576 .await?;
577
578 this.authenticator
579 .credential_store
580 .save_credential(encryption_context)
581 .await?;
582
583 Ok(())
584 }
585
586 inner(self, cred).await.map_err(|error| {
587 error!(%error, "Error updating credential.");
588 VendorError::try_from(0xF2)
589 .expect("Valid vendor error code")
590 .into()
591 })
592 }
593
594 async fn get_info(&self) -> StoreInfo {
595 StoreInfo {
596 discoverability: DiscoverabilitySupport::Full,
597 }
598 }
599}
600
601#[async_trait::async_trait]
602impl passkey::authenticator::UserValidationMethod for UserValidationMethodImpl<'_> {
603 type PasskeyItem = CipherViewContainer;
604
605 async fn check_user<'a>(
606 &self,
607 hint: UiHint<'a, Self::PasskeyItem>,
608 presence: bool,
609 _verification: bool,
610 ) -> Result<UserCheck, Ctap2Error> {
611 let verification = self
612 .authenticator
613 .requested_uv
614 .lock()
615 .expect("Mutex is not poisoned")
616 .ok_or(Ctap2Error::UserVerificationInvalid)?;
617
618 let options = CheckUserOptions {
619 require_presence: presence,
620 require_verification: verification.into(),
621 };
622
623 let result = match hint {
624 UiHint::RequestNewCredential(user, rp) => {
625 let new_credential = try_from_credential_new_view(user, rp)
626 .map_err(|_| Ctap2Error::InvalidCredential)?;
627
628 let (cipher_view, user_check) = self
629 .authenticator
630 .user_interface
631 .check_user_and_pick_credential_for_creation(options, new_credential)
632 .await
633 .map_err(|_| Ctap2Error::OperationDenied)?;
634
635 self.authenticator
636 .selected_cipher
637 .lock()
638 .expect("Mutex is not poisoned")
639 .replace(cipher_view);
640
641 Ok(user_check)
642 }
643 _ => {
644 self.authenticator
645 .user_interface
646 .check_user(options, map_ui_hint(hint))
647 .await
648 }
649 };
650
651 let result = result.map_err(|error| {
652 error!(%error, "Error checking user.");
653 Ctap2Error::UserVerificationInvalid
654 })?;
655
656 Ok(UserCheck {
657 presence: result.user_present,
658 verification: result.user_verified,
659 })
660 }
661
662 fn is_presence_enabled(&self) -> bool {
663 true
664 }
665
666 fn is_verification_enabled(&self) -> Option<bool> {
667 Some(self.authenticator.user_interface.is_verification_enabled())
668 }
669}
670
671fn map_ui_hint(hint: UiHint<'_, CipherViewContainer>) -> UiHint<'_, CipherView> {
672 use UiHint::*;
673 match hint {
674 InformExcludedCredentialFound(c) => InformExcludedCredentialFound(&c.cipher),
675 InformNoCredentialsFound => InformNoCredentialsFound,
676 RequestNewCredential(u, r) => RequestNewCredential(u, r),
677 RequestExistingCredential(c) => RequestExistingCredential(&c.cipher),
678 }
679}
680
681#[cfg(test)]
682mod tests {
683 use async_trait::async_trait;
684 use bitwarden_core::{
685 Client,
686 key_management::{KeySlotIds, SymmetricKeySlotId},
687 };
688 use bitwarden_crypto::{KeyStoreContext, PrimitiveEncryptable, SymmetricCryptoKey};
689 use bitwarden_encoding::B64Url;
690 use bitwarden_vault::{
691 CipherListView, CipherRepromptType, CipherType, CipherView, EncryptionContext,
692 Fido2Credential, Fido2CredentialNewView, LoginView,
693 };
694 use passkey::authenticator::UiHint;
695
696 use super::Fido2Authenticator;
697 use crate::{
698 CheckUserOptions, CheckUserResult, Fido2CallbackError, Fido2CredentialStore,
699 Fido2UserInterface, GetAssertionExtensionsInput, GetAssertionPrfInput, PrfInputValues,
700 guid_bytes_to_string,
701 types::{GetAssertionRequest, Options, UV},
702 };
703
704 struct MockUserInterface;
705
706 #[async_trait]
707 impl Fido2UserInterface for MockUserInterface {
708 async fn check_user<'a>(
709 &self,
710 _options: CheckUserOptions,
711 _hint: UiHint<'a, CipherView>,
712 ) -> Result<CheckUserResult, Fido2CallbackError> {
713 Ok(CheckUserResult {
714 user_present: true,
715 user_verified: true,
716 })
717 }
718
719 async fn pick_credential_for_authentication(
720 &self,
721 available_credentials: Vec<CipherView>,
722 ) -> Result<CipherView, Fido2CallbackError> {
723 available_credentials
724 .into_iter()
725 .next()
726 .ok_or(Fido2CallbackError::Unknown("no credentials".to_string()))
727 }
728
729 async fn check_user_and_pick_credential_for_creation(
730 &self,
731 _options: CheckUserOptions,
732 _new_credential: Fido2CredentialNewView,
733 ) -> Result<(CipherView, CheckUserResult), Fido2CallbackError> {
734 unimplemented!("not needed for this test")
735 }
736
737 fn is_verification_enabled(&self) -> bool {
738 true
739 }
740 }
741
742 struct MockCredentialStore {
743 cipher: CipherView,
744 }
745
746 #[async_trait]
747 impl Fido2CredentialStore for MockCredentialStore {
748 async fn find_credentials(
749 &self,
750 _ids: Option<Vec<Vec<u8>>>,
751 _rp_id: String,
752 _user_handle: Option<Vec<u8>>,
753 ) -> Result<Vec<CipherView>, Fido2CallbackError> {
754 Ok(vec![self.cipher.clone()])
755 }
756
757 async fn all_credentials(&self) -> Result<Vec<CipherListView>, Fido2CallbackError> {
758 Ok(vec![])
759 }
760
761 async fn save_credential(
762 &self,
763 _cred: EncryptionContext,
764 ) -> Result<(), Fido2CallbackError> {
765 Ok(())
766 }
767 }
768
769 static TEST_FIDO_CREDENTIAL_ID: &str = "a36f3d35-5dae-4d07-8b24-f89e11082090";
770 static TEST_FIDO_RP_ID: &str = "example.com";
771 static TEST_FIDO_USER_HANDLE: &str = "YWJjZA";
772 static TEST_FIDO_P256_KEY: &[u8] = &[
774 0x30, 0x81, 0x87, 0x02, 0x01, 0x00, 0x30, 0x13, 0x06, 0x07, 0x2a, 0x86, 0x48, 0xce, 0x3d,
775 0x02, 0x01, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x03, 0x01, 0x07, 0x04, 0x6d, 0x30,
776 0x6b, 0x02, 0x01, 0x01, 0x04, 0x20, 0x06, 0x76, 0x5e, 0x85, 0xe0, 0x7f, 0xef, 0x43, 0xaa,
777 0x17, 0xe0, 0x7a, 0xd7, 0x85, 0x63, 0x01, 0x80, 0x70, 0x8c, 0x6c, 0x61, 0x43, 0x7d, 0xc3,
778 0xb1, 0xe6, 0xf9, 0x09, 0x24, 0xeb, 0x1f, 0xf5, 0xa1, 0x44, 0x03, 0x42, 0x00, 0x04, 0x35,
779 0x9a, 0x52, 0xf3, 0x82, 0x44, 0x66, 0x5f, 0x3f, 0xe2, 0xc4, 0x0b, 0x1c, 0x16, 0x34, 0xc5,
780 0x60, 0x07, 0x3a, 0x25, 0xfe, 0x7e, 0x7f, 0x7f, 0xda, 0xd4, 0x1c, 0x36, 0x90, 0x00, 0xee,
781 0xb1, 0x8e, 0x92, 0xb3, 0xac, 0x91, 0x7f, 0xb1, 0x8c, 0xa4, 0x85, 0xe7, 0x03, 0x07, 0xd1,
782 0xf5, 0x5b, 0xd3, 0x7b, 0xc3, 0x56, 0x11, 0xdf, 0xbc, 0x7a, 0x97, 0x70, 0x32, 0x4b, 0x3c,
783 0x84, 0x05, 0x71,
784 ];
785
786 fn create_test_cipher(ctx: &mut KeyStoreContext<KeySlotIds>) -> CipherView {
787 let key = SymmetricKeySlotId::User;
788 let key_value = B64Url::from(TEST_FIDO_P256_KEY).to_string();
789
790 let fido2_credential = Fido2Credential {
791 credential_id: TEST_FIDO_CREDENTIAL_ID.encrypt(ctx, key).unwrap(),
792 key_type: "public-key".to_string().encrypt(ctx, key).unwrap(),
793 key_algorithm: "ECDSA".to_string().encrypt(ctx, key).unwrap(),
794 key_curve: "P-256".to_string().encrypt(ctx, key).unwrap(),
795 key_value: key_value.encrypt(ctx, key).unwrap(),
796 rp_id: TEST_FIDO_RP_ID.encrypt(ctx, key).unwrap(),
797 user_handle: Some(TEST_FIDO_USER_HANDLE.encrypt(ctx, key).unwrap()),
798 user_name: None,
799 counter: "0".to_string().encrypt(ctx, key).unwrap(),
800 rp_name: None,
801 user_display_name: None,
802 discoverable: "true".to_string().encrypt(ctx, key).unwrap(),
803 creation_date: "2024-06-07T14:12:36.150Z".parse().unwrap(),
804 };
805
806 CipherView {
807 id: Some("c2c7e624-dcfd-4f23-af41-b177014ffcb5".parse().unwrap()),
808 organization_id: None,
809 folder_id: None,
810 collection_ids: vec![],
811 key: None,
812 name: "Test Login".to_string(),
813 notes: None,
814 r#type: CipherType::Login,
815 login: Some(LoginView {
816 username: None,
817 password: None,
818 password_revision_date: None,
819 uris: None,
820 totp: None,
821 autofill_on_page_load: None,
822 fido2_credentials: Some(vec![fido2_credential]),
823 }),
824 identity: None,
825 card: None,
826 secure_note: None,
827 ssh_key: None,
828 bank_account: None,
829 passport: None,
830 drivers_license: None,
831 favorite: false,
832 reprompt: CipherRepromptType::None,
833 organization_use_totp: false,
834 edit: true,
835 permissions: None,
836 view_password: true,
837 local_data: None,
838 attachments: None,
839 attachment_decryption_failures: None,
840 fields: None,
841 password_history: None,
842 creation_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
843 deleted_date: None,
844 revision_date: "2024-01-30T17:55:36.150Z".parse().unwrap(),
845 archived_date: None,
846 }
847 }
848
849 #[tokio::test]
854 async fn test_prf_is_not_evaluated() {
855 let client = Client::new(None);
856 let user_key: SymmetricCryptoKey =
857 "w2LO+nwV4oxwswVYCxlOfRUseXfvU03VzvKQHrqeklPgiMZrspUe6sOBToCnDn9Ay0tuCBn8ykVVRb7PWhub2Q=="
858 .to_string()
859 .try_into()
860 .unwrap();
861
862 #[allow(deprecated)]
863 client
864 .internal
865 .get_key_store()
866 .context_mut()
867 .set_symmetric_key(SymmetricKeySlotId::User, user_key)
868 .unwrap();
869
870 let cipher = {
871 let mut ctx = client.internal.get_key_store().context();
872 create_test_cipher(&mut ctx)
873 };
874
875 let user_interface = MockUserInterface;
876 let credential_store = MockCredentialStore { cipher };
877 let mut authenticator =
878 Fido2Authenticator::new(&client, &user_interface, &credential_store);
879
880 let request = GetAssertionRequest {
881 rp_id: "example.com".to_string(),
882 client_data_hash: vec![0u8; 32],
883 allow_list: None,
884 options: Options {
885 rk: false,
886 uv: UV::Preferred,
887 },
888 extensions: Some(GetAssertionExtensionsInput {
889 prf: Some(GetAssertionPrfInput {
890 eval: Some(PrfInputValues {
891 first: vec![1u8; 32],
892 second: None,
893 }),
894 eval_by_credential: None,
895 }),
896 }),
897 };
898
899 let result = authenticator.get_assertion(request).await.unwrap();
900 assert_eq!(
901 TEST_FIDO_CREDENTIAL_ID,
902 guid_bytes_to_string(&result.credential_id).unwrap()
903 );
904 assert!(
905 result.extensions.prf.is_none(),
906 "PRF should not be evaluated"
907 );
908 }
909}