Skip to main content

bitwarden_fido/
lib.rs

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