1#![doc = include_str!("../README.md")]
2
3use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
4use bitwarden_core::key_management::KeyIds;
5use bitwarden_crypto::KeyStoreContext;
6use bitwarden_vault::{
7 CipherError, CipherView, Fido2CredentialFullView, Fido2CredentialNewView, Fido2CredentialView,
8};
9use crypto::{CoseKeyToPkcs8Error, PrivateKeyFromSecretKeyError};
10use passkey::types::{ctap2::Aaguid, Passkey};
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)]
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#[derive(Debug, Error)]
78pub enum Fido2Error {
79 #[error(transparent)]
80 DecodeError(#[from] base64::DecodeError),
81
82 #[error(transparent)]
83 UnknownEnum(#[from] UnknownEnum),
84
85 #[error(transparent)]
86 InvalidGuid(#[from] InvalidGuid),
87
88 #[error(transparent)]
89 PrivateKeyFromSecretKeyError(#[from] PrivateKeyFromSecretKeyError),
90
91 #[error("No Fido2 credentials found")]
92 NoFido2CredentialsFound,
93}
94
95impl TryFrom<CipherViewContainer> for Passkey {
96 type Error = Fido2Error;
97
98 fn try_from(value: CipherViewContainer) -> Result<Self, Self::Error> {
99 let cred = value
100 .fido2_credentials
101 .first()
102 .ok_or(Fido2Error::NoFido2CredentialsFound)?;
103
104 try_from_credential_full_view(cred.clone())
105 }
106}
107
108fn try_from_credential_full_view(value: Fido2CredentialFullView) -> Result<Passkey, Fido2Error> {
109 let counter: u32 = value.counter.parse().expect("Invalid counter");
110 let counter = (counter != 0).then_some(counter);
111 let key_value = URL_SAFE_NO_PAD.decode(value.key_value)?;
112 let user_handle = value
113 .user_handle
114 .map(|u| URL_SAFE_NO_PAD.decode(u))
115 .transpose()?;
116
117 let key = pkcs8_to_cose_key(&key_value)?;
118
119 Ok(Passkey {
120 key,
121 credential_id: string_to_guid_bytes(&value.credential_id)?.into(),
122 rp_id: value.rp_id.clone(),
123 user_handle: user_handle.map(|u| u.into()),
124 counter,
125 })
126}
127
128#[derive(Debug, Error)]
129pub enum FillCredentialError {
130 #[error(transparent)]
131 InvalidInputLength(#[from] InvalidInputLength),
132 #[error(transparent)]
133 CoseKeyToPkcs8Error(#[from] CoseKeyToPkcs8Error),
134}
135
136pub fn fill_with_credential(
137 view: &Fido2CredentialView,
138 value: Passkey,
139) -> Result<Fido2CredentialFullView, FillCredentialError> {
140 let cred_id: Vec<u8> = value.credential_id.into();
141 let user_handle = value
142 .user_handle
143 .map(|u| URL_SAFE_NO_PAD.encode(u.to_vec()));
144 let key_value = URL_SAFE_NO_PAD.encode(cose_key_to_pkcs8(&value.key)?);
145
146 Ok(Fido2CredentialFullView {
147 credential_id: guid_bytes_to_string(&cred_id)?,
148 key_type: "public-key".to_owned(),
149 key_algorithm: "ECDSA".to_owned(),
150 key_curve: "P-256".to_owned(),
151 key_value,
152 rp_id: value.rp_id,
153 rp_name: view.rp_name.clone(),
154 user_handle,
155
156 counter: value.counter.unwrap_or(0).to_string(),
157 user_name: view.user_name.clone(),
158 user_display_name: view.user_display_name.clone(),
159 discoverable: "true".to_owned(),
160 creation_date: chrono::offset::Utc::now(),
161 })
162}
163
164pub(crate) fn try_from_credential_new_view(
165 user: &passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
166 rp: &passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
167) -> Result<Fido2CredentialNewView, InvalidInputLength> {
168 let cred_id: Vec<u8> = vec![0; 16];
169 let user_handle = URL_SAFE_NO_PAD.encode(user.id.to_vec());
170
171 Ok(Fido2CredentialNewView {
172 credential_id: guid_bytes_to_string(&cred_id)?,
174 key_type: "public-key".to_owned(),
175 key_algorithm: "ECDSA".to_owned(),
176 key_curve: "P-256".to_owned(),
177 rp_id: rp.id.clone(),
178 rp_name: rp.name.clone(),
179 user_handle: Some(user_handle),
180
181 counter: 0.to_string(),
182 user_name: user.name.clone(),
183 user_display_name: user.display_name.clone(),
184 creation_date: chrono::offset::Utc::now(),
185 })
186}
187
188pub(crate) fn try_from_credential_full(
189 value: Passkey,
190 user: passkey::types::ctap2::make_credential::PublicKeyCredentialUserEntity,
191 rp: passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity,
192 options: passkey::types::ctap2::get_assertion::Options,
193) -> Result<Fido2CredentialFullView, FillCredentialError> {
194 let cred_id: Vec<u8> = value.credential_id.into();
195 let key_value = URL_SAFE_NO_PAD.encode(cose_key_to_pkcs8(&value.key)?);
196 let user_handle = URL_SAFE_NO_PAD.encode(user.id.to_vec());
197
198 Ok(Fido2CredentialFullView {
199 credential_id: guid_bytes_to_string(&cred_id)?,
200 key_type: "public-key".to_owned(),
201 key_algorithm: "ECDSA".to_owned(),
202 key_curve: "P-256".to_owned(),
203 key_value,
204 rp_id: value.rp_id,
205 rp_name: rp.name,
206 user_handle: Some(user_handle),
207
208 counter: value.counter.unwrap_or(0).to_string(),
209 user_name: user.name,
210 user_display_name: user.display_name,
211 discoverable: options.rk.to_string(),
212 creation_date: chrono::offset::Utc::now(),
213 })
214}
215
216#[derive(Debug, Error)]
217#[error("Input should be a 16 byte array")]
218pub struct InvalidInputLength;
219
220pub fn guid_bytes_to_string(source: &[u8]) -> Result<String, InvalidInputLength> {
221 if source.len() != 16 {
222 return Err(InvalidInputLength);
223 }
224 Ok(uuid::Uuid::from_bytes(source.try_into().expect("Invalid length")).to_string())
225}
226
227#[derive(Debug, Error)]
228#[error("Invalid GUID")]
229pub struct InvalidGuid;
230
231pub fn string_to_guid_bytes(source: &str) -> Result<Vec<u8>, InvalidGuid> {
232 if source.starts_with("b64.") {
233 let bytes = URL_SAFE_NO_PAD
234 .decode(source.trim_start_matches("b64."))
235 .map_err(|_| InvalidGuid)?;
236 Ok(bytes)
237 } else {
238 let Ok(uuid) = uuid::Uuid::try_parse(source) else {
239 return Err(InvalidGuid);
240 };
241 Ok(uuid.as_bytes().to_vec())
242 }
243}
244
245#[derive(Debug, Error)]
246#[error("Unknown enum value")]
247pub struct UnknownEnum;
248
249fn get_enum_from_string_name<T: serde::de::DeserializeOwned>(s: &str) -> Result<T, UnknownEnum> {
251 let serialized = format!(r#""{}""#, s);
252 let deserialized: T = serde_json::from_str(&serialized).map_err(|_| UnknownEnum)?;
253 Ok(deserialized)
254}
255
256fn get_string_name_from_enum(s: impl serde::Serialize) -> Result<String, serde_json::Error> {
257 let serialized = serde_json::to_string(&s)?;
258 let deserialized: String = serde_json::from_str(&serialized)?;
259 Ok(deserialized)
260}
261
262#[cfg(test)]
263mod tests {
264 use passkey::types::webauthn::AuthenticatorAttachment;
265
266 use super::{get_enum_from_string_name, get_string_name_from_enum};
267
268 #[test]
269 fn test_enum_string_conversion_works_as_expected() {
270 assert_eq!(
271 get_string_name_from_enum(AuthenticatorAttachment::CrossPlatform).unwrap(),
272 "cross-platform"
273 );
274
275 assert_eq!(
276 get_enum_from_string_name::<AuthenticatorAttachment>("cross-platform").unwrap(),
277 AuthenticatorAttachment::CrossPlatform
278 );
279 }
280
281 #[test]
282 fn string_to_guid_with_uuid_works() {
283 let uuid = "d548826e-79b4-db40-a3d8-11116f7e8349";
284 let bytes = super::string_to_guid_bytes(uuid).unwrap();
285 assert_eq!(
286 bytes,
287 vec![213, 72, 130, 110, 121, 180, 219, 64, 163, 216, 17, 17, 111, 126, 131, 73]
288 );
289 }
290
291 #[test]
292 fn string_to_guid_with_b64_works() {
293 let b64 = "b64.1UiCbnm020Cj2BERb36DSQ";
294 let bytes = super::string_to_guid_bytes(b64).unwrap();
295 assert_eq!(
296 bytes,
297 vec![213, 72, 130, 110, 121, 180, 219, 64, 163, 216, 17, 17, 111, 126, 131, 73]
298 );
299 }
300}