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