1use std::{borrow::Cow, collections::HashMap};
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
226#[allow(missing_docs)]
227#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
228pub struct MakeCredentialRequest {
229 pub client_data_hash: Vec<u8>,
230 pub rp: PublicKeyCredentialRpEntity,
231 pub user: PublicKeyCredentialUserEntity,
232 pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
233 pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
234 pub options: Options,
235
236 pub extensions: Option<MakeCredentialExtensionsInput>,
240}
241
242#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
248pub struct MakeCredentialResult {
249 pub authenticator_data: Vec<u8>,
252
253 pub attestation_object: Vec<u8>,
259
260 pub credential_id: Vec<u8>,
264
265 pub extensions: MakeCredentialExtensionsOutput,
272}
273
274#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
276#[derive(Debug, Default)]
277pub struct MakeCredentialExtensionsInput {
278 pub prf: Option<MakeCredentialPrfInput>,
280}
281
282impl From<MakeCredentialExtensionsInput>
283 for passkey::types::ctap2::make_credential::ExtensionInputs
284{
285 fn from(value: MakeCredentialExtensionsInput) -> Self {
286 Self {
287 hmac_secret: None,
288 hmac_secret_mc: None,
289 prf: value
290 .prf
291 .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
292 }
293 }
294}
295
296#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
298#[derive(Debug)]
299pub struct MakeCredentialExtensionsOutput {
300 pub prf: Option<MakeCredentialPrfOutput>,
302}
303
304impl From<Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>>
305 for MakeCredentialExtensionsOutput
306{
307 fn from(
308 value: Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>,
309 ) -> Self {
310 if let Some(ext) = value {
311 MakeCredentialExtensionsOutput::from(ext)
312 } else {
313 MakeCredentialExtensionsOutput { prf: None }
314 }
315 }
316}
317
318impl From<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>
319 for MakeCredentialExtensionsOutput
320{
321 fn from(value: passkey::types::ctap2::make_credential::UnsignedExtensionOutputs) -> Self {
322 let prf = value.prf.map(|prf| MakeCredentialPrfOutput {
323 enabled: prf.enabled,
324 results: prf.results.map(|results| results.into()),
325 });
326 MakeCredentialExtensionsOutput { prf }
327 }
328}
329
330#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
332#[derive(Debug)]
333pub struct MakeCredentialPrfInput {
334 pub eval: Option<PrfInputValues>,
336}
337
338impl From<MakeCredentialPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
339 fn from(value: MakeCredentialPrfInput) -> Self {
340 Self {
341 eval: value.eval.map(|v| v.into()),
342 eval_by_credential: None,
343 }
344 }
345}
346
347#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
349#[derive(Debug)]
350pub struct MakeCredentialPrfOutput {
351 pub enabled: bool,
353
354 pub results: Option<PrfOutputValues>,
356}
357
358#[allow(missing_docs)]
359#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
364pub struct GetAssertionRequest {
365 pub rp_id: String,
367
368 pub client_data_hash: Vec<u8>,
370
371 pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
375
376 pub options: Options,
377
378 pub extensions: Option<GetAssertionExtensionsInput>,
380}
381
382#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
388pub struct GetAssertionResult {
389 pub credential_id: Vec<u8>,
393
394 pub authenticator_data: Vec<u8>,
396
397 pub signature: Vec<u8>,
399
400 pub user_handle: Vec<u8>,
402
403 pub selected_credential: SelectedCredential,
405
406 pub extensions: GetAssertionExtensionsOutput,
409}
410
411#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
413#[derive(Debug)]
414pub struct GetAssertionExtensionsInput {
415 pub prf: Option<GetAssertionPrfInput>,
417}
418
419impl From<GetAssertionExtensionsInput> for passkey::types::ctap2::get_assertion::ExtensionInputs {
420 fn from(value: GetAssertionExtensionsInput) -> Self {
421 Self {
422 hmac_secret: None,
423 prf: value
424 .prf
425 .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
426 }
427 }
428}
429
430#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
432#[derive(Debug)]
433pub struct GetAssertionExtensionsOutput {
434 pub prf: Option<GetAssertionPrfOutput>,
436}
437
438impl From<Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>>
439 for GetAssertionExtensionsOutput
440{
441 fn from(value: Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>) -> Self {
442 if let Some(value) = value {
443 value.into()
444 } else {
445 Self { prf: None }
446 }
447 }
448}
449
450impl From<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>
451 for GetAssertionExtensionsOutput
452{
453 fn from(value: passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs) -> Self {
454 let prf = value.prf.map(|prf| GetAssertionPrfOutput {
455 results: prf.results.into(),
456 });
457 GetAssertionExtensionsOutput { prf }
458 }
459}
460
461#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
463#[derive(Debug)]
464pub struct GetAssertionPrfInput {
465 pub eval: Option<PrfInputValues>,
470
471 pub eval_by_credential: Option<HashMap<Vec<u8>, PrfInputValues>>,
475}
476
477impl From<GetAssertionPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
478 fn from(value: GetAssertionPrfInput) -> Self {
479 let eval_by_credential = if let Some(values) = value.eval_by_credential {
480 let map: HashMap<
481 passkey::types::Bytes,
482 passkey::types::ctap2::extensions::AuthenticatorPrfValues,
483 > = values
484 .into_iter()
485 .map(|(k, v)| (k.into(), v.into()))
486 .collect();
487 Some(map)
488 } else {
489 None
490 };
491 Self {
492 eval: value.eval.map(|v| v.into()),
493 eval_by_credential,
494 }
495 }
496}
497
498#[allow(missing_docs)]
500#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
501#[derive(Debug)]
502pub struct GetAssertionPrfOutput {
503 pub results: PrfOutputValues,
505}
506
507#[allow(missing_docs)]
508#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
509pub struct Options {
510 pub rk: bool,
511 pub uv: UV,
512}
513
514impl From<super::CheckUserOptions> for Options {
515 fn from(value: super::CheckUserOptions) -> Self {
516 Self {
517 rk: value.require_presence,
518 uv: value.require_verification.into(),
519 }
520 }
521}
522
523impl From<Options> for super::CheckUserOptions {
524 fn from(value: Options) -> Self {
525 Self {
526 require_presence: value.rk,
527 require_verification: value.uv.into(),
528 }
529 }
530}
531
532#[derive(Eq, PartialEq, Clone, Copy)]
533#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
534pub enum UV {
535 Discouraged,
536 Preferred,
537 Required,
538}
539
540impl From<UV> for Verification {
541 fn from(value: UV) -> Self {
542 match value {
543 UV::Discouraged => Verification::Discouraged,
544 UV::Preferred => Verification::Preferred,
545 UV::Required => Verification::Required,
546 }
547 }
548}
549
550impl From<Verification> for UV {
551 fn from(value: Verification) -> Self {
552 match value {
553 Verification::Discouraged => UV::Discouraged,
554 Verification::Preferred => UV::Preferred,
555 Verification::Required => UV::Required,
556 }
557 }
558}
559
560impl From<UserVerificationRequirement> for UV {
561 fn from(value: UserVerificationRequirement) -> Self {
562 match value {
563 UserVerificationRequirement::Discouraged => UV::Discouraged,
564 UserVerificationRequirement::Preferred => UV::Preferred,
565 UserVerificationRequirement::Required => UV::Required,
566 }
567 }
568}
569
570#[allow(missing_docs)]
571#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
572pub enum ClientData {
573 DefaultWithExtraData { android_package_name: String },
574 DefaultWithCustomHash { hash: Vec<u8> },
575}
576
577#[derive(Serialize, Clone)]
578#[serde(rename_all = "camelCase")]
579pub(super) struct AndroidClientData {
580 android_package_name: String,
581}
582
583impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
584 fn extra_client_data(&self) -> Option<AndroidClientData> {
585 match self {
586 ClientData::DefaultWithExtraData {
587 android_package_name,
588 } => Some(AndroidClientData {
589 android_package_name: android_package_name.clone(),
590 }),
591 ClientData::DefaultWithCustomHash { .. } => None,
592 }
593 }
594
595 fn client_data_hash(&self) -> Option<Vec<u8>> {
596 match self {
597 ClientData::DefaultWithExtraData { .. } => None,
598 ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
599 }
600 }
601}
602
603#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
605pub struct PrfInputValues {
606 pub first: Vec<u8>,
608
609 pub second: Option<Vec<u8>>,
611}
612
613impl PrfInputValues {
614 const WEBAUTHN_PRF_CONTEXT_STRING: &[u8] = b"WebAuthn PRF\0";
615
616 fn hash_webauthn_prf_input(input: &[u8]) -> [u8; 32] {
617 passkey::types::crypto::sha256(&[Self::WEBAUTHN_PRF_CONTEXT_STRING, input].concat())
618 }
619}
620
621impl std::fmt::Debug for PrfInputValues {
622 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
623 f.debug_struct("PrfInputValues")
624 .field("first", &"********")
625 .field("second", &self.second.as_ref().map(|_| "********"))
626 .finish()
627 }
628}
629
630impl From<PrfInputValues> for passkey::types::ctap2::extensions::AuthenticatorPrfValues {
631 fn from(value: PrfInputValues) -> Self {
634 let first = PrfInputValues::hash_webauthn_prf_input(value.first.as_ref());
637 let second = value
638 .second
639 .as_deref()
640 .map(PrfInputValues::hash_webauthn_prf_input);
641 Self { first, second }
642 }
643}
644
645#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
647pub struct PrfOutputValues {
648 pub first: Vec<u8>,
650
651 pub second: Option<Vec<u8>>,
653}
654
655impl std::fmt::Debug for PrfOutputValues {
656 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
657 f.debug_struct("PrfOutputValues")
658 .field("first", &"********")
659 .field("second", &self.second.as_ref().map(|_| "********"))
660 .finish()
661 }
662}
663
664impl From<passkey::types::ctap2::extensions::AuthenticatorPrfValues> for PrfOutputValues {
665 fn from(value: passkey::types::ctap2::extensions::AuthenticatorPrfValues) -> Self {
666 Self {
667 first: value.first.to_vec(),
668 second: value.second.map(|s| s.to_vec()),
669 }
670 }
671}
672#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
673pub struct ClientExtensionResults {
674 pub cred_props: Option<CredPropsResult>,
675}
676
677#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
678pub struct CredPropsResult {
679 pub rk: Option<bool>,
680}
681
682impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
683 fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
684 Self {
685 rk: value.discoverable,
686 }
687 }
688}
689
690#[allow(missing_docs)]
691#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
692pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
693 pub id: String,
694 pub raw_id: Vec<u8>,
695 pub ty: String,
696 pub authenticator_attachment: Option<String>,
697 pub client_extension_results: ClientExtensionResults,
698 pub response: AuthenticatorAttestationResponse,
699 pub selected_credential: SelectedCredential,
700}
701
702#[allow(missing_docs)]
703#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
704pub struct AuthenticatorAttestationResponse {
705 pub client_data_json: Vec<u8>,
706 pub authenticator_data: Vec<u8>,
707 pub public_key: Option<Vec<u8>>,
708 pub public_key_algorithm: i64,
709 pub attestation_object: Vec<u8>,
710 pub transports: Option<Vec<String>>,
711}
712
713#[allow(missing_docs)]
714#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
715pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
716 pub id: String,
717 pub raw_id: Vec<u8>,
718 pub ty: String,
719 pub authenticator_attachment: Option<String>,
720 pub client_extension_results: ClientExtensionResults,
721 pub response: AuthenticatorAssertionResponse,
722 pub selected_credential: SelectedCredential,
723}
724
725#[allow(missing_docs)]
726#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
727pub struct AuthenticatorAssertionResponse {
728 pub client_data_json: Vec<u8>,
729 pub authenticator_data: Vec<u8>,
730 pub signature: Vec<u8>,
731 pub user_handle: Vec<u8>,
732}
733
734#[derive(Debug, Error)]
735#[error("Invalid origin: {0}")]
736pub struct InvalidOriginError(String);
737
738#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
739pub struct UnverifiedAssetLink {
741 package_name: String,
743 sha256_cert_fingerprint: String,
745 host: String,
747 asset_link_url: Option<String>,
750}
751
752#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
753pub enum Origin {
755 Web(String),
757 Android(UnverifiedAssetLink),
760}
761
762impl TryFrom<Origin> for passkey::client::Origin<'_> {
763 type Error = InvalidOriginError;
764
765 fn try_from(value: Origin) -> Result<Self, Self::Error> {
766 Ok(match value {
767 Origin::Web(url) => {
768 let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?;
769 passkey::client::Origin::Web(Cow::Owned(url))
770 }
771 Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
772 })
773 }
774}
775
776impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
777 type Error = InvalidOriginError;
778
779 fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
780 let asset_link_url = {
781 let url = value
782 .asset_link_url
783 .unwrap_or_else(|| format!("https://{}/.well-known/assetlinks.json", value.host));
784 Url::parse(&url).map_err(|e| InvalidOriginError(e.to_string()))?
785 };
786
787 passkey::client::UnverifiedAssetLink::new(
788 Cow::from(value.package_name),
789 value.sha256_cert_fingerprint.as_str(),
790 Cow::from(value.host),
791 asset_link_url,
792 )
793 .map_err(|e| InvalidOriginError(format!("{e:?}")))
794 }
795}
796
797#[cfg(test)]
798mod tests {
799 use std::collections::HashMap;
800
801 use passkey::types::ctap2::{
802 extensions::{
803 AuthenticatorPrfGetOutputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues,
804 },
805 get_assertion, make_credential,
806 };
807 use serde::{Deserialize, Serialize};
808
809 use super::{
810 AndroidClientData, GetAssertionExtensionsInput, GetAssertionExtensionsOutput,
811 GetAssertionPrfInput, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
812 MakeCredentialPrfInput, PrfInputValues,
813 };
814
815 static TEST_SALT1_RAW_INPUT: &[u8] = b"salt1";
817
818 static TEST_SALT1_WEBAUTHN_INPUT: [u8; 32] = [
821 0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, 0x0F,
822 0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, 0xEE, 0xCB,
823 0xDD, 0x3C,
824 ];
825
826 static TEST_SALT2_RAW_INPUT: &[u8] = b"salt2";
828
829 static TEST_SALT2_WEBAUTHN_INPUT: [u8; 32] = [
833 0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, 0x1D,
834 0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, 0x94, 0x9A,
835 0xDD, 0x65,
836 ];
837
838 #[derive(Serialize, Deserialize)]
842 #[serde(rename_all = "camelCase")]
843 pub struct CollectedClientData<E = ()>
844 where
845 E: Serialize,
846 {
847 pub origin: String,
848
849 #[serde(flatten)]
850 pub extra_data: E,
851 }
852
853 #[test]
854 fn test_serialize_unit_data() {
855 let data = CollectedClientData {
856 origin: "https://example.com".to_owned(),
857 extra_data: (),
858 };
859
860 let serialized = serde_json::to_string(&data).unwrap();
861 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
862 }
863
864 #[test]
865 fn test_serialize_none_data() {
866 let data = CollectedClientData {
867 origin: "https://example.com".to_owned(),
868 extra_data: Option::<AndroidClientData>::None,
869 };
870
871 let serialized = serde_json::to_string(&data).unwrap();
872 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
873 }
874
875 #[test]
876 fn test_serialize_android_data() {
877 let data = CollectedClientData {
878 origin: "https://example.com".to_owned(),
879 extra_data: Some(AndroidClientData {
880 android_package_name: "com.example.app".to_owned(),
881 }),
882 };
883
884 let serialized = serde_json::to_string(&data).unwrap();
885 assert_eq!(
886 serialized,
887 r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
888 );
889 }
890
891 #[test]
892 fn test_transform_make_credential_extension_input() {
893 let input = MakeCredentialExtensionsInput {
894 prf: Some(MakeCredentialPrfInput {
895 eval: Some(PrfInputValues {
896 first: TEST_SALT1_RAW_INPUT.to_vec(),
897 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
898 }),
899 }),
900 };
901 let transformed = make_credential::ExtensionInputs::from(input);
902 let eval = transformed.prf.unwrap().eval.unwrap();
903 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
904 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
905 }
906
907 #[test]
908 fn test_transform_make_credential_extension_output() {
909 let prf1: Vec<u8> = (0..32).collect();
910 let output = make_credential::UnsignedExtensionOutputs {
911 prf: Some(AuthenticatorPrfMakeOutputs {
912 enabled: true,
913 results: Some(AuthenticatorPrfValues {
914 first: prf1.clone().try_into().unwrap(),
915 second: None,
916 }),
917 }),
918 };
919 let transformed = MakeCredentialExtensionsOutput::from(output);
920 assert!(transformed.prf.as_ref().unwrap().enabled);
921 assert_eq!(prf1, transformed.prf.unwrap().results.unwrap().first);
922 }
923
924 #[test]
925 fn test_transform_get_assertion_extension_input() {
926 let input = GetAssertionExtensionsInput {
927 prf: Some(GetAssertionPrfInput {
928 eval: Some(PrfInputValues {
929 first: TEST_SALT1_RAW_INPUT.to_vec(),
930 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
931 }),
932 eval_by_credential: None,
933 }),
934 };
935 let transformed = get_assertion::ExtensionInputs::from(input);
936 let eval = transformed.prf.unwrap().eval.unwrap();
937 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
938 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
939 }
940
941 #[test]
942 fn test_transform_get_assertion_extension_input_with_eval_by_credential() {
943 let cred_id = b"credential_id1".to_vec();
944 let input = GetAssertionExtensionsInput {
945 prf: Some(GetAssertionPrfInput {
946 eval: None,
947 eval_by_credential: Some(HashMap::from([(
948 cred_id.clone(),
949 PrfInputValues {
950 first: TEST_SALT1_RAW_INPUT.to_vec(),
951 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
952 },
953 )])),
954 }),
955 };
956 let transformed = get_assertion::ExtensionInputs::from(input);
957 let output = transformed.prf.unwrap().eval_by_credential.unwrap();
958 let results = output.get(&cred_id.into()).unwrap();
959 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, results.first);
960 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, results.second.unwrap());
961 }
962
963 #[test]
964 fn test_transform_get_assertion_extension_output() {
965 let prf1: Vec<u8> = (0..32).collect();
966 let output = get_assertion::UnsignedExtensionOutputs {
967 prf: Some(AuthenticatorPrfGetOutputs {
968 results: AuthenticatorPrfValues {
969 first: prf1.clone().try_into().unwrap(),
970 second: None,
971 },
972 }),
973 };
974 let transformed = GetAssertionExtensionsOutput::from(output);
975 assert_eq!(prf1, transformed.prf.unwrap().results.first);
976 }
977}