Skip to main content

bitwarden_fido/
device_auth_key.rs

1use bitwarden_api_api::models::{
2    AuthenticatorAttestationRawResponse, CredentialCreateOptions, PublicKeyCredentialType,
3    ResponseData, SecretVerificationRequestModel, UserVerificationRequirement,
4    WebAuthnCredentialCreateOptionsResponseModel, WebAuthnLoginCredentialCreateRequestModel,
5};
6use bitwarden_core::{
7    Client, key_management::SymmetricKeySlotId, mobile::KdfClient,
8    platform::SecretVerificationRequest,
9};
10use bitwarden_crypto::{HashPurpose, Kdf, RotateableKeySet};
11use chrono::{DateTime, Utc};
12use coset::{CborSerializable, CoseKey};
13use passkey::{
14    authenticator::{
15        DiscoverabilitySupport, StoreInfo, UiHint, UserCheck, extensions::HmacSecretConfig,
16    },
17    types::{
18        CredentialExtensions, Passkey, StoredHmacSecret,
19        crypto::sha256,
20        ctap2::{
21            self, Ctap2Code, Ctap2Error, StatusCode, VendorError,
22            extensions::{AuthenticatorPrfInputs, AuthenticatorPrfValues},
23            make_credential::Options,
24        },
25    },
26};
27use reqwest::Url;
28
29use crate::{
30    GetAssertionRequest, MakeCredentialResult, PublicKeyCredentialRpEntity,
31    PublicKeyCredentialUserEntity,
32    types::{
33        GetAssertionExtensionsOutput, PublicKeyCredentialDescriptor, PublicKeyCredentialParameters,
34        UV, WebAuthnEntityError,
35    },
36};
37
38/// A FIDO authenticator that uses the device auth key for its key material.
39pub struct DeviceAuthKeyAuthenticator<'a> {
40    /// The SDK client.
41    pub client: &'a Client,
42
43    /// Callbacks for storing and retrieving the device auth key during FIDO operations.
44    pub store: &'a mut dyn DeviceAuthKeyStore,
45}
46
47impl DeviceAuthKeyAuthenticator<'_> {
48    /// Create a device auth key by registering an unlock passkey and PRF keyset with the server.
49    /// The passkey private key and metadata will be stored on the device using the provided trait
50    /// implementation.
51    pub async fn create_device_auth_key(
52        &mut self,
53        client_name: String,
54        web_vault_url: String,
55        // TODO(PM-22681): We should define an enum to accept all the different
56        // SecretVerificationRequest input methods that the server can accept,
57        // and have a centralized place where the secret verification can be
58        // derived from the input.
59        //
60        // For now, we are hard-coding master password hash and OTP.
61        // When PM-22681 is complete, we can use that implementation here as a
62        // breaking change.
63        email: String,
64        secret_verification_request: SecretVerificationRequest,
65        kdf_params: Kdf,
66    ) -> Result<(), DeviceAuthKeyError> {
67        // Derive secret verification request
68        let config = self.client.internal.get_api_configurations();
69        let api_client = &config.api_client;
70
71        // Request WebAuthn credential creation options
72        let secret_verification_request_model = build_secret_verification_request(
73            &secret_verification_request,
74            email,
75            kdf_params,
76            &self.client.kdf(),
77        )
78        .await?;
79        let options_response = api_client
80            .web_authn_api()
81            .attestation_options(Some(secret_verification_request_model))
82            .await
83            .map_err(|err| {
84                tracing::error!(%err, "Failed to retrieve attestation options");
85                DeviceAuthKeyError::RetrieveRegistrationOptionsFailure
86            })?;
87        let WebAuthnCredentialCreateOptionsResponseModel { options, token, .. } = options_response;
88
89        // Convert creation options
90        let (default_rp_id, origin) = {
91            let url =
92                Url::parse(&web_vault_url).map_err(|_| DeviceAuthKeyError::InvalidWebVaultUrl)?;
93            let Some(default_rp_id) = url.host().map(|host| host.to_string()) else {
94                return Err(DeviceAuthKeyError::InvalidWebVaultUrl);
95            };
96            let origin = url.origin().ascii_serialization();
97            (default_rp_id, origin)
98        };
99        let (request, client_data_json) = convert_creation_options(options.as_ref(), default_rp_id, origin).map_err(|err| {
100            tracing::error!(%err, ?options, "Received invalid WebAuthn attestation options from server");
101            DeviceAuthKeyError::RetrieveRegistrationOptionsFailure
102        })?;
103
104        // Extract user/RP data from request before make_credential consumes it.
105        let rp_id = request.rp.id.clone();
106        let user_handle = request.user.id.to_vec();
107        let user_name = request.user.name.clone();
108        let user_display_name = request.user.display_name.clone();
109
110        // Create credential with passkey-rs, store record on device with given trait implementation
111        let store = DeviceAuthKeyStoreInternal { store: self.store };
112        let ui = DeviceAuthKeyUiInternal {};
113        let mut authenticator =
114            passkey::authenticator::Authenticator::new(super::AAGUID, store, ui)
115                .hmac_secret(HmacSecretConfig::new_with_uv_only().enable_on_make_credential());
116        let response = authenticator
117            .make_credential(request)
118            .await
119            .map_err(|status_code| {
120                tracing::error!(?status_code, "Failed to make FIDO credential");
121                if let StatusCode::Ctap2(Ctap2Code::Known(Ctap2Error::CredentialExcluded)) =
122                    status_code
123                {
124                    DeviceAuthKeyError::CredentialExcluded
125                } else {
126                    DeviceAuthKeyError::AuthenticatorFailure
127                }
128            })?;
129
130        // Convert response
131        let result: MakeCredentialResult = response
132            .try_into()
133            .map_err(|_| DeviceAuthKeyError::AuthenticatorFailure)?;
134
135        // Make PRF key set
136        let prf_result = result
137            .extensions
138            .prf
139            .and_then(|prf| prf.results)
140            .ok_or_else(|| {
141                tracing::error!("No PRF output received from authenticator response");
142                DeviceAuthKeyError::PrfFailure
143            })?
144            .first;
145        let prf_key =
146            bitwarden_crypto::derive_symmetric_key_from_prf(&prf_result).map_err(|err| {
147                tracing::error!(?err, "Failed to derive symmetric key from PRF output");
148                DeviceAuthKeyError::PrfFailure
149            })?;
150        let key_set = {
151            let ctx = self.client.internal.get_key_store().context();
152            RotateableKeySet::new(&ctx, &prf_key, SymmetricKeySlotId::User).map_err(|err| {
153                tracing::error!(%err, "Failed to gen/Conerate rotateable key set from PRF output");
154                DeviceAuthKeyError::PrfFailure
155            })?
156        };
157
158        // Send registration request to server
159        let credential_id = result.credential_id.clone();
160        let create_request = WebAuthnLoginCredentialCreateRequestModel {
161            device_response: Box::new(AuthenticatorAttestationRawResponse {
162                id: Some(result.credential_id.clone()),
163                raw_id: Some(result.credential_id),
164                r#type: Some(PublicKeyCredentialType::PublicKey),
165                response: Some(Box::new(ResponseData {
166                    attestation_object: Some(result.attestation_object),
167                    client_data_json: Some(client_data_json.into_bytes()),
168                })),
169                extensions: None,
170            }),
171            name: client_name,
172            token,
173            supports_prf: true,
174            encrypted_user_key: Some(key_set.encapsulated_downstream_key.to_string()),
175            encrypted_public_key: Some(key_set.encrypted_encapsulation_key.to_string()),
176            encrypted_private_key: Some(key_set.encrypted_decapsulation_key.to_string()),
177        };
178        let server_response = api_client
179            .web_authn_api()
180            .post(Some(create_request))
181            .await
182            .map_err(|err| {
183                tracing::error!(%err, "Failed to submit passkey and PRF key set to server");
184                DeviceAuthKeyError::SubmitRegistrationFailure
185            })?;
186        let record_identifier = server_response
187            .id
188            .ok_or(DeviceAuthKeyError::SubmitRegistrationFailure)?;
189
190        // Save metadata now that we have the server-assigned record identifier
191        let metadata = DeviceAuthKeyMetadata {
192            record_identifier,
193            creation_date: chrono::offset::Utc::now(),
194            credential_id,
195            rp_id,
196            user_handle,
197            user_name,
198            user_display_name,
199        };
200        self.store.create_metadata(metadata).await.map_err(|err| {
201            tracing::error!(%err, "Failed to save device auth key metadata");
202            err
203        })?;
204        Ok(())
205    }
206
207    /// Satisfy the given FIDO assertion `request` using the device auth key.
208    /// The device auth key will be looked up from the
209    /// [DeviceAuthKeyAuthenticator::store] provided in the initializer.
210    pub async fn assert_device_auth_key(
211        &mut self,
212        request: GetAssertionRequest,
213    ) -> Result<DeviceAuthKeyGetAssertionResult, DeviceAuthKeyError> {
214        // Convert request
215        let request = ctap2::get_assertion::Request {
216            rp_id: request.rp_id,
217            client_data_hash: request.client_data_hash.into(),
218            allow_list: request
219                .allow_list
220                .map(|l| {
221                    l.into_iter()
222                        .map(TryInto::try_into)
223                        .collect::<Result<Vec<_>, _>>()
224                        .map_err(|_| DeviceAuthKeyError::InvalidPublicKeyCredentialDescriptor)
225                })
226                .transpose()?,
227            extensions: request
228                .extensions
229                .map(passkey::types::ctap2::get_assertion::ExtensionInputs::from),
230            options: passkey::types::ctap2::make_credential::Options {
231                rk: request.options.rk,
232                up: true,
233                uv: match request.options.uv {
234                    UV::Discouraged => false,
235                    UV::Preferred => true,
236                    UV::Required => true,
237                },
238            },
239            pin_auth: None,
240            pin_protocol: None,
241        };
242
243        // Only use the requested credential ID if exactly one is specified.
244        let requested_cred_id = if let Some([cred]) = request.allow_list.as_deref() {
245            Some(cred.id.to_vec())
246        } else {
247            None
248        };
249
250        // Get signature
251        let store = DeviceAuthKeyStoreInternal { store: self.store };
252        let ui = DeviceAuthKeyUiInternal {};
253        let mut authenticator =
254            passkey::authenticator::Authenticator::new(super::AAGUID, store, ui)
255                .hmac_secret(HmacSecretConfig::new_with_uv_only().enable_on_make_credential());
256        let response = authenticator
257            .get_assertion(request)
258            .await
259            .map_err(|status_code| {
260                tracing::error!(?status_code, "Authenticator failed to assert credential");
261                DeviceAuthKeyError::AuthenticatorFailure
262            })?;
263
264        // Convert response
265        let authenticator_data = response.auth_data.to_vec();
266        // Credential ID may be omitted if there is only one credential ID
267        // specified in the allow list. We currently use device auth keys exclusively as a
268        // discoverable credentials, which means the allow list will always be
269        // empty and the credential ID should always be returned, but if that
270        // changes, we should attempt to read it from the allow list, just in case.
271        let credential_id = response
272            .credential
273            .map(|cred| cred.id.to_vec())
274            .or(requested_cred_id)
275            .ok_or(DeviceAuthKeyError::MissingCredentialId)?;
276        let extensions: GetAssertionExtensionsOutput = response.unsigned_extension_outputs.into();
277        let user_handle = response
278            .user
279            .map(|u| u.id.to_vec())
280            .ok_or(DeviceAuthKeyError::MissingUserHandle)?;
281        Ok(DeviceAuthKeyGetAssertionResult {
282            credential_id,
283            authenticator_data,
284            signature: response.signature.to_vec(),
285            user_handle,
286            extensions,
287        })
288    }
289
290    /// Delete the device auth key from the device and unregister it from the server.
291    pub async fn unregister_device_auth_key(
292        &mut self,
293        email: String,
294        secret_verification_request: SecretVerificationRequest,
295        kdf_params: Kdf,
296    ) -> Result<(), DeviceAuthKeyError> {
297        // Retrieve metadata before we delete it
298        let metadata = self
299            .store
300            .get_metadata()
301            .await?
302            .ok_or(DeviceAuthKeyError::MissingDeviceAuthKey)?;
303
304        self.store.delete_record_and_metadata().await?;
305
306        let record_id = metadata
307            .record_identifier
308            .parse::<uuid::Uuid>()
309            .map_err(|err| {
310                tracing::error!(%err, "Failed to parse record identifier as UUID");
311                DeviceAuthKeyError::InvalidRecordIdentifier
312            })?;
313
314        // Attempt to unregister the device auth key from the server.
315        let config = self.client.internal.get_api_configurations();
316        let api_client = &config.api_client;
317        let secret_verification_request_model = build_secret_verification_request(
318            &secret_verification_request,
319            email,
320            kdf_params,
321            &self.client.kdf(),
322        )
323        .await?;
324        api_client
325            .web_authn_api()
326            .delete(record_id, Some(secret_verification_request_model))
327            .await
328            .map_err(|err| {
329                tracing::error!(%err, "Failed to unregister device auth key from server");
330                DeviceAuthKeyError::UnregisterFailure
331            })?;
332
333        Ok(())
334    }
335}
336
337async fn build_secret_verification_request(
338    input: &SecretVerificationRequest,
339    email: String,
340    kdf_params: Kdf,
341    kdf_client: &KdfClient,
342) -> Result<SecretVerificationRequestModel, DeviceAuthKeyError> {
343    let master_password_hash = if let Some(master_password) = &input.master_password {
344        Some(
345            kdf_client
346                .hash_password(
347                    email,
348                    master_password.to_string(),
349                    kdf_params,
350                    HashPurpose::ServerAuthorization,
351                )
352                .await
353                .map_err(|_| DeviceAuthKeyError::MasterPasswordHash)?
354                .to_string(),
355        )
356    } else {
357        None
358    };
359
360    Ok(SecretVerificationRequestModel {
361        master_password_hash,
362        otp: input.otp.clone(),
363        auth_request_access_code: None,
364        secret: None,
365    })
366}
367
368/// Create a CTAP2 makeCredential request and clientDataJSON from the WebAuthn credential
369/// attestations options received from the server. Generates clientDataJSON from given origin and
370/// challenge, and injects the default RP ID if it's missing.
371fn convert_creation_options(
372    options: &CredentialCreateOptions,
373    default_rp_id: String,
374    origin: String,
375) -> Result<(passkey::types::ctap2::make_credential::Request, String), WebAuthnEntityError> {
376    let mut missing_fields = Vec::with_capacity(0);
377    if options.rp.is_none() {
378        missing_fields.push("rp".to_string());
379    }
380    if options.user.is_none() {
381        missing_fields.push("user".to_string());
382    }
383    if options.challenge.is_none() {
384        missing_fields.push("challenge".to_string());
385    }
386    if options.pub_key_cred_params.is_none() {
387        missing_fields.push("pubKeyCredParams".to_string());
388    }
389    if !missing_fields.is_empty() {
390        return Err(WebAuthnEntityError::MissingRequiredFields(missing_fields));
391    }
392
393    let CredentialCreateOptions {
394        rp: Some(rp),
395        user: Some(user),
396        challenge: Some(challenge),
397        pub_key_cred_params: Some(pub_key_cred_params),
398        authenticator_selection,
399        exclude_credentials,
400        extensions,
401        ..
402    } = options
403    else {
404        // these required fields should be manually checked above, so this shouldn't be reached.
405        unreachable!("Missing required fields on options");
406    };
407
408    let challenge_b64 = bitwarden_encoding::B64Url::from(challenge.as_ref())
409        .to_string()
410        .trim_end_matches('=')
411        .to_string();
412    let client_data_json = format!(
413        r#"{{"type":"webauthn.create","challenge":"{}","origin":"{}","crossOrigin":false}}"#,
414        challenge_b64, origin
415    );
416    let client_data_hash = passkey::types::crypto::sha256(client_data_json.as_bytes()).to_vec();
417
418    // Inject default RP ID
419    let mut rp = rp.clone();
420    rp.id.get_or_insert(default_rp_id);
421    let rp = TryInto::<PublicKeyCredentialRpEntity>::try_into(rp.as_ref())?.into();
422
423    let user_entity = TryInto::<PublicKeyCredentialUserEntity>::try_into(user.as_ref())?.into();
424    let pub_key_cred_params = pub_key_cred_params
425        .iter()
426        .map(|p| {
427            PublicKeyCredentialParameters::try_from(p).and_then(|ours| {
428                passkey::types::webauthn::PublicKeyCredentialParameters::try_from(ours)
429            })
430        })
431        .collect::<Result<Vec<passkey::types::webauthn::PublicKeyCredentialParameters>, _>>()?;
432    let exclude_list = exclude_credentials
433        .as_ref()
434        .map(|l| {
435            l.iter()
436                .map(|c| {
437                    let descriptor = PublicKeyCredentialDescriptor::try_from(c);
438
439                    descriptor.and_then(|c| c.try_into().map_err(WebAuthnEntityError::from))
440                })
441                .collect()
442        })
443        .transpose()?;
444    let authenticator_options = authenticator_selection
445        .as_ref()
446        .map(|o| Options {
447            rk: o.require_resident_key.unwrap_or_default(),
448            uv: !matches!(
449                o.user_verification,
450                Some(UserVerificationRequirement::Discouraged)
451            ),
452            up: true,
453        })
454        .unwrap_or_else(|| Options {
455            rk: false,
456            uv: true,
457            up: true,
458        });
459
460    // Note, we currently hard-code this value instead of getting it from the server.
461    let prf_input = AuthenticatorPrfInputs {
462        eval: Some(AuthenticatorPrfValues {
463            first: sha256("passwordless-login".as_bytes()),
464            second: None,
465        }),
466        eval_by_credential: None,
467    };
468
469    let request = passkey::types::ctap2::make_credential::Request {
470        client_data_hash: client_data_hash.into(),
471        rp,
472        user: user_entity,
473        pub_key_cred_params,
474        exclude_list,
475        options: authenticator_options,
476        extensions: extensions
477            .as_ref()
478            .map(|_| ctap2::make_credential::ExtensionInputs {
479                hmac_secret: None,
480                hmac_secret_mc: None,
481                prf: Some(prf_input),
482            }),
483        pin_auth: None,
484        pin_protocol: None,
485    };
486    Ok((request, client_data_json))
487}
488
489/// Fields corresponding to a WebAuthn [PublicKeyCredential][pub-key-cred]
490/// with an [AuthenticatorAssertionResponse][authenticator-assertion-response].
491///
492/// Similar to [GetAssertionResult][crate::GetAssertionResult], but without the reference to the
493/// vault cipher.
494///
495/// [pub-key-cred]: https://www.w3.org/TR/webauthn-3/#publickeycredential
496/// [authenticator-assertion-response]: https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
497#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
498pub struct DeviceAuthKeyGetAssertionResult {
499    /// ID for this credential, corresponding to [`PublicKeyCredential.rawId`][raw-id].
500    ///
501    /// [raw-id]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid
502    pub credential_id: Vec<u8>,
503
504    /// The authenticator data from the authenticator response.
505    pub authenticator_data: Vec<u8>,
506
507    /// Signature over the authenticator data.
508    pub signature: Vec<u8>,
509
510    /// The user handle returned from the authenticator.
511    pub user_handle: Vec<u8>,
512
513    /// Mix of CTAP unsigned extension output and WebAuthn client extension output.
514    /// Signed extensions can be retrieved from authenticator data.
515    pub extensions: GetAssertionExtensionsOutput,
516}
517
518/// The private key material for the device auth key.
519/// This should be stored separately from the metadata and gated behind
520/// user-verifying access control.
521#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
522pub struct DeviceAuthKeyRecord {
523    /// Credential ID for the WebAuthn credential.
524    pub credential_id: Vec<u8>,
525
526    /// Private key material, formatted as a COSE key.
527    pub key: Vec<u8>,
528
529    /// RP ID of the WebAuthn credential.
530    pub rp_id: String,
531
532    /// User ID for the WebAuthn credential.
533    pub user_id: Vec<u8>,
534
535    /// WebAuthn counter for the credential.
536    pub counter: Option<u32>,
537
538    /// HMAC Secret seed, which can also be used in WebAuthn PRF extension.
539    pub hmac_secret: Vec<u8>,
540}
541
542impl TryFrom<Passkey> for DeviceAuthKeyRecord {
543    type Error = DeviceAuthKeyError;
544    fn try_from(value: Passkey) -> Result<Self, Self::Error> {
545        let credential_id = value.credential_id.to_vec();
546        let key = value.key.to_vec().map_err(|err| {
547            tracing::error!(%err, "Failed to serialize COSE key to bytes.");
548            DeviceAuthKeyError::InvalidCoseKey
549        })?;
550        let user_id = value
551            .user_handle
552            .ok_or(DeviceAuthKeyError::MissingUserHandle)?
553            .to_vec();
554        let hmac_secret = value
555            .extensions
556            .hmac_secret
557            .as_ref()
558            .ok_or(DeviceAuthKeyError::MissingHmacSecret)?
559            .cred_with_uv
560            .clone();
561        Ok(DeviceAuthKeyRecord {
562            credential_id,
563            key,
564            rp_id: value.rp_id,
565            user_id,
566            counter: value.counter,
567            hmac_secret,
568        })
569    }
570}
571
572impl TryFrom<DeviceAuthKeyRecord> for Passkey {
573    type Error = DeviceAuthKeyError;
574    fn try_from(value: DeviceAuthKeyRecord) -> Result<Self, Self::Error> {
575        Ok(Passkey {
576            credential_id: value.credential_id.into(),
577            key: CoseKey::from_slice(&value.key).map_err(|err| {
578                tracing::error!(%err, "Failed to deserialize COSE key from bytes");
579                DeviceAuthKeyError::InvalidCoseKey
580            })?,
581            rp_id: value.rp_id,
582            user_handle: Some(value.user_id.into()),
583            counter: value.counter,
584            extensions: CredentialExtensions {
585                hmac_secret: Some(StoredHmacSecret {
586                    cred_with_uv: value.hmac_secret,
587                    cred_without_uv: None,
588                }),
589            },
590        })
591    }
592}
593
594/// The metadata for the device auth key useful for looking up whether the
595/// authenticator can satisfy a given request before invoking user-verifying
596/// access control.
597#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
598pub struct DeviceAuthKeyMetadata {
599    /// A unique identifier for the device auth key passkey.
600    /// This can be used as a unique identifier in OS autofill stores.
601    pub record_identifier: String,
602
603    /// Date the device auth key was created.
604    pub creation_date: DateTime<Utc>,
605
606    /// FIDO credential ID for the device auth key.
607    pub credential_id: Vec<u8>,
608
609    /// WebAuthn RP ID for the device auth key.
610    pub rp_id: String,
611
612    /// The login or username for user.
613    ///
614    /// Corresponds to the [user.name] in the original WebAuthn request that created the
615    /// credential.
616    pub user_name: String,
617
618    /// The ID for the user.
619    ///
620    /// Corresponds to the [user.id] in the original WebAuthn request that created the credential.
621    pub user_handle: Vec<u8>,
622
623    /// The display name for the user
624    ///
625    /// Corresponds to the [user.displayName] in the original WebAuthn request that created the
626    /// credential.
627    pub user_display_name: String,
628}
629
630/// Errors related to processing the device auth key.
631#[derive(Debug, thiserror::Error)]
632#[cfg_attr(feature = "uniffi", derive(uniffi::Error), uniffi(flat_error))]
633pub enum DeviceAuthKeyError {
634    /// Authenticator failed to produce a valid response.
635    #[error("The authenticator failed to produce a valid response")]
636    AuthenticatorFailure,
637
638    /// Failed to convert between Rust types.
639    #[error("Failed to convert between Rust types")]
640    Conversion,
641
642    /// Credential excluded.
643    #[error("The existing device auth key is already registered on the server.")]
644    CredentialExcluded,
645
646    /// The record identifier stored in metadata is not a valid UUID.
647    #[error("The record identifier is not a valid UUID")]
648    InvalidRecordIdentifier,
649
650    /// Invalid Web Vault URL specified.
651    #[error("Invalid Web Vault URL specified")]
652    InvalidWebVaultUrl,
653
654    /// No device auth key exists on this device.
655    #[error("No device auth key exists on this device")]
656    MissingDeviceAuthKey,
657
658    /// Failed to unregister device auth key from server.
659    #[error("Failed to unregister device auth key from server")]
660    UnregisterFailure,
661
662    /// Failed to de-/serialize COSE key data.
663    #[error("Failed to de-/serialize COSE key data")]
664    InvalidCoseKey,
665
666    /// An invalid public key credential descriptor was passed in the allow list.
667    #[error("An invalid public key credential descriptor was passed in the allow list")]
668    InvalidPublicKeyCredentialDescriptor,
669
670    /// A master password hash could not be generated for the given master password.
671    #[error("A master password hash could not be generated for the given master password")]
672    MasterPasswordHash,
673
674    /// Credential ID was not returned in the response and was not passed in the request.
675    #[error(
676        "No credential ID was returned in the response nor was a single credential ID passed in the request"
677    )]
678    MissingCredentialId,
679
680    /// No HMAC secret was returned with the credential.
681    #[error("No HMAC secret was returned with the credential")]
682    MissingHmacSecret,
683
684    /// User handle was not returned in the response.
685    #[error("User handle was not returned in the response")]
686    MissingUserHandle,
687
688    /// Feature is not yet implemented.
689    #[error("Feature is not yet implemented")]
690    NotImplemented,
691
692    /// Failed to retrieve the registration options from the server.
693    #[error("Failed to retrieve the registration options from the server")]
694    RetrieveRegistrationOptionsFailure,
695
696    /// Failed to generate rotateable key set from PRF output.
697    #[error("Failed to generate rotateable key set from PRF output")]
698    PrfFailure,
699
700    /// Failed to submit registration request to the server.
701    #[error("Failed to submit registration request to the server")]
702    SubmitRegistrationFailure,
703
704    /// User cancelled the operation.
705    #[error("User cancelled the operation")]
706    UserCancelled,
707
708    /// An unknown error occurred.
709    #[error("An unknown error occurred")]
710    Unknown {
711        /// Reason for the error.
712        reason: String,
713    },
714}
715
716// Need to implement this From<> impl in order to handle unexpected callback errors.  See the
717// following page in the Uniffi user guide:
718// <https://mozilla.github.io/uniffi-rs/foreign_traits.html#error-handling>
719#[cfg(feature = "uniffi")]
720impl From<uniffi::UnexpectedUniFFICallbackError> for DeviceAuthKeyError {
721    fn from(e: uniffi::UnexpectedUniFFICallbackError) -> Self {
722        Self::Unknown { reason: e.reason }
723    }
724}
725
726/// A trait used to interact with the device auth key data on the device.
727#[async_trait::async_trait]
728pub trait DeviceAuthKeyStore: Send + Sync {
729    /// Create a record (private key material).
730    ///
731    /// The record should be stored in device-bound storage and protected with user-verifying access
732    /// controls.
733    async fn create_record(
734        &mut self,
735        record: DeviceAuthKeyRecord,
736    ) -> Result<(), DeviceAuthKeyError>;
737
738    /// Create metadata for the device auth key.
739    ///
740    /// The metadata should be stored separately without access controls that require UI.
741    async fn create_metadata(
742        &mut self,
743        metadata: DeviceAuthKeyMetadata,
744    ) -> Result<(), DeviceAuthKeyError>;
745
746    /// Retrieve the device auth key metadata.
747    async fn get_metadata(&self) -> Result<Option<DeviceAuthKeyMetadata>, DeviceAuthKeyError>;
748
749    /// Retrieve the device auth key private key material.
750    async fn get_record(&self) -> Result<Option<DeviceAuthKeyRecord>, DeviceAuthKeyError>;
751
752    /// Delete the device auth key (both the record and metadata) from the device.
753    async fn delete_record_and_metadata(&mut self) -> Result<(), DeviceAuthKeyError>;
754}
755
756struct DeviceAuthKeyStoreInternal<'a> {
757    store: &'a mut dyn DeviceAuthKeyStore,
758}
759
760#[async_trait::async_trait]
761impl passkey::authenticator::CredentialStore for DeviceAuthKeyStoreInternal<'_> {
762    type PasskeyItem = DeviceAuthKeyRecord;
763
764    async fn find_credentials(
765        &self,
766        _ids: Option<&[passkey::types::webauthn::PublicKeyCredentialDescriptor]>,
767        _rp_id: &str,
768        _user_handle: Option<&[u8]>,
769    ) -> Result<Vec<Self::PasskeyItem>, StatusCode> {
770        match self.store.get_record().await {
771            Ok(Some(key)) => Ok(vec![key]),
772            Ok(None) => return Ok(vec![]),
773            Err(_) => Err(VendorError::try_from(0xf0)
774                .expect("valid vendor error")
775                .into()),
776        }
777    }
778
779    async fn save_credential(
780        &mut self,
781        cred: Passkey,
782        _user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
783        _rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
784        _options: passkey::types::ctap2::get_assertion::Options,
785    ) -> Result<(), StatusCode> {
786        let record = cred
787            .try_into()
788            .map_err(|_| VendorError::try_from(0xf0).expect("valid vendor error"))?;
789
790        self.store.create_record(record).await.map_err(|_| {
791            StatusCode::from(VendorError::try_from(0xf0).expect("valid vendor error"))
792        })?;
793        Ok(())
794    }
795
796    async fn update_credential(&mut self, _cred: Passkey) -> Result<(), StatusCode> {
797        // This is only used to update the counter, which we're not currently using.
798        tracing::warn!("called update_credential() on device auth key, which is not supported");
799        Err(StatusCode::Ctap2(
800            VendorError::try_from(0xF3)
801                .expect("valid vendor error")
802                .into(),
803        ))
804    }
805
806    async fn get_info(&self) -> StoreInfo {
807        StoreInfo {
808            discoverability: DiscoverabilitySupport::Full,
809        }
810    }
811}
812
813struct DeviceAuthKeyUiInternal {}
814
815#[async_trait::async_trait]
816impl passkey::authenticator::UserValidationMethod for DeviceAuthKeyUiInternal {
817    type PasskeyItem = DeviceAuthKeyRecord;
818
819    async fn check_user<'a>(
820        &self,
821        _hint: UiHint<'a, Self::PasskeyItem>,
822        _presence: bool,
823        _verification: bool,
824    ) -> Result<UserCheck, Ctap2Error> {
825        // The DeviceAuthKeyStore trait should store with user-verifying access
826        // control, so we assume that user presence and verification has been
827        // achieved.
828        Ok(UserCheck {
829            presence: true,
830            verification: true,
831        })
832    }
833
834    fn is_presence_enabled(&self) -> bool {
835        true
836    }
837
838    fn is_verification_enabled(&self) -> Option<bool> {
839        Some(true)
840    }
841}