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