1use std::borrow::Cow;
2
3use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
4use bitwarden_core::key_management::KeyIds;
5use bitwarden_crypto::{CryptoError, KeyStoreContext};
6use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView};
7use passkey::types::webauthn::UserVerificationRequirement;
8use reqwest::Url;
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use super::{
13 get_enum_from_string_name, string_to_guid_bytes, InvalidGuid, SelectedCredential, UnknownEnum,
14 Verification,
15};
16
17#[allow(missing_docs)]
18#[derive(Serialize, Deserialize, Debug, Clone)]
19#[serde(rename_all = "camelCase", deny_unknown_fields)]
20#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
21pub struct Fido2CredentialAutofillView {
22 pub credential_id: Vec<u8>,
23 pub cipher_id: uuid::Uuid,
24 pub rp_id: String,
25 pub user_name_for_ui: Option<String>,
26 pub user_handle: Vec<u8>,
27}
28
29trait NoneWhitespace {
30 fn none_whitespace(&self) -> Option<String>;
32}
33
34impl NoneWhitespace for String {
35 fn none_whitespace(&self) -> Option<String> {
36 match self.trim() {
37 "" => None,
38 s => Some(s.to_owned()),
39 }
40 }
41}
42
43impl NoneWhitespace for Option<String> {
44 fn none_whitespace(&self) -> Option<String> {
45 self.as_ref().and_then(|s| s.none_whitespace())
46 }
47}
48
49#[allow(missing_docs)]
50#[derive(Debug, Error)]
51pub enum Fido2CredentialAutofillViewError {
52 #[error(
53 "Autofill credentials can only be created from existing ciphers that have a cipher id"
54 )]
55 MissingCipherId,
56
57 #[error(transparent)]
58 InvalidGuid(#[from] InvalidGuid),
59
60 #[error(transparent)]
61 CryptoError(#[from] CryptoError),
62
63 #[error(transparent)]
64 Base64DecodeError(#[from] base64::DecodeError),
65}
66
67impl Fido2CredentialAutofillView {
68 #[allow(missing_docs)]
69 pub fn from_cipher_view(
70 cipher: &CipherView,
71 ctx: &mut KeyStoreContext<KeyIds>,
72 ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
73 let credentials = cipher.decrypt_fido2_credentials(ctx)?;
74
75 credentials
76 .iter()
77 .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
78 c.user_handle
79 .as_ref()
80 .map(|u| URL_SAFE_NO_PAD.decode(u))
81 .map(|user_handle| {
82 Ok(Fido2CredentialAutofillView {
83 credential_id: string_to_guid_bytes(&c.credential_id)?,
84 cipher_id: cipher
85 .id
86 .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?,
87 rp_id: c.rp_id.clone(),
88 user_handle: user_handle?,
89 user_name_for_ui: c
90 .user_name
91 .none_whitespace()
92 .or(c.user_display_name.none_whitespace())
93 .or(cipher
94 .login
95 .as_ref()
96 .and_then(|l| l.username.none_whitespace()))
97 .or(cipher.name.none_whitespace()),
98 })
99 })
100 })
101 .collect()
102 }
103
104 #[allow(missing_docs)]
105 pub fn from_cipher_list_view(
106 cipher: &CipherListView,
107 ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
108 match &cipher.r#type {
109 CipherListViewType::Login(LoginListView {
110 fido2_credentials: Some(fido2_credentials),
111 username,
112 ..
113 }) => fido2_credentials
114 .iter()
115 .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
116 c.user_handle
117 .as_ref()
118 .map(|u| URL_SAFE_NO_PAD.decode(u))
119 .map(|user_handle| {
120 Ok(Fido2CredentialAutofillView {
121 credential_id: string_to_guid_bytes(&c.credential_id)?,
122 cipher_id: cipher
123 .id
124 .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?,
125 rp_id: c.rp_id.clone(),
126 user_handle: user_handle?,
127 user_name_for_ui: c
128 .user_name
129 .none_whitespace()
130 .or(c.user_display_name.none_whitespace())
131 .or(username.none_whitespace())
132 .or(cipher.name.none_whitespace()),
133 })
134 })
135 })
136 .collect(),
137 _ => Ok(vec![]),
138 }
139 }
140}
141
142#[allow(missing_docs)]
143#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
144pub struct PublicKeyCredentialRpEntity {
145 pub id: String,
146 pub name: Option<String>,
147}
148
149#[allow(missing_docs)]
150#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
151pub struct PublicKeyCredentialUserEntity {
152 pub id: Vec<u8>,
153 pub display_name: String,
154 pub name: String,
155}
156
157#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
158pub struct PublicKeyCredentialParameters {
159 pub ty: String,
160 pub alg: i64,
161}
162
163#[derive(Debug, Error)]
164pub enum PublicKeyCredentialParametersError {
165 #[error("Invalid algorithm")]
166 InvalidAlgorithm,
167
168 #[error("Unknown type")]
169 UnknownEnum(#[from] UnknownEnum),
170}
171
172impl TryFrom<PublicKeyCredentialParameters>
173 for passkey::types::webauthn::PublicKeyCredentialParameters
174{
175 type Error = PublicKeyCredentialParametersError;
176
177 fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
178 use coset::iana::EnumI64;
179 Ok(Self {
180 ty: get_enum_from_string_name(&value.ty)?,
181 alg: coset::iana::Algorithm::from_i64(value.alg)
182 .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
183 })
184 }
185}
186
187#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
188pub struct PublicKeyCredentialDescriptor {
189 pub ty: String,
190 pub id: Vec<u8>,
191 pub transports: Option<Vec<String>>,
192}
193
194impl TryFrom<PublicKeyCredentialDescriptor>
195 for passkey::types::webauthn::PublicKeyCredentialDescriptor
196{
197 type Error = UnknownEnum;
198
199 fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
200 Ok(Self {
201 ty: get_enum_from_string_name(&value.ty)?,
202 id: value.id.into(),
203 transports: value
204 .transports
205 .map(|tt| {
206 tt.into_iter()
207 .map(|t| get_enum_from_string_name(&t))
208 .collect::<Result<Vec<_>, Self::Error>>()
209 })
210 .transpose()?,
211 })
212 }
213}
214
215pub type Extensions = Option<String>;
216
217#[allow(missing_docs)]
218#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
219pub struct MakeCredentialRequest {
220 pub client_data_hash: Vec<u8>,
221 pub rp: PublicKeyCredentialRpEntity,
222 pub user: PublicKeyCredentialUserEntity,
223 pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
224 pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
225 pub options: Options,
226 pub extensions: Extensions,
227}
228
229#[allow(missing_docs)]
230#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
231pub struct MakeCredentialResult {
232 pub authenticator_data: Vec<u8>,
233 pub attestation_object: Vec<u8>,
234 pub credential_id: Vec<u8>,
235}
236
237#[allow(missing_docs)]
238#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
239pub struct GetAssertionRequest {
240 pub rp_id: String,
241 pub client_data_hash: Vec<u8>,
242 pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
243 pub options: Options,
244 pub extensions: Extensions,
245}
246
247#[allow(missing_docs)]
248#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
249pub struct Options {
250 pub rk: bool,
251 pub uv: UV,
252}
253
254impl From<super::CheckUserOptions> for Options {
255 fn from(value: super::CheckUserOptions) -> Self {
256 Self {
257 rk: value.require_presence,
258 uv: value.require_verification.into(),
259 }
260 }
261}
262
263impl From<Options> for super::CheckUserOptions {
264 fn from(value: Options) -> Self {
265 Self {
266 require_presence: value.rk,
267 require_verification: value.uv.into(),
268 }
269 }
270}
271
272#[derive(Eq, PartialEq, Clone, Copy)]
273#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
274pub enum UV {
275 Discouraged,
276 Preferred,
277 Required,
278}
279
280impl From<UV> for Verification {
281 fn from(value: UV) -> Self {
282 match value {
283 UV::Discouraged => Verification::Discouraged,
284 UV::Preferred => Verification::Preferred,
285 UV::Required => Verification::Required,
286 }
287 }
288}
289
290impl From<Verification> for UV {
291 fn from(value: Verification) -> Self {
292 match value {
293 Verification::Discouraged => UV::Discouraged,
294 Verification::Preferred => UV::Preferred,
295 Verification::Required => UV::Required,
296 }
297 }
298}
299
300impl From<UserVerificationRequirement> for UV {
301 fn from(value: UserVerificationRequirement) -> Self {
302 match value {
303 UserVerificationRequirement::Discouraged => UV::Discouraged,
304 UserVerificationRequirement::Preferred => UV::Preferred,
305 UserVerificationRequirement::Required => UV::Required,
306 }
307 }
308}
309
310#[allow(missing_docs)]
311#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
312pub struct GetAssertionResult {
313 pub credential_id: Vec<u8>,
314 pub authenticator_data: Vec<u8>,
315 pub signature: Vec<u8>,
316 pub user_handle: Vec<u8>,
317
318 pub selected_credential: SelectedCredential,
319}
320
321#[allow(missing_docs)]
322#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
323pub enum ClientData {
324 DefaultWithExtraData { android_package_name: String },
325 DefaultWithCustomHash { hash: Vec<u8> },
326}
327
328#[derive(Serialize, Clone)]
329#[serde(rename_all = "camelCase")]
330pub(super) struct AndroidClientData {
331 android_package_name: String,
332}
333
334impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
335 fn extra_client_data(&self) -> Option<AndroidClientData> {
336 match self {
337 ClientData::DefaultWithExtraData {
338 android_package_name,
339 } => Some(AndroidClientData {
340 android_package_name: android_package_name.clone(),
341 }),
342 ClientData::DefaultWithCustomHash { .. } => None,
343 }
344 }
345
346 fn client_data_hash(&self) -> Option<Vec<u8>> {
347 match self {
348 ClientData::DefaultWithExtraData { .. } => None,
349 ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
350 }
351 }
352}
353
354#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
355pub struct ClientExtensionResults {
356 pub cred_props: Option<CredPropsResult>,
357}
358
359#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
360pub struct CredPropsResult {
361 pub rk: Option<bool>,
362 pub authenticator_display_name: Option<String>,
363}
364
365impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
366 fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
367 Self {
368 rk: value.discoverable,
369 authenticator_display_name: value.authenticator_display_name,
370 }
371 }
372}
373
374#[allow(missing_docs)]
375#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
376pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
377 pub id: String,
378 pub raw_id: Vec<u8>,
379 pub ty: String,
380 pub authenticator_attachment: Option<String>,
381 pub client_extension_results: ClientExtensionResults,
382 pub response: AuthenticatorAttestationResponse,
383 pub selected_credential: SelectedCredential,
384}
385
386#[allow(missing_docs)]
387#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
388pub struct AuthenticatorAttestationResponse {
389 pub client_data_json: Vec<u8>,
390 pub authenticator_data: Vec<u8>,
391 pub public_key: Option<Vec<u8>>,
392 pub public_key_algorithm: i64,
393 pub attestation_object: Vec<u8>,
394 pub transports: Option<Vec<String>>,
395}
396
397#[allow(missing_docs)]
398#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
399pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
400 pub id: String,
401 pub raw_id: Vec<u8>,
402 pub ty: String,
403 pub authenticator_attachment: Option<String>,
404 pub client_extension_results: ClientExtensionResults,
405 pub response: AuthenticatorAssertionResponse,
406 pub selected_credential: SelectedCredential,
407}
408
409#[allow(missing_docs)]
410#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
411pub struct AuthenticatorAssertionResponse {
412 pub client_data_json: Vec<u8>,
413 pub authenticator_data: Vec<u8>,
414 pub signature: Vec<u8>,
415 pub user_handle: Vec<u8>,
416}
417
418#[derive(Debug, Error)]
419#[error("Invalid origin: {0}")]
420pub struct InvalidOriginError(String);
421
422#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
423pub struct UnverifiedAssetLink {
425 package_name: String,
427 sha256_cert_fingerprint: String,
429 host: String,
431 asset_link_url: Option<String>,
434}
435
436#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
437pub enum Origin {
439 Web(String),
441 Android(UnverifiedAssetLink),
444}
445
446impl TryFrom<Origin> for passkey::client::Origin<'_> {
447 type Error = InvalidOriginError;
448
449 fn try_from(value: Origin) -> Result<Self, Self::Error> {
450 Ok(match value {
451 Origin::Web(url) => {
452 let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{}", e)))?;
453 passkey::client::Origin::Web(Cow::Owned(url))
454 }
455 Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
456 })
457 }
458}
459
460impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
461 type Error = InvalidOriginError;
462
463 fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
464 let asset_link_url = match value.asset_link_url {
465 Some(url) => Some(Url::parse(&url).map_err(|e| InvalidOriginError(format!("{}", e)))?),
466 None => None,
467 };
468
469 passkey::client::UnverifiedAssetLink::new(
470 Cow::from(value.package_name),
471 value.sha256_cert_fingerprint.as_str(),
472 Cow::from(value.host),
473 asset_link_url,
474 )
475 .map_err(|e| InvalidOriginError(format!("{:?}", e)))
476 }
477}
478
479#[cfg(test)]
480mod tests {
481 use serde::{Deserialize, Serialize};
482
483 use super::AndroidClientData;
484
485 #[derive(Serialize, Deserialize)]
489 #[serde(rename_all = "camelCase")]
490 pub struct CollectedClientData<E = ()>
491 where
492 E: Serialize,
493 {
494 pub origin: String,
495
496 #[serde(flatten)]
497 pub extra_data: E,
498 }
499
500 #[test]
501 fn test_serialize_unit_data() {
502 let data = CollectedClientData {
503 origin: "https://example.com".to_owned(),
504 extra_data: (),
505 };
506
507 let serialized = serde_json::to_string(&data).unwrap();
508 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
509 }
510
511 #[test]
512 fn test_serialize_none_data() {
513 let data = CollectedClientData {
514 origin: "https://example.com".to_owned(),
515 extra_data: Option::<AndroidClientData>::None,
516 };
517
518 let serialized = serde_json::to_string(&data).unwrap();
519 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
520 }
521
522 #[test]
523 fn test_serialize_android_data() {
524 let data = CollectedClientData {
525 origin: "https://example.com".to_owned(),
526 extra_data: Some(AndroidClientData {
527 android_package_name: "com.example.app".to_owned(),
528 }),
529 };
530
531 let serialized = serde_json::to_string(&data).unwrap();
532 assert_eq!(
533 serialized,
534 r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
535 );
536 }
537}