1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-14 15:33:55 +00:00

Linux support

This commit is contained in:
Bernd Schoolmann
2025-08-28 04:57:24 +02:00
parent 77d17a7ed8
commit b478e8eb5e
8 changed files with 289 additions and 32 deletions

View File

@@ -0,0 +1,100 @@
//! This file implements Polkit based system unlock.
//!
//! # Security
//! This section describes the assumed security model and security guarantees achieved. In the required security
//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space)
//! is compromised in this state.
//!
//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory,
//! protected by memfd_secret. This makes it inaccessible to other processes, even if they compromise root, a kernel compromise
//! has circumventable best-effort protections. While the app is running this key is held in memory, even if locked.
//! When unlocking, the app will prompt the user via `polkit` to get a yes/no decision on whether to release the key to the app.
use anyhow::{anyhow, Result};
use tokio::sync::Mutex;
use zbus::Connection;
use zbus_polkit::policykit1::{AuthorityProxy, CheckAuthorizationFlags, Subject};
use std::sync::Arc;
use crate::{
secure_memory::*
};
pub struct BiometricLockSystem {
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure
// locked vaults cannot be unlocked
secure_memory: Arc<Mutex<crate::secure_memory::memfd_secret::MemfdSecretKVStore>>
}
impl BiometricLockSystem {
pub fn new() -> Self {
Self {
secure_memory: Arc::new(Mutex::new(crate::secure_memory::memfd_secret::MemfdSecretKVStore::new()))
}
}
}
impl super::BiometricV2Trait for BiometricLockSystem {
async fn authenticate(&self, _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) => Ok(result.is_authorized),
Err(e) => {
println!("polkit biometric error: {:?}", e);
Ok(false)
}
}
}
async fn authenticate_available(&self) -> Result<bool> {
let connection = Connection::system().await?;
let proxy = AuthorityProxy::new(&connection).await?;
let actions = proxy.enumerate_actions("en").await?;
for action in actions {
if action.action_id == "com.bitwarden.Bitwarden.unlock" {
return Ok(true);
}
}
Ok(false)
}
async fn enroll_persistent(&self, _user_id: &str, _key: &[u8]) -> Result<()> {
Ok(())
}
async fn provide_key(&self, user_id: &str, key: &[u8]) {
let mut secure_memory = self.secure_memory.lock().await;
secure_memory.put(user_id.to_string(), key);
}
async fn unlock(&self, user_id: &str, _hwnd: Vec<u8>) -> Result<Vec<u8>> {
if self.authenticate(Vec::new(), "".to_string()).await? == false {
return Err(anyhow!("Authentication failed"));
}
let secure_memory = self.secure_memory.lock().await;
secure_memory.get(user_id).ok_or(anyhow!("No key found"))
}
async fn unlock_available(&self, user_id: &str) -> Result<bool> {
let secure_memory = self.secure_memory.lock().await;
return Ok(secure_memory.has(user_id));
}
async fn has_persistent(&self, _user_id: &str) -> Result<bool> {
return Ok(false);
}
}

View File

@@ -1,12 +1,12 @@
use anyhow::{Result};
#[allow(clippy::module_inception)]
#[cfg_attr(target_os = "linux", path = "unimplemented.rs")]
#[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "macos", path = "unimplemented.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")]
mod biometric_v2;
#[cfg_attr(target_os = "windows", path = "windows_focus.rs")]
#[cfg(target_os = "windows")]
mod windows_focus;
pub use biometric_v2::BiometricLockSystem;

View File

@@ -20,6 +20,7 @@
//! Since the keychain can be accessed by all user-space processes, the challenge is known to all userspace processes.
//! Therefore, to circumvent the security measure, the attacker would need to create a fake Windows-Hello prompt, and
//! get the user to confirm it.
use std::{ffi::c_void, sync::{atomic::AtomicBool, Arc}};
use aes::cipher::KeyInit;

View File

@@ -0,0 +1,106 @@
use std::{collections::HashMap, ptr::NonNull, sync::LazyLock};
use crate::secure_memory::SecureMemoryStore;
/// https://man.archlinux.org/man/memfd_secret.2.en
/// The memfd_secret store protects the data using the `memfd_secret` syscall. The
/// data is inaccessible to other user-mode processes, and even to root in most cases.
/// If arbitrary data can be executed in the kernel, the data can still be retrieved:
/// https://github.com/JonathonReinhart/nosecmem
pub(crate) struct MemfdSecretKVStore {
map: HashMap<String, std::ptr::NonNull<[u8]>>,
}
impl MemfdSecretKVStore {
pub(crate) fn new() -> Self {
MemfdSecretKVStore {
map: HashMap::new(),
}
}
}
impl SecureMemoryStore for MemfdSecretKVStore {
fn put(&mut self, key: String, value: &[u8]) {
let mut ptr: std::ptr::NonNull<[u8]> = unsafe {
memsec::memfd_secret_sized(value.len()).expect("memfd_secret_sized should work")
};
unsafe {
std::ptr::copy_nonoverlapping(value.as_ptr(), ptr.as_mut().as_mut_ptr(), value.len());
}
self.map.insert(key, ptr);
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
let ptr = self.map.get(key)?;
let value = unsafe { ptr.as_ref() };
Some(value.to_vec())
}
fn has(&self, key: &str) -> bool {
self.map.contains_key(key)
}
fn remove(&mut self, key: &str) {
if let Some(value) = self.map.remove(key) {
unsafe {
memsec::free_memfd_secret(value);
}
}
}
fn clear(&mut self) {
for (_, value) in self.map.drain() {
unsafe {
memsec::free_memfd_secret(value);
}
}
}
}
impl Drop for MemfdSecretKVStore {
fn drop(&mut self) {
self.clear();
}
}
pub(super) fn is_supported() -> bool {
// To test if memfd_secret is supported, we try to allocate a 1 byte and see if that
// succeeds.
static IS_SUPPORTED: LazyLock<bool> = LazyLock::new(|| {
let Some(ptr): Option<NonNull<[u8]>> = (unsafe { memsec::memfd_secret_sized(1) }) else {
return false;
};
// Check that the pointer is readable and writable
let result = unsafe {
let ptr = ptr.as_ptr() as *mut u8;
*ptr = 30;
*ptr += 107;
*ptr == 137
};
unsafe { memsec::free_memfd_secret(ptr) };
result
});
*IS_SUPPORTED
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_memfd_secret_kv_store() {
let mut store = MemfdSecretKVStore::new();
let key = "test_key".to_string();
let value = vec![1, 2, 3, 4, 5];
store.put(key.clone(), &value);
assert!(store.has(&key));
assert_eq!(store.get(&key), Some(value.clone()));
store.remove(&key);
assert!(!store.has(&key));
assert_eq!(store.get(&key), None);
}
}

View File

@@ -1,7 +1,7 @@
#[cfg(target_os = "windows")]
pub(crate) mod dpapi;
#[cfg(target_os = "linux")]
mod unimplemented;
pub(crate) mod memfd_secret;
#[cfg(target_os = "macos")]
mod unimplemented;