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 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    /// Returns all Fido2 credentials that can be used for autofill, in a view
283    /// tailored for integration with OS autofill systems.
284    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        // This is just a wrapper around the actual implementation to allow for ? error handling
377        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            // Remove any that don't have Fido2 credentials
393            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            // When using the credential for authentication we have to ask the user to pick one.
406            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                // Store the selected credential for later use
419                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        // This is just a wrapper around the actual implementation to allow for ? error handling
465        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            // Get the previously selected cipher and add the new credential to it
475            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            // Store the updated credential for later use
488            this.authenticator
489                .selected_cipher
490                .lock()
491                .expect("Mutex is not poisoned")
492                .replace(selected.clone());
493
494            // Encrypt the updated cipher before sending it to the clients to be stored
495            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        // This is just a wrapper around the actual implementation to allow for ? error handling
541        async fn inner(
542            this: &mut CredentialStoreImpl<'_>,
543            cred: Passkey,
544        ) -> Result<(), InnerError> {
545            // Get the previously selected cipher and update the credential
546            let selected = this.authenticator.get_selected_credential()?;
547
548            // Check that the provided credential ID matches the selected credential
549            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            // Store the updated credential for later use
563            this.authenticator
564                .selected_cipher
565                .lock()
566                .expect("Mutex is not poisoned")
567                .replace(selected.clone());
568
569            // Encrypt the updated cipher before sending it to the clients to be stored
570            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    // Hardcoded P-256 private key in PKCS8 DER format for testing
773    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    /// TODO(PM-30510): Even though we forward the extensions to the
850    /// authenticator, we have disabled the configuration.
851    /// When we implement PRF, this test should be updated to test that PRF _is_
852    /// evaluated when PRF extension input is received.
853    #[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}