1
0
mirror of https://github.com/bitwarden/browser.git synced 2025-01-03 18:28:13 +01:00

[PM-990] Unix biometrics unlock via Polkit (#4586)

* Update unix biometrics for desktop biometrics rework

* Implement polkit policy setup

* Enable browser integration on Linux

* Remove polkit policy file

* Undo change to messages.json

* Fix biometrics setup, implement missing functions

* Implement osSupportsBiometrics

* Fix polkit settings message

* Remove unwraps in biometrics unix rust module

* Force password reprompt on start on linux with biometrics

* Merge branch 'main' into feature/unix-biometrics

* Allow browser extension to be unlocked on Linux via Polkit

* Implement availability check

* Cleanup

* Add auto-setup, manual setup, setup detection and change localized prompts

* Implement missing methods

* Add i18n to polkit message

* Implement missing method

* Small cleanup

* Update polkit consent message

* Fix unlock and print errors on failed biometrics

* Add dependencies to core crate

* Fix reference and update polkit policy

* Remove async-trait

* Add tsdoc

* Add comment about auto setup

* Delete unused init

* Update help link

* Remove additional settings for polkit

* Add availability-check to passwords implementation on linux

* Add availability test

* Add availability check to libsecret

* Expose availability check in napi crate

* Update d.ts

* Update osSupportsBiometric check to detect libsecret presence

* Improve secret service detection

* Add client half to Linux biometrics

* Fix windows build

* Remove unencrypted key handling for biometric key

* Move rng to rust, align linux bio implementation with windows

* Consolidate elevated commands into one

* Disable snap support in linux biometrics

---------

Co-authored-by: DigitallyRefined <129616584+DigitallyRefined@users.noreply.github.com>
This commit is contained in:
Bernd Schoolmann 2024-08-06 17:04:17 +02:00 committed by GitHub
parent 320e4f18ce
commit 2ce8500391
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 557 additions and 80 deletions

View File

@ -278,12 +278,24 @@ export abstract class BrowserPlatformUtilsService implements PlatformUtilsServic
async supportsBiometric() {
const platformInfo = await BrowserApi.getPlatformInfo();
if (platformInfo.os === "mac" || platformInfo.os === "win") {
if (platformInfo.os === "mac" || platformInfo.os === "win" || platformInfo.os === "linux") {
return true;
}
return false;
}
async biometricsNeedsSetup(): Promise<boolean> {
return false;
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return false;
}
async biometricsSetup(): Promise<void> {
return;
}
authenticateBiometric() {
return this.biometricCallback();
}

View File

@ -282,12 +282,6 @@ dependencies = [
"piper",
]
[[package]]
name = "bytes"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
[[package]]
name = "cbc"
version = "0.1.2"
@ -510,6 +504,7 @@ dependencies = [
"widestring",
"windows",
"zbus",
"zbus_polkit",
]
[[package]]
@ -2283,6 +2278,19 @@ dependencies = [
"zvariant",
]
[[package]]
name = "zbus_polkit"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00a29bfa927b29f91b7feb4e1990f2dd1b4604072f493dc2f074cf59e4e0ba90"
dependencies = [
"enumflags2",
"serde",
"serde_repr",
"static_assertions",
"zbus",
]
[[package]]
name = "zvariant"
version = "4.1.2"

View File

@ -52,3 +52,4 @@ security-framework-sys = "=2.11.0"
gio = "=0.19.5"
libsecret = "=0.5.0"
zbus = "4.3.1"
zbus_polkit = "4.0.0"

View File

@ -6,11 +6,11 @@ use crate::biometric::{KeyMaterial, OsDerivedKey};
pub struct Biometric {}
impl super::BiometricTrait for Biometric {
fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
async fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
bail!("platform not supported");
}
fn available() -> Result<bool> {
async fn available() -> Result<bool> {
bail!("platform not supported");
}

View File

@ -1,4 +1,5 @@
use anyhow::Result;
use aes::cipher::generic_array::GenericArray;
use anyhow::{anyhow, Result};
#[cfg_attr(target_os = "linux", path = "unix.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
@ -6,6 +7,10 @@ use anyhow::Result;
mod biometric;
pub use biometric::Biometric;
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
use sha2::{Digest, Sha256};
use crate::crypto::{self, CipherString};
pub struct KeyMaterial {
pub os_key_part_b64: String,
@ -18,8 +23,10 @@ pub struct OsDerivedKey {
}
pub trait BiometricTrait {
fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool>;
fn available() -> Result<bool>;
#[allow(async_fn_in_trait)]
async fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool>;
#[allow(async_fn_in_trait)]
async fn available() -> Result<bool>;
fn derive_key_material(secret: Option<&str>) -> Result<OsDerivedKey>;
fn set_biometric_secret(
service: &str,
@ -34,3 +41,40 @@ pub trait BiometricTrait {
key_material: Option<KeyMaterial>,
) -> Result<String>;
}
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
let iv = base64_engine
.decode(iv_b64)?
.try_into()
.map_err(|e: Vec<_>| anyhow!("Expected length {}, got {}", 16, e.len()))?;
let encrypted = crypto::encrypt_aes256(secret.as_bytes(), iv, key_material.derive_key()?)?;
Ok(encrypted.to_string())
}
fn decrypt(secret: &CipherString, key_material: &KeyMaterial) -> Result<String> {
if let CipherString::AesCbc256_B64 { iv, data } = secret {
let decrypted = crypto::decrypt_aes256(&iv, &data, key_material.derive_key()?)?;
Ok(String::from_utf8(decrypted)?)
} else {
Err(anyhow!("Invalid cipher string"))
}
}
impl KeyMaterial {
fn digest_material(&self) -> String {
match self.client_key_part_b64.as_deref() {
Some(client_key_part_b64) => {
format!("{}|{}", self.os_key_part_b64, client_key_part_b64)
}
None => self.os_key_part_b64.clone(),
}
}
pub fn derive_key(&self) -> Result<GenericArray<u8, typenum::U32>> {
Ok(Sha256::digest(self.digest_material()))
}
}

View File

@ -1,38 +1,109 @@
use anyhow::{bail, Result};
use std::str::FromStr;
use crate::biometric::{KeyMaterial, OsDerivedKey};
use anyhow::Result;
use base64::Engine;
use rand::RngCore;
use sha2::{Digest, Sha256};
use crate::biometric::{KeyMaterial, OsDerivedKey, base64_engine};
use zbus::Connection;
use zbus_polkit::policykit1::*;
use super::{decrypt, encrypt};
use anyhow::anyhow;
use crate::crypto::CipherString;
/// The Unix implementation of the biometric trait.
pub struct Biometric {}
impl super::BiometricTrait for Biometric {
fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
bail!("platform not supported");
async fn prompt(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
let connection = Connection::system().await?;
let proxy = AuthorityProxy::new(&connection).await?;
let subject = Subject::new_for_owner(std::process::id(), None, None)?;
let details = std::collections::HashMap::new();
let result = proxy.check_authorization(
&subject,
"com.bitwarden.Bitwarden.unlock",
&details,
CheckAuthorizationFlags::AllowUserInteraction.into(),
"",
).await;
match result {
Ok(result) => {
return Ok(result.is_authorized);
}
Err(e) => {
println!("polkit biometric error: {:?}", e);
return Ok(false);
}
}
}
fn available() -> Result<bool> {
bail!("platform not supported");
async fn available() -> Result<bool> {
let connection = Connection::system().await?;
let proxy = AuthorityProxy::new(&connection).await?;
let res = proxy.enumerate_actions("en").await?;
for action in res {
if action.action_id == "com.bitwarden.Bitwarden.unlock" {
return Ok(true);
}
}
return Ok(false);
}
fn derive_key_material(_iv_str: Option<&str>) -> Result<OsDerivedKey> {
bail!("platform not supported");
}
fn derive_key_material(challenge_str: Option<&str>) -> Result<OsDerivedKey> {
let challenge: [u8; 16] = match challenge_str {
Some(challenge_str) => base64_engine
.decode(challenge_str)?
.try_into()
.map_err(|e: Vec<_>| anyhow!("Expect length {}, got {}", 16, e.len()))?,
None => random_challenge(),
};
fn get_biometric_secret(
_service: &str,
_account: &str,
_key_material: Option<KeyMaterial>,
) -> Result<String> {
bail!("platform not supported");
// there is no windows hello like interactive bio protected secret at the moment on linux
// so we use a a key derived from the iv. this key is not intended to add any security
// but only a place-holder
let key = Sha256::digest(challenge);
let key_b64 = base64_engine.encode(&key);
let iv_b64 = base64_engine.encode(&challenge);
Ok(OsDerivedKey { key_b64, iv_b64 })
}
fn set_biometric_secret(
_service: &str,
_account: &str,
_secret: &str,
_key_material: Option<KeyMaterial>,
_iv_b64: &str,
service: &str,
account: &str,
secret: &str,
key_material: Option<KeyMaterial>,
iv_b64: &str,
) -> Result<String> {
bail!("platform not supported");
let key_material = key_material.ok_or(anyhow!(
"Key material is required for polkit protected keys"
))?;
let encrypted_secret = encrypt(secret, &key_material, iv_b64)?;
crate::password::set_password(service, account, &encrypted_secret)?;
Ok(encrypted_secret)
}
fn get_biometric_secret(
service: &str,
account: &str,
key_material: Option<KeyMaterial>,
) -> Result<String> {
let key_material = key_material.ok_or(anyhow!(
"Key material is required for polkit protected keys"
))?;
let encrypted_secret = crate::password::get_password(service, account)?;
let secret = CipherString::from_str(&encrypted_secret)?;
return Ok(decrypt(&secret, &key_material)?);
}
}
fn random_challenge() -> [u8; 16] {
let mut challenge = [0u8; 16];
rand::thread_rng().fill_bytes(&mut challenge);
challenge
}

View File

@ -1,6 +1,5 @@
use std::str::FromStr;
use aes::cipher::generic_array::GenericArray;
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
use rand::RngCore;
@ -30,14 +29,16 @@ use windows::{
use crate::{
biometric::{KeyMaterial, OsDerivedKey},
crypto::{self, CipherString},
crypto::CipherString,
};
use super::{decrypt, encrypt};
/// The Windows OS implementation of the biometric trait.
pub struct Biometric {}
impl super::BiometricTrait for Biometric {
fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
async fn prompt(hwnd: Vec<u8>, message: String) -> Result<bool> {
let h = isize::from_le_bytes(hwnd.clone().try_into().unwrap());
let window = HWND(h);
@ -56,7 +57,7 @@ impl super::BiometricTrait for Biometric {
}
}
fn available() -> Result<bool> {
async fn available() -> Result<bool> {
let ucv_available = UserConsentVerifier::CheckAvailabilityAsync()?.get()?;
match ucv_available {
@ -159,26 +160,6 @@ impl super::BiometricTrait for Biometric {
}
}
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {
let iv = base64_engine
.decode(iv_b64)?
.try_into()
.map_err(|e: Vec<_>| anyhow!("Expected length {}, got {}", 16, e.len()))?;
let encrypted = crypto::encrypt_aes256(secret.as_bytes(), iv, key_material.derive_key()?)?;
Ok(encrypted.to_string())
}
fn decrypt(secret: &CipherString, key_material: &KeyMaterial) -> Result<String> {
if let CipherString::AesCbc256_B64 { iv, data } = secret {
let decrypted = crypto::decrypt_aes256(&iv, &data, key_material.derive_key()?)?;
Ok(String::from_utf8(decrypted)?)
} else {
Err(anyhow!("Invalid cipher string"))
}
}
fn random_challenge() -> [u8; 16] {
let mut challenge = [0u8; 16];
@ -237,26 +218,11 @@ fn set_focus(window: HWND) {
}
}
impl KeyMaterial {
fn digest_material(&self) -> String {
match self.client_key_part_b64.as_deref() {
Some(client_key_part_b64) => {
format!("{}|{}", self.os_key_part_b64, client_key_part_b64)
}
None => self.os_key_part_b64.clone(),
}
}
pub fn derive_key(&self) -> Result<GenericArray<u8, typenum::U32>> {
Ok(Sha256::digest(self.digest_material()))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::biometric::BiometricTrait;
use crate::biometric::{encrypt, BiometricTrait};
#[test]
#[cfg(feature = "manual_test")]

View File

@ -22,6 +22,10 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
Ok(result)
}
pub fn is_available() -> Result<bool> {
Ok(true)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -40,6 +40,17 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
Ok(result)
}
pub fn is_available() -> Result<bool> {
let result = password_clear_sync(Some(&get_schema()), build_attributes("bitwardenSecretsAvailabilityTest", "test"), gio::Cancellable::NONE);
match result {
Ok(_) => Ok(true),
Err(_) => {
println!("secret-service unavailable: {:?}", result);
Ok(false)
}
}
}
fn get_schema() -> Schema {
let mut attributes = std::collections::HashMap::new();
attributes.insert("service", libsecret::SchemaAttributeType::String);

View File

@ -122,6 +122,10 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> {
Ok(())
}
pub fn is_available() -> Result<bool> {
Ok(true)
}
fn target_name(service: &str, account: &str) -> String {
format!("{}/{}", service, account)
}

View File

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

View File

@ -33,6 +33,12 @@ pub mod passwords {
desktop_core::password::delete_password(&service, &account)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
// Checks if the os secure storage is available
#[napi]
pub async fn is_available() -> napi::Result<bool> {
desktop_core::password::is_available().map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
@ -45,12 +51,12 @@ pub mod biometrics {
hwnd: napi::bindgen_prelude::Buffer,
message: String,
) -> napi::Result<bool> {
Biometric::prompt(hwnd.into(), message).map_err(|e| napi::Error::from_reason(e.to_string()))
Biometric::prompt(hwnd.into(), message).await.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn available() -> napi::Result<bool> {
Biometric::available().map_err(|e| napi::Error::from_reason(e.to_string()))
Biometric::available().await.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]

View File

@ -126,11 +126,14 @@
{{ biometricText | i18n }}
</label>
</div>
<small class="help-block" *ngIf="this.form.value.biometric">{{
<small class="help-block" *ngIf="this.form.value.biometric && !this.isLinux">{{
additionalBiometricSettingsText | i18n
}}</small>
</div>
<div class="form-group" *ngIf="supportsBiometric && this.form.value.biometric">
<div
class="form-group"
*ngIf="supportsBiometric && this.form.value.biometric && !this.isLinux"
>
<div class="checkbox form-group-child">
<label for="autoPromptBiometrics">
<input
@ -148,7 +151,8 @@
*ngIf="
supportsBiometric &&
this.form.value.biometric &&
(userHasMasterPassword || (this.form.value.pin && userHasPinSet))
(userHasMasterPassword || (this.form.value.pin && userHasPinSet)) &&
!this.isLinux
"
>
<div class="checkbox form-group-child">

View File

@ -55,6 +55,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
requireEnableTray = false;
showDuckDuckGoIntegrationOption = false;
isWindows: boolean;
isLinux: boolean;
enableTrayText: string;
enableTrayDescText: string;
@ -197,6 +198,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.userHasMasterPassword = await this.userVerificationService.hasMasterPassword();
this.isWindows = (await this.platformUtilsService.getDevice()) === DeviceType.WindowsDesktop;
this.isLinux = (await this.platformUtilsService.getDevice()) === DeviceType.LinuxDesktop;
if ((await this.stateService.getUserId()) == null) {
return;
@ -464,6 +466,26 @@ export class SettingsComponent implements OnInit, OnDestroy {
return;
}
const needsSetup = await this.platformUtilsService.biometricsNeedsSetup();
const supportsBiometricAutoSetup =
await this.platformUtilsService.biometricsSupportsAutoSetup();
if (needsSetup) {
if (supportsBiometricAutoSetup) {
await this.platformUtilsService.biometricsSetup();
} else {
const confirmed = await this.dialogService.openSimpleDialog({
title: { key: "biometricsManualSetupTitle" },
content: { key: "biometricsManualSetupDesc" },
type: "warning",
});
if (confirmed) {
this.platformUtilsService.launchUri("https://bitwarden.com/help/biometrics/");
}
return;
}
}
await this.biometricStateService.setBiometricUnlockEnabled(true);
if (this.isWindows) {
// Recommended settings for Windows Hello
@ -472,6 +494,13 @@ export class SettingsComponent implements OnInit, OnDestroy {
await this.biometricStateService.setPromptAutomatically(false);
await this.biometricStateService.setRequirePasswordOnStart(true);
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
} else if (this.isLinux) {
// Similar to Windows
this.form.controls.requirePasswordOnStart.setValue(true);
this.form.controls.autoPromptBiometrics.setValue(false);
await this.biometricStateService.setPromptAutomatically(false);
await this.biometricStateService.setRequirePasswordOnStart(true);
await this.biometricStateService.setDismissedRequirePasswordOnStartCallout();
}
await this.cryptoService.refreshAdditionalKeys();
@ -624,7 +653,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
this.form.controls.enableBrowserIntegration.setValue(false);
return;
} else if (ipc.platform.deviceType === DeviceType.LinuxDesktop) {
} else if (ipc.platform.isSnapStore || ipc.platform.isFlatpak) {
await this.dialogService.openSimpleDialog({
title: { key: "browserIntegrationUnsupportedTitle" },
content: { key: "browserIntegrationLinuxDesc" },
@ -735,6 +764,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
return "unlockWithTouchId";
case DeviceType.WindowsDesktop:
return "unlockWithWindowsHello";
case DeviceType.LinuxDesktop:
return "unlockWithPolkit";
default:
throw new Error("Unsupported platform");
}
@ -746,6 +777,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
return "autoPromptTouchId";
case DeviceType.WindowsDesktop:
return "autoPromptWindowsHello";
case DeviceType.LinuxDesktop:
return "autoPromptPolkit";
default:
throw new Error("Unsupported platform");
}

View File

@ -217,6 +217,8 @@ export class LockComponent extends BaseLockComponent implements OnInit, OnDestro
return "unlockWithTouchId";
case DeviceType.WindowsDesktop:
return "unlockWithWindowsHello";
case DeviceType.LinuxDesktop:
return "unlockWithPolkit";
default:
throw new Error("Unsupported platform");
}

View File

@ -1510,9 +1510,15 @@
"additionalWindowsHelloSettings": {
"message": "Additional Windows Hello settings"
},
"unlockWithPolkit": {
"message": "Unlock with system authentication"
},
"windowsHelloConsentMessage": {
"message": "Verify for Bitwarden."
},
"polkitConsentMessage": {
"message": "Authenticate to unlock Bitwarden."
},
"unlockWithTouchId": {
"message": "Unlock with Touch ID"
},
@ -1525,6 +1531,9 @@
"autoPromptWindowsHello": {
"message": "Ask for Windows Hello on app start"
},
"autoPromptPolkit": {
"message": "Ask for system authentication on launch"
},
"autoPromptTouchId": {
"message": "Ask for Touch ID on app start"
},
@ -1804,6 +1813,12 @@
"biometricsNotEnabledDesc": {
"message": "Browser biometrics requires desktop biometrics to be set up in the settings first."
},
"biometricsManualSetupTitle": {
"message": "Autometic setup not available"
},
"biometricsManualSetupDesc": {
"message": "Due to the installation method, biometrics support could not be automatically enabled. Would you like to open the documentation on how to do this manually?"
},
"personalOwnershipSubmitError": {
"message": "Due to an enterprise policy, you are restricted from saving items to your individual vault. Change the ownership option to an organization and choose from available collections."
},

View File

@ -51,4 +51,14 @@ export default class BiometricDarwinMain implements OsBiometricService {
return false;
}
}
async osBiometricsNeedsSetup() {
return false;
}
async osBiometricsCanAutoSetup(): Promise<boolean> {
return false;
}
async osBiometricsSetup(): Promise<void> {}
}

View File

@ -9,6 +9,16 @@ export default class NoopBiometricsService implements OsBiometricService {
return false;
}
async osBiometricsNeedsSetup(): Promise<boolean> {
return false;
}
async osBiometricsCanAutoSetup(): Promise<boolean> {
return false;
}
async osBiometricsSetup(): Promise<void> {}
async getBiometricKey(
service: string,
storageKey: string,

View File

@ -0,0 +1,160 @@
import { spawn } from "child_process";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { EncString } from "@bitwarden/common/platform/models/domain/enc-string";
import { biometrics, passwords } from "@bitwarden/desktop-napi";
import { WindowMain } from "../../../main/window.main";
import { isFlatpak, isLinux, isSnapStore } from "../../../utils";
import { OsBiometricService } from "./biometrics.service.abstraction";
const polkitPolicy = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE policyconfig PUBLIC
"-//freedesktop//DTD PolicyKit Policy Configuration 1.0//EN"
"http://www.freedesktop.org/standards/PolicyKit/1.0/policyconfig.dtd">
<policyconfig>
<action id="com.bitwarden.Bitwarden.unlock">
<description>Unlock Bitwarden</description>
<message>Authenticate to unlock Bitwarden</message>
<defaults>
<allow_any>no</allow_any>
<allow_inactive>no</allow_inactive>
<allow_active>auth_self</allow_active>
</defaults>
</action>
</policyconfig>`;
const policyFileName = "com.bitwarden.Bitwarden.policy";
const policyPath = "/usr/share/polkit-1/actions/";
export default class BiometricUnixMain implements OsBiometricService {
constructor(
private i18nservice: I18nService,
private windowMain: WindowMain,
) {}
private _iv: string | null = null;
// Use getKeyMaterial helper instead of direct access
private _osKeyHalf: string | null = null;
async setBiometricKey(
service: string,
key: string,
value: string,
clientKeyPartB64: string | undefined,
): Promise<void> {
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
await biometrics.setBiometricSecret(
service,
key,
value,
storageDetails.key_material,
storageDetails.ivB64,
);
}
async deleteBiometricKey(service: string, key: string): Promise<void> {
await passwords.deletePassword(service, key);
}
async getBiometricKey(
service: string,
storageKey: string,
clientKeyPartB64: string | undefined,
): Promise<string | null> {
const success = await this.authenticateBiometric();
if (!success) {
throw new Error("Biometric authentication failed");
}
const value = await passwords.getPassword(service, storageKey);
if (value == null || value == "") {
return null;
} else {
const encValue = new EncString(value);
this.setIv(encValue.iv);
const storageDetails = await this.getStorageDetails({ clientKeyHalfB64: clientKeyPartB64 });
const storedValue = await biometrics.getBiometricSecret(
service,
storageKey,
storageDetails.key_material,
);
return storedValue;
}
}
async authenticateBiometric(): Promise<boolean> {
const hwnd = this.windowMain.win.getNativeWindowHandle();
return await biometrics.prompt(hwnd, this.i18nservice.t("polkitConsentMessage"));
}
async osSupportsBiometric(): Promise<boolean> {
// We assume all linux distros have some polkit implementation
// that either has bitwarden set up or not, which is reflected in osBiomtricsNeedsSetup.
// Snap does not have access at the moment to polkit
// This could be dynamically detected on dbus in the future.
// We should check if a libsecret implementation is available on the system
// because otherwise we cannot offlod the protected userkey to secure storage.
return (await passwords.isAvailable()) && !isSnapStore();
}
async osBiometricsNeedsSetup(): Promise<boolean> {
// check whether the polkit policy is loaded via dbus call to polkit
return !(await biometrics.available());
}
async osBiometricsCanAutoSetup(): Promise<boolean> {
// We cannot auto setup on snap or flatpak since the filesystem is sandboxed.
// The user needs to manually set up the polkit policy outside of the sandbox
// since we allow access to polkit via dbus for the sandboxed clients, the authentication works from
// the sandbox, once the policy is set up outside of the sandbox.
return isLinux() && !isSnapStore() && !isFlatpak();
}
async osBiometricsSetup(): Promise<void> {
const process = spawn("pkexec", [
"bash",
"-c",
`echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`,
]);
await new Promise((resolve, reject) => {
process.on("close", (code) => {
if (code !== 0) {
reject("Failed to set up polkit policy");
} else {
resolve(null);
}
});
});
}
// Nulls out key material in order to force a re-derive. This should only be used in getBiometricKey
// when we want to force a re-derive of the key material.
private setIv(iv: string) {
this._iv = iv;
this._osKeyHalf = null;
}
private async getStorageDetails({
clientKeyHalfB64,
}: {
clientKeyHalfB64: string;
}): Promise<{ key_material: biometrics.KeyMaterial; ivB64: string }> {
if (this._osKeyHalf == null) {
const keyMaterial = await biometrics.deriveKeyMaterial(this._iv);
// osKeyHalf is based on the iv and in contrast to windows is not locked behind user verefication!
this._osKeyHalf = keyMaterial.keyB64;
this._iv = keyMaterial.ivB64;
}
return {
key_material: {
osKeyPartB64: this._osKeyHalf,
clientKeyPartB64: clientKeyHalfB64,
},
ivB64: this._iv,
};
}
}

View File

@ -214,4 +214,14 @@ export default class BiometricWindowsMain implements OsBiometricService {
clientKeyPartB64,
};
}
async osBiometricsNeedsSetup() {
return false;
}
async osBiometricsCanAutoSetup(): Promise<boolean> {
return false;
}
async osBiometricsSetup(): Promise<void> {}
}

View File

@ -1,5 +1,8 @@
export abstract class BiometricsServiceAbstraction {
abstract osSupportsBiometric(): Promise<boolean>;
abstract osBiometricsNeedsSetup: () => Promise<boolean>;
abstract osBiometricsCanAutoSetup: () => Promise<boolean>;
abstract osBiometricsSetup: () => Promise<void>;
abstract canAuthBiometric({
service,
key,
@ -26,6 +29,22 @@ export abstract class BiometricsServiceAbstraction {
export interface OsBiometricService {
osSupportsBiometric(): Promise<boolean>;
/**
* Check whether support for biometric unlock requires setup. This can be automatic or manual.
*
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
*/
osBiometricsNeedsSetup: () => Promise<boolean>;
/**
* Check whether biometrics can be automatically setup, or requires user interaction.
*
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
*/
osBiometricsCanAutoSetup: () => Promise<boolean>;
/**
* Starts automatic biometric setup, which places the required configuration files / changes the required settings.
*/
osBiometricsSetup: () => Promise<void>;
authenticateBiometric(): Promise<boolean>;
getBiometricKey(
service: string,

View File

@ -28,6 +28,8 @@ export class BiometricsService implements BiometricsServiceAbstraction {
this.loadWindowsHelloService();
} else if (platform === "darwin") {
this.loadMacOSService();
} else if (platform === "linux") {
this.loadUnixService();
} else {
this.loadNoopBiometricsService();
}
@ -49,6 +51,12 @@ export class BiometricsService implements BiometricsServiceAbstraction {
this.platformSpecificService = new BiometricDarwinMain(this.i18nService);
}
private loadUnixService() {
// eslint-disable-next-line
const BiometricUnixMain = require("./biometric.unix.main").default;
this.platformSpecificService = new BiometricUnixMain(this.i18nService, this.windowMain);
}
private loadNoopBiometricsService() {
// eslint-disable-next-line
const NoopBiometricsService = require("./biometric.noop.main").default;
@ -59,6 +67,18 @@ export class BiometricsService implements BiometricsServiceAbstraction {
return await this.platformSpecificService.osSupportsBiometric();
}
async osBiometricsNeedsSetup() {
return await this.platformSpecificService.osBiometricsNeedsSetup();
}
async osBiometricsCanAutoSetup() {
return await this.platformSpecificService.osBiometricsCanAutoSetup();
}
async osBiometricsSetup() {
await this.platformSpecificService.osBiometricsSetup();
}
async canAuthBiometric({
service,
key,

View File

@ -79,6 +79,15 @@ export class DesktopCredentialStorageListener {
case BiometricAction.OsSupported:
val = await this.biometricService.osSupportsBiometric();
break;
case BiometricAction.NeedsSetup:
val = await this.biometricService.osBiometricsNeedsSetup();
break;
case BiometricAction.Setup:
await this.biometricService.osBiometricsSetup();
break;
case BiometricAction.CanAutoSetup:
val = await this.biometricService.osBiometricsCanAutoSetup();
break;
default:
}

View File

@ -11,7 +11,7 @@ import {
UnencryptedMessageResponse,
} from "../models/native-messaging";
import { BiometricMessage, BiometricAction } from "../types/biometric-message";
import { isDev, isMacAppStore, isWindowsStore } from "../utils";
import { isDev, isFlatpak, isMacAppStore, isSnapStore, isWindowsStore } from "../utils";
import { ClipboardWriteMessage } from "./types/clipboard";
@ -48,6 +48,18 @@ const biometric = {
ipcRenderer.invoke("biometric", {
action: BiometricAction.OsSupported,
} satisfies BiometricMessage),
biometricsNeedsSetup: (): Promise<boolean> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.NeedsSetup,
} satisfies BiometricMessage),
biometricsSetup: (): Promise<void> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.Setup,
} satisfies BiometricMessage),
biometricsCanAutoSetup: (): Promise<boolean> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.CanAutoSetup,
} satisfies BiometricMessage),
authenticate: (): Promise<boolean> =>
ipcRenderer.invoke("biometric", {
action: BiometricAction.Authenticate,
@ -115,6 +127,8 @@ export default {
isDev: isDev(),
isMacAppStore: isMacAppStore(),
isWindowsStore: isWindowsStore(),
isFlatpak: isFlatpak(),
isSnapStore: isSnapStore(),
reloadProcess: () => ipcRenderer.send("reload-process"),
log: (level: LogLevelType, message?: any, ...optionalParams: any[]) =>
ipcRenderer.invoke("ipc.log", { level, message, optionalParams }),

View File

@ -135,6 +135,18 @@ export class ElectronPlatformUtilsService implements PlatformUtilsService {
return await ipc.platform.biometric.osSupported();
}
async biometricsNeedsSetup(): Promise<boolean> {
return await ipc.platform.biometric.biometricsNeedsSetup();
}
async biometricsSupportsAutoSetup(): Promise<boolean> {
return await ipc.platform.biometric.biometricsCanAutoSetup();
}
async biometricsSetup(): Promise<void> {
return await ipc.platform.biometric.biometricsSetup();
}
/** This method is used to authenticate the user presence _only_.
* It should not be used in the process to retrieve
* biometric keys, which has a separate authentication mechanism.

View File

@ -2,6 +2,9 @@ export enum BiometricAction {
EnabledForUser = "enabled",
OsSupported = "osSupported",
Authenticate = "authenticate",
NeedsSetup = "needsSetup",
Setup = "setup",
CanAutoSetup = "canAutoSetup",
}
export type BiometricMessage = {

View File

@ -62,6 +62,10 @@ export function isWindowsStore() {
return windows && windowsStore === true;
}
export function isFlatpak() {
return process.platform === "linux" && process.env.container != null;
}
export function isWindowsPortable() {
return isWindows() && process.env.PORTABLE_EXECUTABLE_DIR != null;
}

View File

@ -194,6 +194,12 @@ export class WebPlatformUtilsService implements PlatformUtilsService {
return Promise.resolve(false);
}
biometricsNeedsSetup: () => Promise<boolean>;
biometricsSupportsAutoSetup(): Promise<boolean> {
throw new Error("Method not implemented.");
}
biometricsSetup: () => Promise<void>;
supportsSecureStorage() {
return false;
}

View File

@ -44,6 +44,24 @@ export abstract class PlatformUtilsService {
abstract copyToClipboard(text: string, options?: ClipboardOptions): void | boolean;
abstract readFromClipboard(): Promise<string>;
abstract supportsBiometric(): Promise<boolean>;
/**
* Determine whether biometrics support requires going through a setup process.
* This is currently only needed on Linux.
*
* @returns true if biometrics support requires setup, false if it does not (is already setup, or did not require it in the first place)
*/
abstract biometricsNeedsSetup: () => Promise<boolean>;
/**
* Determine whether biometrics support can be automatically setup, or requires user interaction.
* Auto-setup is prevented by sandboxed environments, such as Snap and Flatpak.
*
* @returns true if biometrics support can be automatically setup, false if it requires user interaction.
*/
abstract biometricsSupportsAutoSetup(): Promise<boolean>;
/**
* Start automatic biometric setup, which places the required configuration files / changes the required settings.
*/
abstract biometricsSetup: () => Promise<void>;
abstract authenticateBiometric(): Promise<boolean>;
abstract supportsSecureStorage(): boolean;
abstract getAutofillKeyboardShortcut(): Promise<string>;