1
0
mirror of https://github.com/bitwarden/desktop.git synced 2024-12-25 16:47:55 +01:00

Switch to our own rust based windows hello

This commit is contained in:
Hinton 2022-04-25 15:45:42 +02:00
parent 865e92c94c
commit 97d6b4bac7
17 changed files with 625 additions and 2113 deletions

View File

@ -3,3 +3,4 @@ index.node
**/node_modules **/node_modules
**/.DS_Store **/.DS_Store
npm-debug.log* npm-debug.log*
*.node

View File

@ -25,9 +25,11 @@ widestring = "0.5.1"
windows = {version = "0.32.0", features = [ windows = {version = "0.32.0", features = [
"alloc", "alloc",
"Foundation", "Foundation",
"Security_Credentials_UI",
"Storage_Streams", "Storage_Streams",
"Win32_Foundation", "Win32_Foundation",
"Win32_Security_Credentials", "Win32_Security_Credentials",
"Win32_System_WinRT",
]} ]}
[target.'cfg(windows)'.dev-dependencies] [target.'cfg(windows)'.dev-dependencies]

View File

@ -13,3 +13,7 @@ export namespace passwords {
/** Delete the stored password from the keychain. */ /** Delete the stored password from the keychain. */
export function deletePassword(service: string, account: string): Promise<void> export function deletePassword(service: string, account: string): Promise<void>
} }
export namespace biometrics {
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function available(): Promise<boolean>
}

View File

@ -1,32 +1,97 @@
/* eslint-disable @typescript-eslint/no-var-requires */ const { existsSync, readFileSync } = require('fs')
const { readFileSync } = require('fs') const { join } = require('path')
const { platform, arch } = process const { platform, arch } = process
let nativeBinding = null let nativeBinding = null
let isMusl = false let localFileExisted = false
let loadError = null let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
return readFileSync('/usr/bin/ldd', 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) { switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'desktop_native.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./desktop_native.android-arm64.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'desktop_native.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./desktop_native.android-arm-eabi.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32': case 'win32':
switch (arch) { switch (arch) {
case 'x64': case 'x64':
localFileExisted = existsSync(
join(__dirname, 'desktop_native.win32-x64-msvc.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.win32-x64-msvc') if (localFileExisted) {
nativeBinding = require('./desktop_native.win32-x64-msvc.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-win32-x64-msvc')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
break break
case 'ia32': case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'desktop_native.win32-ia32-msvc.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.win32-ia32-msvc') if (localFileExisted) {
nativeBinding = require('./desktop_native.win32-ia32-msvc.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-win32-ia32-msvc')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
break break
case 'arm64': case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'desktop_native.win32-arm64-msvc.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.win32-arm64-msvc') if (localFileExisted) {
nativeBinding = require('./desktop_native.win32-arm64-msvc.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-win32-arm64-msvc')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
@ -38,15 +103,27 @@ switch (platform) {
case 'darwin': case 'darwin':
switch (arch) { switch (arch) {
case 'x64': case 'x64':
localFileExisted = existsSync(join(__dirname, 'desktop_native.darwin-x64.node'))
try { try {
nativeBinding = require('./dist/desktop_native.darwin-x64') if (localFileExisted) {
nativeBinding = require('./desktop_native.darwin-x64.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-darwin-x64')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
break break
case 'arm64': case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'desktop_native.darwin-arm64.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.darwin-arm64') if (localFileExisted) {
nativeBinding = require('./desktop_native.darwin-arm64.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-darwin-arm64')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
@ -55,43 +132,91 @@ switch (platform) {
throw new Error(`Unsupported architecture on macOS: ${arch}`) throw new Error(`Unsupported architecture on macOS: ${arch}`)
} }
break break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, 'desktop_native.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./desktop_native.freebsd-x64.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux': case 'linux':
switch (arch) { switch (arch) {
case 'x64': case 'x64':
isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl') if (isMusl()) {
if (isMusl) { localFileExisted = existsSync(
join(__dirname, 'desktop_native.linux-x64-musl.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.linux-x64-musl') if (localFileExisted) {
nativeBinding = require('./desktop_native.linux-x64-musl.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-linux-x64-musl')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
} else { } else {
localFileExisted = existsSync(
join(__dirname, 'desktop_native.linux-x64-gnu.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.linux-x64-gnu') if (localFileExisted) {
nativeBinding = require('./desktop_native.linux-x64-gnu.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-linux-x64-gnu')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
} }
break break
case 'arm64': case 'arm64':
isMusl = readFileSync('/usr/bin/ldd', 'utf8').includes('musl') if (isMusl()) {
if (isMusl) { localFileExisted = existsSync(
join(__dirname, 'desktop_native.linux-arm64-musl.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.linux-arm64-musl') if (localFileExisted) {
nativeBinding = require('./desktop_native.linux-arm64-musl.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-linux-arm64-musl')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
} else { } else {
localFileExisted = existsSync(
join(__dirname, 'desktop_native.linux-arm64-gnu.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.linux-arm64-gnu') if (localFileExisted) {
nativeBinding = require('./desktop_native.linux-arm64-gnu.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-linux-arm64-gnu')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
} }
break break
case 'arm': case 'arm':
localFileExisted = existsSync(
join(__dirname, 'desktop_native.linux-arm-gnueabihf.node')
)
try { try {
nativeBinding = require('./dist/desktop_native.linux-arm-gnueabihf') if (localFileExisted) {
nativeBinding = require('./desktop_native.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('@bitwarden/desktop-native-linux-arm-gnueabihf')
}
} catch (e) { } catch (e) {
loadError = e loadError = e
} }
@ -111,4 +236,7 @@ if (!nativeBinding) {
throw new Error(`Failed to load native binding`) throw new Error(`Failed to load native binding`)
} }
module.exports.nativeBinding const { passwords, biometrics } = nativeBinding
module.exports.passwords = passwords
module.exports.biometrics = biometrics

View File

@ -1,13 +1,12 @@
{ {
"name": "@bitwarden/desktop_native", "name": "@bitwarden/desktop-native",
"version": "0.1.0", "version": "0.1.0",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@bitwarden/desktop_native", "name": "@bitwarden/desktop-native",
"version": "0.1.0", "version": "0.1.0",
"hasInstallScript": true,
"license": "GPL-3.0", "license": "GPL-3.0",
"devDependencies": { "devDependencies": {
"@napi-rs/cli": "^2.6.2" "@napi-rs/cli": "^2.6.2"

View File

@ -1,11 +1,10 @@
{ {
"name": "@bitwarden/desktop_native", "name": "@bitwarden/desktop-native",
"version": "0.1.0", "version": "0.1.0",
"description": "", "description": "",
"main": "index.node",
"scripts": { "scripts": {
"build": "napi build dist --platform --release --js true", "build": "napi build --platform --release",
"build:debug": "napi build dist --platform --js true", "build:debug": "napi build --platform",
"build:cross-platform": "node build.js", "build:cross-platform": "node build.js",
"test": "cargo test" "test": "cargo test"
}, },

View File

@ -0,0 +1,5 @@
#[cfg_attr(target_os = "linux", path = "unix.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
#[cfg_attr(target_os = "macos", path = "macos.rs")]
mod biometric;
pub use biometric::*;

View File

@ -0,0 +1,51 @@
use anyhow::Result;
use windows::{
core::factory, Foundation::IAsyncOperation, Security::Credentials::UI::*,
Win32::Foundation::HWND, Win32::System::WinRT::IUserConsentVerifierInterop,
};
pub fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
let interop = factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
let h = isize::from_le_bytes(hwnd.try_into().unwrap());
let window = HWND(h);
let operation: IAsyncOperation<UserConsentVerificationResult> =
unsafe { interop.RequestVerificationForWindowAsync(window, message)? };
let result: UserConsentVerificationResult = operation.get()?;
match result {
UserConsentVerificationResult::Verified => Ok(true),
_ => Ok(false),
}
}
pub fn available() -> Result<bool> {
let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?;
match ucv_available {
UserConsentVerifierAvailability::Available => Ok(true),
UserConsentVerifierAvailability::DeviceBusy => Ok(true), // TODO: Look into removing this and making the check more ad-hoc
_ => Ok(false),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_prompt() {
prompt(
vec![0, 0, 0, 0, 0, 0, 0, 0],
String::from("Hello from Rust"),
)
.unwrap();
}
#[test]
fn test_available() {
assert!(available().unwrap())
}
}

View File

@ -1,6 +1,7 @@
#[macro_use] #[macro_use]
extern crate napi_derive; extern crate napi_derive;
mod biometric;
mod password; mod password;
#[napi] #[napi]
@ -37,3 +38,21 @@ pub mod passwords {
.map_err(|e| napi::Error::from_reason(e.to_string())) .map_err(|e| napi::Error::from_reason(e.to_string()))
} }
} }
#[napi]
pub mod biometrics {
// Prompt for biometric confirmation
#[napi]
pub async fn prompt(
hwnd: napi::bindgen_prelude::Buffer,
message: String,
) -> napi::Result<bool> {
super::biometric::prompt(hwnd.into(), message)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn available() -> napi::Result<bool> {
super::biometric::available().map_err(|e| napi::Error::from_reason(e.to_string()))
}
}

1286
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -84,7 +84,7 @@
"**/*", "**/*",
"!**/node_modules/@bitwarden/desktop-native/**/*", "!**/node_modules/@bitwarden/desktop-native/**/*",
"**/node_modules/@bitwarden/desktop-native/index.js", "**/node_modules/@bitwarden/desktop-native/index.js",
"**/node_modules/@bitwarden/desktop-native/dist/desktop_native.${platform}-${arch}*.node" "**/node_modules/@bitwarden/desktop-native/desktop_native.${platform}-${arch}*.node"
], ],
"mac": { "mac": {
"electronUpdaterCompatibility": ">=0.0.1", "electronUpdaterCompatibility": ">=0.0.1",
@ -314,8 +314,6 @@
"@bitwarden/jslib-angular": "file:jslib/angular", "@bitwarden/jslib-angular": "file:jslib/angular",
"@bitwarden/jslib-common": "file:jslib/common", "@bitwarden/jslib-common": "file:jslib/common",
"@bitwarden/jslib-electron": "file:jslib/electron", "@bitwarden/jslib-electron": "file:jslib/electron",
"@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4",
"forcefocus": "^1.1.0",
"ngx-toastr": "14.1.4", "ngx-toastr": "14.1.4",
"node-ipc": "^9.1.4", "node-ipc": "^9.1.4",
"nord": "^0.2.1", "nord": "^0.2.1",

View File

@ -6,8 +6,6 @@ import { StateService } from "jslib-common/abstractions/state.service";
import { BiometricMain } from "../biometric/biometric.main"; import { BiometricMain } from "../biometric/biometric.main";
export default class BiometricDarwinMain implements BiometricMain { export default class BiometricDarwinMain implements BiometricMain {
isError = false;
constructor(private i18nservice: I18nService, private stateService: StateService) {} constructor(private i18nservice: I18nService, private stateService: StateService) {}
async init() { async init() {

View File

@ -1,5 +1,4 @@
export abstract class BiometricMain { export abstract class BiometricMain {
isError: boolean;
init: () => Promise<void>; init: () => Promise<void>;
supportsBiometric: () => Promise<boolean>; supportsBiometric: () => Promise<boolean>;
authenticateBiometric: () => Promise<boolean>; authenticateBiometric: () => Promise<boolean>;

View File

@ -1,18 +1,15 @@
import { biometrics } from "@bitwarden/desktop-native";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import forceFocus from "forcefocus";
import { I18nService } from "jslib-common/abstractions/i18n.service"; import { I18nService } from "jslib-common/abstractions/i18n.service";
import { LogService } from "jslib-common/abstractions/log.service"; import { LogService } from "jslib-common/abstractions/log.service";
import { StateService } from "jslib-common/abstractions/state.service"; import { StateService } from "jslib-common/abstractions/state.service";
import { WindowMain } from "jslib-electron/window.main"; import { WindowMain } from "jslib-electron/window.main";
import { BiometricMain } from "src/main/biometric/biometric.main"; import { BiometricMain } from "src/main/biometric/biometric.main";
export default class BiometricWindowsMain implements BiometricMain { export default class BiometricWindowsMain implements BiometricMain {
isError = false;
private windowsSecurityCredentialsUiModule: any;
constructor( constructor(
private i18nservice: I18nService, private i18nservice: I18nService,
private windowMain: WindowMain, private windowMain: WindowMain,
@ -21,126 +18,32 @@ export default class BiometricWindowsMain implements BiometricMain {
) {} ) {}
async init() { async init() {
this.windowsSecurityCredentialsUiModule = this.getWindowsSecurityCredentialsUiModule();
let supportsBiometric = false; let supportsBiometric = false;
try { try {
supportsBiometric = await this.supportsBiometric(); supportsBiometric = await this.supportsBiometric();
} catch { } catch (e) {
// store error state so we can let the user know on the settings page this.logService.error(e);
this.isError = true;
} }
await this.stateService.setEnableBiometric(supportsBiometric); await this.stateService.setEnableBiometric(supportsBiometric);
await this.stateService.setBiometricText("unlockWithWindowsHello"); await this.stateService.setBiometricText("unlockWithWindowsHello");
await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptWindowsHello"); await this.stateService.setNoAutoPromptBiometricsText("noAutoPromptWindowsHello");
ipcMain.on("biometric", async (event: any, message: any) => { ipcMain.handle("biometric", async (event: any, message: any) => {
event.returnValue = await this.authenticateBiometric(); return await this.authenticateBiometric();
}); });
} }
async supportsBiometric(): Promise<boolean> { async supportsBiometric(): Promise<boolean> {
const availability = await this.checkAvailabilityAsync(); return Promise.resolve(true);
return this.getAllowedAvailabilities().includes(availability);
} }
async authenticateBiometric(): Promise<boolean> { async authenticateBiometric(): Promise<boolean> {
const module = this.getWindowsSecurityCredentialsUiModule(); const hwnd = this.windowMain.win.getNativeWindowHandle();
if (module == null) { return await biometrics.prompt(hwnd, this.i18nservice.t("windowsHelloConsentMessage"));
return false;
}
const verification = await this.requestVerificationAsync(
this.i18nservice.t("windowsHelloConsentMessage")
);
return verification === module.UserConsentVerificationResult.verified;
}
getWindowsSecurityCredentialsUiModule(): any {
try {
if (this.windowsSecurityCredentialsUiModule == null && this.getWindowsMajorVersion() >= 10) {
this.windowsSecurityCredentialsUiModule = require("@nodert-win10-rs4/windows.security.credentials.ui");
}
return this.windowsSecurityCredentialsUiModule;
} catch {
this.isError = true;
}
return null;
} }
// TODO: Get someone with a w7 to verify this doesn't crash
async checkAvailabilityAsync(): Promise<any> { async checkAvailabilityAsync(): Promise<any> {
const module = this.getWindowsSecurityCredentialsUiModule(); return await biometrics.available();
if (module != null) {
// eslint-disable-next-line
return new Promise((resolve, reject) => {
try {
module.UserConsentVerifier.checkAvailabilityAsync((error: Error, result: any) => {
if (error) {
return resolve(null);
}
return resolve(result);
});
} catch {
this.isError = true;
return resolve(null);
}
});
}
return Promise.resolve(null);
}
async requestVerificationAsync(message: string): Promise<any> {
const module = this.getWindowsSecurityCredentialsUiModule();
if (module != null) {
return new Promise((resolve, reject) => {
try {
module.UserConsentVerifier.requestVerificationAsync(
message,
(error: Error, result: any) => {
if (error) {
return resolve(null);
}
return resolve(result);
}
);
forceFocus.focusWindow(this.windowMain.win);
} catch (error) {
this.isError = true;
return reject(error);
}
});
}
return Promise.resolve(null);
}
getAllowedAvailabilities(): any[] {
try {
const module = this.getWindowsSecurityCredentialsUiModule();
if (module != null) {
return [
module.UserConsentVerifierAvailability.available,
module.UserConsentVerifierAvailability.deviceBusy,
];
}
} catch {
/*Ignore error*/
}
return [];
}
getWindowsMajorVersion(): number {
if (process.platform !== "win32") {
return -1;
}
try {
// eslint-disable-next-line
const version = require("os").release();
return Number.parseInt(version.split(".")[0], 10);
} catch {
this.logService.error("Unable to resolve windows major version number");
}
return -1;
} }
} }

View File

@ -10,7 +10,7 @@ export class DesktopCredentialStorageListener {
constructor(private serviceName: string, private biometricService: BiometricMain) {} constructor(private serviceName: string, private biometricService: BiometricMain) {}
init() { init() {
ipcMain.on("keytar", async (event: any, message: any) => { ipcMain.handle("keytar", async (event: any, message: any) => {
try { try {
let serviceName = this.serviceName; let serviceName = this.serviceName;
message.keySuffix = "_" + (message.keySuffix ?? ""); message.keySuffix = "_" + (message.keySuffix ?? "");
@ -35,9 +35,9 @@ export class DesktopCredentialStorageListener {
await passwords.deletePassword(serviceName, message.key); await passwords.deletePassword(serviceName, message.key);
} }
} }
event.returnValue = val; return val;
} catch { } catch {
event.returnValue = null; return null;
} }
}); });
} }

1058
src/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -12,8 +12,6 @@
"url": "https://github.com/bitwarden/desktop" "url": "https://github.com/bitwarden/desktop"
}, },
"dependencies": { "dependencies": {
"@bitwarden/desktop-native": "file:../desktop_native", "@bitwarden/desktop-native": "file:../desktop_native"
"@nodert-win10-rs4/windows.security.credentials.ui": "^0.4.4",
"forcefocus": "^1.1.0"
} }
} }