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