Skip to main content

bitwarden_fido/
types.rs

1use std::{borrow::Cow, collections::HashMap};
2
3use bitwarden_crypto::CryptoError;
4use bitwarden_encoding::{B64Url, NotB64UrlEncodedError};
5use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView};
6use passkey::types::webauthn::UserVerificationRequirement;
7use reqwest::Url;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use super::{
12    InvalidGuidError, SelectedCredential, UnknownEnumError, Verification,
13    get_enum_from_string_name, string_to_guid_bytes,
14};
15
16#[allow(missing_docs)]
17#[derive(Serialize, Deserialize, Debug, Clone)]
18#[serde(rename_all = "camelCase", deny_unknown_fields)]
19#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
20pub struct Fido2CredentialAutofillView {
21    pub credential_id: Vec<u8>,
22    pub cipher_id: uuid::Uuid,
23    pub rp_id: String,
24    pub user_name_for_ui: Option<String>,
25    pub user_handle: Vec<u8>,
26    /// Indicates if this credential uses a signature counter (legacy passkeys).
27    /// When true, mobile clients must sync before authentication to ensure
28    /// counter values are current. Modern passkeys (counter = 0) can work offline.
29    pub has_counter: bool,
30}
31
32trait NoneWhitespace {
33    /// Convert only whitespace to None
34    fn none_whitespace(&self) -> Option<String>;
35}
36
37impl NoneWhitespace for String {
38    fn none_whitespace(&self) -> Option<String> {
39        match self.trim() {
40            "" => None,
41            s => Some(s.to_owned()),
42        }
43    }
44}
45
46impl NoneWhitespace for Option<String> {
47    fn none_whitespace(&self) -> Option<String> {
48        self.as_ref().and_then(|s| s.none_whitespace())
49    }
50}
51
52#[allow(missing_docs)]
53#[derive(Debug, Error)]
54pub enum Fido2CredentialAutofillViewError {
55    #[error("Autofill credentials can only be created from existing ciphers that have a cipher id")]
56    MissingCipherId,
57
58    #[error(transparent)]
59    InvalidGuid(#[from] InvalidGuidError),
60
61    #[error(transparent)]
62    Crypto(#[from] CryptoError),
63
64    #[error(transparent)]
65    Base64Decode(#[from] NotB64UrlEncodedError),
66}
67
68impl Fido2CredentialAutofillView {
69    #[allow(missing_docs)]
70    pub fn from_cipher_view(
71        cipher: &CipherView,
72    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
73        let credentials = cipher.get_fido2_credentials();
74
75        credentials
76            .iter()
77            .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
78                c.user_handle
79                    .as_ref()
80                    .map(|u| B64Url::try_from(u.as_str()))
81                    .map(|user_handle| {
82                        Ok(Fido2CredentialAutofillView {
83                            credential_id: string_to_guid_bytes(&c.credential_id)?,
84                            cipher_id: cipher
85                                .id
86                                .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
87                                .into(),
88                            rp_id: c.rp_id.clone(),
89                            user_handle: user_handle?.into_bytes(),
90                            user_name_for_ui: c
91                                .user_name
92                                .none_whitespace()
93                                .or(c.user_display_name.none_whitespace())
94                                .or(cipher
95                                    .login
96                                    .as_ref()
97                                    .and_then(|l| l.username.none_whitespace()))
98                                .or(cipher.name.none_whitespace()),
99                            has_counter: Self::has_signature_counter(&c.counter),
100                        })
101                    })
102            })
103            .collect()
104    }
105
106    #[allow(missing_docs)]
107    pub fn from_cipher_list_view(
108        cipher: &CipherListView,
109    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
110        match &cipher.r#type {
111            CipherListViewType::Login(LoginListView {
112                fido2_credentials: Some(fido2_credentials),
113                username,
114                ..
115            }) => fido2_credentials
116                .iter()
117                .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
118                    c.user_handle
119                        .as_ref()
120                        .map(|u| B64Url::try_from(u.as_str()))
121                        .map(|user_handle| {
122                            Ok(Fido2CredentialAutofillView {
123                                credential_id: string_to_guid_bytes(&c.credential_id)?,
124                                cipher_id: cipher
125                                    .id
126                                    .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
127                                    .into(),
128                                rp_id: c.rp_id.clone(),
129                                user_handle: user_handle?.into_bytes(),
130                                user_name_for_ui: c
131                                    .user_name
132                                    .none_whitespace()
133                                    .or(c.user_display_name.none_whitespace())
134                                    .or(username.none_whitespace())
135                                    .or(cipher.name.none_whitespace()),
136                                has_counter: Self::has_signature_counter(&c.counter),
137                            })
138                        })
139                })
140                .collect(),
141            _ => Ok(vec![]),
142        }
143    }
144
145    fn has_signature_counter(str: &String) -> bool {
146        str.none_whitespace()
147            .is_some_and(|counter_str| counter_str.parse::<u64>().is_ok_and(|counter| counter > 0))
148    }
149}
150
151#[allow(missing_docs)]
152#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
153pub struct PublicKeyCredentialRpEntity {
154    pub id: String,
155    pub name: Option<String>,
156}
157
158impl From<PublicKeyCredentialRpEntity>
159    for passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity
160{
161    fn from(value: PublicKeyCredentialRpEntity) -> Self {
162        Self {
163            id: value.id,
164            name: value.name,
165        }
166    }
167}
168
169impl TryFrom<&bitwarden_api_api::models::PublicKeyCredentialRpEntity>
170    for PublicKeyCredentialRpEntity
171{
172    type Error = WebAuthnEntityError;
173    fn try_from(
174        value: &bitwarden_api_api::models::PublicKeyCredentialRpEntity,
175    ) -> Result<Self, Self::Error> {
176        let id = value
177            .id
178            .as_ref()
179            .ok_or(WebAuthnEntityError::InvalidRpId)?
180            .clone();
181        Ok(Self {
182            id,
183            name: value.name.clone(),
184        })
185    }
186}
187
188#[allow(missing_docs)]
189#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
190pub struct PublicKeyCredentialUserEntity {
191    pub id: Vec<u8>,
192    pub display_name: String,
193    pub name: String,
194}
195
196impl From<PublicKeyCredentialUserEntity>
197    for passkey::types::webauthn::PublicKeyCredentialUserEntity
198{
199    fn from(value: PublicKeyCredentialUserEntity) -> Self {
200        Self {
201            id: value.id.into(),
202            name: value.name,
203            display_name: value.display_name,
204        }
205    }
206}
207
208impl TryFrom<&bitwarden_api_api::models::Fido2User> for PublicKeyCredentialUserEntity {
209    type Error = WebAuthnEntityError;
210    fn try_from(value: &bitwarden_api_api::models::Fido2User) -> Result<Self, Self::Error> {
211        let mut missing_fields = Vec::with_capacity(0);
212        if value.id.is_none() {
213            missing_fields.push("id".to_string())
214        }
215        if value.display_name.is_none() {
216            missing_fields.push("displayName".to_string())
217        }
218        if value.name.is_none() {
219            missing_fields.push("name".to_string())
220        }
221        if missing_fields.is_empty() {
222            Ok(Self {
223                id: value.id.as_ref().expect("checked manually").clone(),
224                display_name: value
225                    .display_name
226                    .as_ref()
227                    .expect("checked manually")
228                    .clone(),
229                name: value.name.as_ref().expect("checked manually").clone(),
230            })
231        } else {
232            Err(WebAuthnEntityError::MissingRequiredFields(missing_fields))
233        }
234    }
235}
236
237#[derive(Debug, Error)]
238pub enum WebAuthnEntityError {
239    #[error("Missing required fields: {0:?}")]
240    MissingRequiredFields(Vec<String>),
241
242    #[error("Invalid RP ID")]
243    InvalidRpId,
244
245    #[error("Invalid public key credential parameters")]
246    InvalidPublicKeyCredentialParameters(#[from] PublicKeyCredentialParametersError),
247
248    #[error("Unknown type")]
249    UnknownEnum(#[from] UnknownEnumError),
250}
251
252#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
253pub struct PublicKeyCredentialParameters {
254    pub ty: String,
255    pub alg: i64,
256}
257
258impl TryFrom<&bitwarden_api_api::models::PubKeyCredParam> for PublicKeyCredentialParameters {
259    type Error = PublicKeyCredentialParametersError;
260    fn try_from(value: &bitwarden_api_api::models::PubKeyCredParam) -> Result<Self, Self::Error> {
261        let ty = value
262            .r#type
263            .as_ref()
264            .ok_or(PublicKeyCredentialParametersError::UnknownEnum(
265                UnknownEnumError,
266            ))?
267            .to_string();
268        let alg = value
269            .alg
270            .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?
271            .as_i64();
272        Ok(Self { ty, alg })
273    }
274}
275
276#[derive(Debug, Error)]
277pub enum PublicKeyCredentialParametersError {
278    #[error("Invalid algorithm")]
279    InvalidAlgorithm,
280
281    #[error("Unknown type")]
282    UnknownEnum(#[from] UnknownEnumError),
283}
284
285impl TryFrom<PublicKeyCredentialParameters>
286    for passkey::types::webauthn::PublicKeyCredentialParameters
287{
288    type Error = PublicKeyCredentialParametersError;
289
290    fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
291        use coset::iana::EnumI64;
292        Ok(Self {
293            ty: get_enum_from_string_name(&value.ty)?,
294            alg: coset::iana::Algorithm::from_i64(value.alg)
295                .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
296        })
297    }
298}
299
300#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
301pub struct PublicKeyCredentialDescriptor {
302    pub ty: String,
303    pub id: Vec<u8>,
304    pub transports: Option<Vec<String>>,
305}
306
307impl TryFrom<PublicKeyCredentialDescriptor>
308    for passkey::types::webauthn::PublicKeyCredentialDescriptor
309{
310    type Error = UnknownEnumError;
311
312    fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
313        Ok(Self {
314            ty: get_enum_from_string_name(&value.ty)?,
315            id: value.id.into(),
316            transports: value
317                .transports
318                .map(|tt| {
319                    tt.into_iter()
320                        .map(|t| get_enum_from_string_name(&t))
321                        .collect::<Result<Vec<_>, Self::Error>>()
322                })
323                .transpose()?,
324        })
325    }
326}
327
328impl TryFrom<&PublicKeyCredentialDescriptor>
329    for passkey::types::webauthn::PublicKeyCredentialDescriptor
330{
331    type Error = UnknownEnumError;
332
333    fn try_from(value: &PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
334        Ok(Self {
335            ty: get_enum_from_string_name(&value.ty)?,
336            id: value.id.clone().into(),
337            transports: value
338                .transports
339                .as_ref()
340                .map(|tt| {
341                    tt.iter()
342                        .map(|t| get_enum_from_string_name(t))
343                        .collect::<Result<Vec<_>, Self::Error>>()
344                })
345                .transpose()?,
346        })
347    }
348}
349
350impl TryFrom<&bitwarden_api_api::models::PublicKeyCredentialDescriptor>
351    for PublicKeyCredentialDescriptor
352{
353    type Error = WebAuthnEntityError;
354    fn try_from(
355        value: &bitwarden_api_api::models::PublicKeyCredentialDescriptor,
356    ) -> Result<Self, Self::Error> {
357        let ty = value
358            .r#type
359            .as_ref()
360            .ok_or(WebAuthnEntityError::UnknownEnum(UnknownEnumError))?
361            .to_string();
362        let id = value
363            .id
364            .as_ref()
365            .ok_or(WebAuthnEntityError::MissingRequiredFields(vec![
366                "id".to_string(),
367            ]))?
368            .clone();
369        let transports = value
370            .transports
371            .as_ref()
372            .map(|l| l.iter().map(|t| t.to_string()).collect());
373        Ok(Self { ty, id, transports })
374    }
375}
376
377#[allow(missing_docs)]
378#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
379pub struct MakeCredentialRequest {
380    pub client_data_hash: Vec<u8>,
381    pub rp: PublicKeyCredentialRpEntity,
382    pub user: PublicKeyCredentialUserEntity,
383    pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
384    pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
385    pub options: Options,
386
387    /// WebAuthn client extension inputs for credential creation requests.
388    ///
389    /// Cf. <https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialcreationoptions-extensions>.
390    pub extensions: Option<MakeCredentialExtensionsInput>,
391}
392
393/// Fields corresponding to a WebAuthn [PublicKeyCredential][pub-key-cred]
394/// with an [AuthenticatorAttestationResponse][authenticator-attestation-response].
395///
396/// [pub-key-cred]: https://www.w3.org/TR/webauthn-3/#publickeycredential
397/// [authenticator-attestation-response]: https://www.w3.org/TR/webauthn-3/#authenticatorattestationresponse
398#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
399pub struct MakeCredentialResult {
400    /// The authenticator data extracted from within the
401    /// [`attestation_object`][Self::attestation_object].
402    pub authenticator_data: Vec<u8>,
403
404    /// [WebAuthn attestation object][webauthn-attestation-object] for the
405    /// authenticator response containing both the authenticator data and
406    /// attestation statement for the credential.
407    ///
408    /// [webauthn-attestation-object]: https://www.w3.org/TR/webauthn-3/#dom-authenticatorattestationresponse-attestationobject
409    pub attestation_object: Vec<u8>,
410
411    /// ID for this credential, corresponding to [PublicKeyCredential.rawId][raw-id].
412    ///
413    /// [raw-id]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid
414    pub credential_id: Vec<u8>,
415
416    /// Mix of CTAP [unsigned extension output][unsigned-extensions] and
417    /// [WebAuthn client extensions][webauthn-client-extensions] output returned
418    /// by the authenticator.
419    ///
420    /// [unsigned-extensions]: https://www.w3.org/TR/webauthn-3/#unsigned-extension-outputs
421    /// [webauthn-client-extensions]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-clientextensionsresults-slot
422    pub extensions: MakeCredentialExtensionsOutput,
423}
424
425impl TryFrom<passkey::types::ctap2::make_credential::Response> for MakeCredentialResult {
426    type Error = WebAuthnEntityError;
427
428    fn try_from(
429        value: passkey::types::ctap2::make_credential::Response,
430    ) -> Result<Self, Self::Error> {
431        let authenticator_data = value.auth_data.to_vec();
432        let attestation_object = value.as_webauthn_bytes().to_vec();
433        let attested_credential_data = value.auth_data.attested_credential_data.ok_or(
434            WebAuthnEntityError::MissingRequiredFields(vec!["attestedCredentialData".to_string()]),
435        )?;
436        let credential_id = attested_credential_data.credential_id().to_vec();
437        let extensions: MakeCredentialExtensionsOutput = value.unsigned_extension_outputs.into();
438        Ok(MakeCredentialResult {
439            authenticator_data,
440            attestation_object,
441            credential_id,
442            extensions,
443        })
444    }
445}
446
447/// WebAuthn extension input for WebAuthn registration extensions.
448#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
449#[derive(Debug, Default)]
450pub struct MakeCredentialExtensionsInput {
451    /// PRF input for WebAuthn registration request.
452    pub prf: Option<MakeCredentialPrfInput>,
453}
454
455impl From<MakeCredentialExtensionsInput>
456    for passkey::types::ctap2::make_credential::ExtensionInputs
457{
458    fn from(value: MakeCredentialExtensionsInput) -> Self {
459        Self {
460            hmac_secret: None,
461            hmac_secret_mc: None,
462            prf: value
463                .prf
464                .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
465        }
466    }
467}
468
469impl From<bitwarden_api_api::models::AuthenticationExtensionsClientInputs>
470    for MakeCredentialExtensionsInput
471{
472    fn from(_value: bitwarden_api_api::models::AuthenticationExtensionsClientInputs) -> Self {
473        MakeCredentialExtensionsInput {
474            // The server doesn't support sending the PRF extension, but at this
475            // time we only use it for the device auth key, which uses a static,
476            // hard-coded value, so set it to `None` here.
477            prf: None,
478        }
479    }
480}
481
482/// WebAuthn extension output for registration extensions.
483#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
484#[derive(Debug)]
485pub struct MakeCredentialExtensionsOutput {
486    /// PRF output for registration extensions.
487    pub prf: Option<MakeCredentialPrfOutput>,
488}
489
490impl From<Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>>
491    for MakeCredentialExtensionsOutput
492{
493    fn from(
494        value: Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>,
495    ) -> Self {
496        if let Some(ext) = value {
497            MakeCredentialExtensionsOutput::from(ext)
498        } else {
499            MakeCredentialExtensionsOutput { prf: None }
500        }
501    }
502}
503
504impl From<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>
505    for MakeCredentialExtensionsOutput
506{
507    fn from(value: passkey::types::ctap2::make_credential::UnsignedExtensionOutputs) -> Self {
508        let prf = value.prf.map(|prf| MakeCredentialPrfOutput {
509            enabled: prf.enabled,
510            results: prf.results.map(|results| results.into()),
511        });
512        MakeCredentialExtensionsOutput { prf }
513    }
514}
515
516/// WebAuthn PRF extension input for use during registration.
517#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
518#[derive(Debug)]
519pub struct MakeCredentialPrfInput {
520    /// PRF inputs.
521    pub eval: Option<PrfInputValues>,
522}
523
524impl From<MakeCredentialPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
525    fn from(value: MakeCredentialPrfInput) -> Self {
526        Self {
527            eval: value.eval.map(|v| v.into()),
528            eval_by_credential: None,
529        }
530    }
531}
532
533/// WebAuthn PRF extension output used during registration.
534#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
535#[derive(Debug)]
536pub struct MakeCredentialPrfOutput {
537    /// Whether PRF is successfully processed for the newly created credential.
538    pub enabled: bool,
539
540    /// PRF outputs.
541    pub results: Option<PrfOutputValues>,
542}
543
544#[allow(missing_docs)]
545/// Type representing data from WebAuthn's
546/// [`PublicKeyCredentialRequestOptions`][pubkey-cred-request-options].
547///
548/// [pubkey-cred-request-options]: https://www.w3.org/TR/webauthn-3/#dictdef-publickeycredentialrequestoptions
549#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
550pub struct GetAssertionRequest {
551    /// The RP ID for the request used to select credentials.
552    pub rp_id: String,
553
554    /// Hash of the clientDataJSON for the request.
555    pub client_data_hash: Vec<u8>,
556
557    /// Credential IDs known to the RP. If specified, it is a list of
558    /// credentials to filter by, ordered from most to least preferable. If
559    /// empty, only discoverable credentials will be returned.
560    pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
561
562    pub options: Options,
563
564    /// WebAuthn extension input for use during assertion.
565    pub extensions: Option<GetAssertionExtensionsInput>,
566}
567
568/// Fields corresponding to a WebAuthn [PublicKeyCredential][pub-key-cred]
569/// with an [AuthenticatorAssertionResponse][authenticator-assertion-response].
570///
571/// [pub-key-cred]: https://www.w3.org/TR/webauthn-3/#publickeycredential
572/// [authenticator-assertion-response]: https://www.w3.org/TR/webauthn-3/#authenticatorassertionresponse
573#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
574pub struct GetAssertionResult {
575    /// ID for this credential, corresponding to [PublicKeyCredential.rawId][raw-id].
576    ///
577    /// [raw-id]: https://www.w3.org/TR/webauthn-3/#dom-publickeycredential-rawid
578    pub credential_id: Vec<u8>,
579
580    /// The authenticator data from the authenticator response.
581    pub authenticator_data: Vec<u8>,
582
583    /// Signature over the authenticator data.
584    pub signature: Vec<u8>,
585
586    /// The user handle returned from the authenticator.
587    pub user_handle: Vec<u8>,
588
589    /// A reference to the Bitwarden cipher for the selected credential.
590    pub selected_credential: SelectedCredential,
591
592    /// Mix of CTAP unsigned extension output and WebAuthn client extension output.
593    /// Signed extensions can be retrieved from authenticator data.
594    pub extensions: GetAssertionExtensionsOutput,
595}
596
597/// WebAuthn extension input for WebAuthn authentication extensions.
598#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
599#[derive(Debug)]
600pub struct GetAssertionExtensionsInput {
601    /// PRF input for the authentication ceremony.
602    pub prf: Option<GetAssertionPrfInput>,
603}
604
605impl From<GetAssertionExtensionsInput> for passkey::types::ctap2::get_assertion::ExtensionInputs {
606    fn from(value: GetAssertionExtensionsInput) -> Self {
607        Self {
608            hmac_secret: None,
609            prf: value
610                .prf
611                .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
612        }
613    }
614}
615
616/// WebAuthn extension output of an authentication ceremony.
617#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
618#[derive(Debug)]
619pub struct GetAssertionExtensionsOutput {
620    /// PRF output for an authentication ceremony.
621    pub prf: Option<GetAssertionPrfOutput>,
622}
623
624impl From<Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>>
625    for GetAssertionExtensionsOutput
626{
627    fn from(value: Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>) -> Self {
628        if let Some(value) = value {
629            value.into()
630        } else {
631            Self { prf: None }
632        }
633    }
634}
635
636impl From<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>
637    for GetAssertionExtensionsOutput
638{
639    fn from(value: passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs) -> Self {
640        let prf = value.prf.map(|prf| GetAssertionPrfOutput {
641            results: prf.results.into(),
642        });
643        GetAssertionExtensionsOutput { prf }
644    }
645}
646
647/// Input for WebAuthn PRF extension during authentication ceremonies.
648#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
649#[derive(Debug)]
650pub struct GetAssertionPrfInput {
651    /// A PRF input to use for authentication. If a map of credential IDs to PRF
652    /// inputs is specified in [`Self::eval_by_credential`] along with this
653    /// value, the extension will fallback to this
654    /// value if the returned credential ID is not contained in the map.
655    pub eval: Option<PrfInputValues>,
656
657    /// A map of credential IDs to PRF input for a set of credentials specified in the
658    /// [`GetAssertionRequest::allow_list`] field of the request. If a key of
659    /// this map does not exist in the allow list, the extension will fail.
660    pub eval_by_credential: Option<HashMap<Vec<u8>, PrfInputValues>>,
661}
662
663impl From<GetAssertionPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
664    fn from(value: GetAssertionPrfInput) -> Self {
665        let eval_by_credential = if let Some(values) = value.eval_by_credential {
666            let map: HashMap<
667                passkey::types::Bytes,
668                passkey::types::ctap2::extensions::AuthenticatorPrfValues,
669            > = values
670                .into_iter()
671                .map(|(k, v)| (k.into(), v.into()))
672                .collect();
673            Some(map)
674        } else {
675            None
676        };
677        Self {
678            eval: value.eval.map(|v| v.into()),
679            eval_by_credential,
680        }
681    }
682}
683
684/// WebAuthn PRF extension output during an authentication ceremony.
685#[allow(missing_docs)]
686#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
687#[derive(Debug)]
688pub struct GetAssertionPrfOutput {
689    /// The PRF output for the ceremony.
690    pub results: PrfOutputValues,
691}
692
693#[allow(missing_docs)]
694#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
695pub struct Options {
696    pub rk: bool,
697    pub uv: UV,
698}
699
700impl From<super::CheckUserOptions> for Options {
701    fn from(value: super::CheckUserOptions) -> Self {
702        Self {
703            rk: value.require_presence,
704            uv: value.require_verification.into(),
705        }
706    }
707}
708
709impl From<Options> for super::CheckUserOptions {
710    fn from(value: Options) -> Self {
711        Self {
712            require_presence: value.rk,
713            require_verification: value.uv.into(),
714        }
715    }
716}
717
718#[derive(Eq, PartialEq, Clone, Copy)]
719#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
720pub enum UV {
721    Discouraged,
722    Preferred,
723    Required,
724}
725
726impl From<UV> for Verification {
727    fn from(value: UV) -> Self {
728        match value {
729            UV::Discouraged => Verification::Discouraged,
730            UV::Preferred => Verification::Preferred,
731            UV::Required => Verification::Required,
732        }
733    }
734}
735
736impl From<bitwarden_api_api::models::UserVerificationRequirement> for UV {
737    fn from(value: bitwarden_api_api::models::UserVerificationRequirement) -> Self {
738        match value {
739            bitwarden_api_api::models::UserVerificationRequirement::Discouraged => UV::Discouraged,
740            bitwarden_api_api::models::UserVerificationRequirement::Preferred => UV::Preferred,
741            bitwarden_api_api::models::UserVerificationRequirement::Required => UV::Required,
742            bitwarden_api_api::models::UserVerificationRequirement::__Unknown(_) => UV::Preferred,
743        }
744    }
745}
746
747impl From<Verification> for UV {
748    fn from(value: Verification) -> Self {
749        match value {
750            Verification::Discouraged => UV::Discouraged,
751            Verification::Preferred => UV::Preferred,
752            Verification::Required => UV::Required,
753        }
754    }
755}
756
757impl From<UserVerificationRequirement> for UV {
758    fn from(value: UserVerificationRequirement) -> Self {
759        match value {
760            UserVerificationRequirement::Discouraged => UV::Discouraged,
761            UserVerificationRequirement::Preferred => UV::Preferred,
762            UserVerificationRequirement::Required => UV::Required,
763        }
764    }
765}
766
767#[allow(missing_docs)]
768#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
769pub enum ClientData {
770    DefaultWithExtraData { android_package_name: String },
771    DefaultWithCustomHash { hash: Vec<u8> },
772}
773
774#[derive(Serialize, Clone)]
775#[serde(rename_all = "camelCase")]
776pub(super) struct AndroidClientData {
777    android_package_name: String,
778}
779
780impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
781    fn extra_client_data(&self) -> Option<AndroidClientData> {
782        match self {
783            ClientData::DefaultWithExtraData {
784                android_package_name,
785            } => Some(AndroidClientData {
786                android_package_name: android_package_name.clone(),
787            }),
788            ClientData::DefaultWithCustomHash { .. } => None,
789        }
790    }
791
792    fn client_data_hash(&self) -> Option<Vec<u8>> {
793        match self {
794            ClientData::DefaultWithExtraData { .. } => None,
795            ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
796        }
797    }
798}
799
800/// Salt inputs for WebAuthn PRF extension.
801#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
802pub struct PrfInputValues {
803    /// An input on which to evaluate PRF. Required.
804    pub first: Vec<u8>,
805
806    /// An optional secondary input on which to evaluate PRF.
807    pub second: Option<Vec<u8>>,
808}
809
810impl PrfInputValues {
811    const WEBAUTHN_PRF_CONTEXT_STRING: &[u8] = b"WebAuthn PRF\0";
812
813    fn hash_webauthn_prf_input(input: &[u8]) -> [u8; 32] {
814        passkey::types::crypto::sha256(&[Self::WEBAUTHN_PRF_CONTEXT_STRING, input].concat())
815    }
816}
817
818impl std::fmt::Debug for PrfInputValues {
819    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
820        f.debug_struct("PrfInputValues")
821            .field("first", &"********")
822            .field("second", &self.second.as_ref().map(|_| "********"))
823            .finish()
824    }
825}
826
827impl From<PrfInputValues> for passkey::types::ctap2::extensions::AuthenticatorPrfValues {
828    /// This converts PRF input received from a client into the format that
829    /// passkey-rs expects. This is not valid for converting output received from passkey-rs.
830    fn from(value: PrfInputValues) -> Self {
831        // passkey-rs expects the salt input to be hashed already according to
832        // WebAuthn PRF extension client processing rules.
833        let first = PrfInputValues::hash_webauthn_prf_input(value.first.as_ref());
834        let second = value
835            .second
836            .as_deref()
837            .map(PrfInputValues::hash_webauthn_prf_input);
838        Self { first, second }
839    }
840}
841
842/// WebAuthn PRF output values.
843#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
844pub struct PrfOutputValues {
845    /// The output of the PRF evaluation of the first PRF input.
846    pub first: Vec<u8>,
847
848    /// The output of the PRF evaluation of the second PRF input, if it was specified.
849    pub second: Option<Vec<u8>>,
850}
851
852impl std::fmt::Debug for PrfOutputValues {
853    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854        f.debug_struct("PrfOutputValues")
855            .field("first", &"********")
856            .field("second", &self.second.as_ref().map(|_| "********"))
857            .finish()
858    }
859}
860
861impl From<passkey::types::ctap2::extensions::AuthenticatorPrfValues> for PrfOutputValues {
862    fn from(value: passkey::types::ctap2::extensions::AuthenticatorPrfValues) -> Self {
863        Self {
864            first: value.first.to_vec(),
865            second: value.second.map(|s| s.to_vec()),
866        }
867    }
868}
869#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
870pub struct ClientExtensionResults {
871    pub cred_props: Option<CredPropsResult>,
872}
873
874#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
875pub struct CredPropsResult {
876    pub rk: Option<bool>,
877}
878
879impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
880    fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
881        Self {
882            rk: value.discoverable,
883        }
884    }
885}
886
887#[allow(missing_docs)]
888#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
889pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
890    pub id: String,
891    pub raw_id: Vec<u8>,
892    pub ty: String,
893    pub authenticator_attachment: Option<String>,
894    pub client_extension_results: ClientExtensionResults,
895    pub response: AuthenticatorAttestationResponse,
896    pub selected_credential: SelectedCredential,
897}
898
899#[allow(missing_docs)]
900#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
901pub struct AuthenticatorAttestationResponse {
902    pub client_data_json: Vec<u8>,
903    pub authenticator_data: Vec<u8>,
904    pub public_key: Option<Vec<u8>>,
905    pub public_key_algorithm: i64,
906    pub attestation_object: Vec<u8>,
907    pub transports: Option<Vec<String>>,
908}
909
910#[allow(missing_docs)]
911#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
912pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
913    pub id: String,
914    pub raw_id: Vec<u8>,
915    pub ty: String,
916    pub authenticator_attachment: Option<String>,
917    pub client_extension_results: ClientExtensionResults,
918    pub response: AuthenticatorAssertionResponse,
919    pub selected_credential: SelectedCredential,
920}
921
922#[allow(missing_docs)]
923#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
924pub struct AuthenticatorAssertionResponse {
925    pub client_data_json: Vec<u8>,
926    pub authenticator_data: Vec<u8>,
927    pub signature: Vec<u8>,
928    pub user_handle: Vec<u8>,
929}
930
931#[derive(Debug, Error)]
932#[error("Invalid origin: {0}")]
933pub struct InvalidOriginError(String);
934
935#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
936/// An Unverified asset link.
937pub struct UnverifiedAssetLink {
938    /// Application package name.
939    package_name: String,
940    /// Fingerprint to compare.
941    sha256_cert_fingerprint: String,
942    /// Host to lookup the well known asset link.
943    host: String,
944    /// When sourced from the application statement list or parsed from host for passkeys.
945    /// Will be generated from `host` if not provided.
946    asset_link_url: Option<String>,
947}
948
949#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
950/// The origin of a WebAuthn request.
951pub enum Origin {
952    /// A Url, meant for a request in the web browser.
953    Web(String),
954    /// An android digital asset fingerprint.
955    /// Meant for a request coming from an android application.
956    Android(UnverifiedAssetLink),
957}
958
959impl TryFrom<Origin> for passkey::client::Origin<'_> {
960    type Error = InvalidOriginError;
961
962    fn try_from(value: Origin) -> Result<Self, Self::Error> {
963        Ok(match value {
964            Origin::Web(url) => {
965                let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?;
966                passkey::client::Origin::Web(Cow::Owned(url))
967            }
968            Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
969        })
970    }
971}
972
973impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
974    type Error = InvalidOriginError;
975
976    fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
977        let asset_link_url = {
978            let url = value
979                .asset_link_url
980                .unwrap_or_else(|| format!("https://{}/.well-known/assetlinks.json", value.host));
981            Url::parse(&url).map_err(|e| InvalidOriginError(e.to_string()))?
982        };
983
984        passkey::client::UnverifiedAssetLink::new(
985            Cow::from(value.package_name),
986            value.sha256_cert_fingerprint.as_str(),
987            Cow::from(value.host),
988            asset_link_url,
989        )
990        .map_err(|e| InvalidOriginError(format!("{e:?}")))
991    }
992}
993
994#[cfg(test)]
995mod tests {
996    use std::collections::HashMap;
997
998    use passkey::types::ctap2::{
999        extensions::{
1000            AuthenticatorPrfGetOutputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues,
1001        },
1002        get_assertion, make_credential,
1003    };
1004    use serde::{Deserialize, Serialize};
1005
1006    use super::{
1007        AndroidClientData, GetAssertionExtensionsInput, GetAssertionExtensionsOutput,
1008        GetAssertionPrfInput, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
1009        MakeCredentialPrfInput, PrfInputValues,
1010    };
1011
1012    /// Raw PRF input for testing.
1013    static TEST_SALT1_RAW_INPUT: &[u8] = b"salt1";
1014
1015    /// PRF input of after applying WebAuthn PRF domain separation to [TEST_SALT1_RAW_INPUT].
1016    // SHA-256(UTF-8("WebAuthn PRF") || 0x00 || TEST_SALT1_RAW_INPUT)
1017    static TEST_SALT1_WEBAUTHN_INPUT: [u8; 32] = [
1018        0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, 0x0F,
1019        0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, 0xEE, 0xCB,
1020        0xDD, 0x3C,
1021    ];
1022
1023    /// Raw PRF input for testing.
1024    static TEST_SALT2_RAW_INPUT: &[u8] = b"salt2";
1025
1026    /// PRF input after applying WebAuthn PRF domain separation to [TEST_SALT2_RAW_INPUT].
1027    ///
1028    /// SHA-256(UTF-8("WebAuthn PRF") || 0x00 || TEST_SALT2_RAW_INPUT)
1029    static TEST_SALT2_WEBAUTHN_INPUT: [u8; 32] = [
1030        0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, 0x1D,
1031        0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, 0x94, 0x9A,
1032        0xDD, 0x65,
1033    ];
1034
1035    // This is a stripped down of the passkey-rs implementation, to test the
1036    // serialization of the `ClientData` enum, and to make sure that () and None
1037    // are serialized the same way when going through #[serde(flatten)].
1038    #[derive(Serialize, Deserialize)]
1039    #[serde(rename_all = "camelCase")]
1040    pub struct CollectedClientData<E = ()>
1041    where
1042        E: Serialize,
1043    {
1044        pub origin: String,
1045
1046        #[serde(flatten)]
1047        pub extra_data: E,
1048    }
1049
1050    #[test]
1051    fn test_serialize_unit_data() {
1052        let data = CollectedClientData {
1053            origin: "https://example.com".to_owned(),
1054            extra_data: (),
1055        };
1056
1057        let serialized = serde_json::to_string(&data).unwrap();
1058        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
1059    }
1060
1061    #[test]
1062    fn test_serialize_none_data() {
1063        let data = CollectedClientData {
1064            origin: "https://example.com".to_owned(),
1065            extra_data: Option::<AndroidClientData>::None,
1066        };
1067
1068        let serialized = serde_json::to_string(&data).unwrap();
1069        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
1070    }
1071
1072    #[test]
1073    fn test_serialize_android_data() {
1074        let data = CollectedClientData {
1075            origin: "https://example.com".to_owned(),
1076            extra_data: Some(AndroidClientData {
1077                android_package_name: "com.example.app".to_owned(),
1078            }),
1079        };
1080
1081        let serialized = serde_json::to_string(&data).unwrap();
1082        assert_eq!(
1083            serialized,
1084            r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
1085        );
1086    }
1087
1088    #[test]
1089    fn test_transform_make_credential_extension_input() {
1090        let input = MakeCredentialExtensionsInput {
1091            prf: Some(MakeCredentialPrfInput {
1092                eval: Some(PrfInputValues {
1093                    first: TEST_SALT1_RAW_INPUT.to_vec(),
1094                    second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1095                }),
1096            }),
1097        };
1098        let transformed = make_credential::ExtensionInputs::from(input);
1099        let eval = transformed.prf.unwrap().eval.unwrap();
1100        assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
1101        assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
1102    }
1103
1104    #[test]
1105    fn test_transform_make_credential_extension_output() {
1106        let prf1: Vec<u8> = (0..32).collect();
1107        let output = make_credential::UnsignedExtensionOutputs {
1108            prf: Some(AuthenticatorPrfMakeOutputs {
1109                enabled: true,
1110                results: Some(AuthenticatorPrfValues {
1111                    first: prf1.clone().try_into().unwrap(),
1112                    second: None,
1113                }),
1114            }),
1115        };
1116        let transformed = MakeCredentialExtensionsOutput::from(output);
1117        assert!(transformed.prf.as_ref().unwrap().enabled);
1118        assert_eq!(prf1, transformed.prf.unwrap().results.unwrap().first);
1119    }
1120
1121    #[test]
1122    fn test_transform_get_assertion_extension_input() {
1123        let input = GetAssertionExtensionsInput {
1124            prf: Some(GetAssertionPrfInput {
1125                eval: Some(PrfInputValues {
1126                    first: TEST_SALT1_RAW_INPUT.to_vec(),
1127                    second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1128                }),
1129                eval_by_credential: None,
1130            }),
1131        };
1132        let transformed = get_assertion::ExtensionInputs::from(input);
1133        let eval = transformed.prf.unwrap().eval.unwrap();
1134        assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
1135        assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
1136    }
1137
1138    #[test]
1139    fn test_transform_get_assertion_extension_input_with_eval_by_credential() {
1140        let cred_id = b"credential_id1".to_vec();
1141        let input = GetAssertionExtensionsInput {
1142            prf: Some(GetAssertionPrfInput {
1143                eval: None,
1144                eval_by_credential: Some(HashMap::from([(
1145                    cred_id.clone(),
1146                    PrfInputValues {
1147                        first: TEST_SALT1_RAW_INPUT.to_vec(),
1148                        second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1149                    },
1150                )])),
1151            }),
1152        };
1153        let transformed = get_assertion::ExtensionInputs::from(input);
1154        let output = transformed.prf.unwrap().eval_by_credential.unwrap();
1155        let results = output.get(&cred_id.into()).unwrap();
1156        assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, results.first);
1157        assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, results.second.unwrap());
1158    }
1159
1160    #[test]
1161    fn test_transform_get_assertion_extension_output() {
1162        let prf1: Vec<u8> = (0..32).collect();
1163        let output = get_assertion::UnsignedExtensionOutputs {
1164            prf: Some(AuthenticatorPrfGetOutputs {
1165                results: AuthenticatorPrfValues {
1166                    first: prf1.clone().try_into().unwrap(),
1167                    second: None,
1168                },
1169            }),
1170        };
1171        let transformed = GetAssertionExtensionsOutput::from(output);
1172        assert_eq!(prf1, transformed.prf.unwrap().results.first);
1173    }
1174}