Skip to main content

bitwarden_fido/
types.rs

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