diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index eebf0a08a22..fd3f22384c2 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -51,8 +51,10 @@ import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/ma import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { NavigatorCredentialsService } from "@bitwarden/common/auth/abstractions/webauthn/navigator-credentials.service"; import { AuthRequestAnsweringService } from "@bitwarden/common/auth/services/auth-request-answering/auth-request-answering.service"; import { PendingAuthRequestsStateService } from "@bitwarden/common/auth/services/auth-request-answering/pending-auth-requests.state"; +import { DefaultNavigatorCredentialsService } from "@bitwarden/common/auth/services/webauthn-login/default-navigator-credentials.service"; import { AutofillSettingsService, AutofillSettingsServiceAbstraction, @@ -717,6 +719,11 @@ const safeProviders: SafeProvider[] = [ useClass: ExtensionNewDeviceVerificationComponentService, deps: [], }), + safeProvider({ + provide: NavigatorCredentialsService, + useClass: DefaultNavigatorCredentialsService, + deps: [WINDOW, PlatformUtilsService], + }), safeProvider({ provide: SessionTimeoutSettingsComponentService, useClass: BrowserSessionTimeoutSettingsComponentService, diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 4da82144305..43c57b87eed 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -193,6 +193,45 @@ dependencies = [ "nom", ] +[[package]] +name = "asn1-rs" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "async-broadcast" version = "0.7.2" @@ -608,7 +647,7 @@ dependencies = [ "async-trait", "base64", "cbc", - "dirs", + "dirs 6.0.0", "hex", "oo7", "pbkdf2", @@ -692,7 +731,7 @@ checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81" dependencies = [ "serde", "termcolor", - "unicode-width", + "unicode-width 0.2.0", ] [[package]] @@ -779,6 +818,29 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctap-hid-fido2" +version = "3.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6473a333d82796d5b23529fde19386de33820ad19d368402f8e9de780e1723" +dependencies = [ + "aes", + "anyhow", + "base64", + "byteorder", + "cbc", + "hex", + "hidapi", + "num", + "pad", + "ring", + "serde", + "serde_cbor", + "strum", + "strum_macros", + "x509-parser", +] + [[package]] name = "ctor" version = "0.2.9" @@ -900,6 +962,12 @@ dependencies = [ "syn", ] +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + [[package]] name = "der" version = "0.7.10" @@ -911,6 +979,29 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + [[package]] name = "desktop_core" version = "0.0.0" @@ -927,7 +1018,7 @@ dependencies = [ "chacha20poly1305", "core-foundation", "desktop_objc", - "dirs", + "dirs 6.0.0", "ed25519", "futures", "homedir", @@ -975,6 +1066,7 @@ dependencies = [ "base64", "chromium_importer", "desktop_core", + "fido2_client", "hex", "napi", "napi-build", @@ -1029,13 +1121,34 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys 0.4.1", +] + [[package]] name = "dirs" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" dependencies = [ - "dirs-sys", + "dirs-sys 0.5.0", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users 0.4.6", + "windows-sys 0.48.0", ] [[package]] @@ -1046,7 +1159,7 @@ checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" dependencies = [ "libc", "option-ext", - "redox_users", + "redox_users 0.5.0", "windows-sys 0.61.2", ] @@ -1143,6 +1256,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "elliptic-curve" version = "0.13.8" @@ -1288,6 +1407,18 @@ version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" +[[package]] +name = "fido2_client" +version = "0.0.0" +dependencies = [ + "base64", + "ctap-hid-fido2", + "pinentry", + "secrecy", + "serde", + "sha2", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1520,6 +1651,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + [[package]] name = "hashbrown" version = "0.15.3" @@ -1556,6 +1693,18 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hidapi" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "798154e4b6570af74899d71155fb0072d5b17e6aa12f39c8ef22c60fb8ec99e7" +dependencies = [ + "cc", + "libc", + "pkg-config", + "winapi", +] + [[package]] name = "hkdf" version = "0.12.4" @@ -2157,6 +2306,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -2290,6 +2445,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "oid-registry" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -2397,6 +2561,15 @@ dependencies = [ "sha2", ] +[[package]] +name = "pad" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3" +dependencies = [ + "unicode-width 0.1.14", +] + [[package]] name = "parking" version = "2.2.1" @@ -2499,6 +2672,20 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pinentry" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72268b7db3a2075ea65d4b93b755d086e99196e327837e690db6559b393a8d69" +dependencies = [ + "log", + "nom", + "percent-encoding", + "secrecy", + "which", + "zeroize", +] + [[package]] name = "piper" version = "0.2.4" @@ -2607,6 +2794,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -2777,6 +2970,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.16", + "libredox", + "thiserror 1.0.69", +] + [[package]] name = "redox_users" version = "0.5.0" @@ -2827,6 +3031,20 @@ dependencies = [ "subtle", ] +[[package]] +name = "ring" +version = "0.17.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75ec5e92c4d8aede845126adc388046234541629e76029599ed35a003c7ed24" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.6" @@ -2887,6 +3105,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "0.38.44" @@ -3031,6 +3258,15 @@ dependencies = [ "windows 0.61.1", ] +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + [[package]] name = "security-framework" version = "3.5.0" @@ -3072,6 +3308,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + [[package]] name = "serde_derive" version = "1.0.209" @@ -3315,6 +3561,25 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.6.1" @@ -3443,6 +3708,37 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -3693,6 +3989,12 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + [[package]] name = "unicode-width" version = "0.2.0" @@ -3839,6 +4141,12 @@ dependencies = [ "subtle", ] +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -4007,6 +4315,18 @@ dependencies = [ "nom", ] +[[package]] +name = "which" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad25fe5717e59ada8ea33511bbbf7420b11031730a24c65e82428766c307006" +dependencies = [ + "dirs 5.0.1", + "either", + "once_cell", + "rustix 0.38.44", +] + [[package]] name = "widestring" version = "1.2.0" @@ -4242,6 +4562,15 @@ dependencies = [ "windows-targets 0.42.2", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" @@ -4600,6 +4929,23 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ec107c4503ea0b4a98ef47356329af139c0a4f7750e621cf2973cd3385ebcb3d" +[[package]] +name = "x509-parser" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror 1.0.69", + "time", +] + [[package]] name = "yoke" version = "0.8.0" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index ccf7c1f3796..008eaa3d12c 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -5,6 +5,7 @@ members = [ "bitwarden_chromium_import_helper", "chromium_importer", "core", + "fido2_client", "macos_provider", "napi", "process_isolation", diff --git a/apps/desktop/desktop_native/fido2_client/Cargo.toml b/apps/desktop/desktop_native/fido2_client/Cargo.toml new file mode 100644 index 00000000000..d493a08251c --- /dev/null +++ b/apps/desktop/desktop_native/fido2_client/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "fido2_client" +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } +publish = { workspace = true } + +[dependencies] +base64 = { workspace = true } +ctap-hid-fido2 = "3.5.1" +pinentry = "0.5.0" +serde = { workspace = true, features = ["derive"] } +secrecy = "0.8.0" +sha2 = { workspace = true } diff --git a/apps/desktop/desktop_native/fido2_client/src/ctap_hid_fido2.rs b/apps/desktop/desktop_native/fido2_client/src/ctap_hid_fido2.rs new file mode 100644 index 00000000000..bc21b3f714a --- /dev/null +++ b/apps/desktop/desktop_native/fido2_client/src/ctap_hid_fido2.rs @@ -0,0 +1,145 @@ +use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine}; +use ctap_hid_fido2::{ + fidokey::{AssertionExtension, GetAssertionArgsBuilder}, + Cfg, FidoKeyHidFactory, +}; +use pinentry::PassphraseInput; +use secrecy::ExposeSecret; + +use crate::{ + prf_to_hmac, AuthenticatorAssertionResponse, Fido2ClientError, PublicKeyCredential, + PublicKeyCredentialRequestOptions, +}; + +fn get_pin() -> Option { + if let Some(mut input) = PassphraseInput::with_default_binary() { + input + .with_description("Enter your FIDO2 Authenticator PIN:") + .with_prompt("PIN:") + .interact() + .ok() + .map(|p| p.expose_secret().to_owned()) + } else { + None + } +} + +pub fn available() -> bool { + true +} + +fn make_assertion( + options: PublicKeyCredentialRequestOptions, + client_data_json: String, + credential: Option<&[u8]>, +) -> Result { + let mut get_assertion_args = + GetAssertionArgsBuilder::new(options.rp_id.as_str(), client_data_json.as_bytes()) + .extensions(&[AssertionExtension::HmacSecret(Some(prf_to_hmac( + &options.prf_eval_first, + )))]); + + if let Some(cred) = credential { + get_assertion_args = get_assertion_args.credential_id(cred); + } + Ok(get_assertion_args) +} + +pub fn get( + options: PublicKeyCredentialRequestOptions, +) -> Result { + let device = FidoKeyHidFactory::create(&Cfg::init()).map_err(|_| Fido2ClientError::NoDevice)?; + + let client_data_json = format!( + r#"{{"type":"webauthn.get","challenge":"{}","origin":"https://{}","crossOrigin": true}}"#, + BASE64_URL_SAFE_NO_PAD.encode(&options.challenge), + options.rp_id + ); + + let mut get_assertion_args = make_assertion( + options.clone(), + client_data_json.clone(), + options.allow_credentials.get(0).map(|v| v.as_slice()), + )?; + + let pin: String; + if options.user_verification == crate::UserVerification::Required + || options.user_verification == crate::UserVerification::Preferred + { + pin = get_pin().ok_or(Fido2ClientError::WrongPin)?; + get_assertion_args = get_assertion_args.pin(pin.as_str()); + } + + let mut assertions = device + .get_assertion_with_args(&get_assertion_args.build()) + .map_err(|_e| Fido2ClientError::AssertionError)?; + + let assertion = if assertions.len() > 1 { + let first_assertion = &assertions[0]; + let mut get_assertion_args = make_assertion( + options.clone(), + client_data_json.clone(), + Some(&first_assertion.credential_id), + )?; + let pin: String; + if options.user_verification == crate::UserVerification::Required + || options.user_verification == crate::UserVerification::Preferred + { + pin = get_pin().ok_or(Fido2ClientError::WrongPin)?; + get_assertion_args = get_assertion_args.pin(pin.as_str()); + } + + assertions = device + .get_assertion_with_args(&get_assertion_args.build()) + .map_err(|_e| Fido2ClientError::AssertionError)?; + assertions.get(0).ok_or(Fido2ClientError::AssertionError)? + } else { + assertions.get(0).ok_or(Fido2ClientError::AssertionError)? + }; + + let prf_extension = assertion + .extensions + .iter() + .find_map(|ext| { + if let AssertionExtension::HmacSecret(results) = ext { + Some(*results) + } else { + None + } + }) + .flatten(); + + Ok(PublicKeyCredential { + authenticator_attachment: "cross-platform".to_string(), + id: BASE64_URL_SAFE_NO_PAD.encode(&assertion.credential_id), + raw_id: assertion.credential_id.clone(), + response: AuthenticatorAssertionResponse { + authenticator_data: assertion.auth_data.clone(), + client_data_json: client_data_json.as_bytes().to_vec(), + signature: assertion.signature.clone(), + user_handle: assertion.user.id.clone(), + }, + r#type: "public-key".to_string(), + prf: prf_extension, + }) +} + +#[cfg(test)] +mod tests { + use crate::{ctap_hid_fido2::get, PublicKeyCredentialRequestOptions}; + + #[test] + #[ignore] + fn assertion() { + get(PublicKeyCredentialRequestOptions { + challenge: vec![], + timeout: 0, + rp_id: "vault.usdev.bitwarden.pw".to_string(), + user_verification: crate::UserVerification::Required, + allow_credentials: vec![], + prf_eval_first: [0u8; 32], + prf_eval_second: None, + }) + .unwrap(); + } +} diff --git a/apps/desktop/desktop_native/fido2_client/src/lib.rs b/apps/desktop/desktop_native/fido2_client/src/lib.rs new file mode 100644 index 00000000000..addcc588776 --- /dev/null +++ b/apps/desktop/desktop_native/fido2_client/src/lib.rs @@ -0,0 +1,78 @@ +#[cfg(all(target_os = "linux", target_env = "gnu"))] +mod ctap_hid_fido2; +#[cfg(all(target_os = "linux", target_env = "gnu"))] +use ctap_hid_fido2::*; + +#[cfg(not(all(target_os = "linux", target_env = "gnu")))] +mod unimplemented; +#[cfg(not(all(target_os = "linux", target_env = "gnu")))] +use unimplemented::*; + +#[cfg(all(target_os = "linux", target_env = "gnu"))] +/// Depending on the platform API, the platform MAY do this for you, or may require you to do it manually. +fn prf_to_hmac(prf_salt: &[u8]) -> [u8; 32] { + use sha2::Digest; + sha2::Sha256::digest(&[b"WebAuthn PRF".as_slice(), &[0], prf_salt].concat()).into() +} + +#[derive(Debug, PartialEq, Clone)] +pub enum UserVerification { + Discouraged, + Preferred, + Required, +} + +#[derive(Debug, Clone)] +pub struct PrfConfig { + pub first: Vec, + pub second: Option>, +} + +#[derive(Debug, Clone)] +pub struct PublicKeyCredentialRequestOptions { + pub challenge: Vec, + pub timeout: u64, + pub rp_id: String, + pub user_verification: UserVerification, + pub allow_credentials: Vec>, + pub prf: Option, +} + +#[derive(Debug)] +pub struct AuthenticatorAssertionResponse { + pub authenticator_data: Vec, + pub client_data_json: Vec, + pub signature: Vec, + pub user_handle: Vec, +} + +#[derive(Debug)] +pub struct PublicKeyCredential { + pub authenticator_attachment: String, + pub id: String, + pub raw_id: Vec, + pub response: AuthenticatorAssertionResponse, + pub r#type: String, + pub prf: Option<[u8; 32]>, +} + +#[derive(Debug)] +pub enum Fido2ClientError { + WrongPin, + NoCredentials, + NoDevice, + InvalidInput, + AssertionError, +} + +pub mod fido2_client { + pub fn get( + assertion_options: super::PublicKeyCredentialRequestOptions, + ) -> Result { + super::get(assertion_options) + } + + pub fn available() -> bool { + super::available() + } +} diff --git a/apps/desktop/desktop_native/fido2_client/src/unimplemented.rs b/apps/desktop/desktop_native/fido2_client/src/unimplemented.rs new file mode 100644 index 00000000000..9653c0ebaee --- /dev/null +++ b/apps/desktop/desktop_native/fido2_client/src/unimplemented.rs @@ -0,0 +1,11 @@ +use crate::{Fido2ClientError, PublicKeyCredential, PublicKeyCredentialRequestOptions}; + +pub fn get( + _options: PublicKeyCredentialRequestOptions, +) -> Result { + todo!("Fido2Client is unimplemented on this platform"); +} + +pub fn available() -> bool { + false +} diff --git a/apps/desktop/desktop_native/napi/Cargo.toml b/apps/desktop/desktop_native/napi/Cargo.toml index 4198baa4b5a..21e976dfd20 100644 --- a/apps/desktop/desktop_native/napi/Cargo.toml +++ b/apps/desktop/desktop_native/napi/Cargo.toml @@ -19,6 +19,7 @@ autotype = { path = "../autotype" } base64 = { workspace = true } chromium_importer = { path = "../chromium_importer" } desktop_core = { path = "../core" } +fido2_client = { path = "../fido2_client" } hex = { workspace = true } napi = { workspace = true, features = ["async"] } napi-derive = { workspace = true } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 0a8beb8c427..a84953b3ba8 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -254,3 +254,38 @@ export declare namespace autotype { export function getForegroundWindowTitle(): string export function typeInput(input: Array, keyboardShortcut: Array): void } +export declare namespace navigator_credentials { + export const enum UserVerification { + Preferred = 'Preferred', + Required = 'Required', + Discouraged = 'Discouraged' + } + export interface PrfConfig { + first: Uint8Array + second?: Uint8Array + } + export interface PublicKeyCredentialRequestOptions { + challenge: Uint8Array + timeout: number + rpId: string + userVerification: UserVerification + allowCredentials: Array + prf?: PrfConfig + } + export interface AuthenticatorAssertionResponse { + authenticatorData: Uint8Array + clientDataJson: Uint8Array + signature: Uint8Array + userHandle: Uint8Array + } + export interface PublicKeyCredential { + authenticatorAttachment: string + id: string + rawId: Uint8Array + response: AuthenticatorAssertionResponse + type: string + prf?: Uint8Array + } + export function get(assertionOptions: PublicKeyCredentialRequestOptions): PublicKeyCredential + export function available(): boolean +} diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 39e57bd0bb5..f4ab8aca29c 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1199,3 +1199,125 @@ pub mod autotype { }) } } + +#[napi] +pub mod navigator_credentials { + use napi::bindgen_prelude::Uint8Array; + + #[napi(string_enum)] + pub enum UserVerification { + Preferred, + Required, + Discouraged, + } + + impl Into for UserVerification { + fn into(self) -> fido2_client::UserVerification { + match self { + UserVerification::Preferred => fido2_client::UserVerification::Preferred, + UserVerification::Required => fido2_client::UserVerification::Required, + UserVerification::Discouraged => fido2_client::UserVerification::Discouraged, + } + } + } + + #[napi(object)] + pub struct PrfConfig { + pub first: Uint8Array, + pub second: Option, + } + + impl Into for PrfConfig { + fn into(self) -> fido2_client::PrfConfig { + fido2_client::PrfConfig { + first: self.first.to_vec(), + second: self.second.map(|s| s.to_vec()), + } + } + } + + #[napi(object)] + pub struct PublicKeyCredentialRequestOptions { + pub challenge: Uint8Array, + pub timeout: i64, + pub rp_id: String, + pub user_verification: UserVerification, + pub allow_credentials: Vec, + pub prf: Option, + } + + impl TryInto + for PublicKeyCredentialRequestOptions + { + type Error = napi::Error; + + fn try_into(self) -> Result { + Ok(fido2_client::PublicKeyCredentialRequestOptions { + challenge: self.challenge.to_vec(), + timeout: self.timeout as u64, + rp_id: self.rp_id, + user_verification: self.user_verification.into(), + allow_credentials: self.allow_credentials.iter().map(|c| c.to_vec()).collect(), + prf: self.prf.map(|p| p.into()), + }) + } + } + + #[napi(object)] + pub struct AuthenticatorAssertionResponse { + pub authenticator_data: Uint8Array, + pub client_data_json: Uint8Array, + pub signature: Uint8Array, + pub user_handle: Uint8Array, + } + + impl From for AuthenticatorAssertionResponse { + fn from(response: fido2_client::AuthenticatorAssertionResponse) -> Self { + AuthenticatorAssertionResponse { + authenticator_data: Uint8Array::from(response.authenticator_data), + client_data_json: Uint8Array::from(response.client_data_json), + signature: Uint8Array::from(response.signature), + user_handle: Uint8Array::from(response.user_handle), + } + } + } + + #[napi(object)] + pub struct PublicKeyCredential { + pub authenticator_attachment: String, + pub id: String, + pub raw_id: Uint8Array, + pub response: AuthenticatorAssertionResponse, + pub r#type: String, + pub prf: Option, + } + + impl Into for fido2_client::PublicKeyCredential { + fn into(self) -> PublicKeyCredential { + PublicKeyCredential { + authenticator_attachment: self.authenticator_attachment, + id: self.id, + raw_id: Uint8Array::from(self.raw_id), + response: self.response.into(), + r#type: self.r#type, + prf: self.prf.map(|p| Uint8Array::from(p)), + } + } + } + + #[napi] + pub fn get( + assertion_options: PublicKeyCredentialRequestOptions, + ) -> napi::Result { + let options: fido2_client::PublicKeyCredentialRequestOptions = + assertion_options.try_into()?; + fido2_client::fido2_client::get(options) + .map_err(|e| napi::Error::from_reason(format!("FIDO2 Authentication failed: {:?}", e))) + .map(|credential| credential.into()) + } + + #[napi] + pub fn available() -> bool { + fido2_client::fido2_client::available() + } +} diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index b6e86ba19ff..2deaa02dc46 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -12,6 +12,7 @@ import { tdeDecryptionRequiredGuard, unauthGuardFn, } from "@bitwarden/angular/auth/guards"; +import { LoginViaWebAuthnComponent } from "@bitwarden/angular/auth/login-via-webauthn/login-via-webauthn.component"; import { ChangePasswordComponent } from "@bitwarden/angular/auth/password-management/change-password"; import { SetInitialPasswordComponent } from "@bitwarden/angular/auth/password-management/set-initial-password/set-initial-password.component"; import { @@ -23,6 +24,7 @@ import { VaultIcon, LockIcon, DomainIcon, + TwoFactorAuthSecurityKeyIcon, } from "@bitwarden/assets/svg"; import { LoginComponent, @@ -123,6 +125,27 @@ const routes: Routes = [ path: "", component: AnonLayoutWrapperComponent, children: [ + { + path: AuthRoute.LoginWithPasskey, + canActivate: [unauthGuardFn()], + data: { + pageIcon: TwoFactorAuthSecurityKeyIcon, + pageTitle: { + key: "logInWithPasskey", + }, + pageSubtitle: { + key: "readingPasskeyLoadingInfo", + }, + } satisfies RouteDataProperties & AnonLayoutWrapperData, + children: [ + { path: "", component: LoginViaWebAuthnComponent }, + { + path: "", + component: EnvironmentSelectorComponent, + outlet: "environment-selector", + }, + ], + }, { path: AuthRoute.SignUp, canActivate: [unauthGuardFn()], diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index 03d6eb5c908..8de6c15b464 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -51,6 +51,7 @@ import { } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { NavigatorCredentialsService } from "@bitwarden/common/auth/abstractions/webauthn/navigator-credentials.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { ClientType } from "@bitwarden/common/enums"; @@ -119,6 +120,7 @@ import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarde import { DesktopLoginApprovalDialogComponentService } from "../../auth/login/desktop-login-approval-dialog-component.service"; import { DesktopLoginComponentService } from "../../auth/login/desktop-login-component.service"; import { DesktopTwoFactorAuthDuoComponentService } from "../../auth/services/desktop-two-factor-auth-duo-component.service"; +import { RendererNavigatorCredentialsService } from "../../auth/services/renderer-navigator-credentials.service"; import { DesktopAutofillSettingsService } from "../../autofill/services/desktop-autofill-settings.service"; import { DesktopAutofillService } from "../../autofill/services/desktop-autofill.service"; import { DesktopAutotypeDefaultSettingPolicy } from "../../autofill/services/desktop-autotype-policy.service"; @@ -310,6 +312,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebCryptoFunctionService, deps: [WINDOW], }), + safeProvider({ + provide: NavigatorCredentialsService, + useClass: RendererNavigatorCredentialsService, + deps: [], + }), safeProvider({ provide: KeyServiceAbstraction, useClass: ElectronKeyService, diff --git a/apps/desktop/src/auth/preload.ts b/apps/desktop/src/auth/preload.ts index 7c213934659..bb6c624d49e 100644 --- a/apps/desktop/src/auth/preload.ts +++ b/apps/desktop/src/auth/preload.ts @@ -1,5 +1,7 @@ import { ipcRenderer } from "electron"; +import { navigator_credentials } from "@bitwarden/desktop-napi"; + export default { loginRequest: (alertTitle: string, alertBody: string, buttonText: string): Promise => ipcRenderer.invoke("loginRequest", { @@ -7,4 +9,10 @@ export default { alertBody, buttonText, }), + navigatorCredentialsGet: ( + options: navigator_credentials.PublicKeyCredentialRequestOptions, + ): Promise => + ipcRenderer.invoke("navigatorCredentials.get", options), + navigatorCredentialsAvailable: (): Promise => + ipcRenderer.invoke("navigatorCredentials.available"), }; diff --git a/apps/desktop/src/auth/services/main-navigator-credentials.serivce.ts b/apps/desktop/src/auth/services/main-navigator-credentials.serivce.ts new file mode 100644 index 00000000000..88f1def5673 --- /dev/null +++ b/apps/desktop/src/auth/services/main-navigator-credentials.serivce.ts @@ -0,0 +1,17 @@ +import { ipcMain } from "electron"; + +import { navigator_credentials } from "@bitwarden/desktop-napi"; + +export class MainNavigatorCredentialsService { + constructor() { + ipcMain.handle( + "navigatorCredentials.get", + async (_event: any, message: navigator_credentials.PublicKeyCredentialRequestOptions) => { + return navigator_credentials.get(message); + }, + ); + ipcMain.handle("navigatorCredentials.available", async (_event: any, _message: any) => { + return navigator_credentials.available(); + }); + } +} diff --git a/apps/desktop/src/auth/services/renderer-navigator-credentials.service.ts b/apps/desktop/src/auth/services/renderer-navigator-credentials.service.ts new file mode 100644 index 00000000000..3b015c4f66a --- /dev/null +++ b/apps/desktop/src/auth/services/renderer-navigator-credentials.service.ts @@ -0,0 +1,52 @@ +import { navigator_credentials } from "apps/desktop/desktop_native/napi"; + +import { NavigatorCredentialsService } from "@bitwarden/common/auth/abstractions/webauthn/navigator-credentials.service"; + +export class RendererNavigatorCredentialsService implements NavigatorCredentialsService { + constructor() {} + + async get(options: CredentialRequestOptions): Promise { + return await ipc.auth.navigatorCredentialsGet({ + challenge: arrayBufferSourceToUint8Array(options.publicKey.challenge), + timeout: options.publicKey.timeout, + rpId: options.publicKey.rpId, + userVerification: convertUserVerification(options.publicKey.userVerification), + allowCredentials: options.publicKey.allowCredentials.map((cred) => { + return arrayBufferSourceToUint8Array(cred.id); + }), + prf: options.publicKey.extensions?.prf + ? { + first: arrayBufferSourceToUint8Array(options.publicKey.extensions!.prf!.eval.first), + second: undefined, + } + : undefined, + }); + } + + async available(): Promise { + return await ipc.auth.navigatorCredentialsAvailable(); + } +} + +function arrayBufferSourceToUint8Array(source: BufferSource): Uint8Array { + if (source instanceof ArrayBuffer) { + return new Uint8Array(source); + } else { + return new Uint8Array(source.buffer, source.byteOffset, source.byteLength); + } +} + +function convertUserVerification( + userVerification: UserVerificationRequirement | undefined, +): navigator_credentials.UserVerification | undefined { + switch (userVerification) { + case "required": + return navigator_credentials.UserVerification.Required; + case "preferred": + return navigator_credentials.UserVerification.Preferred; + case "discouraged": + return navigator_credentials.UserVerification.Discouraged; + default: + return undefined; + } +} diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index fbb83a1bf56..bd919a115e8 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -34,6 +34,7 @@ import { import { SerializedMemoryStorageService, StorageServiceProvider } from "@bitwarden/storage-core"; import { ChromiumImporterService } from "./app/tools/import/chromium-importer.service"; +import { MainNavigatorCredentialsService } from "./auth/services/main-navigator-credentials.serivce"; import { MainDesktopAutotypeService } from "./autofill/main/main-desktop-autotype.service"; import { MainSshAgentService } from "./autofill/main/main-ssh-agent.service"; import { DesktopAutofillSettingsService } from "./autofill/services/desktop-autofill-settings.service"; @@ -284,6 +285,7 @@ export class Main { app.getPath("exe"), app.getAppPath(), ); + new MainNavigatorCredentialsService(); this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider); diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c0716d99716..78ce4495f88 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -62,7 +62,9 @@ import { AccountService } from "@bitwarden/common/auth/abstractions/account.serv import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { MasterPasswordApiService } from "@bitwarden/common/auth/abstractions/master-password-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { NavigatorCredentialsService } from "@bitwarden/common/auth/abstractions/webauthn/navigator-credentials.service"; import { OrganizationInviteService } from "@bitwarden/common/auth/services/organization-invite/organization-invite.service"; +import { DefaultNavigatorCredentialsService } from "@bitwarden/common/auth/services/webauthn-login/default-navigator-credentials.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ClientType } from "@bitwarden/common/enums"; import { ProcessReloadServiceAbstraction } from "@bitwarden/common/key-management/abstractions/process-reload.service"; @@ -469,6 +471,11 @@ const safeProviders: SafeProvider[] = [ useClass: WebSystemService, deps: [], }), + safeProvider({ + provide: NavigatorCredentialsService, + useClass: DefaultNavigatorCredentialsService, + deps: [WINDOW, PlatformUtilsService], + }), safeProvider({ provide: SessionTimeoutSettingsComponentService, useClass: WebSessionTimeoutSettingsComponentService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 9dbc6679963..9aa95cb8245 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -105,6 +105,7 @@ import { TokenService as TokenServiceAbstraction } from "@bitwarden/common/auth/ import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification-api.service.abstraction"; import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { NavigatorCredentialsService } from "@bitwarden/common/auth/abstractions/webauthn/navigator-credentials.service"; import { WebAuthnLoginApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-api.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn/webauthn-login.service.abstraction"; @@ -1346,7 +1347,7 @@ const safeProviders: SafeProvider[] = [ WebAuthnLoginApiServiceAbstraction, LoginStrategyServiceAbstraction, WebAuthnLoginPrfKeyServiceAbstraction, - WINDOW, + NavigatorCredentialsService, LogService, ], }), diff --git a/libs/auth/src/angular/login/default-login-component.service.ts b/libs/auth/src/angular/login/default-login-component.service.ts index 7f98040d9c2..6928e9da24e 100644 --- a/libs/auth/src/angular/login/default-login-component.service.ts +++ b/libs/auth/src/angular/login/default-login-component.service.ts @@ -23,10 +23,6 @@ export class DefaultLoginComponentService implements LoginComponentService { this.clientType = this.platformUtilsService.getClientType(); } - isLoginWithPasskeySupported(): boolean { - return this.clientType === ClientType.Web; - } - /** * Redirects the user to the SSO login page, either via route or in a new browser window. * @param email The email address of the user attempting to log in diff --git a/libs/auth/src/angular/login/login-component.service.ts b/libs/auth/src/angular/login/login-component.service.ts index 5ca83c97c5f..36e0353b64c 100644 --- a/libs/auth/src/angular/login/login-component.service.ts +++ b/libs/auth/src/angular/login/login-component.service.ts @@ -25,11 +25,6 @@ export abstract class LoginComponentService { */ getOrgPoliciesFromOrgInvite?: (email: string) => Promise; - /** - * Indicates whether login with passkey is supported on the given client - */ - isLoginWithPasskeySupported: () => boolean; - /** * Redirects the user to the SSO login page, either via route or in a new browser window. */ diff --git a/libs/auth/src/angular/login/login.component.ts b/libs/auth/src/angular/login/login.component.ts index 51379ed213e..36082877b95 100644 --- a/libs/auth/src/angular/login/login.component.ts +++ b/libs/auth/src/angular/login/login.component.ts @@ -27,6 +27,7 @@ import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/mod import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { DevicesApiServiceAbstraction } from "@bitwarden/common/auth/abstractions/devices-api.service.abstraction"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; +import { WebauthnLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/webauthn-login/webauthn-login.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ClientType, HttpStatusCode } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -146,6 +147,7 @@ export class LoginComponent implements OnInit, OnDestroy { private configService: ConfigService, private ssoLoginService: SsoLoginServiceAbstraction, private environmentService: EnvironmentService, + private webauthnLoginService: WebauthnLoginServiceAbstraction, ) { this.clientType = this.platformUtilsService.getClientType(); } @@ -525,7 +527,7 @@ export class LoginComponent implements OnInit, OnDestroy { } isLoginWithPasskeySupported() { - return this.loginComponentService.isLoginWithPasskeySupported(); + return this.webauthnLoginService.available(); } protected async goToHint(): Promise { diff --git a/libs/common/src/auth/abstractions/webauthn/navigator-credentials.service.ts b/libs/common/src/auth/abstractions/webauthn/navigator-credentials.service.ts new file mode 100644 index 00000000000..815cbdb13b0 --- /dev/null +++ b/libs/common/src/auth/abstractions/webauthn/navigator-credentials.service.ts @@ -0,0 +1,4 @@ +export abstract class NavigatorCredentialsService { + abstract get(options: CredentialRequestOptions): Promise; + abstract available(): Promise; +} diff --git a/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts b/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts index c482b1a214e..5256536d62f 100644 --- a/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/webauthn/webauthn-login.service.abstraction.ts @@ -38,4 +38,9 @@ export abstract class WebAuthnLoginServiceAbstraction { * that needs to be validated for login. */ abstract logIn(assertion: WebAuthnLoginCredentialAssertionView): Promise; + + /** + * Checks if WebAuthnLogin is available in the current environment. + */ + abstract available(): Promise; } diff --git a/libs/common/src/auth/services/webauthn-login/default-navigator-credentials.service.ts b/libs/common/src/auth/services/webauthn-login/default-navigator-credentials.service.ts new file mode 100644 index 00000000000..6c76d6cfea5 --- /dev/null +++ b/libs/common/src/auth/services/webauthn-login/default-navigator-credentials.service.ts @@ -0,0 +1,23 @@ +import { ClientType } from "@bitwarden/client-type"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; + +import { NavigatorCredentialsServiceAbstraction } from "../../abstractions/webauthn/navigator-credentials.service"; + +export class DefaultNavigatorCredentialsService implements NavigatorCredentialsServiceAbstraction { + private navigatorCredentials: CredentialsContainer; + + constructor( + private window: Window, + private platformUtilsService: PlatformUtilsService, + ) { + this.navigatorCredentials = this.window.navigator.credentials; + } + + async get(options: CredentialRequestOptions): Promise { + return await this.navigatorCredentials.get(options); + } + + async available(): Promise { + return this.platformUtilsService.getClientType() === ClientType.Web; + } +} diff --git a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts index 2d42329d27a..0ac383b13fe 100644 --- a/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts +++ b/libs/common/src/auth/services/webauthn-login/webauthn-login.service.ts @@ -6,6 +6,7 @@ import { LoginStrategyServiceAbstraction, WebAuthnLoginCredentials } from "@bitw import { LogService } from "../../../platform/abstractions/log.service"; import { PrfKey } from "../../../types/key"; +import { NavigatorCredentialsService } from "../../abstractions/webauthn/navigator-credentials.service"; import { WebAuthnLoginApiServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-api.service.abstraction"; import { WebAuthnLoginPrfKeyServiceAbstraction } from "../../abstractions/webauthn/webauthn-login-prf-key.service.abstraction"; import { WebAuthnLoginServiceAbstraction } from "../../abstractions/webauthn/webauthn-login.service.abstraction"; @@ -16,17 +17,13 @@ import { WebAuthnLoginCredentialAssertionView } from "../../models/view/webauthn import { WebAuthnLoginAssertionResponseRequest } from "./request/webauthn-login-assertion-response.request"; export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction { - private navigatorCredentials: CredentialsContainer; - constructor( private webAuthnLoginApiService: WebAuthnLoginApiServiceAbstraction, - private loginStrategyService: LoginStrategyServiceAbstraction, - private webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction, - private window: Window, - private logService?: LogService, - ) { - this.navigatorCredentials = this.window.navigator.credentials; - } + protected loginStrategyService: LoginStrategyServiceAbstraction, + protected webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction, + protected navigatorCredentialsService: NavigatorCredentialsService, + protected logService?: LogService, + ) {} async getCredentialAssertionOptions(): Promise { const response = await this.webAuthnLoginApiService.getCredentialAssertionOptions(); @@ -45,7 +42,7 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction { } as any; try { - const response = await this.navigatorCredentials.get(nativeOptions); + const response = await this.navigatorCredentialsService.get(nativeOptions); if (!(response instanceof PublicKeyCredential)) { return undefined; } @@ -85,4 +82,8 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction { const result = await this.loginStrategyService.logIn(credential); return result; } + + async available(): Promise { + return this.navigatorCredentialsService.available(); + } }