1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-22 03:03:43 +00: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
29 changed files with 557 additions and 80 deletions

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)
}