Skip to main content

bitwarden_fido/
authenticator.rs

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        // Insert the received UV to be able to return it later in check_user
127        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                // TODO(PM-30510): Even though we forward the extensions to the
156                // authenticator, they will not be processed until they are
157                // enabled in the authenticator configuration.
158                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        // Insert the received UV to be able to return it later in check_user
199        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                // TODO(PM-30510): Even though we forward the extensions to the
219                // authenticator, they will not be processed until they are
220                // enabled in the authenticator configuration.
221                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    /// Returns all Fido2 credentials that can be used for autofill, in a view
281    /// tailored for integration with OS autofill systems.
282    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        // This is just a wrapper around the actual implementation to allow for ? error handling
373        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            // Remove any that don't have Fido2 credentials
389            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            // When using the credential for authentication we have to ask the user to pick one.
400            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                // Store the selected credential for later use
413                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        // This is just a wrapper around the actual implementation to allow for ? error handling
456        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            // Get the previously selected cipher and add the new credential to it
466            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            // Store the updated credential for later use
477            this.authenticator
478                .selected_cipher
479                .lock()
480                .expect("Mutex is not poisoned")
481                .replace(selected.clone());
482
483            // Encrypt the updated cipher before sending it to the clients to be stored
484            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        // This is just a wrapper around the actual implementation to allow for ? error handling
530        async fn inner(
531            this: &mut CredentialStoreImpl<'_>,
532            cred: Passkey,
533        ) -> Result<(), InnerError> {
534            // Get the previously selected cipher and update the credential
535            let selected = this.authenticator.get_selected_credential()?;
536
537            // Check that the provided credential ID matches the selected credential
538            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            // Store the updated credential for later use
550            this.authenticator
551                .selected_cipher
552                .lock()
553                .expect("Mutex is not poisoned")
554                .replace(selected.clone());
555
556            // Encrypt the updated cipher before sending it to the clients to be stored
557            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    // Hardcoded P-256 private key in PKCS8 DER format for testing
757    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    /// TODO(PM-30510): Even though we forward the extensions to the
834    /// authenticator, we have disabled the configuration.
835    /// When we implement PRF, this test should be updated to test that PRF _is_
836    /// evaluated when PRF extension input is received.
837    #[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}