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