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
49const 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#[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 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
263fn 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}