mirror of
https://github.com/bitwarden/browser
synced 2026-02-04 02:33:33 +00:00
Initial commit
This commit is contained in:
1435
apps/desktop/desktop_native/Cargo.lock
generated
1435
apps/desktop/desktop_native/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ members = [
|
||||
"bitwarden_chromium_import_helper",
|
||||
"chromium_importer",
|
||||
"core",
|
||||
"fido2_client",
|
||||
"macos_provider",
|
||||
"napi",
|
||||
"process_isolation",
|
||||
|
||||
22
apps/desktop/desktop_native/fido2_client/Cargo.toml
Normal file
22
apps/desktop/desktop_native/fido2_client/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[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"
|
||||
home = "=0.5.0"
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json.workspace = true
|
||||
secrecy = "0.8.0"
|
||||
hex.workspace = true
|
||||
webauthn-authenticator-rs = { version = "0.5.3", features = ["ctap2", "cable", "usb"] }
|
||||
tokio.workspace = true
|
||||
futures-util = "0.3.31"
|
||||
webauthn-rs-proto = "0.5.3"
|
||||
sha2.workspace = true
|
||||
|
||||
104
apps/desktop/desktop_native/fido2_client/src/ctap_hid_fido2.rs
Normal file
104
apps/desktop/desktop_native/fido2_client/src/ctap_hid_fido2.rs
Normal file
@@ -0,0 +1,104 @@
|
||||
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, AssertionOptions, AuthenticatorAssertionResponse, Fido2ClientError,
|
||||
PublicKeyCredential,
|
||||
};
|
||||
|
||||
fn get_pin() -> Option<String> {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn get(options: AssertionOptions) -> Result<PublicKeyCredential, Fido2ClientError> {
|
||||
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.rpid
|
||||
);
|
||||
|
||||
let mut get_assertion_args =
|
||||
GetAssertionArgsBuilder::new(options.rpid.as_str(), client_data_json.as_bytes())
|
||||
.extensions(&[AssertionExtension::HmacSecret(Some(prf_to_hmac(
|
||||
&options.prf_eval_first,
|
||||
)))]);
|
||||
|
||||
let mut pin: Option<String> = None;
|
||||
if options.user_verification == crate::UserVerification::Required
|
||||
|| options.user_verification == crate::UserVerification::Preferred
|
||||
{
|
||||
pin = Some(get_pin().ok_or(Fido2ClientError::WrongPin)?);
|
||||
get_assertion_args = get_assertion_args.pin(pin.as_ref().unwrap());
|
||||
}
|
||||
|
||||
let assertions = device
|
||||
.get_assertion_with_args(&get_assertion_args.build())
|
||||
.map_err(|_e| Fido2ClientError::AssertionError)?;
|
||||
let assertion = 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, AssertionOptions};
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn assertion() {
|
||||
get(AssertionOptions {
|
||||
challenge: vec![],
|
||||
timeout: 0,
|
||||
rpid: "example.com".to_string(),
|
||||
user_verification: crate::UserVerification::Required,
|
||||
allow_credentials: vec![],
|
||||
prf_eval_first: [0u8; 32],
|
||||
prf_eval_second: None,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
69
apps/desktop/desktop_native/fido2_client/src/lib.rs
Normal file
69
apps/desktop/desktop_native/fido2_client/src/lib.rs
Normal file
@@ -0,0 +1,69 @@
|
||||
#[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::*;
|
||||
|
||||
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)]
|
||||
pub enum UserVerification {
|
||||
Discouraged,
|
||||
Preferred,
|
||||
Required,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AssertionOptions {
|
||||
pub challenge: Vec<u8>,
|
||||
pub timeout: u64,
|
||||
pub rpid: String,
|
||||
pub user_verification: UserVerification,
|
||||
pub allow_credentials: Vec<Vec<u8>>,
|
||||
pub prf_eval_first: [u8; 32],
|
||||
pub prf_eval_second: Option<[u8; 32]>,
|
||||
}
|
||||
|
||||
pub struct AuthenticatorAssertionResponse {
|
||||
pub authenticator_data: Vec<u8>,
|
||||
pub client_data_json: Vec<u8>,
|
||||
pub signature: Vec<u8>,
|
||||
pub user_handle: Vec<u8>,
|
||||
}
|
||||
|
||||
pub struct PublicKeyCredential {
|
||||
pub authenticator_attachment: String,
|
||||
pub id: String,
|
||||
pub raw_id: Vec<u8>,
|
||||
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::AssertionOptions,
|
||||
) -> Result<super::PublicKeyCredential, super::Fido2ClientError> {
|
||||
super::get(assertion_options)
|
||||
}
|
||||
|
||||
pub fn available() -> bool {
|
||||
super::available()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub fn get(options: AssertionOptions) -> Result<PublicKeyCredential, Fido2ClientError> {
|
||||
todo!("Fido2Client is unimplemented on this platform");
|
||||
}
|
||||
|
||||
pub fn available() -> bool {
|
||||
false
|
||||
}
|
||||
@@ -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 }
|
||||
|
||||
31
apps/desktop/desktop_native/napi/index.d.ts
vendored
31
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -3,6 +3,33 @@
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
export const enum UserVerification {
|
||||
Preferred = 'Preferred',
|
||||
Required = 'Required',
|
||||
Discouraged = 'Discouraged'
|
||||
}
|
||||
export interface CredentialAssertionOptions {
|
||||
challenge: Uint8Array
|
||||
timeout: number
|
||||
rpid: string
|
||||
userVerification: UserVerification
|
||||
allowCredentials: Array<Array<number>>
|
||||
prfEvalFirst: Uint8Array
|
||||
}
|
||||
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 declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
@@ -254,3 +281,7 @@ export declare namespace autotype {
|
||||
export function getForegroundWindowTitle(): string
|
||||
export function typeInput(input: Array<number>, keyboardShortcut: Array<string>): void
|
||||
}
|
||||
export declare namespace navigator_credentials {
|
||||
export function get(assertionOptions: CredentialAssertionOptions): PublicKeyCredential
|
||||
export function available(): boolean
|
||||
}
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
use napi::bindgen_prelude::{Buffer, Uint8Array};
|
||||
use tokio_util::bytes::Buf;
|
||||
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
@@ -1199,3 +1202,107 @@ pub mod autotype {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(string_enum)]
|
||||
pub enum UserVerification {
|
||||
Preferred,
|
||||
Required,
|
||||
Discouraged,
|
||||
}
|
||||
|
||||
impl Into<fido2_client::UserVerification> 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 CredentialAssertionOptions {
|
||||
pub challenge: Uint8Array,
|
||||
pub timeout: i64,
|
||||
pub rpid: String,
|
||||
pub user_verification: UserVerification,
|
||||
pub allow_credentials: Vec<Vec<u8>>,
|
||||
pub prf_eval_first: Uint8Array,
|
||||
}
|
||||
|
||||
impl Into<fido2_client::AssertionOptions> for CredentialAssertionOptions {
|
||||
fn into(self) -> fido2_client::AssertionOptions {
|
||||
fido2_client::AssertionOptions {
|
||||
challenge: self.challenge.to_vec(),
|
||||
timeout: self.timeout as u64,
|
||||
rpid: self.rpid,
|
||||
user_verification: self.user_verification.into(),
|
||||
allow_credentials: self.allow_credentials,
|
||||
prf_eval_first: self.prf_eval_first.to_vec().try_into().unwrap_or([0u8; 32]),
|
||||
prf_eval_second: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
pub struct AuthenticatorAssertionResponse {
|
||||
pub authenticator_data: Uint8Array,
|
||||
pub client_data_json: Uint8Array,
|
||||
pub signature: Uint8Array,
|
||||
pub user_handle: Uint8Array,
|
||||
}
|
||||
|
||||
impl From<fido2_client::AuthenticatorAssertionResponse> 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<Uint8Array>,
|
||||
}
|
||||
|
||||
impl Into<PublicKeyCredential> 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 mod navigator_credentials {
|
||||
use crate::CredentialAssertionOptions;
|
||||
|
||||
#[napi]
|
||||
pub fn get(
|
||||
assertion_options: CredentialAssertionOptions,
|
||||
) -> napi::Result<crate::PublicKeyCredential> {
|
||||
let options: fido2_client::AssertionOptions = assertion_options.into();
|
||||
let resp = fido2_client::fido2_client::get(options).map_err(|e| {
|
||||
napi::Error::from_reason(format!("FIDO2 Authentication failed: {:?}", e))
|
||||
})?;
|
||||
Ok(resp.into())
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn available() -> bool {
|
||||
fido2_client::fido2_client::available()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()],
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
import {
|
||||
InternalUserDecryptionOptionsServiceAbstraction,
|
||||
LoginEmailService,
|
||||
LoginStrategyServiceAbstraction,
|
||||
SsoUrlService,
|
||||
} from "@bitwarden/auth/common";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
@@ -51,6 +52,9 @@ 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 { 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";
|
||||
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 +123,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 { DesktopWebAuthnLoginService } from "../../auth/services/desktop-webauthn-login.serivce";
|
||||
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 +315,17 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebCryptoFunctionService,
|
||||
deps: [WINDOW],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: WebAuthnLoginServiceAbstraction,
|
||||
useClass: DesktopWebAuthnLoginService,
|
||||
deps: [
|
||||
WebAuthnLoginApiServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
WINDOW,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: KeyServiceAbstraction,
|
||||
useClass: ElectronKeyService,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import { PublicKeyCredential } from "@bitwarden/desktop-napi";
|
||||
|
||||
export default {
|
||||
loginRequest: (alertTitle: string, alertBody: string, buttonText: string): Promise<void> =>
|
||||
ipcRenderer.invoke("loginRequest", {
|
||||
@@ -7,4 +9,7 @@ export default {
|
||||
alertBody,
|
||||
buttonText,
|
||||
}),
|
||||
navigatorCredentialsGet: (
|
||||
options: CredentialRequestOptions,
|
||||
): Promise<PublicKeyCredential | null> => ipcRenderer.invoke("navigatorCredentialsGet", options),
|
||||
};
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import { LoginStrategyServiceAbstraction, WebAuthnLoginCredentials } from "@bitwarden/auth/common";
|
||||
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 { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result";
|
||||
import { WebAuthnLoginCredentialAssertionOptionsView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion-options.view";
|
||||
import { WebAuthnLoginCredentialAssertionView } from "@bitwarden/common/auth/models/view/webauthn-login/webauthn-login-credential-assertion.view";
|
||||
import { WebAuthnLoginAssertionResponseRequest } from "@bitwarden/common/auth/services/webauthn-login/request/webauthn-login-assertion-response.request";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import { PrfKey } from "@bitwarden/common/types/key";
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
|
||||
export class DesktopWebAuthnLoginService extends WebAuthnLoginService {
|
||||
constructor(
|
||||
webAuthnLoginApiService: WebAuthnLoginApiServiceAbstraction,
|
||||
loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
window: Window,
|
||||
logService?: LogService,
|
||||
) {
|
||||
super(
|
||||
webAuthnLoginApiService,
|
||||
loginStrategyService,
|
||||
webAuthnLoginPrfKeyService,
|
||||
window,
|
||||
logService,
|
||||
);
|
||||
}
|
||||
|
||||
async assertCredential(
|
||||
credentialAssertionOptions: WebAuthnLoginCredentialAssertionOptionsView,
|
||||
): Promise<WebAuthnLoginCredentialAssertionView> {
|
||||
const nativeOptions: CredentialRequestOptions = {
|
||||
publicKey: credentialAssertionOptions.options,
|
||||
};
|
||||
// TODO: Remove `any` when typescript typings add support for PRF
|
||||
nativeOptions.publicKey.extensions = {
|
||||
prf: { eval: { first: await this.webAuthnLoginPrfKeyService.getLoginWithPrfSalt() } },
|
||||
} as any;
|
||||
|
||||
try {
|
||||
const response = await ipc.auth.navigatorCredentialsGet(nativeOptions);
|
||||
this.logService.info("navigator.credentials.get response received", response);
|
||||
// if (!(response instanceof PublicKeyCredential)) {
|
||||
// return undefined;
|
||||
// }
|
||||
// TODO: Remove `any` when typescript typings add support for PRF
|
||||
const prfResult = (response as any).prf as Uint8Array | undefined;
|
||||
let symmetricPrfKey: PrfKey | undefined;
|
||||
if (prfResult != undefined) {
|
||||
// Ensure we pass a plain ArrayBuffer (not a SharedArrayBuffer) by copying the bytes.
|
||||
symmetricPrfKey = await this.webAuthnLoginPrfKeyService.createSymmetricKeyFromPrf(
|
||||
prfResult.slice().buffer,
|
||||
);
|
||||
}
|
||||
|
||||
const deviceResponse = new WebAuthnLoginAssertionResponseRequest(
|
||||
response as any as PublicKeyCredential,
|
||||
);
|
||||
|
||||
// Verify that we aren't going to send PRF information to the server in any case.
|
||||
// Note: this will only happen if a dev has done something wrong.
|
||||
if ("prf" in deviceResponse.extensions) {
|
||||
throw new Error("PRF information is not allowed to be sent to the server.");
|
||||
}
|
||||
|
||||
return new WebAuthnLoginCredentialAssertionView(
|
||||
credentialAssertionOptions.token,
|
||||
deviceResponse,
|
||||
symmetricPrfKey,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logService?.error(error);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async logIn(assertion: WebAuthnLoginCredentialAssertionView): Promise<AuthResult> {
|
||||
const credential = new WebAuthnLoginCredentials(
|
||||
assertion.token,
|
||||
assertion.deviceResponse,
|
||||
assertion.prfKey,
|
||||
);
|
||||
const result = await this.loginStrategyService.logIn(credential);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { navigator_credentials, UserVerification } from "@bitwarden/desktop-napi";
|
||||
|
||||
export class MainNavigatorCredentialsService {
|
||||
constructor(private logService: LogService) {
|
||||
ipcMain.handle("navigatorCredentialsGet", async (event: any, message: any) => {
|
||||
this.logService.info("Handling navigatorCredentials.get request from renderer", message);
|
||||
const challenge = Utils.fromB64ToArray(message.publicKey.response.challenge);
|
||||
this.logService.info("navigatorCredentials.get challenge", challenge);
|
||||
this.logService.info(
|
||||
"navigatorCredentials.get prfEvalFirst",
|
||||
message.publicKey.extensions.prf.eval.first,
|
||||
);
|
||||
const result = navigator_credentials.get({
|
||||
challenge: Uint8Array.from(challenge),
|
||||
timeout: message.publicKey.timeout,
|
||||
rpid: message.publicKey.rpId,
|
||||
userVerification: UserVerification.Required,
|
||||
allowCredentials: [],
|
||||
prfEvalFirst: Uint8Array.from(message.publicKey.extensions.prf.eval.first),
|
||||
});
|
||||
this.logService.info("navigatorCredentials.get result", result);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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.logService);
|
||||
|
||||
this.desktopAutofillSettingsService = new DesktopAutofillSettingsService(stateProvider);
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
|
||||
}
|
||||
|
||||
isDev(): boolean {
|
||||
return ipc.platform.isDev;
|
||||
return true;
|
||||
}
|
||||
|
||||
isSelfHost(): boolean {
|
||||
|
||||
@@ -98,6 +98,7 @@ export class LoginViaWebAuthnComponent implements OnInit {
|
||||
let assertion: WebAuthnLoginCredentialAssertionView;
|
||||
try {
|
||||
const options = await this.webAuthnLoginService.getCredentialAssertionOptions();
|
||||
this.logService.info("Starting WebAuthn assertion with options:", options);
|
||||
assertion = await this.webAuthnLoginService.assertCredential(options);
|
||||
} catch (error) {
|
||||
this.validationService.showError(error);
|
||||
|
||||
@@ -107,7 +107,6 @@ import { UserVerificationApiServiceAbstraction } from "@bitwarden/common/auth/ab
|
||||
import { UserVerificationService as UserVerificationServiceAbstraction } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
|
||||
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";
|
||||
import { SendTokenService, DefaultSendTokenService } from "@bitwarden/common/auth/send-access";
|
||||
import { AccountApiServiceImplementation } from "@bitwarden/common/auth/services/account-api.service";
|
||||
import { AccountServiceImplementation } from "@bitwarden/common/auth/services/account.service";
|
||||
@@ -130,8 +129,6 @@ import { UserVerificationApiService } from "@bitwarden/common/auth/services/user
|
||||
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
|
||||
import { WebAuthnLoginApiService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-api.service";
|
||||
import { WebAuthnLoginPrfKeyService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login-prf-key.service";
|
||||
import { WebAuthnLoginService } from "@bitwarden/common/auth/services/webauthn-login/webauthn-login.service";
|
||||
import { TwoFactorApiService, DefaultTwoFactorApiService } from "@bitwarden/common/auth/two-factor";
|
||||
import {
|
||||
AutofillSettingsService,
|
||||
AutofillSettingsServiceAbstraction,
|
||||
@@ -1339,17 +1336,17 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: WebAuthnLoginApiService,
|
||||
deps: [ApiServiceAbstraction, EnvironmentService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: WebAuthnLoginServiceAbstraction,
|
||||
useClass: WebAuthnLoginService,
|
||||
deps: [
|
||||
WebAuthnLoginApiServiceAbstraction,
|
||||
LoginStrategyServiceAbstraction,
|
||||
WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
WINDOW,
|
||||
LogService,
|
||||
],
|
||||
}),
|
||||
// safeProvider({
|
||||
// provide: WebAuthnLoginServiceAbstraction,
|
||||
// useClass: WebAuthnLoginService,
|
||||
// deps: [
|
||||
// WebAuthnLoginApiServiceAbstraction,
|
||||
// LoginStrategyServiceAbstraction,
|
||||
// WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
// WINDOW,
|
||||
// LogService,
|
||||
// ],
|
||||
// }),
|
||||
safeProvider({
|
||||
provide: StorageServiceProvider,
|
||||
useClass: StorageServiceProvider,
|
||||
|
||||
@@ -24,7 +24,7 @@ export class DefaultLoginComponentService implements LoginComponentService {
|
||||
}
|
||||
|
||||
isLoginWithPasskeySupported(): boolean {
|
||||
return this.clientType === ClientType.Web;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -20,15 +20,16 @@ export class WebAuthnLoginAssertionResponseRequest extends WebAuthnLoginResponse
|
||||
constructor(credential: PublicKeyCredential) {
|
||||
super(credential);
|
||||
|
||||
if (!(credential.response instanceof AuthenticatorAssertionResponse)) {
|
||||
throw new Error("Invalid authenticator response");
|
||||
}
|
||||
// if (!(credential.response instanceof AuthenticatorAssertionResponse)) {
|
||||
// throw new Error("Invalid authenticator response");
|
||||
// }
|
||||
const resp = credential.response as AuthenticatorAssertionResponse;
|
||||
|
||||
this.response = {
|
||||
authenticatorData: Utils.fromBufferToUrlB64(credential.response.authenticatorData),
|
||||
signature: Utils.fromBufferToUrlB64(credential.response.signature),
|
||||
clientDataJSON: Utils.fromBufferToUrlB64(credential.response.clientDataJSON),
|
||||
userHandle: Utils.fromBufferToUrlB64(credential.response.userHandle),
|
||||
authenticatorData: Utils.fromBufferToUrlB64(resp.authenticatorData),
|
||||
signature: Utils.fromBufferToUrlB64(resp.signature),
|
||||
clientDataJSON: Utils.fromBufferToUrlB64((resp as any).clientDataJson),
|
||||
userHandle: Utils.fromBufferToUrlB64(resp.userHandle),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,10 +20,10 @@ export class WebAuthnLoginService implements WebAuthnLoginServiceAbstraction {
|
||||
|
||||
constructor(
|
||||
private webAuthnLoginApiService: WebAuthnLoginApiServiceAbstraction,
|
||||
private loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
private webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
protected loginStrategyService: LoginStrategyServiceAbstraction,
|
||||
protected webAuthnLoginPrfKeyService: WebAuthnLoginPrfKeyServiceAbstraction,
|
||||
private window: Window,
|
||||
private logService?: LogService,
|
||||
protected logService?: LogService,
|
||||
) {
|
||||
this.navigatorCredentials = this.window.navigator.credentials;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user