1use std::{borrow::Cow, collections::HashMap};
2
3use bitwarden_crypto::CryptoError;
4use bitwarden_encoding::{B64Url, NotB64UrlEncodedError};
5use bitwarden_vault::{CipherListView, CipherListViewType, CipherView, LoginListView};
6use passkey::types::webauthn::UserVerificationRequirement;
7use reqwest::Url;
8use serde::{Deserialize, Serialize};
9use thiserror::Error;
10
11use super::{
12 InvalidGuidError, SelectedCredential, UnknownEnumError, Verification,
13 get_enum_from_string_name, string_to_guid_bytes,
14};
15
16#[allow(missing_docs)]
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 pub has_counter: bool,
30}
31
32trait NoneWhitespace {
33 fn none_whitespace(&self) -> Option<String>;
35}
36
37impl NoneWhitespace for String {
38 fn none_whitespace(&self) -> Option<String> {
39 match self.trim() {
40 "" => None,
41 s => Some(s.to_owned()),
42 }
43 }
44}
45
46impl NoneWhitespace for Option<String> {
47 fn none_whitespace(&self) -> Option<String> {
48 self.as_ref().and_then(|s| s.none_whitespace())
49 }
50}
51
52#[allow(missing_docs)]
53#[derive(Debug, Error)]
54pub enum Fido2CredentialAutofillViewError {
55 #[error("Autofill credentials can only be created from existing ciphers that have a cipher id")]
56 MissingCipherId,
57
58 #[error(transparent)]
59 InvalidGuid(#[from] InvalidGuidError),
60
61 #[error(transparent)]
62 Crypto(#[from] CryptoError),
63
64 #[error(transparent)]
65 Base64Decode(#[from] NotB64UrlEncodedError),
66}
67
68impl Fido2CredentialAutofillView {
69 #[allow(missing_docs)]
70 pub fn from_cipher_view(
71 cipher: &CipherView,
72 ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
73 let credentials = cipher.get_fido2_credentials();
74
75 credentials
76 .iter()
77 .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
78 c.user_handle
79 .as_ref()
80 .map(|u| B64Url::try_from(u.as_str()))
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?.into_bytes(),
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 has_counter: Self::has_signature_counter(&c.counter),
100 })
101 })
102 })
103 .collect()
104 }
105
106 #[allow(missing_docs)]
107 pub fn from_cipher_list_view(
108 cipher: &CipherListView,
109 ) -> Result<Vec<Fido2CredentialAutofillView>, Fido2CredentialAutofillViewError> {
110 match &cipher.r#type {
111 CipherListViewType::Login(LoginListView {
112 fido2_credentials: Some(fido2_credentials),
113 username,
114 ..
115 }) => fido2_credentials
116 .iter()
117 .filter_map(|c| -> Option<Result<_, Fido2CredentialAutofillViewError>> {
118 c.user_handle
119 .as_ref()
120 .map(|u| B64Url::try_from(u.as_str()))
121 .map(|user_handle| {
122 Ok(Fido2CredentialAutofillView {
123 credential_id: string_to_guid_bytes(&c.credential_id)?,
124 cipher_id: cipher
125 .id
126 .ok_or(Fido2CredentialAutofillViewError::MissingCipherId)?
127 .into(),
128 rp_id: c.rp_id.clone(),
129 user_handle: user_handle?.into_bytes(),
130 user_name_for_ui: c
131 .user_name
132 .none_whitespace()
133 .or(c.user_display_name.none_whitespace())
134 .or(username.none_whitespace())
135 .or(cipher.name.none_whitespace()),
136 has_counter: Self::has_signature_counter(&c.counter),
137 })
138 })
139 })
140 .collect(),
141 _ => Ok(vec![]),
142 }
143 }
144
145 fn has_signature_counter(str: &String) -> bool {
146 str.none_whitespace()
147 .is_some_and(|counter_str| counter_str.parse::<u64>().is_ok_and(|counter| counter > 0))
148 }
149}
150
151#[allow(missing_docs)]
152#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
153pub struct PublicKeyCredentialRpEntity {
154 pub id: String,
155 pub name: Option<String>,
156}
157
158impl From<PublicKeyCredentialRpEntity>
159 for passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity
160{
161 fn from(value: PublicKeyCredentialRpEntity) -> Self {
162 Self {
163 id: value.id,
164 name: value.name,
165 }
166 }
167}
168
169impl TryFrom<&bitwarden_api_api::models::PublicKeyCredentialRpEntity>
170 for PublicKeyCredentialRpEntity
171{
172 type Error = WebAuthnEntityError;
173 fn try_from(
174 value: &bitwarden_api_api::models::PublicKeyCredentialRpEntity,
175 ) -> Result<Self, Self::Error> {
176 let id = value
177 .id
178 .as_ref()
179 .ok_or(WebAuthnEntityError::InvalidRpId)?
180 .clone();
181 Ok(Self {
182 id,
183 name: value.name.clone(),
184 })
185 }
186}
187
188#[allow(missing_docs)]
189#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
190pub struct PublicKeyCredentialUserEntity {
191 pub id: Vec<u8>,
192 pub display_name: String,
193 pub name: String,
194}
195
196impl From<PublicKeyCredentialUserEntity>
197 for passkey::types::webauthn::PublicKeyCredentialUserEntity
198{
199 fn from(value: PublicKeyCredentialUserEntity) -> Self {
200 Self {
201 id: value.id.into(),
202 name: value.name,
203 display_name: value.display_name,
204 }
205 }
206}
207
208impl TryFrom<&bitwarden_api_api::models::Fido2User> for PublicKeyCredentialUserEntity {
209 type Error = WebAuthnEntityError;
210 fn try_from(value: &bitwarden_api_api::models::Fido2User) -> Result<Self, Self::Error> {
211 let mut missing_fields = Vec::with_capacity(0);
212 if value.id.is_none() {
213 missing_fields.push("id".to_string())
214 }
215 if value.display_name.is_none() {
216 missing_fields.push("displayName".to_string())
217 }
218 if value.name.is_none() {
219 missing_fields.push("name".to_string())
220 }
221 if missing_fields.is_empty() {
222 Ok(Self {
223 id: value.id.as_ref().expect("checked manually").clone(),
224 display_name: value
225 .display_name
226 .as_ref()
227 .expect("checked manually")
228 .clone(),
229 name: value.name.as_ref().expect("checked manually").clone(),
230 })
231 } else {
232 Err(WebAuthnEntityError::MissingRequiredFields(missing_fields))
233 }
234 }
235}
236
237#[derive(Debug, Error)]
238pub enum WebAuthnEntityError {
239 #[error("Missing required fields: {0:?}")]
240 MissingRequiredFields(Vec<String>),
241
242 #[error("Invalid RP ID")]
243 InvalidRpId,
244
245 #[error("Invalid public key credential parameters")]
246 InvalidPublicKeyCredentialParameters(#[from] PublicKeyCredentialParametersError),
247
248 #[error("Unknown type")]
249 UnknownEnum(#[from] UnknownEnumError),
250}
251
252#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
253pub struct PublicKeyCredentialParameters {
254 pub ty: String,
255 pub alg: i64,
256}
257
258impl TryFrom<&bitwarden_api_api::models::PubKeyCredParam> for PublicKeyCredentialParameters {
259 type Error = PublicKeyCredentialParametersError;
260 fn try_from(value: &bitwarden_api_api::models::PubKeyCredParam) -> Result<Self, Self::Error> {
261 let ty = value
262 .r#type
263 .as_ref()
264 .ok_or(PublicKeyCredentialParametersError::UnknownEnum(
265 UnknownEnumError,
266 ))?
267 .to_string();
268 let alg = value
269 .alg
270 .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?
271 .as_i64();
272 Ok(Self { ty, alg })
273 }
274}
275
276#[derive(Debug, Error)]
277pub enum PublicKeyCredentialParametersError {
278 #[error("Invalid algorithm")]
279 InvalidAlgorithm,
280
281 #[error("Unknown type")]
282 UnknownEnum(#[from] UnknownEnumError),
283}
284
285impl TryFrom<PublicKeyCredentialParameters>
286 for passkey::types::webauthn::PublicKeyCredentialParameters
287{
288 type Error = PublicKeyCredentialParametersError;
289
290 fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
291 use coset::iana::EnumI64;
292 Ok(Self {
293 ty: get_enum_from_string_name(&value.ty)?,
294 alg: coset::iana::Algorithm::from_i64(value.alg)
295 .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
296 })
297 }
298}
299
300#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
301pub struct PublicKeyCredentialDescriptor {
302 pub ty: String,
303 pub id: Vec<u8>,
304 pub transports: Option<Vec<String>>,
305}
306
307impl TryFrom<PublicKeyCredentialDescriptor>
308 for passkey::types::webauthn::PublicKeyCredentialDescriptor
309{
310 type Error = UnknownEnumError;
311
312 fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
313 Ok(Self {
314 ty: get_enum_from_string_name(&value.ty)?,
315 id: value.id.into(),
316 transports: value
317 .transports
318 .map(|tt| {
319 tt.into_iter()
320 .map(|t| get_enum_from_string_name(&t))
321 .collect::<Result<Vec<_>, Self::Error>>()
322 })
323 .transpose()?,
324 })
325 }
326}
327
328impl TryFrom<&PublicKeyCredentialDescriptor>
329 for passkey::types::webauthn::PublicKeyCredentialDescriptor
330{
331 type Error = UnknownEnumError;
332
333 fn try_from(value: &PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
334 Ok(Self {
335 ty: get_enum_from_string_name(&value.ty)?,
336 id: value.id.clone().into(),
337 transports: value
338 .transports
339 .as_ref()
340 .map(|tt| {
341 tt.iter()
342 .map(|t| get_enum_from_string_name(t))
343 .collect::<Result<Vec<_>, Self::Error>>()
344 })
345 .transpose()?,
346 })
347 }
348}
349
350impl TryFrom<&bitwarden_api_api::models::PublicKeyCredentialDescriptor>
351 for PublicKeyCredentialDescriptor
352{
353 type Error = WebAuthnEntityError;
354 fn try_from(
355 value: &bitwarden_api_api::models::PublicKeyCredentialDescriptor,
356 ) -> Result<Self, Self::Error> {
357 let ty = value
358 .r#type
359 .as_ref()
360 .ok_or(WebAuthnEntityError::UnknownEnum(UnknownEnumError))?
361 .to_string();
362 let id = value
363 .id
364 .as_ref()
365 .ok_or(WebAuthnEntityError::MissingRequiredFields(vec![
366 "id".to_string(),
367 ]))?
368 .clone();
369 let transports = value
370 .transports
371 .as_ref()
372 .map(|l| l.iter().map(|t| t.to_string()).collect());
373 Ok(Self { ty, id, transports })
374 }
375}
376
377#[allow(missing_docs)]
378#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
379pub struct MakeCredentialRequest {
380 pub client_data_hash: Vec<u8>,
381 pub rp: PublicKeyCredentialRpEntity,
382 pub user: PublicKeyCredentialUserEntity,
383 pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
384 pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
385 pub options: Options,
386
387 pub extensions: Option<MakeCredentialExtensionsInput>,
391}
392
393#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
399pub struct MakeCredentialResult {
400 pub authenticator_data: Vec<u8>,
403
404 pub attestation_object: Vec<u8>,
410
411 pub credential_id: Vec<u8>,
415
416 pub extensions: MakeCredentialExtensionsOutput,
423}
424
425impl TryFrom<passkey::types::ctap2::make_credential::Response> for MakeCredentialResult {
426 type Error = WebAuthnEntityError;
427
428 fn try_from(
429 value: passkey::types::ctap2::make_credential::Response,
430 ) -> Result<Self, Self::Error> {
431 let authenticator_data = value.auth_data.to_vec();
432 let attestation_object = value.as_webauthn_bytes().to_vec();
433 let attested_credential_data = value.auth_data.attested_credential_data.ok_or(
434 WebAuthnEntityError::MissingRequiredFields(vec!["attestedCredentialData".to_string()]),
435 )?;
436 let credential_id = attested_credential_data.credential_id().to_vec();
437 let extensions: MakeCredentialExtensionsOutput = value.unsigned_extension_outputs.into();
438 Ok(MakeCredentialResult {
439 authenticator_data,
440 attestation_object,
441 credential_id,
442 extensions,
443 })
444 }
445}
446
447#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
449#[derive(Debug, Default)]
450pub struct MakeCredentialExtensionsInput {
451 pub prf: Option<MakeCredentialPrfInput>,
453}
454
455impl From<MakeCredentialExtensionsInput>
456 for passkey::types::ctap2::make_credential::ExtensionInputs
457{
458 fn from(value: MakeCredentialExtensionsInput) -> Self {
459 Self {
460 hmac_secret: None,
461 hmac_secret_mc: None,
462 prf: value
463 .prf
464 .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
465 }
466 }
467}
468
469impl From<bitwarden_api_api::models::AuthenticationExtensionsClientInputs>
470 for MakeCredentialExtensionsInput
471{
472 fn from(_value: bitwarden_api_api::models::AuthenticationExtensionsClientInputs) -> Self {
473 MakeCredentialExtensionsInput {
474 prf: None,
478 }
479 }
480}
481
482#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
484#[derive(Debug)]
485pub struct MakeCredentialExtensionsOutput {
486 pub prf: Option<MakeCredentialPrfOutput>,
488}
489
490impl From<Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>>
491 for MakeCredentialExtensionsOutput
492{
493 fn from(
494 value: Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>,
495 ) -> Self {
496 if let Some(ext) = value {
497 MakeCredentialExtensionsOutput::from(ext)
498 } else {
499 MakeCredentialExtensionsOutput { prf: None }
500 }
501 }
502}
503
504impl From<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>
505 for MakeCredentialExtensionsOutput
506{
507 fn from(value: passkey::types::ctap2::make_credential::UnsignedExtensionOutputs) -> Self {
508 let prf = value.prf.map(|prf| MakeCredentialPrfOutput {
509 enabled: prf.enabled,
510 results: prf.results.map(|results| results.into()),
511 });
512 MakeCredentialExtensionsOutput { prf }
513 }
514}
515
516#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
518#[derive(Debug)]
519pub struct MakeCredentialPrfInput {
520 pub eval: Option<PrfInputValues>,
522}
523
524impl From<MakeCredentialPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
525 fn from(value: MakeCredentialPrfInput) -> Self {
526 Self {
527 eval: value.eval.map(|v| v.into()),
528 eval_by_credential: None,
529 }
530 }
531}
532
533#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
535#[derive(Debug)]
536pub struct MakeCredentialPrfOutput {
537 pub enabled: bool,
539
540 pub results: Option<PrfOutputValues>,
542}
543
544#[allow(missing_docs)]
545#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
550pub struct GetAssertionRequest {
551 pub rp_id: String,
553
554 pub client_data_hash: Vec<u8>,
556
557 pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
561
562 pub options: Options,
563
564 pub extensions: Option<GetAssertionExtensionsInput>,
566}
567
568#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
574pub struct GetAssertionResult {
575 pub credential_id: Vec<u8>,
579
580 pub authenticator_data: Vec<u8>,
582
583 pub signature: Vec<u8>,
585
586 pub user_handle: Vec<u8>,
588
589 pub selected_credential: SelectedCredential,
591
592 pub extensions: GetAssertionExtensionsOutput,
595}
596
597#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
599#[derive(Debug)]
600pub struct GetAssertionExtensionsInput {
601 pub prf: Option<GetAssertionPrfInput>,
603}
604
605impl From<GetAssertionExtensionsInput> for passkey::types::ctap2::get_assertion::ExtensionInputs {
606 fn from(value: GetAssertionExtensionsInput) -> Self {
607 Self {
608 hmac_secret: None,
609 prf: value
610 .prf
611 .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
612 }
613 }
614}
615
616#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
618#[derive(Debug)]
619pub struct GetAssertionExtensionsOutput {
620 pub prf: Option<GetAssertionPrfOutput>,
622}
623
624impl From<Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>>
625 for GetAssertionExtensionsOutput
626{
627 fn from(value: Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>) -> Self {
628 if let Some(value) = value {
629 value.into()
630 } else {
631 Self { prf: None }
632 }
633 }
634}
635
636impl From<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>
637 for GetAssertionExtensionsOutput
638{
639 fn from(value: passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs) -> Self {
640 let prf = value.prf.map(|prf| GetAssertionPrfOutput {
641 results: prf.results.into(),
642 });
643 GetAssertionExtensionsOutput { prf }
644 }
645}
646
647#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
649#[derive(Debug)]
650pub struct GetAssertionPrfInput {
651 pub eval: Option<PrfInputValues>,
656
657 pub eval_by_credential: Option<HashMap<Vec<u8>, PrfInputValues>>,
661}
662
663impl From<GetAssertionPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
664 fn from(value: GetAssertionPrfInput) -> Self {
665 let eval_by_credential = if let Some(values) = value.eval_by_credential {
666 let map: HashMap<
667 passkey::types::Bytes,
668 passkey::types::ctap2::extensions::AuthenticatorPrfValues,
669 > = values
670 .into_iter()
671 .map(|(k, v)| (k.into(), v.into()))
672 .collect();
673 Some(map)
674 } else {
675 None
676 };
677 Self {
678 eval: value.eval.map(|v| v.into()),
679 eval_by_credential,
680 }
681 }
682}
683
684#[allow(missing_docs)]
686#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
687#[derive(Debug)]
688pub struct GetAssertionPrfOutput {
689 pub results: PrfOutputValues,
691}
692
693#[allow(missing_docs)]
694#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
695pub struct Options {
696 pub rk: bool,
697 pub uv: UV,
698}
699
700impl From<super::CheckUserOptions> for Options {
701 fn from(value: super::CheckUserOptions) -> Self {
702 Self {
703 rk: value.require_presence,
704 uv: value.require_verification.into(),
705 }
706 }
707}
708
709impl From<Options> for super::CheckUserOptions {
710 fn from(value: Options) -> Self {
711 Self {
712 require_presence: value.rk,
713 require_verification: value.uv.into(),
714 }
715 }
716}
717
718#[derive(Eq, PartialEq, Clone, Copy)]
719#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
720pub enum UV {
721 Discouraged,
722 Preferred,
723 Required,
724}
725
726impl From<UV> for Verification {
727 fn from(value: UV) -> Self {
728 match value {
729 UV::Discouraged => Verification::Discouraged,
730 UV::Preferred => Verification::Preferred,
731 UV::Required => Verification::Required,
732 }
733 }
734}
735
736impl From<bitwarden_api_api::models::UserVerificationRequirement> for UV {
737 fn from(value: bitwarden_api_api::models::UserVerificationRequirement) -> Self {
738 match value {
739 bitwarden_api_api::models::UserVerificationRequirement::Discouraged => UV::Discouraged,
740 bitwarden_api_api::models::UserVerificationRequirement::Preferred => UV::Preferred,
741 bitwarden_api_api::models::UserVerificationRequirement::Required => UV::Required,
742 bitwarden_api_api::models::UserVerificationRequirement::__Unknown(_) => UV::Preferred,
743 }
744 }
745}
746
747impl From<Verification> for UV {
748 fn from(value: Verification) -> Self {
749 match value {
750 Verification::Discouraged => UV::Discouraged,
751 Verification::Preferred => UV::Preferred,
752 Verification::Required => UV::Required,
753 }
754 }
755}
756
757impl From<UserVerificationRequirement> for UV {
758 fn from(value: UserVerificationRequirement) -> Self {
759 match value {
760 UserVerificationRequirement::Discouraged => UV::Discouraged,
761 UserVerificationRequirement::Preferred => UV::Preferred,
762 UserVerificationRequirement::Required => UV::Required,
763 }
764 }
765}
766
767#[allow(missing_docs)]
768#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
769pub enum ClientData {
770 DefaultWithExtraData { android_package_name: String },
771 DefaultWithCustomHash { hash: Vec<u8> },
772}
773
774#[derive(Serialize, Clone)]
775#[serde(rename_all = "camelCase")]
776pub(super) struct AndroidClientData {
777 android_package_name: String,
778}
779
780impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
781 fn extra_client_data(&self) -> Option<AndroidClientData> {
782 match self {
783 ClientData::DefaultWithExtraData {
784 android_package_name,
785 } => Some(AndroidClientData {
786 android_package_name: android_package_name.clone(),
787 }),
788 ClientData::DefaultWithCustomHash { .. } => None,
789 }
790 }
791
792 fn client_data_hash(&self) -> Option<Vec<u8>> {
793 match self {
794 ClientData::DefaultWithExtraData { .. } => None,
795 ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
796 }
797 }
798}
799
800#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
802pub struct PrfInputValues {
803 pub first: Vec<u8>,
805
806 pub second: Option<Vec<u8>>,
808}
809
810impl PrfInputValues {
811 const WEBAUTHN_PRF_CONTEXT_STRING: &[u8] = b"WebAuthn PRF\0";
812
813 fn hash_webauthn_prf_input(input: &[u8]) -> [u8; 32] {
814 passkey::types::crypto::sha256(&[Self::WEBAUTHN_PRF_CONTEXT_STRING, input].concat())
815 }
816}
817
818impl std::fmt::Debug for PrfInputValues {
819 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
820 f.debug_struct("PrfInputValues")
821 .field("first", &"********")
822 .field("second", &self.second.as_ref().map(|_| "********"))
823 .finish()
824 }
825}
826
827impl From<PrfInputValues> for passkey::types::ctap2::extensions::AuthenticatorPrfValues {
828 fn from(value: PrfInputValues) -> Self {
831 let first = PrfInputValues::hash_webauthn_prf_input(value.first.as_ref());
834 let second = value
835 .second
836 .as_deref()
837 .map(PrfInputValues::hash_webauthn_prf_input);
838 Self { first, second }
839 }
840}
841
842#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
844pub struct PrfOutputValues {
845 pub first: Vec<u8>,
847
848 pub second: Option<Vec<u8>>,
850}
851
852impl std::fmt::Debug for PrfOutputValues {
853 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
854 f.debug_struct("PrfOutputValues")
855 .field("first", &"********")
856 .field("second", &self.second.as_ref().map(|_| "********"))
857 .finish()
858 }
859}
860
861impl From<passkey::types::ctap2::extensions::AuthenticatorPrfValues> for PrfOutputValues {
862 fn from(value: passkey::types::ctap2::extensions::AuthenticatorPrfValues) -> Self {
863 Self {
864 first: value.first.to_vec(),
865 second: value.second.map(|s| s.to_vec()),
866 }
867 }
868}
869#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
870pub struct ClientExtensionResults {
871 pub cred_props: Option<CredPropsResult>,
872}
873
874#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
875pub struct CredPropsResult {
876 pub rk: Option<bool>,
877}
878
879impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
880 fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
881 Self {
882 rk: value.discoverable,
883 }
884 }
885}
886
887#[allow(missing_docs)]
888#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
889pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
890 pub id: String,
891 pub raw_id: Vec<u8>,
892 pub ty: String,
893 pub authenticator_attachment: Option<String>,
894 pub client_extension_results: ClientExtensionResults,
895 pub response: AuthenticatorAttestationResponse,
896 pub selected_credential: SelectedCredential,
897}
898
899#[allow(missing_docs)]
900#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
901pub struct AuthenticatorAttestationResponse {
902 pub client_data_json: Vec<u8>,
903 pub authenticator_data: Vec<u8>,
904 pub public_key: Option<Vec<u8>>,
905 pub public_key_algorithm: i64,
906 pub attestation_object: Vec<u8>,
907 pub transports: Option<Vec<String>>,
908}
909
910#[allow(missing_docs)]
911#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
912pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
913 pub id: String,
914 pub raw_id: Vec<u8>,
915 pub ty: String,
916 pub authenticator_attachment: Option<String>,
917 pub client_extension_results: ClientExtensionResults,
918 pub response: AuthenticatorAssertionResponse,
919 pub selected_credential: SelectedCredential,
920}
921
922#[allow(missing_docs)]
923#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
924pub struct AuthenticatorAssertionResponse {
925 pub client_data_json: Vec<u8>,
926 pub authenticator_data: Vec<u8>,
927 pub signature: Vec<u8>,
928 pub user_handle: Vec<u8>,
929}
930
931#[derive(Debug, Error)]
932#[error("Invalid origin: {0}")]
933pub struct InvalidOriginError(String);
934
935#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
936pub struct UnverifiedAssetLink {
938 package_name: String,
940 sha256_cert_fingerprint: String,
942 host: String,
944 asset_link_url: Option<String>,
947}
948
949#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
950pub enum Origin {
952 Web(String),
954 Android(UnverifiedAssetLink),
957}
958
959impl TryFrom<Origin> for passkey::client::Origin<'_> {
960 type Error = InvalidOriginError;
961
962 fn try_from(value: Origin) -> Result<Self, Self::Error> {
963 Ok(match value {
964 Origin::Web(url) => {
965 let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?;
966 passkey::client::Origin::Web(Cow::Owned(url))
967 }
968 Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
969 })
970 }
971}
972
973impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
974 type Error = InvalidOriginError;
975
976 fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
977 let asset_link_url = {
978 let url = value
979 .asset_link_url
980 .unwrap_or_else(|| format!("https://{}/.well-known/assetlinks.json", value.host));
981 Url::parse(&url).map_err(|e| InvalidOriginError(e.to_string()))?
982 };
983
984 passkey::client::UnverifiedAssetLink::new(
985 Cow::from(value.package_name),
986 value.sha256_cert_fingerprint.as_str(),
987 Cow::from(value.host),
988 asset_link_url,
989 )
990 .map_err(|e| InvalidOriginError(format!("{e:?}")))
991 }
992}
993
994#[cfg(test)]
995mod tests {
996 use std::collections::HashMap;
997
998 use passkey::types::ctap2::{
999 extensions::{
1000 AuthenticatorPrfGetOutputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues,
1001 },
1002 get_assertion, make_credential,
1003 };
1004 use serde::{Deserialize, Serialize};
1005
1006 use super::{
1007 AndroidClientData, GetAssertionExtensionsInput, GetAssertionExtensionsOutput,
1008 GetAssertionPrfInput, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
1009 MakeCredentialPrfInput, PrfInputValues,
1010 };
1011
1012 static TEST_SALT1_RAW_INPUT: &[u8] = b"salt1";
1014
1015 static TEST_SALT1_WEBAUTHN_INPUT: [u8; 32] = [
1018 0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, 0x0F,
1019 0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, 0xEE, 0xCB,
1020 0xDD, 0x3C,
1021 ];
1022
1023 static TEST_SALT2_RAW_INPUT: &[u8] = b"salt2";
1025
1026 static TEST_SALT2_WEBAUTHN_INPUT: [u8; 32] = [
1030 0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, 0x1D,
1031 0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, 0x94, 0x9A,
1032 0xDD, 0x65,
1033 ];
1034
1035 #[derive(Serialize, Deserialize)]
1039 #[serde(rename_all = "camelCase")]
1040 pub struct CollectedClientData<E = ()>
1041 where
1042 E: Serialize,
1043 {
1044 pub origin: String,
1045
1046 #[serde(flatten)]
1047 pub extra_data: E,
1048 }
1049
1050 #[test]
1051 fn test_serialize_unit_data() {
1052 let data = CollectedClientData {
1053 origin: "https://example.com".to_owned(),
1054 extra_data: (),
1055 };
1056
1057 let serialized = serde_json::to_string(&data).unwrap();
1058 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
1059 }
1060
1061 #[test]
1062 fn test_serialize_none_data() {
1063 let data = CollectedClientData {
1064 origin: "https://example.com".to_owned(),
1065 extra_data: Option::<AndroidClientData>::None,
1066 };
1067
1068 let serialized = serde_json::to_string(&data).unwrap();
1069 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
1070 }
1071
1072 #[test]
1073 fn test_serialize_android_data() {
1074 let data = CollectedClientData {
1075 origin: "https://example.com".to_owned(),
1076 extra_data: Some(AndroidClientData {
1077 android_package_name: "com.example.app".to_owned(),
1078 }),
1079 };
1080
1081 let serialized = serde_json::to_string(&data).unwrap();
1082 assert_eq!(
1083 serialized,
1084 r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
1085 );
1086 }
1087
1088 #[test]
1089 fn test_transform_make_credential_extension_input() {
1090 let input = MakeCredentialExtensionsInput {
1091 prf: Some(MakeCredentialPrfInput {
1092 eval: Some(PrfInputValues {
1093 first: TEST_SALT1_RAW_INPUT.to_vec(),
1094 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1095 }),
1096 }),
1097 };
1098 let transformed = make_credential::ExtensionInputs::from(input);
1099 let eval = transformed.prf.unwrap().eval.unwrap();
1100 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
1101 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
1102 }
1103
1104 #[test]
1105 fn test_transform_make_credential_extension_output() {
1106 let prf1: Vec<u8> = (0..32).collect();
1107 let output = make_credential::UnsignedExtensionOutputs {
1108 prf: Some(AuthenticatorPrfMakeOutputs {
1109 enabled: true,
1110 results: Some(AuthenticatorPrfValues {
1111 first: prf1.clone().try_into().unwrap(),
1112 second: None,
1113 }),
1114 }),
1115 };
1116 let transformed = MakeCredentialExtensionsOutput::from(output);
1117 assert!(transformed.prf.as_ref().unwrap().enabled);
1118 assert_eq!(prf1, transformed.prf.unwrap().results.unwrap().first);
1119 }
1120
1121 #[test]
1122 fn test_transform_get_assertion_extension_input() {
1123 let input = GetAssertionExtensionsInput {
1124 prf: Some(GetAssertionPrfInput {
1125 eval: Some(PrfInputValues {
1126 first: TEST_SALT1_RAW_INPUT.to_vec(),
1127 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1128 }),
1129 eval_by_credential: None,
1130 }),
1131 };
1132 let transformed = get_assertion::ExtensionInputs::from(input);
1133 let eval = transformed.prf.unwrap().eval.unwrap();
1134 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
1135 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
1136 }
1137
1138 #[test]
1139 fn test_transform_get_assertion_extension_input_with_eval_by_credential() {
1140 let cred_id = b"credential_id1".to_vec();
1141 let input = GetAssertionExtensionsInput {
1142 prf: Some(GetAssertionPrfInput {
1143 eval: None,
1144 eval_by_credential: Some(HashMap::from([(
1145 cred_id.clone(),
1146 PrfInputValues {
1147 first: TEST_SALT1_RAW_INPUT.to_vec(),
1148 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1149 },
1150 )])),
1151 }),
1152 };
1153 let transformed = get_assertion::ExtensionInputs::from(input);
1154 let output = transformed.prf.unwrap().eval_by_credential.unwrap();
1155 let results = output.get(&cred_id.into()).unwrap();
1156 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, results.first);
1157 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, results.second.unwrap());
1158 }
1159
1160 #[test]
1161 fn test_transform_get_assertion_extension_output() {
1162 let prf1: Vec<u8> = (0..32).collect();
1163 let output = get_assertion::UnsignedExtensionOutputs {
1164 prf: Some(AuthenticatorPrfGetOutputs {
1165 results: AuthenticatorPrfValues {
1166 first: prf1.clone().try_into().unwrap(),
1167 second: None,
1168 },
1169 }),
1170 };
1171 let transformed = GetAssertionExtensionsOutput::from(output);
1172 assert_eq!(prf1, transformed.prf.unwrap().results.first);
1173 }
1174}