Skip to main content

bitwarden_fido/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use bitwarden_encoding::{B64Url, NotB64UrlEncodedError};
4use bitwarden_vault::{
5    CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView,
6};
7use crypto::{CoseKeyToPkcs8Error, PrivateKeyFromSecretKeyError};
8use passkey::types::{CredentialExtensions, Passkey, ctap2::Aaguid};
9
10#[cfg(feature = "uniffi")]
11uniffi::setup_scaffolding!();
12#[cfg(feature = "uniffi")]
13mod uniffi_support;
14
15mod authenticator;
16mod client;
17mod client_fido;
18mod crypto;
19mod device_auth_key;
20mod traits;
21mod types;
22pub use authenticator::{
23    CredentialsForAutofillError, Fido2Authenticator, GetAssertionError, MakeCredentialError,
24    SilentlyDiscoverCredentialsError,
25};
26pub use client::{Fido2Client, Fido2ClientError};
27pub use client_fido::{ClientFido2, ClientFido2Ext, GetFido2AutofillCredentialsError};
28pub use device_auth_key::{
29    DeviceAuthKeyAuthenticator, DeviceAuthKeyError, DeviceAuthKeyGetAssertionResult,
30    DeviceAuthKeyMetadata, DeviceAuthKeyRecord, DeviceAuthKeyStore,
31};
32pub use passkey::authenticator::UiHint;
33use thiserror::Error;
34pub use traits::{
35    CheckUserOptions, CheckUserResult, Fido2CallbackError, Fido2CredentialStore,
36    Fido2UserInterface, Verification,
37};
38pub use types::{
39    AuthenticatorAssertionResponse, AuthenticatorAttestationResponse, ClientData,
40    Fido2CredentialAutofillView, Fido2CredentialAutofillViewError, GetAssertionExtensionsInput,
41    GetAssertionExtensionsOutput, GetAssertionPrfInput, GetAssertionPrfOutput, GetAssertionRequest,
42    GetAssertionResult, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
43    MakeCredentialPrfInput, MakeCredentialPrfOutput, MakeCredentialRequest, MakeCredentialResult,
44    Options, Origin, PrfInputValues, PrfOutputValues,
45    PublicKeyCredentialAuthenticatorAssertionResponse,
46    PublicKeyCredentialAuthenticatorAttestationResponse, PublicKeyCredentialRpEntity,
47    PublicKeyCredentialUserEntity, UnverifiedAssetLink,
48};
49
50use self::crypto::{cose_key_to_pkcs8, pkcs8_to_cose_key};
51
52// This is the AAGUID for the Bitwarden Passkey provider (d548826e-79b4-db40-a3d8-11116f7e8349)
53// It is used for the Relaying Parties to identify the authenticator during registration
54const AAGUID: Aaguid = Aaguid([
55    0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49,
56]);
57
58#[allow(dead_code, missing_docs)]
59#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
60pub struct SelectedCredential {
61    cipher: CipherView,
62    credential: Fido2CredentialView,
63}
64
65// This container is needed so we can properly implement the TryFrom trait for Passkey
66// Otherwise we need to decrypt the Fido2 credentials every time we create a CipherView
67#[derive(Clone)]
68pub(crate) struct CipherViewContainer {
69    cipher: CipherView,
70    fido2_credentials: Vec<Fido2CredentialFullView>,
71}
72
73impl CipherViewContainer {
74    fn new(cipher: CipherView) -> Result<Self, CipherError> {
75        let fido2_credentials = cipher
76            .get_fido2_credentials()
77            .into_iter()
78            .map(Fido2CredentialFullView::from)
79            .collect();
80        Ok(Self {
81            cipher,
82            fido2_credentials,
83        })
84    }
85}
86
87#[allow(missing_docs)]
88#[derive(Debug, Error)]
89pub enum Fido2Error {
90    #[error(transparent)]
91    Decode(#[from] NotB64UrlEncodedError),
92
93    #[error(transparent)]
94    UnknownEnum(#[from] UnknownEnumError),
95
96    #[error(transparent)]
97    InvalidGuid(#[from] InvalidGuidError),
98
99    #[error(transparent)]
100    PrivateKeyFromSecretKey(#[from] PrivateKeyFromSecretKeyError),
101
102    #[error("No Fido2 credentials found")]
103    NoFido2CredentialsFound,
104
105    #[error("Invalid counter")]
106    InvalidCounter,
107}
108
109impl TryFrom<CipherViewContainer> for Passkey {
110    type Error = Fido2Error;
111
112    fn try_from(value: CipherViewContainer) -> Result<Self, Self::Error> {
113        let cred = value
114            .fido2_credentials
115            .first()
116            .ok_or(Fido2Error::NoFido2CredentialsFound)?;
117
118        try_from_credential_full_view(cred.clone())
119    }
120}
121
122fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result<Passkey, Fido2Error> {
123    let counter: u32 = value
124        .counter
125        .parse()
126        .map_err(|_| Fido2Error::InvalidCounter)?;
127    let counter = (counter != 0).then_some(counter);
128    let key_value = B64Url::try_from(value.key_value)?;
129    let user_handle = value.user_handle.map(B64Url::try_from).transpose()?;
130
131    let key = pkcs8_to_cose_key(key_value.as_bytes())?;
132
133    Ok(Passkey {
134        key,
135        credential_id: string_to_guid_bytes(&value.credential_id)?.into(),
136        rp_id: value.rp_id.clone(),
137        user_handle: user_handle.map(|u| u.into_bytes().into()),
138        counter,
139        extensions: CredentialExtensions { hmac_secret: None },
140    })
141}
142
143#[allow(missing_docs)]
144#[derive(Debug, Error)]
145pub enum FillCredentialError {
146    #[error(transparent)]
147    InvalidInputLength(#[from] InvalidInputLengthError),
148    #[error(transparent)]
149    CoseKeyToPkcs8(#[from] CoseKeyToPkcs8Error),
150}
151
152#[allow(missing_docs)]
153pub fn fill_with_credential(
154    view: &Fido2CredentialView,
155    value: Passkey,
156) -> Result<Fido2CredentialFullView, FillCredentialError> {
157    let cred_id: Vec<u8> = value.credential_id.into();
158    let user_handle = value
159        .user_handle
160        .map(|u| B64Url::from(u.to_vec()).to_string());
161    let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string();
162
163    Ok(Fido2CredentialFullView {
164        credential_id: guid_bytes_to_string(&cred_id)?,
165        key_type: "public-key".to_owned(),
166        key_algorithm: "ECDSA".to_owned(),
167        key_curve: "P-256".to_owned(),
168        key_value,
169        rp_id: value.rp_id,
170        rp_name: view.rp_name.clone(),
171        user_handle,
172
173        counter: value.counter.unwrap_or(0).to_string(),
174        user_name: view.user_name.clone(),
175        user_display_name: view.user_display_name.clone(),
176        discoverable: "true".to_owned(),
177        creation_date: view.creation_date,
178    })
179}
180
181pub(crate) fn try_from_credential_new_view(
182    user: &passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
183    rp: &passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
184) -> Result<Fido2CredentialNewView, InvalidInputLengthError> {
185    let cred_id: Vec<u8> = vec![0; 16];
186    let user_handle = B64Url::from(user.id.to_vec()).to_string();
187
188    Ok(Fido2CredentialNewView {
189        // TODO: Why do we have a credential id here?
190        credential_id: guid_bytes_to_string(&cred_id)?,
191        key_type: "public-key".to_owned(),
192        key_algorithm: "ECDSA".to_owned(),
193        key_curve: "P-256".to_owned(),
194        rp_id: rp.id.clone(),
195        rp_name: rp.name.clone(),
196        user_handle: Some(user_handle),
197
198        counter: 0.to_string(),
199        user_name: user.name.clone(),
200        user_display_name: user.display_name.clone(),
201        creation_date: chrono::offset::Utc::now(),
202    })
203}
204
205pub(crate) fn try_from_credential_full(
206    value: Passkey,
207    user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
208    rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
209    options: passkey::types::ctap2::get_assertion::Options,
210) -> Result<Fido2CredentialFullView, FillCredentialError> {
211    let cred_id: Vec<u8> = value.credential_id.into();
212    let key_value = B64Url::from(cose_key_to_pkcs8(&value.key)?).to_string();
213    let user_handle = B64Url::from(user.id.to_vec()).to_string();
214
215    Ok(Fido2CredentialFullView {
216        credential_id: guid_bytes_to_string(&cred_id)?,
217        key_type: "public-key".to_owned(),
218        key_algorithm: "ECDSA".to_owned(),
219        key_curve: "P-256".to_owned(),
220        key_value,
221        rp_id: value.rp_id,
222        rp_name: rp.name,
223        user_handle: Some(user_handle),
224
225        counter: value.counter.unwrap_or(0).to_string(),
226        user_name: user.name,
227        user_display_name: user.display_name,
228        discoverable: options.rk.to_string(),
229        creation_date: chrono::offset::Utc::now(),
230    })
231}
232
233#[allow(missing_docs)]
234#[derive(Debug, Error)]
235#[error("Input should be a 16 byte array")]
236pub struct InvalidInputLengthError;
237
238#[allow(missing_docs)]
239pub fn guid_bytes_to_string(source: &[u8]) -> Result<String, InvalidInputLengthError> {
240    if source.len() != 16 {
241        return Err(InvalidInputLengthError);
242    }
243    Ok(uuid::Uuid::from_bytes(source.try_into().expect("Invalid length")).to_string())
244}
245
246#[allow(missing_docs)]
247#[derive(Debug, Error)]
248#[error("Invalid GUID")]
249pub struct InvalidGuidError;
250
251#[allow(missing_docs)]
252pub fn string_to_guid_bytes(source: &str) -> Result<Vec<u8>, InvalidGuidError> {
253    if source.starts_with("b64.") {
254        let bytes =
255            B64Url::try_from(source.trim_start_matches("b64.")).map_err(|_| InvalidGuidError)?;
256        Ok(bytes.as_bytes().to_vec())
257    } else {
258        let Ok(uuid) = uuid::Uuid::try_parse(source) else {
259            return Err(InvalidGuidError);
260        };
261        Ok(uuid.as_bytes().to_vec())
262    }
263}
264
265#[allow(missing_docs)]
266#[derive(Debug, Error)]
267#[error("Unknown enum value")]
268pub struct UnknownEnumError;
269
270// Some utilities to convert back and forth between enums and strings
271fn get_enum_from_string_name<T: serde::de::DeserializeOwned>(
272    s: &str,
273) -> Result<T, UnknownEnumError> {
274    let serialized = format!(r#""{s}""#);
275    let deserialized: T = serde_json::from_str(&serialized).map_err(|_| UnknownEnumError)?;
276    Ok(deserialized)
277}
278
279fn get_string_name_from_enum(s: impl serde::Serialize) -> Result<String, serde_json::Error> {
280    let serialized = serde_json::to_string(&s)?;
281    let deserialized: String = serde_json::from_str(&serialized)?;
282    Ok(deserialized)
283}
284
285#[cfg(test)]
286mod tests {
287    use passkey::types::webauthn::AuthenticatorAttachment;
288
289    use super::{get_enum_from_string_name, get_string_name_from_enum};
290
291    #[test]
292    fn test_enum_string_conversion_works_as_expected() {
293        assert_eq!(
294            get_string_name_from_enum(AuthenticatorAttachment::CrossPlatform).unwrap(),
295            "cross-platform"
296        );
297
298        assert_eq!(
299            get_enum_from_string_name::<AuthenticatorAttachment>("cross-platform").unwrap(),
300            AuthenticatorAttachment::CrossPlatform
301        );
302    }
303
304    #[test]
305    fn string_to_guid_with_uuid_works() {
306        let uuid = "d548826e-79b4-db40-a3d8-11116f7e8349";
307        let bytes = super::string_to_guid_bytes(uuid).unwrap();
308        assert_eq!(
309            bytes,
310            vec![
311                213, 72, 130, 110, 121, 180, 219, 64, 163, 216, 17, 17, 111, 126, 131, 73
312            ]
313        );
314    }
315
316    #[test]
317    fn string_to_guid_with_b64_works() {
318        let b64 = "b64.1UiCbnm020Cj2BERb36DSQ";
319        let bytes = super::string_to_guid_bytes(b64).unwrap();
320        assert_eq!(
321            bytes,
322            vec![
323                213, 72, 130, 110, 121, 180, 219, 64, 163, 216, 17, 17, 111, 126, 131, 73
324            ]
325        );
326    }
327}