1use std::{borrow::Cow, collections::HashMap};
2
3use bitwarden_core::key_management::KeySlotIds;
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<KeySlotIds>,
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
160impl From<PublicKeyCredentialRpEntity>
161 for passkey::types::ctap2::make_credential::PublicKeyCredentialRpEntity
162{
163 fn from(value: PublicKeyCredentialRpEntity) -> Self {
164 Self {
165 id: value.id,
166 name: value.name,
167 }
168 }
169}
170
171impl TryFrom<&bitwarden_api_api::models::PublicKeyCredentialRpEntity>
172 for PublicKeyCredentialRpEntity
173{
174 type Error = WebAuthnEntityError;
175 fn try_from(
176 value: &bitwarden_api_api::models::PublicKeyCredentialRpEntity,
177 ) -> Result<Self, Self::Error> {
178 let id = value
179 .id
180 .as_ref()
181 .ok_or(WebAuthnEntityError::InvalidRpId)?
182 .clone();
183 Ok(Self {
184 id,
185 name: value.name.clone(),
186 })
187 }
188}
189
190#[allow(missing_docs)]
191#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
192pub struct PublicKeyCredentialUserEntity {
193 pub id: Vec<u8>,
194 pub display_name: String,
195 pub name: String,
196}
197
198impl From<PublicKeyCredentialUserEntity>
199 for passkey::types::webauthn::PublicKeyCredentialUserEntity
200{
201 fn from(value: PublicKeyCredentialUserEntity) -> Self {
202 Self {
203 id: value.id.into(),
204 name: value.name,
205 display_name: value.display_name,
206 }
207 }
208}
209
210impl TryFrom<&bitwarden_api_api::models::Fido2User> for PublicKeyCredentialUserEntity {
211 type Error = WebAuthnEntityError;
212 fn try_from(value: &bitwarden_api_api::models::Fido2User) -> Result<Self, Self::Error> {
213 let mut missing_fields = Vec::with_capacity(0);
214 if value.id.is_none() {
215 missing_fields.push("id".to_string())
216 }
217 if value.display_name.is_none() {
218 missing_fields.push("displayName".to_string())
219 }
220 if value.name.is_none() {
221 missing_fields.push("name".to_string())
222 }
223 if missing_fields.is_empty() {
224 Ok(Self {
225 id: value.id.as_ref().expect("checked manually").clone(),
226 display_name: value
227 .display_name
228 .as_ref()
229 .expect("checked manually")
230 .clone(),
231 name: value.name.as_ref().expect("checked manually").clone(),
232 })
233 } else {
234 Err(WebAuthnEntityError::MissingRequiredFields(missing_fields))
235 }
236 }
237}
238
239#[derive(Debug, Error)]
240pub enum WebAuthnEntityError {
241 #[error("Missing required fields: {0:?}")]
242 MissingRequiredFields(Vec<String>),
243
244 #[error("Invalid RP ID")]
245 InvalidRpId,
246
247 #[error("Invalid public key credential parameters")]
248 InvalidPublicKeyCredentialParameters(#[from] PublicKeyCredentialParametersError),
249
250 #[error("Unknown type")]
251 UnknownEnum(#[from] UnknownEnumError),
252}
253
254#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
255pub struct PublicKeyCredentialParameters {
256 pub ty: String,
257 pub alg: i64,
258}
259
260impl TryFrom<&bitwarden_api_api::models::PubKeyCredParam> for PublicKeyCredentialParameters {
261 type Error = PublicKeyCredentialParametersError;
262 fn try_from(value: &bitwarden_api_api::models::PubKeyCredParam) -> Result<Self, Self::Error> {
263 let ty = value
264 .r#type
265 .as_ref()
266 .ok_or(PublicKeyCredentialParametersError::UnknownEnum(
267 UnknownEnumError,
268 ))?
269 .to_string();
270 let alg = value
271 .alg
272 .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?
273 .as_i64();
274 Ok(Self { ty, alg })
275 }
276}
277
278#[derive(Debug, Error)]
279pub enum PublicKeyCredentialParametersError {
280 #[error("Invalid algorithm")]
281 InvalidAlgorithm,
282
283 #[error("Unknown type")]
284 UnknownEnum(#[from] UnknownEnumError),
285}
286
287impl TryFrom<PublicKeyCredentialParameters>
288 for passkey::types::webauthn::PublicKeyCredentialParameters
289{
290 type Error = PublicKeyCredentialParametersError;
291
292 fn try_from(value: PublicKeyCredentialParameters) -> Result<Self, Self::Error> {
293 use coset::iana::EnumI64;
294 Ok(Self {
295 ty: get_enum_from_string_name(&value.ty)?,
296 alg: coset::iana::Algorithm::from_i64(value.alg)
297 .ok_or(PublicKeyCredentialParametersError::InvalidAlgorithm)?,
298 })
299 }
300}
301
302#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
303pub struct PublicKeyCredentialDescriptor {
304 pub ty: String,
305 pub id: Vec<u8>,
306 pub transports: Option<Vec<String>>,
307}
308
309impl TryFrom<PublicKeyCredentialDescriptor>
310 for passkey::types::webauthn::PublicKeyCredentialDescriptor
311{
312 type Error = UnknownEnumError;
313
314 fn try_from(value: PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
315 Ok(Self {
316 ty: get_enum_from_string_name(&value.ty)?,
317 id: value.id.into(),
318 transports: value
319 .transports
320 .map(|tt| {
321 tt.into_iter()
322 .map(|t| get_enum_from_string_name(&t))
323 .collect::<Result<Vec<_>, Self::Error>>()
324 })
325 .transpose()?,
326 })
327 }
328}
329
330impl TryFrom<&PublicKeyCredentialDescriptor>
331 for passkey::types::webauthn::PublicKeyCredentialDescriptor
332{
333 type Error = UnknownEnumError;
334
335 fn try_from(value: &PublicKeyCredentialDescriptor) -> Result<Self, Self::Error> {
336 Ok(Self {
337 ty: get_enum_from_string_name(&value.ty)?,
338 id: value.id.clone().into(),
339 transports: value
340 .transports
341 .as_ref()
342 .map(|tt| {
343 tt.iter()
344 .map(|t| get_enum_from_string_name(t))
345 .collect::<Result<Vec<_>, Self::Error>>()
346 })
347 .transpose()?,
348 })
349 }
350}
351
352impl TryFrom<&bitwarden_api_api::models::PublicKeyCredentialDescriptor>
353 for PublicKeyCredentialDescriptor
354{
355 type Error = WebAuthnEntityError;
356 fn try_from(
357 value: &bitwarden_api_api::models::PublicKeyCredentialDescriptor,
358 ) -> Result<Self, Self::Error> {
359 let ty = value
360 .r#type
361 .as_ref()
362 .ok_or(WebAuthnEntityError::UnknownEnum(UnknownEnumError))?
363 .to_string();
364 let id = value
365 .id
366 .as_ref()
367 .ok_or(WebAuthnEntityError::MissingRequiredFields(vec![
368 "id".to_string(),
369 ]))?
370 .clone();
371 let transports = value
372 .transports
373 .as_ref()
374 .map(|l| l.iter().map(|t| t.to_string()).collect());
375 Ok(Self { ty, id, transports })
376 }
377}
378
379#[allow(missing_docs)]
380#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
381pub struct MakeCredentialRequest {
382 pub client_data_hash: Vec<u8>,
383 pub rp: PublicKeyCredentialRpEntity,
384 pub user: PublicKeyCredentialUserEntity,
385 pub pub_key_cred_params: Vec<PublicKeyCredentialParameters>,
386 pub exclude_list: Option<Vec<PublicKeyCredentialDescriptor>>,
387 pub options: Options,
388
389 pub extensions: Option<MakeCredentialExtensionsInput>,
393}
394
395#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
401pub struct MakeCredentialResult {
402 pub authenticator_data: Vec<u8>,
405
406 pub attestation_object: Vec<u8>,
412
413 pub credential_id: Vec<u8>,
417
418 pub extensions: MakeCredentialExtensionsOutput,
425}
426
427impl TryFrom<passkey::types::ctap2::make_credential::Response> for MakeCredentialResult {
428 type Error = WebAuthnEntityError;
429
430 fn try_from(
431 value: passkey::types::ctap2::make_credential::Response,
432 ) -> Result<Self, Self::Error> {
433 let authenticator_data = value.auth_data.to_vec();
434 let attestation_object = value.as_webauthn_bytes().to_vec();
435 let attested_credential_data = value.auth_data.attested_credential_data.ok_or(
436 WebAuthnEntityError::MissingRequiredFields(vec!["attestedCredentialData".to_string()]),
437 )?;
438 let credential_id = attested_credential_data.credential_id().to_vec();
439 let extensions: MakeCredentialExtensionsOutput = value.unsigned_extension_outputs.into();
440 Ok(MakeCredentialResult {
441 authenticator_data,
442 attestation_object,
443 credential_id,
444 extensions,
445 })
446 }
447}
448
449#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
451#[derive(Debug, Default)]
452pub struct MakeCredentialExtensionsInput {
453 pub prf: Option<MakeCredentialPrfInput>,
455}
456
457impl From<MakeCredentialExtensionsInput>
458 for passkey::types::ctap2::make_credential::ExtensionInputs
459{
460 fn from(value: MakeCredentialExtensionsInput) -> Self {
461 Self {
462 hmac_secret: None,
463 hmac_secret_mc: None,
464 prf: value
465 .prf
466 .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
467 }
468 }
469}
470
471impl From<bitwarden_api_api::models::AuthenticationExtensionsClientInputs>
472 for MakeCredentialExtensionsInput
473{
474 fn from(_value: bitwarden_api_api::models::AuthenticationExtensionsClientInputs) -> Self {
475 MakeCredentialExtensionsInput {
476 prf: None,
480 }
481 }
482}
483
484#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
486#[derive(Debug)]
487pub struct MakeCredentialExtensionsOutput {
488 pub prf: Option<MakeCredentialPrfOutput>,
490}
491
492impl From<Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>>
493 for MakeCredentialExtensionsOutput
494{
495 fn from(
496 value: Option<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>,
497 ) -> Self {
498 if let Some(ext) = value {
499 MakeCredentialExtensionsOutput::from(ext)
500 } else {
501 MakeCredentialExtensionsOutput { prf: None }
502 }
503 }
504}
505
506impl From<passkey::types::ctap2::make_credential::UnsignedExtensionOutputs>
507 for MakeCredentialExtensionsOutput
508{
509 fn from(value: passkey::types::ctap2::make_credential::UnsignedExtensionOutputs) -> Self {
510 let prf = value.prf.map(|prf| MakeCredentialPrfOutput {
511 enabled: prf.enabled,
512 results: prf.results.map(|results| results.into()),
513 });
514 MakeCredentialExtensionsOutput { prf }
515 }
516}
517
518#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
520#[derive(Debug)]
521pub struct MakeCredentialPrfInput {
522 pub eval: Option<PrfInputValues>,
524}
525
526impl From<MakeCredentialPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
527 fn from(value: MakeCredentialPrfInput) -> Self {
528 Self {
529 eval: value.eval.map(|v| v.into()),
530 eval_by_credential: None,
531 }
532 }
533}
534
535#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
537#[derive(Debug)]
538pub struct MakeCredentialPrfOutput {
539 pub enabled: bool,
541
542 pub results: Option<PrfOutputValues>,
544}
545
546#[allow(missing_docs)]
547#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
552pub struct GetAssertionRequest {
553 pub rp_id: String,
555
556 pub client_data_hash: Vec<u8>,
558
559 pub allow_list: Option<Vec<PublicKeyCredentialDescriptor>>,
563
564 pub options: Options,
565
566 pub extensions: Option<GetAssertionExtensionsInput>,
568}
569
570#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
576pub struct GetAssertionResult {
577 pub credential_id: Vec<u8>,
581
582 pub authenticator_data: Vec<u8>,
584
585 pub signature: Vec<u8>,
587
588 pub user_handle: Vec<u8>,
590
591 pub selected_credential: SelectedCredential,
593
594 pub extensions: GetAssertionExtensionsOutput,
597}
598
599#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
601#[derive(Debug)]
602pub struct GetAssertionExtensionsInput {
603 pub prf: Option<GetAssertionPrfInput>,
605}
606
607impl From<GetAssertionExtensionsInput> for passkey::types::ctap2::get_assertion::ExtensionInputs {
608 fn from(value: GetAssertionExtensionsInput) -> Self {
609 Self {
610 hmac_secret: None,
611 prf: value
612 .prf
613 .map(passkey::types::ctap2::extensions::AuthenticatorPrfInputs::from),
614 }
615 }
616}
617
618#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
620#[derive(Debug)]
621pub struct GetAssertionExtensionsOutput {
622 pub prf: Option<GetAssertionPrfOutput>,
624}
625
626impl From<Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>>
627 for GetAssertionExtensionsOutput
628{
629 fn from(value: Option<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>) -> Self {
630 if let Some(value) = value {
631 value.into()
632 } else {
633 Self { prf: None }
634 }
635 }
636}
637
638impl From<passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs>
639 for GetAssertionExtensionsOutput
640{
641 fn from(value: passkey::types::ctap2::get_assertion::UnsignedExtensionOutputs) -> Self {
642 let prf = value.prf.map(|prf| GetAssertionPrfOutput {
643 results: prf.results.into(),
644 });
645 GetAssertionExtensionsOutput { prf }
646 }
647}
648
649#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
651#[derive(Debug)]
652pub struct GetAssertionPrfInput {
653 pub eval: Option<PrfInputValues>,
658
659 pub eval_by_credential: Option<HashMap<Vec<u8>, PrfInputValues>>,
663}
664
665impl From<GetAssertionPrfInput> for passkey::types::ctap2::extensions::AuthenticatorPrfInputs {
666 fn from(value: GetAssertionPrfInput) -> Self {
667 let eval_by_credential = if let Some(values) = value.eval_by_credential {
668 let map: HashMap<
669 passkey::types::Bytes,
670 passkey::types::ctap2::extensions::AuthenticatorPrfValues,
671 > = values
672 .into_iter()
673 .map(|(k, v)| (k.into(), v.into()))
674 .collect();
675 Some(map)
676 } else {
677 None
678 };
679 Self {
680 eval: value.eval.map(|v| v.into()),
681 eval_by_credential,
682 }
683 }
684}
685
686#[allow(missing_docs)]
688#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
689#[derive(Debug)]
690pub struct GetAssertionPrfOutput {
691 pub results: PrfOutputValues,
693}
694
695#[allow(missing_docs)]
696#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
697pub struct Options {
698 pub rk: bool,
699 pub uv: UV,
700}
701
702impl From<super::CheckUserOptions> for Options {
703 fn from(value: super::CheckUserOptions) -> Self {
704 Self {
705 rk: value.require_presence,
706 uv: value.require_verification.into(),
707 }
708 }
709}
710
711impl From<Options> for super::CheckUserOptions {
712 fn from(value: Options) -> Self {
713 Self {
714 require_presence: value.rk,
715 require_verification: value.uv.into(),
716 }
717 }
718}
719
720#[derive(Eq, PartialEq, Clone, Copy)]
721#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
722pub enum UV {
723 Discouraged,
724 Preferred,
725 Required,
726}
727
728impl From<UV> for Verification {
729 fn from(value: UV) -> Self {
730 match value {
731 UV::Discouraged => Verification::Discouraged,
732 UV::Preferred => Verification::Preferred,
733 UV::Required => Verification::Required,
734 }
735 }
736}
737
738impl From<bitwarden_api_api::models::UserVerificationRequirement> for UV {
739 fn from(value: bitwarden_api_api::models::UserVerificationRequirement) -> Self {
740 match value {
741 bitwarden_api_api::models::UserVerificationRequirement::Discouraged => UV::Discouraged,
742 bitwarden_api_api::models::UserVerificationRequirement::Preferred => UV::Preferred,
743 bitwarden_api_api::models::UserVerificationRequirement::Required => UV::Required,
744 bitwarden_api_api::models::UserVerificationRequirement::__Unknown(_) => UV::Preferred,
745 }
746 }
747}
748
749impl From<Verification> for UV {
750 fn from(value: Verification) -> Self {
751 match value {
752 Verification::Discouraged => UV::Discouraged,
753 Verification::Preferred => UV::Preferred,
754 Verification::Required => UV::Required,
755 }
756 }
757}
758
759impl From<UserVerificationRequirement> for UV {
760 fn from(value: UserVerificationRequirement) -> Self {
761 match value {
762 UserVerificationRequirement::Discouraged => UV::Discouraged,
763 UserVerificationRequirement::Preferred => UV::Preferred,
764 UserVerificationRequirement::Required => UV::Required,
765 }
766 }
767}
768
769#[allow(missing_docs)]
770#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
771pub enum ClientData {
772 DefaultWithExtraData { android_package_name: String },
773 DefaultWithCustomHash { hash: Vec<u8> },
774}
775
776#[derive(Serialize, Clone)]
777#[serde(rename_all = "camelCase")]
778pub(super) struct AndroidClientData {
779 android_package_name: String,
780}
781
782impl passkey::client::ClientData<Option<AndroidClientData>> for ClientData {
783 fn extra_client_data(&self) -> Option<AndroidClientData> {
784 match self {
785 ClientData::DefaultWithExtraData {
786 android_package_name,
787 } => Some(AndroidClientData {
788 android_package_name: android_package_name.clone(),
789 }),
790 ClientData::DefaultWithCustomHash { .. } => None,
791 }
792 }
793
794 fn client_data_hash(&self) -> Option<Vec<u8>> {
795 match self {
796 ClientData::DefaultWithExtraData { .. } => None,
797 ClientData::DefaultWithCustomHash { hash } => Some(hash.clone()),
798 }
799 }
800}
801
802#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
804pub struct PrfInputValues {
805 pub first: Vec<u8>,
807
808 pub second: Option<Vec<u8>>,
810}
811
812impl PrfInputValues {
813 const WEBAUTHN_PRF_CONTEXT_STRING: &[u8] = b"WebAuthn PRF\0";
814
815 fn hash_webauthn_prf_input(input: &[u8]) -> [u8; 32] {
816 passkey::types::crypto::sha256(&[Self::WEBAUTHN_PRF_CONTEXT_STRING, input].concat())
817 }
818}
819
820impl std::fmt::Debug for PrfInputValues {
821 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
822 f.debug_struct("PrfInputValues")
823 .field("first", &"********")
824 .field("second", &self.second.as_ref().map(|_| "********"))
825 .finish()
826 }
827}
828
829impl From<PrfInputValues> for passkey::types::ctap2::extensions::AuthenticatorPrfValues {
830 fn from(value: PrfInputValues) -> Self {
833 let first = PrfInputValues::hash_webauthn_prf_input(value.first.as_ref());
836 let second = value
837 .second
838 .as_deref()
839 .map(PrfInputValues::hash_webauthn_prf_input);
840 Self { first, second }
841 }
842}
843
844#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
846pub struct PrfOutputValues {
847 pub first: Vec<u8>,
849
850 pub second: Option<Vec<u8>>,
852}
853
854impl std::fmt::Debug for PrfOutputValues {
855 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
856 f.debug_struct("PrfOutputValues")
857 .field("first", &"********")
858 .field("second", &self.second.as_ref().map(|_| "********"))
859 .finish()
860 }
861}
862
863impl From<passkey::types::ctap2::extensions::AuthenticatorPrfValues> for PrfOutputValues {
864 fn from(value: passkey::types::ctap2::extensions::AuthenticatorPrfValues) -> Self {
865 Self {
866 first: value.first.to_vec(),
867 second: value.second.map(|s| s.to_vec()),
868 }
869 }
870}
871#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
872pub struct ClientExtensionResults {
873 pub cred_props: Option<CredPropsResult>,
874}
875
876#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
877pub struct CredPropsResult {
878 pub rk: Option<bool>,
879}
880
881impl From<passkey::types::webauthn::CredentialPropertiesOutput> for CredPropsResult {
882 fn from(value: passkey::types::webauthn::CredentialPropertiesOutput) -> Self {
883 Self {
884 rk: value.discoverable,
885 }
886 }
887}
888
889#[allow(missing_docs)]
890#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
891pub struct PublicKeyCredentialAuthenticatorAttestationResponse {
892 pub id: String,
893 pub raw_id: Vec<u8>,
894 pub ty: String,
895 pub authenticator_attachment: Option<String>,
896 pub client_extension_results: ClientExtensionResults,
897 pub response: AuthenticatorAttestationResponse,
898 pub selected_credential: SelectedCredential,
899}
900
901#[allow(missing_docs)]
902#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
903pub struct AuthenticatorAttestationResponse {
904 pub client_data_json: Vec<u8>,
905 pub authenticator_data: Vec<u8>,
906 pub public_key: Option<Vec<u8>>,
907 pub public_key_algorithm: i64,
908 pub attestation_object: Vec<u8>,
909 pub transports: Option<Vec<String>>,
910}
911
912#[allow(missing_docs)]
913#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
914pub struct PublicKeyCredentialAuthenticatorAssertionResponse {
915 pub id: String,
916 pub raw_id: Vec<u8>,
917 pub ty: String,
918 pub authenticator_attachment: Option<String>,
919 pub client_extension_results: ClientExtensionResults,
920 pub response: AuthenticatorAssertionResponse,
921 pub selected_credential: SelectedCredential,
922}
923
924#[allow(missing_docs)]
925#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
926pub struct AuthenticatorAssertionResponse {
927 pub client_data_json: Vec<u8>,
928 pub authenticator_data: Vec<u8>,
929 pub signature: Vec<u8>,
930 pub user_handle: Vec<u8>,
931}
932
933#[derive(Debug, Error)]
934#[error("Invalid origin: {0}")]
935pub struct InvalidOriginError(String);
936
937#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
938pub struct UnverifiedAssetLink {
940 package_name: String,
942 sha256_cert_fingerprint: String,
944 host: String,
946 asset_link_url: Option<String>,
949}
950
951#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
952pub enum Origin {
954 Web(String),
956 Android(UnverifiedAssetLink),
959}
960
961impl TryFrom<Origin> for passkey::client::Origin<'_> {
962 type Error = InvalidOriginError;
963
964 fn try_from(value: Origin) -> Result<Self, Self::Error> {
965 Ok(match value {
966 Origin::Web(url) => {
967 let url = Url::parse(&url).map_err(|e| InvalidOriginError(format!("{e}")))?;
968 passkey::client::Origin::Web(Cow::Owned(url))
969 }
970 Origin::Android(link) => passkey::client::Origin::Android(link.try_into()?),
971 })
972 }
973}
974
975impl TryFrom<UnverifiedAssetLink> for passkey::client::UnverifiedAssetLink<'_> {
976 type Error = InvalidOriginError;
977
978 fn try_from(value: UnverifiedAssetLink) -> Result<Self, Self::Error> {
979 let asset_link_url = {
980 let url = value
981 .asset_link_url
982 .unwrap_or_else(|| format!("https://{}/.well-known/assetlinks.json", value.host));
983 Url::parse(&url).map_err(|e| InvalidOriginError(e.to_string()))?
984 };
985
986 passkey::client::UnverifiedAssetLink::new(
987 Cow::from(value.package_name),
988 value.sha256_cert_fingerprint.as_str(),
989 Cow::from(value.host),
990 asset_link_url,
991 )
992 .map_err(|e| InvalidOriginError(format!("{e:?}")))
993 }
994}
995
996#[cfg(test)]
997mod tests {
998 use std::collections::HashMap;
999
1000 use passkey::types::ctap2::{
1001 extensions::{
1002 AuthenticatorPrfGetOutputs, AuthenticatorPrfMakeOutputs, AuthenticatorPrfValues,
1003 },
1004 get_assertion, make_credential,
1005 };
1006 use serde::{Deserialize, Serialize};
1007
1008 use super::{
1009 AndroidClientData, GetAssertionExtensionsInput, GetAssertionExtensionsOutput,
1010 GetAssertionPrfInput, MakeCredentialExtensionsInput, MakeCredentialExtensionsOutput,
1011 MakeCredentialPrfInput, PrfInputValues,
1012 };
1013
1014 static TEST_SALT1_RAW_INPUT: &[u8] = b"salt1";
1016
1017 static TEST_SALT1_WEBAUTHN_INPUT: [u8; 32] = [
1020 0x2A, 0x19, 0x90, 0xF9, 0xC9, 0xBB, 0xFE, 0x1B, 0xBF, 0x56, 0xAB, 0xEE, 0x2B, 0x5A, 0x0F,
1021 0x59, 0xBE, 0x5F, 0x63, 0x3A, 0x35, 0xC2, 0xA5, 0xF0, 0x7D, 0x85, 0x53, 0x3E, 0xEE, 0xCB,
1022 0xDD, 0x3C,
1023 ];
1024
1025 static TEST_SALT2_RAW_INPUT: &[u8] = b"salt2";
1027
1028 static TEST_SALT2_WEBAUTHN_INPUT: [u8; 32] = [
1032 0xA6, 0x42, 0xFA, 0x8B, 0x6E, 0xAC, 0x68, 0xD3, 0x73, 0xCF, 0x08, 0xEA, 0xC8, 0x5E, 0x1D,
1033 0x62, 0x9B, 0x50, 0x10, 0x6D, 0x60, 0xEB, 0x92, 0x48, 0xEC, 0xB6, 0x54, 0xE2, 0x94, 0x9A,
1034 0xDD, 0x65,
1035 ];
1036
1037 #[derive(Serialize, Deserialize)]
1041 #[serde(rename_all = "camelCase")]
1042 pub struct CollectedClientData<E = ()>
1043 where
1044 E: Serialize,
1045 {
1046 pub origin: String,
1047
1048 #[serde(flatten)]
1049 pub extra_data: E,
1050 }
1051
1052 #[test]
1053 fn test_serialize_unit_data() {
1054 let data = CollectedClientData {
1055 origin: "https://example.com".to_owned(),
1056 extra_data: (),
1057 };
1058
1059 let serialized = serde_json::to_string(&data).unwrap();
1060 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
1061 }
1062
1063 #[test]
1064 fn test_serialize_none_data() {
1065 let data = CollectedClientData {
1066 origin: "https://example.com".to_owned(),
1067 extra_data: Option::<AndroidClientData>::None,
1068 };
1069
1070 let serialized = serde_json::to_string(&data).unwrap();
1071 assert_eq!(serialized, r#"{"origin":"https://example.com"}"#);
1072 }
1073
1074 #[test]
1075 fn test_serialize_android_data() {
1076 let data = CollectedClientData {
1077 origin: "https://example.com".to_owned(),
1078 extra_data: Some(AndroidClientData {
1079 android_package_name: "com.example.app".to_owned(),
1080 }),
1081 };
1082
1083 let serialized = serde_json::to_string(&data).unwrap();
1084 assert_eq!(
1085 serialized,
1086 r#"{"origin":"https://example.com","androidPackageName":"com.example.app"}"#
1087 );
1088 }
1089
1090 #[test]
1091 fn test_transform_make_credential_extension_input() {
1092 let input = MakeCredentialExtensionsInput {
1093 prf: Some(MakeCredentialPrfInput {
1094 eval: Some(PrfInputValues {
1095 first: TEST_SALT1_RAW_INPUT.to_vec(),
1096 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1097 }),
1098 }),
1099 };
1100 let transformed = make_credential::ExtensionInputs::from(input);
1101 let eval = transformed.prf.unwrap().eval.unwrap();
1102 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
1103 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
1104 }
1105
1106 #[test]
1107 fn test_transform_make_credential_extension_output() {
1108 let prf1: Vec<u8> = (0..32).collect();
1109 let output = make_credential::UnsignedExtensionOutputs {
1110 prf: Some(AuthenticatorPrfMakeOutputs {
1111 enabled: true,
1112 results: Some(AuthenticatorPrfValues {
1113 first: prf1.clone().try_into().unwrap(),
1114 second: None,
1115 }),
1116 }),
1117 };
1118 let transformed = MakeCredentialExtensionsOutput::from(output);
1119 assert!(transformed.prf.as_ref().unwrap().enabled);
1120 assert_eq!(prf1, transformed.prf.unwrap().results.unwrap().first);
1121 }
1122
1123 #[test]
1124 fn test_transform_get_assertion_extension_input() {
1125 let input = GetAssertionExtensionsInput {
1126 prf: Some(GetAssertionPrfInput {
1127 eval: Some(PrfInputValues {
1128 first: TEST_SALT1_RAW_INPUT.to_vec(),
1129 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1130 }),
1131 eval_by_credential: None,
1132 }),
1133 };
1134 let transformed = get_assertion::ExtensionInputs::from(input);
1135 let eval = transformed.prf.unwrap().eval.unwrap();
1136 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, eval.first);
1137 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, eval.second.unwrap());
1138 }
1139
1140 #[test]
1141 fn test_transform_get_assertion_extension_input_with_eval_by_credential() {
1142 let cred_id = b"credential_id1".to_vec();
1143 let input = GetAssertionExtensionsInput {
1144 prf: Some(GetAssertionPrfInput {
1145 eval: None,
1146 eval_by_credential: Some(HashMap::from([(
1147 cred_id.clone(),
1148 PrfInputValues {
1149 first: TEST_SALT1_RAW_INPUT.to_vec(),
1150 second: Some(TEST_SALT2_RAW_INPUT.to_vec()),
1151 },
1152 )])),
1153 }),
1154 };
1155 let transformed = get_assertion::ExtensionInputs::from(input);
1156 let output = transformed.prf.unwrap().eval_by_credential.unwrap();
1157 let results = output.get(&cred_id.into()).unwrap();
1158 assert_eq!(TEST_SALT1_WEBAUTHN_INPUT, results.first);
1159 assert_eq!(TEST_SALT2_WEBAUTHN_INPUT, results.second.unwrap());
1160 }
1161
1162 #[test]
1163 fn test_transform_get_assertion_extension_output() {
1164 let prf1: Vec<u8> = (0..32).collect();
1165 let output = get_assertion::UnsignedExtensionOutputs {
1166 prf: Some(AuthenticatorPrfGetOutputs {
1167 results: AuthenticatorPrfValues {
1168 first: prf1.clone().try_into().unwrap(),
1169 second: None,
1170 },
1171 }),
1172 };
1173 let transformed = GetAssertionExtensionsOutput::from(output);
1174 assert_eq!(prf1, transformed.prf.unwrap().results.first);
1175 }
1176}