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