bitwarden_fido/
types.rs

1use std::borrow::Cow;
2
3use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
4use bitwarden_core::key_management::KeyIds;
5use bitwarden_crypto::{CryptoError, KeyStoreContext};
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    get_enum_from_string_name, string_to_guid_bytes, InvalidGuid, SelectedCredential, UnknownEnum,
14    Verification,
15};
16
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}
27
28trait NoneWhitespace {
29    /// Convert only whitespace to None
30    fn none_whitespace(&self) -> Option<String>;
31}
32
33impl NoneWhitespace for String {
34    fn none_whitespace(&self) -> Option<String> {
35        match self.trim() {
36            "" => None,
37            s => Some(s.to_owned()),
38        }
39    }
40}
41
42impl NoneWhitespace for Option<String> {
43    fn none_whitespace(&self) -> Option<String> {
44        self.as_ref().and_then(|s| s.none_whitespace())
45    }
46}
47
48#[derive(Debug, Error)]
49pub enum Fido2CredentialAutofillViewError {
50    #[error(
51        "Autofill credentials can only be created from existing ciphers that have a cipher id"
52    )]
53    MissingCipherId,
54
55    #[error(transparent)]
56    InvalidGuid(#[from] InvalidGuid),
57
58    #[error(transparent)]
59    CryptoError(#[from] CryptoError),
60
61    #[error(transparent)]
62    Base64DecodeError(#[from] base64::DecodeError),
63}
64
65impl Fido2CredentialAutofillView {
66    pub fn from_cipher_view(
67        cipher: &CipherView,
68        ctx: &mut KeyStoreContext<KeyIds>,
69    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
70        let credentials = cipher.decrypt_fido2_credentials(ctx)?;
71
72        credentials
73            .iter()
74            .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
75                c.user_handle
76                    .as_ref()
77                    .map(|u| URL_SAFE_NO_PAD.decode(u))
78                    .map(|user_handle| {
79                        Ok(Fido2CredentialAutofillView {
80                            credential_id: string_to_guid_bytes(&c.credential_id)?,
81                            cipher_id: cipher
82                                .id
83                                .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?,
84                            rp_id: c.rp_id.clone(),
85                            user_handle: user_handle?,
86                            user_name_for_ui: c
87                                .user_name
88                                .none_whitespace()
89                                .or(c.user_display_name.none_whitespace())
90                                .or(cipher
91                                    .login
92                                    .as_ref()
93                                    .and_then(|l| l.username.none_whitespace()))
94                                .or(cipher.name.none_whitespace()),
95                        })
96                    })
97            })
98            .collect()
99    }
100
101    pub fn from_cipher_list_view(
102        cipher: &CipherListView,
103    ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
104        match &cipher.r#type {
105            CipherListViewType::Login(LoginListView {
106                fido2_credentials: Some(fido2_credentials),
107                username,
108                ..
109            }) => fido2_credentials
110                .iter()
111                .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
112                    c.user_handle
113                        .as_ref()
114                        .map(|u| URL_SAFE_NO_PAD.decode(u))
115                        .map(|user_handle| {
116                            Ok(Fido2CredentialAutofillView {
117                                credential_id: string_to_guid_bytes(&c.credential_id)?,
118                                cipher_id: cipher
119                                    .id
120                                    .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?,
121                                rp_id: c.rp_id.clone(),
122                                user_handle: user_handle?,
123                                user_name_for_ui: c
124                                    .user_name
125                                    .none_whitespace()
126                                    .or(c.user_display_name.none_whitespace())
127                                    .or(username.none_whitespace())
128                                    .or(cipher.name.none_whitespace()),
129                            })
130                        })
131                })
132                .collect(),
133            _ => Ok(vec![]),
134        }
135    }
136}
137
138#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
139pub struct PublicKeyCredentialRpEntity {
140    pub id: String,
141    pub name: Option<String>,
142}
143
144#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
145pub struct PublicKeyCredentialUserEntity {
146    pub id: Vec<u8>,
147    pub display_name: String,
148    pub name: String,
149}
150
151#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
152pub struct PublicKeyCredentialParameters {
153    pub ty: String,
154    pub alg: i64,
155}
156
157#[derive(Debug, Error)]
158pub enum PublicKeyCredentialParametersError {
159    #[error("Invalid algorithm")]
160    InvalidAlgorithm,
161
162    #[error("Unknown type")]
163    UnknownEnum(#[from] UnknownEnum),
164}
165
166impl TryFrom<PublicKeyCredentialParameters>
167    for passkey::types::webauthn::PublicKeyCredentialParameters
168{
169    type Error = PublicKeyCredentialParametersError;
170
171    fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
172        use coset::iana::EnumI64;
173        Ok(Self {
174            ty: get_enum_from_string_name(&value.ty)?,
175            alg: coset::iana::Algorithm::from_i64(value.alg)
176                .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
177        })
178    }
179}
180
181#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
182pub struct PublicKeyCredentialDescriptor {
183    pub ty: String,
184    pub id: Vec<u8>,
185    pub transports: Option<Vec<String>>,
186}
187
188impl TryFrom<PublicKeyCredentialDescriptor>
189    for passkey::types::webauthn::PublicKeyCredentialDescriptor
190{
191    type Error = UnknownEnum;
192
193    fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
194        Ok(Self {
195            ty: get_enum_from_string_name(&value.ty)?,
196            id: value.id.into(),
197            transports: value
198                .transports
199                .map(|tt| {
200                    tt.into_iter()
201                        .map(|t| get_enum_from_string_name(&t))
202                        .collect::<Result<Vec<_>, Self::Error>>()
203                })
204                .transpose()?,
205        })
206    }
207}
208
209pub type Extensions = Option<String>;
210
211#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
212pub struct MakeCredentialRequest {
213    pub client_data_hash: Vec<u8>,
214    pub rp: PublicKeyCredentialRpEntity,
215    pub user: PublicKeyCredentialUserEntity,
216    pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
217    pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
218    pub options: Options,
219    pub extensions: Extensions,
220}
221
222#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
223pub struct MakeCredentialResult {
224    pub authenticator_data: Vec<u8>,
225    pub attestation_object: Vec<u8>,
226    pub credential_id: Vec<u8>,
227}
228
229#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
230pub struct GetAssertionRequest {
231    pub rp_id: String,
232    pub client_data_hash: Vec<u8>,
233    pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
234    pub options: Options,
235    pub extensions: Extensions,
236}
237
238#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
239pub struct Options {
240    pub rk: bool,
241    pub uv: UV,
242}
243
244impl From<super::CheckUserOptions> for Options {
245    fn from(value: super::CheckUserOptions) -> Self {
246        Self {
247            rk: value.require_presence,
248            uv: value.require_verification.into(),
249        }
250    }
251}
252
253impl From<Options> for super::CheckUserOptions {
254    fn from(value: Options) -> Self {
255        Self {
256            require_presence: value.rk,
257            require_verification: value.uv.into(),
258        }
259    }
260}
261
262#[derive(Eq, PartialEq, Clone, Copy)]
263#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
264pub enum UV {
265    Discouraged,
266    Preferred,
267    Required,
268}
269
270impl From<UV> for Verification {
271    fn from(value: UV) -> Self {
272        match value {
273            UV::Discouraged => Verification::Discouraged,
274            UV::Preferred => Verification::Preferred,
275            UV::Required => Verification::Required,
276        }
277    }
278}
279
280impl From<Verification> for UV {
281    fn from(value: Verification) -> Self {
282        match value {
283            Verification::Discouraged => UV::Discouraged,
284            Verification::Preferred => UV::Preferred,
285            Verification::Required => UV::Required,
286        }
287    }
288}
289
290impl From<UserVerificationRequirement> for UV {
291    fn from(value: UserVerificationRequirement) -> Self {
292        match value {
293            UserVerificationRequirement::Discouraged => UV::Discouraged,
294            UserVerificationRequirement::Preferred => UV::Preferred,
295            UserVerificationRequirement::Required => UV::Required,
296        }
297    }
298}
299
300#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
301pub struct GetAssertionResult {
302    pub credential_id: Vec<u8>,
303    pub authenticator_data: Vec<u8>,
304    pub signature: Vec<u8>,
305    pub user_handle: Vec<u8>,
306
307    pub selected_credential: SelectedCredential,
308}
309
310#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
311pub enum ClientData {
312    DefaultWithExtraData { android_package_name: String },
313    DefaultWithCustomHash { hash: Vec<u8> },
314}
315
316#[derive(Serialize, Clone)]
317#[serde(rename_all = "camelCase")]
318pub(super) struct AndroidClientData {
319    android_package_name: String,
320}
321
322impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
323    fn extra_client_data(&self) -> Option<AndroidClientData> {
324        match self {
325            ClientData::DefaultWithExtraData {
326                android_package_name,
327            } => Some(AndroidClientData {
328                android_package_name: android_package_name.clone(),
329            }),
330            ClientData::DefaultWithCustomHash { .. } => None,
331        }
332    }
333
334    fn client_data_hash(&self) -> Option<Vec<u8>> {
335        match self {
336            ClientData::DefaultWithExtraData { .. } => None,
337            ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
338        }
339    }
340}
341
342#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
343pub struct ClientExtensionResults {
344    pub cred_props: Option<CredPropsResult>,
345}
346
347#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
348pub struct CredPropsResult {
349    pub rk: Option<bool>,
350    pub authenticator_display_name: Option<String>,
351}
352
353impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
354    fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
355        Self {
356            rk: value.discoverable,
357            authenticator_display_name: value.authenticator_display_name,
358        }
359    }
360}
361
362#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
363pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
364    pub id: String,
365    pub raw_id: Vec<u8>,
366    pub ty: String,
367    pub authenticator_attachment: Option<String>,
368    pub client_extension_results: ClientExtensionResults,
369    pub response: AuthenticatorAttestationResponse,
370    pub selected_credential: SelectedCredential,
371}
372
373#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
374pub struct AuthenticatorAttestationResponse {
375    pub client_data_json: Vec<u8>,
376    pub authenticator_data: Vec<u8>,
377    pub public_key: Option<Vec<u8>>,
378    pub public_key_algorithm: i64,
379    pub attestation_object: Vec<u8>,
380    pub transports: Option<Vec<String>>,
381}
382
383#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
384pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
385    pub id: String,
386    pub raw_id: Vec<u8>,
387    pub ty: String,
388    pub authenticator_attachment: Option<String>,
389    pub client_extension_results: ClientExtensionResults,
390    pub response: AuthenticatorAssertionResponse,
391    pub selected_credential: SelectedCredential,
392}
393
394#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
395pub struct AuthenticatorAssertionResponse {
396    pub client_data_json: Vec<u8>,
397    pub authenticator_data: Vec<u8>,
398    pub signature: Vec<u8>,
399    pub user_handle: Vec<u8>,
400}
401
402#[derive(Debug, Error)]
403#[error("Invalid origin: {0}")]
404pub struct InvalidOriginError(String);
405
406#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
407/// An Unverified asset link.
408pub struct UnverifiedAssetLink {
409    /// Application package name.
410    package_name: String,
411    /// Fingerprint to compare.
412    sha256_cert_fingerprint: String,
413    /// Host to lookup the well known asset link.
414    host: String,
415    /// When sourced from the application statement list or parsed from host for passkeys.
416    /// Will be generated from `host` if not provided.
417    asset_link_url: Option<String>,
418}
419
420#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
421/// The origin of a WebAuthn request.
422pub enum Origin {
423    /// A Url, meant for a request in the web browser.
424    Web(String),
425    /// An android digital asset fingerprint.
426    /// Meant for a request coming from an android application.
427    Android(UnverifiedAssetLink),
428}
429
430impl TryFrom<Origin> for passkey::client::Origin<'_> {
431    type Error = InvalidOriginError;
432
433    fn try_from(value: Origin) -> Result<Self, Self::Error> {
434        Ok(match value {
435            Origin::Web(url) => {
436                let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{}", e)))?;
437                passkey::client::Origin::Web(Cow::Owned(url))
438            }
439            Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
440        })
441    }
442}
443
444impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
445    type Error = InvalidOriginError;
446
447    fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
448        let asset_link_url = match value.asset_link_url {
449            Some(url) => Some(Url::parse(&url).map_err(|e| InvalidOriginError(format!("{}", e)))?),
450            None => None,
451        };
452
453        passkey::client::UnverifiedAssetLink::new(
454            Cow::from(value.package_name),
455            value.sha256_cert_fingerprint.as_str(),
456            Cow::from(value.host),
457            asset_link_url,
458        )
459        .map_err(|e| InvalidOriginError(format!("{:?}", e)))
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use serde::{Deserialize, Serialize};
466
467    use super::AndroidClientData;
468
469    // This is a stripped down of the passkey-rs implementation, to test the
470    // serialization of the `ClientData` enum, and to make sure that () and None
471    // are serialized the same way when going through #[serde(flatten)].
472    #[derive(Serialize, Deserialize)]
473    #[serde(rename_all = "camelCase")]
474    pub struct CollectedClientData<E = ()>
475    where
476        E: Serialize,
477    {
478        pub origin: String,
479
480        #[serde(flatten)]
481        pub extra_data: E,
482    }
483
484    #[test]
485    fn test_serialize_unit_data() {
486        let data = CollectedClientData {
487            origin: "https://example.com".to_owned(),
488            extra_data: (),
489        };
490
491        let serialized = serde_json::to_string(&data).unwrap();
492        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
493    }
494
495    #[test]
496    fn test_serialize_none_data() {
497        let data = CollectedClientData {
498            origin: "https://example.com".to_owned(),
499            extra_data: Option::<AndroidClientData>::None,
500        };
501
502        let serialized = serde_json::to_string(&data).unwrap();
503        assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
504    }
505
506    #[test]
507    fn test_serialize_android_data() {
508        let data = CollectedClientData {
509            origin: "https://example.com".to_owned(),
510            extra_data: Some(AndroidClientData {
511                android_package_name: "com.example.app".to_owned(),
512            }),
513        };
514
515        let serialized = serde_json::to_string(&data).unwrap();
516        assert_eq!(
517            serialized,
518            r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
519        );
520    }
521}