diff --git a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts index 047687e09ff..8428a74d430 100644 --- a/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts +++ b/apps/browser/src/platform/services/platform-utils/browser-platform-utils.service.ts @@ -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 { + return false; + } + + async biometricsSupportsAutoSetup(): Promise { + return false; + } + + async biometricsSetup(): Promise { + return; + } + authenticateBiometric() { return this.biometricCallback(); } diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 16c359cf599..e98aba8622d 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -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" diff --git a/apps/desktop/desktop_native/core/Cargo.toml b/apps/desktop/desktop_native/core/Cargo.toml index 8ae4e91c0a2..bd95f1132a6 100644 --- a/apps/desktop/desktop_native/core/Cargo.toml +++ b/apps/desktop/desktop_native/core/Cargo.toml @@ -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" diff --git a/apps/desktop/desktop_native/core/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/macos.rs index 858615d2e7e..01ee4519ce6 100644 --- a/apps/desktop/desktop_native/core/src/biometric/macos.rs +++ b/apps/desktop/desktop_native/core/src/biometric/macos.rs @@ -6,11 +6,11 @@ use crate::biometric::{KeyMaterial, OsDerivedKey}; pub struct Biometric {} impl super::BiometricTrait for Biometric { - fn prompt(_hwnd: Vec, _message: String) -> Result { + async fn prompt(_hwnd: Vec, _message: String) -> Result { bail!("platform not supported"); } - fn available() -> Result { + async fn available() -> Result { bail!("platform not supported"); } diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index f61c4f04443..c41ad9dda53 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -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, message: String) -> Result; - fn available() -> Result; + #[allow(async_fn_in_trait)] + async fn prompt(hwnd: Vec, message: String) -> Result; + #[allow(async_fn_in_trait)] + async fn available() -> Result; fn derive_key_material(secret: Option<&str>) -> Result; fn set_biometric_secret( service: &str, @@ -34,3 +41,40 @@ pub trait BiometricTrait { key_material: Option, ) -> Result; } + + +fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result { + 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 { + 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> { + Ok(Sha256::digest(self.digest_material())) + } +} \ No newline at end of file diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index f9fe1ba57ca..742b736e812 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -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, _message: String) -> Result { - bail!("platform not supported"); + async fn prompt(_hwnd: Vec, _message: String) -> Result { + 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 { - bail!("platform not supported"); + async fn available() -> Result { + 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 { - bail!("platform not supported"); - } + fn derive_key_material(challenge_str: Option<&str>) -> Result { + 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, - ) -> Result { - 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, - _iv_b64: &str, + service: &str, + account: &str, + secret: &str, + key_material: Option, + iv_b64: &str, ) -> Result { - 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, + ) -> Result { + 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 +} \ No newline at end of file diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index 1f5929a3ada..c5db9e3277b 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -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, message: String) -> Result { + async fn prompt(hwnd: Vec, message: String) -> Result { 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 { + async fn available() -> Result { 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 { - 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 { - 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> { - 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")] diff --git a/apps/desktop/desktop_native/core/src/password/macos.rs b/apps/desktop/desktop_native/core/src/password/macos.rs index 7f0c3d9f618..408706423e2 100644 --- a/apps/desktop/desktop_native/core/src/password/macos.rs +++ b/apps/desktop/desktop_native/core/src/password/macos.rs @@ -22,6 +22,10 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { Ok(result) } +pub fn is_available() -> Result { + Ok(true) +} + #[cfg(test)] mod tests { use super::*; diff --git a/apps/desktop/desktop_native/core/src/password/unix.rs b/apps/desktop/desktop_native/core/src/password/unix.rs index fa808613dfc..53053ee467e 100644 --- a/apps/desktop/desktop_native/core/src/password/unix.rs +++ b/apps/desktop/desktop_native/core/src/password/unix.rs @@ -40,6 +40,17 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { Ok(result) } +pub fn is_available() -> Result { + 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); diff --git a/apps/desktop/desktop_native/core/src/password/windows.rs b/apps/desktop/desktop_native/core/src/password/windows.rs index 533604e4bac..d932aabae95 100644 --- a/apps/desktop/desktop_native/core/src/password/windows.rs +++ b/apps/desktop/desktop_native/core/src/password/windows.rs @@ -122,6 +122,10 @@ pub fn delete_password(service: &str, account: &str) -> Result<()> { Ok(()) } +pub fn is_available() -> Result { + Ok(true) +} + fn target_name(service: &str, account: &str) -> String { format!("{}/{}", service, account) } diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 208bebcb542..dc3cc7ec0bb 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -12,6 +12,7 @@ export namespace passwords { export function setPassword(service: string, account: string, password: string): Promise /** Delete the stored password from the keychain. */ export function deletePassword(service: string, account: string): Promise + export function isAvailable(): Promise } export namespace biometrics { export function prompt(hwnd: Buffer, message: string): Promise diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index a4db60d2e2a..dfdc316d259 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -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 { + 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 { - 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 { - 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] diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 9245c51d555..359d856525e 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -126,11 +126,14 @@ {{ biometricText | i18n }} - {{ + {{ additionalBiometricSettingsText | i18n }} -
+