1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 06:23:38 +00:00

Implement rust fido2 for desktop mac and linux

This commit is contained in:
Bernd Schoolmann
2024-11-29 16:44:42 +01:00
parent d76b5b672c
commit 2f0c1610d9
15 changed files with 506 additions and 9 deletions

View File

@@ -338,4 +338,15 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
return "";
}
supportsNativeWebauthn(): boolean {
return false;
}
performNativeWebauthnAuthentication(
challenge: string,
credentials: Array<string>,
origin: string,
): Promise<string> {
throw new Error("Method not implemented.");
}
}

View File

@@ -138,4 +138,15 @@ export class CliPlatformUtilsService implements PlatformUtilsService {
getAutofillKeyboardShortcut(): Promise<string> {
return null;
}
supportsNativeWebauthn(): boolean {
return false;
}
performNativeWebauthnAuthentication(
challenge: string,
credentials: Array<string>,
origin: string,
): Promise<string> {
throw new Error("Method not implemented.");
}
}

View File

@@ -83,6 +83,45 @@ dependencies = [
"x11rb",
]
[[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",
"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.1"
@@ -533,6 +572,29 @@ dependencies = [
"typenum",
]
[[package]]
name = "ctap-hid-fido2"
version = "3.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1452b85807c4da0e06a24df21886c643da501404d40a52da534ef08b8ec01779"
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.8"
@@ -622,6 +684,12 @@ dependencies = [
"syn",
]
[[package]]
name = "data-encoding"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2"
[[package]]
name = "der"
version = "0.7.9"
@@ -633,6 +701,20 @@ 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.3.11"
@@ -666,6 +748,7 @@ dependencies = [
"byteorder",
"cbc",
"core-foundation",
"ctap-hid-fido2",
"dirs",
"ed25519",
"futures",
@@ -686,6 +769,8 @@ dependencies = [
"scopeguard",
"security-framework",
"security-framework-sys",
"serde",
"serde_json",
"sha2",
"ssh-encoding",
"ssh-key",
@@ -764,6 +849,17 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "dlib"
version = "0.5.2"
@@ -1143,6 +1239,12 @@ dependencies = [
"system-deps",
]
[[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.1"
@@ -1173,6 +1275,19 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hidapi"
version = "2.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03b876ecf37e86b359573c16c8366bc3eba52b689884a0fc42ba3f67203d2a8b"
dependencies = [
"cc",
"cfg-if",
"libc",
"pkg-config",
"windows-sys 0.48.0",
]
[[package]]
name = "hmac"
version = "0.12.1"
@@ -1489,6 +1604,30 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "num"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
dependencies = [
"num-bigint",
"num-complex",
"num-integer",
"num-iter",
"num-rational",
"num-traits",
]
[[package]]
name = "num-bigint"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
dependencies = [
"num-integer",
"num-traits",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.4"
@@ -1506,6 +1645,15 @@ dependencies = [
"zeroize",
]
[[package]]
name = "num-complex"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
dependencies = [
"num-traits",
]
[[package]]
name = "num-conv"
version = "0.1.0"
@@ -1532,6 +1680,17 @@ dependencies = [
"num-traits",
]
[[package]]
name = "num-rational"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
dependencies = [
"num-bigint",
"num-integer",
"num-traits",
]
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -1659,6 +1818,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.20.2"
@@ -1697,6 +1865,15 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "pad"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2ad9b889f1b12e0b9ee24db044b5129150d5eada288edc800f789928dc8c0e3"
dependencies = [
"unicode-width",
]
[[package]]
name = "parking"
version = "2.2.1"
@@ -2025,6 +2202,21 @@ dependencies = [
"rand",
]
[[package]]
name = "ring"
version = "0.17.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
"getrandom",
"libc",
"spin",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "rsa"
version = "0.9.6"
@@ -2071,6 +2263,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.37"
@@ -2084,6 +2285,18 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "rustversion"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "salsa20"
version = "0.10.2"
@@ -2153,24 +2366,46 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]]
name = "serde"
version = "1.0.214"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f55c3193aca71c12ad7890f1785d2b73e1b9f63a0bbc353c08ef26fe03fc56b5"
checksum = "6513c1ad0b11a9376da888e3e0baa0077f1aed55c17f50e7b2397136129fb88f"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.214"
name = "serde_cbor"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "de523f781f095e28fa605cdce0f8307e451cc0fd14e2eb4cd2e98a355b147766"
checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5"
dependencies = [
"half",
"serde",
]
[[package]]
name = "serde_derive"
version = "1.0.215"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.133"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fceb2473b9166b2294ef05efcb65a3db80803f0b03ef86a5fc88a2b85ee377"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.19"
@@ -2343,6 +2578,25 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[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"
@@ -2360,6 +2614,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "system-deps"
version = "6.2.2"
@@ -2628,6 +2893,12 @@ dependencies = [
"subtle",
]
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "version-compare"
version = "0.2.0"
@@ -3087,6 +3358,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",
"time",
]
[[package]]
name = "xdg-home"
version = "1.3.0"

View File

@@ -60,6 +60,9 @@ rand_chacha = "=0.3.1"
pkcs8 = { version = "=0.10.2", features = ["alloc", "encryption", "pem"] }
rsa = "=0.9.6"
ed25519 = { version = "=2.2.3", features = ["pkcs8"] }
ctap-hid-fido2 = "3.5.2"
serde = { version = "1.0.215", features = ["derive", "serde_derive"] }
serde_json = "1.0.133"
[target.'cfg(windows)'.dependencies]
widestring = { version = "=1.1.0", optional = true }

View File

@@ -0,0 +1,92 @@
use base64::{prelude::BASE64_URL_SAFE_NO_PAD, Engine};
use ctap_hid_fido2::{fidokey::GetAssertionArgsBuilder, Cfg, FidoKeyHidFactory};
use serde::{Deserialize, Serialize};
#[derive(Debug)]
pub enum Fido2ClientError {
WrongPin,
NoCredentials,
NoDevice,
InvalidInput,
AssertionError,
}
pub fn authenticate(challenge: String, credentials: Vec<String>, rpid: String, pin: Option<String>) -> Result<String, Fido2ClientError> {
let device = FidoKeyHidFactory::create(&Cfg::init()).map_err(|_| Fido2ClientError::NoDevice)?;
let clientdata = format!(r#"{{"type":"webauthn.get","challenge":"{}","origin":"https://{}","crossOrigin": true}}"#, challenge, rpid);
let mut get_assertion_args = GetAssertionArgsBuilder::new(rpid.as_str(), clientdata.as_bytes());
let result = if let Some(pin) = pin {
get_assertion_args = get_assertion_args.pin(&pin.as_str());
let get_assertion_args = get_assertion_args.build();
device.get_assertion_with_args(&get_assertion_args)
} else {
let mut get_assertion_args = get_assertion_args
.without_pin_and_uv();
for cred in credentials {
let credid_bytes = BASE64_URL_SAFE_NO_PAD.decode(cred.as_bytes()).map_err(|_| Fido2ClientError::InvalidInput)?;
get_assertion_args = get_assertion_args.credential_id(&credid_bytes);
}
let get_assertion_args = get_assertion_args.build();
device.get_assertion_with_args(&get_assertion_args)
};
let assertion = match result {
Ok(assertion) => assertion,
Err(_) => {
return Err(Fido2ClientError::NoCredentials);
}
};
let assertion = assertion.get(0).ok_or(Fido2ClientError::AssertionError)?;
let twofa_token = TwoFactorAuthToken{
id: BASE64_URL_SAFE_NO_PAD.encode(assertion.credential_id.as_slice()),
raw_id: BASE64_URL_SAFE_NO_PAD.encode(assertion.credential_id.as_slice()),
type_: "public-key".to_string(),
response: WebauthnResponseData {
authenticator_data: BASE64_URL_SAFE_NO_PAD.encode(
&assertion.auth_data.as_slice()
),
client_data_json: BASE64_URL_SAFE_NO_PAD.encode(
&clientdata.as_bytes()
),
signature: BASE64_URL_SAFE_NO_PAD.encode(
&assertion.signature.as_slice()
),
},
extensions: WebauthnExtensions {
appid: Some(false),
},
};
let twofa_token = serde_json::to_string(&twofa_token).map_err(|_| Fido2ClientError::AssertionError)?;
Ok(twofa_token)
}
#[derive(Debug, Serialize, Deserialize)]
struct TwoFactorAuthToken {
id: String,
#[serde(rename = "rawId")]
raw_id: String,
#[serde(rename = "type")]
type_: String,
extensions: WebauthnExtensions,
#[serde(rename = "response")]
response: WebauthnResponseData,
}
#[derive(Debug, Serialize, Deserialize)]
struct WebauthnExtensions {
appid: Option<bool>,
}
#[derive(Debug, Serialize, Deserialize)]
struct WebauthnResponseData {
#[serde(rename = "authenticatorData")]
authenticator_data: String,
#[serde(rename = "clientDataJson")]
client_data_json: String,
signature: String,
}

View File

@@ -12,5 +12,7 @@ pub mod process_isolation;
#[cfg(feature = "sys")]
pub mod powermonitor;
#[cfg(feature = "sys")]
pub mod ssh_agent;
#[cfg(feature = "sys")]
pub mod fido2_client;

View File

@@ -123,3 +123,6 @@ export declare namespace ipc {
send(message: string): number
}
}
export declare namespace fido2_hid_client {
export function authenticate(challenge: string, credentials: Array<string>, rpid: string, pin: string): string
}

View File

@@ -522,3 +522,13 @@ pub mod ipc {
}
}
}
#[napi]
pub mod fido2_hid_client {
#[napi]
pub fn authenticate(challenge: String, credentials: Vec<String>, rpid: String, pin: String) -> napi::Result<String> {
let pin = if pin.is_empty() { None } else { Some(pin) };
desktop_core::fido2_client::authenticate(challenge, credentials, rpid, pin)
.map_err(|e| napi::Error::from_reason(format!("Error authenticating: {:?}", e)))
}
}

View File

@@ -40,6 +40,7 @@ import { DesktopCredentialStorageListener } from "./platform/main/desktop-creden
import { MainCryptoFunctionService } from "./platform/main/main-crypto-function.service";
import { MainSshAgentService } from "./platform/main/main-ssh-agent.service";
import { VersionMain } from "./platform/main/version.main";
import { WebauthnListener } from "./platform/main/webauthn-listener";
import { DesktopSettingsService } from "./platform/services/desktop-settings.service";
import { ElectronLogMainService } from "./platform/services/electron-log.main.service";
import { ElectronStorageService } from "./platform/services/electron-storage.service";
@@ -75,6 +76,7 @@ export class Main {
desktopAutofillSettingsService: DesktopAutofillSettingsService;
versionMain: VersionMain;
sshAgentService: MainSshAgentService;
webauthnListener: WebauthnListener;
constructor() {
// Set paths for portable builds
@@ -254,6 +256,9 @@ export class Main {
}
});
this.webauthnListener = new WebauthnListener();
this.webauthnListener.init();
new EphemeralValueStorageService();
new SSOLocalhostCallbackService(this.environmentService, this.messagingService);
}

View File

@@ -0,0 +1,18 @@
import { ipcMain } from "electron";
import { fido2_hid_client } from "@bitwarden/desktop-napi";
export class WebauthnListener {
constructor() {}
init() {
ipcMain.handle("webauthn.authenticate", async (event: any, message: any) => {
return fido2_hid_client.authenticate(
message.challenge,
message.credentials,
message.origin,
"",
);
});
}
}

View File

@@ -119,6 +119,16 @@ const localhostCallbackService = {
},
};
const webauthn = {
webauthnAuthenticate: (
challenge: string,
credentials: Array<string>,
origin: string,
): Promise<string> => {
return ipcRenderer.invoke("webauthn.authenticate", { challenge, credentials, origin });
},
};
export default {
versions: {
app: (): Promise<string> => ipcRenderer.invoke("appVersion"),
@@ -190,6 +200,7 @@ export default {
crypto,
ephemeralStore,
localhostCallbackService,
webauthn,
};
function deviceType(): DeviceType {

View File

@@ -75,10 +75,20 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return (await this.getApplicationVersion()).split(/[+|-]/)[0].trim();
}
// Temporarily restricted to only Windows until https://github.com/electron/electron/pull/28349
// has been merged and an updated electron build is available.
supportsWebAuthn(win: Window): boolean {
return this.getDevice() === DeviceType.WindowsDesktop;
return true;
}
supportsNativeWebauthn(): boolean {
return true;
}
performNativeWebauthnAuthentication(
challenge: string,
credentials: Array<string>,
origin: string,
): Promise<string> {
return ipc.platform.webauthn.webauthnAuthenticate(challenge, credentials, origin);
}
supportsDuo(): boolean {

View File

@@ -193,4 +193,15 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
getAutofillKeyboardShortcut(): Promise<string> {
return null;
}
supportsNativeWebauthn(): boolean {
return false;
}
performNativeWebauthnAuthentication(
challenge: string,
credentials: Array<string>,
origin: string,
): Promise<string> {
throw new Error("Method not implemented.");
}
}

View File

@@ -121,6 +121,22 @@ export class TwoFactorAuthWebAuthnComponent implements OnInit, OnDestroy {
return;
}
if (this.platformUtilsService.supportsNativeWebauthn()) {
const challenge = providerData.challenge;
const rpId = providerData.rpId;
const creds = providerData.allowCredentials as any as Array<string>;
const credentials = creds.map((c: any) => {
return c.id;
});
const resp = await this.platformUtilsService.performNativeWebauthnAuthentication(
challenge,
credentials,
rpId,
);
this.token.emit(resp);
return;
}
this.webAuthn.init(providerData);
}

View File

@@ -27,6 +27,12 @@ export abstract class PlatformUtilsService {
abstract getApplicationVersion(): Promise<string>;
abstract getApplicationVersionNumber(): Promise<string>;
abstract supportsWebAuthn(win: Window): boolean;
abstract supportsNativeWebauthn(): boolean;
abstract performNativeWebauthnAuthentication(
challenge: string,
credentials: Array<string>,
origin: string,
): Promise<string>;
abstract supportsDuo(): boolean;
/**
* @deprecated use `@bitwarden/components/ToastService.showToast` instead