1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-05 11:13:44 +00:00
This commit is contained in:
Bernd Schoolmann
2025-08-28 04:32:39 +02:00
parent e10d13faa8
commit 77d17a7ed8
18 changed files with 823 additions and 68 deletions

View File

@@ -573,6 +573,19 @@ dependencies = [
"cpufeatures",
]
[[package]]
name = "chacha20poly1305"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35"
dependencies = [
"aead",
"chacha20",
"cipher",
"poly1305",
"zeroize",
]
[[package]]
name = "cipher"
version = "0.4.4"
@@ -879,6 +892,8 @@ dependencies = [
"byteorder",
"bytes",
"cbc",
"chacha20",
"chacha20poly1305",
"core-foundation",
"desktop_objc",
"dirs",
@@ -896,8 +911,11 @@ dependencies = [
"rsa",
"russh-cryptovec",
"scopeguard",
"secmem-proc",
"security-framework",
"security-framework-sys",
"serde",
"serde_json",
"sha2",
"ssh-encoding",
"ssh-key",
@@ -2693,6 +2711,16 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "rustix-linux-procfs"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fc84bf7e9aa16c4f2c758f27412dc9841341e16aa682d9c7ac308fe3ee12056"
dependencies = [
"once_cell",
"rustix 1.0.7",
]
[[package]]
name = "rustversion"
version = "1.0.20"
@@ -2771,6 +2799,21 @@ dependencies = [
"zeroize",
]
[[package]]
name = "secmem-proc"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "473559b1d28f530c3a9b5f91a2866053e2b1c528a0e43dae83048139c99490c2"
dependencies = [
"anyhow",
"cfg-if",
"libc",
"rustix 1.0.7",
"rustix-linux-procfs",
"thiserror 2.0.12",
"windows 0.61.1",
]
[[package]]
name = "security-framework"
version = "3.1.0"

View File

@@ -14,7 +14,6 @@ anyhow = "=1.0.94"
arboard = { version = "=3.6.0", default-features = false }
ashpd = "=0.11.0"
base64 = "=0.22.1"
bindgen = "=0.72.0"
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
byteorder = "=1.5.0"
bytes = "=1.10.1"
@@ -41,6 +40,7 @@ rand = "=0.9.1"
rsa = "=0.9.6"
russh-cryptovec = "=0.7.3"
scopeguard = "=1.2.0"
secmem-proc = "=0.3.7"
security-framework = "=3.1.0"
security-framework-sys = "=2.13.0"
serde = "=1.0.209"

View File

@@ -35,6 +35,7 @@ log = { workspace = true }
rand = { workspace = true }
russh-cryptovec = { workspace = true }
scopeguard = { workspace = true }
secmem-proc = { workspace = true }
sha2 = { workspace = true }
ssh-encoding = { workspace = true }
ssh-key = { workspace = true, features = [
@@ -55,6 +56,11 @@ ed25519 = { workspace = true, features = ["pkcs8"] }
bytes = { workspace = true }
sysinfo = { workspace = true, features = ["windows"] }
zeroizing-alloc = { workspace = true }
chacha20 = "0.9.1"
chacha20poly1305 = "0.10.1"
serde = { workspace = true, features = ["derive"] }
serde_json.workspace = true
windows = { workspace = true, features = ["UI_Accessibility"] }
[target.'cfg(windows)'.dependencies]
widestring = { workspace = true, optional = true }
@@ -65,6 +71,7 @@ windows = { workspace = true, features = [
"Storage_Streams",
"Win32_Foundation",
"Win32_Security_Credentials",
"Win32_Security_Cryptography",
"Win32_System_WinRT",
"Win32_UI_Input_KeyboardAndMouse",
"Win32_UI_WindowsAndMessaging",

View File

@@ -0,0 +1,40 @@
use anyhow::{Result};
#[allow(clippy::module_inception)]
#[cfg_attr(target_os = "linux", path = "unimplemented.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")]
mod windows_focus;
pub use biometric_v2::BiometricLockSystem;
#[allow(async_fn_in_trait)]
pub trait BiometricV2Trait {
/// Authenticate the user
async fn authenticate(&self, hwnd: Vec<u8>, message: String) -> Result<bool>;
/// Check if biometric authentication is available
async fn authenticate_available(&self) -> Result<bool>;
/// Enroll a key for persistent unlock
async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()>;
async fn has_persistent(&self, user_id: &str) -> Result<bool>;
/// On every unlock, the client provides a key to be held for subsequent biometric unlock
async fn provide_key(
&self,
user_id: &str,
key: &[u8]
);
/// Perform biometric unlock and return the key
async fn unlock(
&self,
user_id: &str,
hwnd: Vec<u8>,
) -> Result<Vec<u8>>;
/// Check if biometric unlock is available based on whether a key is present and whether authentication is possible
async fn unlock_available(
&self,
user_id: &str,
) -> Result<bool>;
}

View File

@@ -0,0 +1,37 @@
pub struct Biometric {}
impl super::BiometricV2Trait for Biometric {
async fn authorize(_hwnd: Vec<u8>, _message: String) -> Result<bool> {
unimplemented!()
}
async fn available_available() -> Result<bool> {
Ok(false)
}
async fn enroll_persistent(
user_id: &str,
key: &[u8]
) -> Result<String> {
unimplemented!()
}
async fn provide_userkey(
user_id: &str,
key: &[u8]
) -> Result<String> {
unimplemented!()
}
async fn unlock(
user_id: &str
) -> Result<String> {
unimplemented!()
}
async fn unlock_available(
user_id: &str,
) -> Result<bool> {
Ok(false)
}
}

View File

@@ -0,0 +1,257 @@
//! This file implements Windows-Hello based biometric unlock.
//!
//! # Security
//! Note: There are two scenarios to consider, with different security implications. 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.
//!
//! 1. Require master password on app restart
//! In this scenario, when first unlocking the app, the app sends the user-key to this module, which holds it in secure memory,
//! protected by DPAPI. This makes it inaccessible to other processes, unless they compromise the system administrator, or kernel.
//! While the app is running this key is held in memory, even if locked. When unlocking, the app will prompt the user via
//! `windows_hello_authenticate` to get a yes/no decision on whether to release the key to the app.
//!
//! 2. Do not require master password on app restart
//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the windows hello key
//! with the Windows Hello prompt. This is done by signing a per-user challenge, which produces a deterministic
//! signature which is hashed to obtain a key. This key is used to encrypt and persist the vault unlock key (user key).
//!
//! 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;
use anyhow::{anyhow, Result};
use chacha20poly1305::{aead::Aead, XChaCha20Poly1305, XNonce};
use sha2::{Digest, Sha256};
use tokio::sync::Mutex;
use windows::{
core::{factory, h, HSTRING},
Security::{Credentials::{KeyCredentialCreationOption, KeyCredentialManager, KeyCredentialStatus, UI::{
UserConsentVerificationResult, UserConsentVerifier, UserConsentVerifierAvailability,
}}, Cryptography::CryptographicBuffer},
Win32::{
Foundation::HWND, System::WinRT::IUserConsentVerifierInterop,
UI::WindowsAndMessaging::GetForegroundWindow,
},
};
use windows_future::IAsyncOperation;
use super::windows_focus::{focus_security_prompt, set_focus};
use crate::{
password, secure_memory::*
};
const KEYCHAIN_SERVICE_NAME: &str = "BitwardenBiometricsV2";
#[derive(serde::Serialize, serde::Deserialize)]
struct WindowsHelloKeychainEntry {
nonce: [u8; 24],
challenge: [u8; 16],
wrapped_key: Vec<u8>,
}
/// The Windows OS implementation of the biometric trait.
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::dpapi::DpapiSecretKVStore>>
}
impl BiometricLockSystem {
pub fn new() -> Self {
Self {
secure_memory: Arc::new(Mutex::new(crate::secure_memory::dpapi::DpapiSecretKVStore::new())),
}
}
}
impl super::BiometricV2Trait for BiometricLockSystem {
async fn authenticate(&self, hwnd: Vec<u8>, message: String) -> Result<bool> {
windows_hello_authenticate(hwnd, message)
}
async fn authenticate_available(&self) -> Result<bool> {
match UserConsentVerifier::CheckAvailabilityAsync()?.get()? {
UserConsentVerifierAvailability::Available => Ok(true),
UserConsentVerifierAvailability::DeviceBusy => Ok(true),
_ => Ok(false),
}
}
async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()> {
// Enrollment works by first generating a random challenge unique to the user / enrollment. Then,
// with the challenge and a Windows-Hello prompt, the "windows hello key" is derived. The windows
// hello key is used to encrypt the key to store with XChaCha20Poly1305. The bundle of nonce,
// challenge and wrapped-key are stored to the keychain
// Each enrollment (per user) has a unique challenge, so that the windows-hello key is unique
let mut challenge = [0u8; 16];
rand::fill(&mut challenge);
// This key is unique to the challenge
let windows_hello_key = windows_hello_authenticate_with_crypto(&challenge)?;
let nonce = {
let mut nonce_bytes = [0u8; 24];
rand::fill(&mut nonce_bytes);
XNonce::clone_from_slice(&nonce_bytes)
};
let wrapped_key = XChaCha20Poly1305::new(&windows_hello_key.into()).encrypt(&nonce, key).map_err(|e| anyhow!(e))?;
set_keychain_entry(user_id, &WindowsHelloKeychainEntry {
nonce: nonce.as_slice().try_into().map_err(|_| anyhow!("Invalid nonce length"))?,
challenge,
wrapped_key,
}).await?;
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>> {
let mut secure_memory = self.secure_memory.lock().await;
if secure_memory.has(user_id) {
println!("[Windows Hello] Key is in secure memory, using UV API");
if self.authenticate(hwnd, "Unlock your vault".to_owned()).await? {
println!("[Windows Hello] Authentication successful");
return secure_memory.get(user_id).clone().ok_or_else(|| anyhow!("No key found for user"));
}
Err(anyhow!("Authentication failed"))
} else {
println!("[Windows Hello] Key not in secure memory, using Signing API");
let keychain_entry = get_keychain_entry(user_id).await?;
let windows_hello_key = windows_hello_authenticate_with_crypto(&keychain_entry.challenge)?;
let decrypted_key = XChaCha20Poly1305::new(&windows_hello_key.into()).decrypt(keychain_entry.nonce.as_slice().try_into().map_err(|_| anyhow!("Invalid nonce length"))?, keychain_entry.wrapped_key.as_slice()).map_err(|e| anyhow!(e))?;
secure_memory.put(user_id.to_string(), &decrypted_key.clone());
Ok(decrypted_key)
}
}
async fn unlock_available(&self, user_id: &str) -> Result<bool> {
let secure_memory = self.secure_memory.lock().await;
let has_key = secure_memory.has(user_id) || has_keychain_entry(user_id).await.unwrap_or(false);
Ok(has_key && self.authenticate_available().await.unwrap_or(false))
}
async fn has_persistent(&self, user_id: &str) -> Result<bool> {
Ok(get_keychain_entry(user_id).await.is_ok())
}
}
/// Get a yes/no authorization without any cryptographic backing.
/// This API has better focusing behavior
fn windows_hello_authenticate(hwnd: Vec<u8>, message: String) -> Result<bool> {
let h = isize::from_le_bytes(hwnd.clone().try_into().unwrap());
let h = h as *mut c_void;
let window = HWND(h);
// The Windows Hello prompt is displayed inside the application window. For best result we
// should set the window to the foreground and focus it.
set_focus(window);
// Windows Hello prompt must be in foreground, focused, otherwise the face or fingerprint
// unlock will not work. We get the current foreground window, which will either be the
// Bitwarden desktop app or the browser extension.
let foreground_window = unsafe { GetForegroundWindow() };
let interop = factory::<UserConsentVerifier, IUserConsentVerifierInterop>()?;
let operation: IAsyncOperation<UserConsentVerificationResult> = unsafe {
interop.RequestVerificationForWindowAsync(foreground_window, &HSTRING::from(message))?
};
let result = operation.get()?;
match result {
UserConsentVerificationResult::Verified => Ok(true),
_ => Ok(false),
}
}
/// Derive the symmetric encryption key from the Windows Hello signature.
///
/// This works by signing a static challenge string with Windows Hello protected key store. The
/// signed challenge is then hashed using SHA-256 and used as the symmetric encryption key for the
/// Windows Hello protected keys.
///
/// Windows will only sign the challenge if the user has successfully authenticated with Windows,
/// ensuring user presence.
///
/// Note: This API has inconsistent focusing behavior when called from another window
fn windows_hello_authenticate_with_crypto(challenge: &[u8; 16]) -> Result<[u8; 32]> {
// Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a new API.
// This is unreliable, and if it does not work, the operation may fail
let stop_focusing = Arc::new(AtomicBool::new(false));
let stop_focusing_clone = stop_focusing.clone();
let _ = std::thread::spawn(move || loop {
if !stop_focusing_clone.load(std::sync::atomic::Ordering::Relaxed) {
focus_security_prompt();
std::thread::sleep(std::time::Duration::from_millis(500));
} else {
break;
}
});
// Only stop focusing once this function exists. The focus MUST run both during the initial creation
// with RequestCreateAsync, and also with the subsequent use with RequestSignAsync.
let _guard = scopeguard::guard((), |_| {
stop_focusing.store(true, std::sync::atomic::Ordering::Relaxed);
});
// First create or replace the Bitwarden signing key
let result = KeyCredentialManager::RequestCreateAsync(
h!("BitwardenBiometricsV2"),
KeyCredentialCreationOption::FailIfExists,
)?
.get()?;
let result = match result.Status()? {
KeyCredentialStatus::CredentialAlreadyExists => {
KeyCredentialManager::OpenAsync(h!("BitwardenBiometricsV2"))?.get()?
}
KeyCredentialStatus::Success => result,
_ => return Err(anyhow!("Failed to create key credential")),
};
let signature = result.Credential()?.RequestSignAsync(&CryptographicBuffer::CreateFromByteArray(challenge.as_slice())?)?.get()?;
if signature.Status()? == KeyCredentialStatus::Success {
let signature_buffer = signature.Result()?;
let mut signature_value =
windows::core::Array::<u8>::with_len(signature_buffer.Length().unwrap() as usize);
CryptographicBuffer::CopyToByteArray(&signature_buffer, &mut signature_value)?;
// The signature is deterministic based on the challenge and keychain key. Thus, it can be hashed to a key.
// It is unclear what entropy this key provides.
Ok(Sha256::digest(signature_value.as_slice()).into())
} else {
Err(anyhow!("Failed to sign data"))
}
}
async fn set_keychain_entry(user_id: &str, entry: &WindowsHelloKeychainEntry) -> Result<()> {
let serialized_entry = serde_json::to_string(entry)?;
password::set_password(
KEYCHAIN_SERVICE_NAME,
user_id,
&serialized_entry,
).await?;
Ok(())
}
async fn get_keychain_entry(user_id: &str) -> Result<WindowsHelloKeychainEntry> {
let entry_str = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?;
let entry: WindowsHelloKeychainEntry = serde_json::from_str(&entry_str)?;
Ok(entry)
}
async fn has_keychain_entry(user_id: &str) -> Result<bool> {
let entry = password::get_password(KEYCHAIN_SERVICE_NAME, user_id).await?;
Ok(!entry.is_empty())
}

View File

@@ -0,0 +1,73 @@
use windows::{
core::s,
Win32::{
Foundation::HWND, System::Threading::{AttachThreadInput, GetCurrentThreadId}, UI::{
Input::KeyboardAndMouse::{EnableWindow, SetActiveWindow, SetCapture, SetFocus},
WindowsAndMessaging::{BringWindowToTop, FindWindowA, GetForegroundWindow, GetWindowThreadProcessId, SetForegroundWindow, SwitchToThisWindow, SystemParametersInfoW, SPIF_SENDCHANGE, SPIF_UPDATEINIFILE, SPI_GETFOREGROUNDLOCKTIMEOUT, SPI_SETFOREGROUNDLOCKTIMEOUT},
}
},
};
/// Searches for a window that looks like a security prompt and set it as focused.
/// Only works when the process has permission to foreground, either by being in foreground
/// Or by being given foreground permission https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-setforegroundwindow#remarks
pub fn focus_security_prompt() {
let class_name = s!("Credential Dialog Xaml Host");
let hwnd = unsafe { FindWindowA(class_name, None) };
if let Ok(hwnd) = hwnd {
set_focus(hwnd);
}
}
pub(crate) fn set_focus(window: HWND) {
unsafe {
// Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello bugs.
// The windows hello signing prompt NEEDS to be focused instantly, or it will error, but it does
// not focus itself.
// This function implements forced focusing of windows using a few hacks.
// The conditions to successfully foreground a window are:
// All of the following conditions are true:
// The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1.
// The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function.
// The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo).
// No menus are active.
// Additionally, at least one of the following conditions is true:
// The calling process is the foreground process.
// The calling process was started by the foreground process.
// There is currently no foreground window, and thus no foreground process.
// The calling process received the last input event.
// Either the foreground process or the calling process is being debugged.
// Attach to the foreground thread once attached, we can foregroud, even if in the background
// Update the foreground lock timeout temporarily
let mut old_timeout = 0;
SystemParametersInfoW(
SPI_GETFOREGROUNDLOCKTIMEOUT,
0,
Some(&mut old_timeout as *mut _ as *mut std::ffi::c_void),
windows::Win32::UI::WindowsAndMessaging::SYSTEM_PARAMETERS_INFO_UPDATE_FLAGS(0),
);
SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, 0, None, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
let _scopeguard = scopeguard::guard((), |_| {
SystemParametersInfoW(SPI_SETFOREGROUNDLOCKTIMEOUT, old_timeout, None, SPIF_UPDATEINIFILE | SPIF_SENDCHANGE);
});
// Attach to the active window's thread
let dwCurrentThread = GetCurrentThreadId();
let dwFGThread = GetWindowThreadProcessId(GetForegroundWindow(), None);
AttachThreadInput(dwCurrentThread, dwFGThread, true);
let hwnd = window;
SetForegroundWindow(hwnd);
SetCapture(hwnd);
SetFocus(Some(hwnd));
SetActiveWindow(hwnd);
EnableWindow(hwnd, true);
BringWindowToTop(hwnd);
SwitchToThisWindow(hwnd, true);
AttachThreadInput(dwCurrentThread, dwFGThread, false);
}
}

View File

@@ -9,6 +9,8 @@ pub mod password;
pub mod powermonitor;
pub mod process_isolation;
pub mod ssh_agent;
pub(crate) mod secure_memory;
pub mod biometric_v2;
use zeroizing_alloc::ZeroAlloc;

View File

@@ -0,0 +1,124 @@
use std::collections::HashMap;
use windows::Win32::Security::Cryptography::{
CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE,
CRYPTPROTECTMEMORY_SAME_PROCESS,
};
use crate::secure_memory::SecureMemoryStore;
/// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
/// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound
/// to the current process, and cannot be decrypted by other user-mode processes.
///
/// Note: Admin processes can still decrypt this memory:
/// https://blog.slowerzs.net/posts/cryptdecryptmemory/
pub(crate) struct DpapiSecretKVStore {
map: HashMap<String, Vec<u8>>,
}
impl DpapiSecretKVStore {
pub(crate) fn new() -> Self {
DpapiSecretKVStore {
map: HashMap::new(),
}
}
}
impl SecureMemoryStore for DpapiSecretKVStore {
fn put(&mut self, key: String, value: &[u8]) {
let length_header_len = std::mem::size_of::<usize>();
// The allocated data has to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE, so we pad it and write the length in front
// We are storing LENGTH|DATA|00..00, where LENGTH is the length of DATA, the total length is a multiple
// of CRYPTPROTECTMEMORY_BLOCK_SIZE, and the padding is filled with zeros.
let data_len = value.len();
let len_with_header = data_len + length_header_len;
let padded_length = len_with_header + CRYPTPROTECTMEMORY_BLOCK_SIZE as usize
- (len_with_header % CRYPTPROTECTMEMORY_BLOCK_SIZE as usize);
let mut padded_data = vec![0u8; padded_length];
padded_data[..length_header_len].copy_from_slice(&data_len.to_le_bytes());
padded_data[length_header_len..][..data_len].copy_from_slice(value);
// Protect the memory using DPAPI
unsafe {
CryptProtectMemory(
padded_data.as_mut_ptr() as *mut core::ffi::c_void,
padded_length as u32,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
}
.expect("crypt_protect_memory should work");
self.map.insert(key, padded_data);
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.map.get(key).map(|data| {
// A copy is created, that is then mutated by the DPAPI unprotect function.
let mut data = data.clone();
unsafe {
CryptUnprotectMemory(
data.as_mut_ptr() as *mut core::ffi::c_void,
data.len() as u32,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
}
.expect("crypt_unprotect_memory should work");
// Unpad the data to retrieve the original value
let length_header_size = std::mem::size_of::<usize>();
let length_bytes = &data[..length_header_size];
let data_length = usize::from_le_bytes(
length_bytes
.try_into()
.expect("length header should be usize"),
);
data[length_header_size..length_header_size + data_length].to_vec()
})
}
fn has(&self, key: &str) -> bool {
self.map.contains_key(key)
}
fn remove(&mut self, key: &str) {
if let Some(mut value) = self.map.remove(key) {
unsafe {
std::ptr::write_bytes(value.as_mut_ptr(), 0, value.len());
}
}
}
fn clear(&mut self) {
for (_, mut value) in self.map.drain() {
unsafe {
std::ptr::write_bytes(value.as_mut_ptr(), 0, value.len());
}
}
}
}
impl Drop for DpapiSecretKVStore {
fn drop(&mut self) {
self.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dpapi_secret_kv_store() {
let mut store = DpapiSecretKVStore::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));
}
}

View File

@@ -0,0 +1,41 @@
#[cfg(target_os = "windows")]
pub(crate) mod dpapi;
#[cfg(target_os = "linux")]
mod unimplemented;
#[cfg(target_os = "macos")]
mod unimplemented;
/// The secure memory store provides an ephemeral key-value store for sensitive data.
/// Data stored in this store is prevented from being swapped to disk and zeroed out. Additionally,
/// platform-specific protections are applied to prevent memory dumps or debugger access from
/// reading the stored values.
pub(crate) trait SecureMemoryStore {
/// Stores a copy of the provided value in secure memory.
fn put(&mut self, key: String, value: &[u8]);
/// Retrieves a copy of the value associated with the given key from secure memory.
/// This copy does not have additional memory protections applied, and should be zeroed when no
/// longer needed.
fn get(&self, key: &str) -> Option<Vec<u8>>;
/// Checks if a value is stored under the given key.
fn has(&self, key: &str) -> bool;
/// Removes the value associated with the given key from secure memory.
fn remove(&mut self, key: &str);
/// Clears all values stored in secure memory.
fn clear(&mut self);
}
/// Creates a new secure memory store based on the platform.
pub fn create_secure_memory_store() -> Box<dyn SecureMemoryStore> {
#[cfg(target_os = "linux")]
{
unimplemented!()
}
#[cfg(target_os = "windows")]
{
Box::new(dpapi::DpapiSecretKVStore::new())
}
#[cfg(target_os = "macos")]
{
unimplemented!()
}
}

View File

@@ -0,0 +1,84 @@
use crate::secure_memory::SecureMemoryStore;
pub struct UnimplementedKVStore {}
impl UnimplementedKVStore {
pub(super) fn new() -> Self {
UnimplementedKVStore {
}
}
}
impl SecureMemoryStore for UnimplementedKVStore {
fn put(&mut self, key: String, value: &[u8]) {
}
fn get(&self, key: &str) -> Option<Vec<u8>> {
self.map.get(key).map(|data| {
// A copy is created, that is then mutated by the DPAPI unprotect function.
let mut data = data.clone();
unsafe {
CryptUnprotectMemory(
data.as_mut_ptr() as *mut core::ffi::c_void,
data.len() as u32,
CRYPTPROTECTMEMORY_SAME_PROCESS,
)
}
.expect("crypt_unprotect_memory should work");
// Unpad the data to retrieve the original value
let length_header_size = std::mem::size_of::<usize>();
let length_bytes = &data[..length_header_size];
let data_length = usize::from_le_bytes(
length_bytes
.try_into()
.expect("length header should be usize"),
);
data[length_header_size..length_header_size + data_length].to_vec()
})
}
fn has(&self, key: &str) -> bool {
self.map.contains_key(key)
}
fn remove(&mut self, key: &str) {
if let Some(mut value) = self.map.remove(key) {
unsafe {
std::ptr::write_bytes(value.as_mut_ptr(), 0, value.len());
}
}
}
fn clear(&mut self) {
for (_, mut value) in self.map.drain() {
unsafe {
std::ptr::write_bytes(value.as_mut_ptr(), 0, value.len());
}
}
}
}
impl Drop for DpapiSecretKVStore {
fn drop(&mut self) {
self.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_dpapi_secret_kv_store() {
let mut store = DpapiSecretKVStore::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));
}
}

View File

@@ -21,6 +21,17 @@ export declare namespace passwords {
/** Checks if the os secure storage is available */
export function isAvailable(): Promise<boolean>
}
export declare namespace biometrics_v2 {
export function initBiometricSystem(): BiometricLockSystem
export function authenticate(biometricLockSystem: BiometricLockSystem, hwnd: Buffer, message: string): Promise<boolean>
export function authenticateAvailable(biometricLockSystem: BiometricLockSystem): Promise<boolean>
export function enrollPersistent(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
export function provideKey(biometricLockSystem: BiometricLockSystem, userId: string, key: Buffer): Promise<void>
export function unlock(biometricLockSystem: BiometricLockSystem, userId: string, hwnd: Buffer): Promise<Buffer>
export function unlockAvailable(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
export function hasPersistent(biometricLockSystem: BiometricLockSystem, userId: string): Promise<boolean>
export class BiometricLockSystem { }
}
export declare namespace biometrics {
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function available(): Promise<boolean>

View File

@@ -49,6 +49,71 @@ pub mod passwords {
}
}
#[napi]
pub mod biometrics_v2 {
use desktop_core::biometric_v2::{BiometricV2Trait};
#[napi]
pub struct BiometricLockSystem {
inner: desktop_core::biometric_v2::BiometricLockSystem,
}
#[napi]
pub fn init_biometric_system() -> napi::Result<BiometricLockSystem> {
Ok(BiometricLockSystem {
inner: desktop_core::biometric_v2::BiometricLockSystem::new()
})
}
#[napi]
pub async fn authenticate(
biometric_lock_system: &BiometricLockSystem,
hwnd: napi::bindgen_prelude::Buffer,
message: String,
) -> napi::Result<bool> {
biometric_lock_system.inner.authenticate(hwnd.into(), message)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn authenticate_available(biometric_lock_system: &BiometricLockSystem) -> napi::Result<bool> {
biometric_lock_system.inner.authenticate_available()
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn enroll_persistent(biometric_lock_system: &BiometricLockSystem, user_id: String, key: napi::bindgen_prelude::Buffer) -> napi::Result<()> {
biometric_lock_system.inner.enroll_persistent(&user_id, &key)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn provide_key(biometric_lock_system: &BiometricLockSystem, user_id: String, key: napi::bindgen_prelude::Buffer) -> napi::Result<()> {
biometric_lock_system.inner.provide_key(&user_id, &key).await;
Ok(())
}
#[napi]
pub async fn unlock(biometric_lock_system: &BiometricLockSystem, user_id: String, hwnd: napi::bindgen_prelude::Buffer) -> napi::Result<napi::bindgen_prelude::Buffer> {
biometric_lock_system.inner.unlock(&user_id, hwnd.into()).await.map_err(|e| napi::Error::from_reason(e.to_string()))
.map(|v| v.into())
}
#[napi]
pub async fn unlock_available(biometric_lock_system: &BiometricLockSystem, user_id: String) -> napi::Result<bool> {
biometric_lock_system.inner.unlock_available(&user_id).await.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn has_persistent(biometric_lock_system: &BiometricLockSystem, user_id: String) -> napi::Result<bool> {
biometric_lock_system.inner.has_persistent(&user_id).await.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod biometrics {
use desktop_core::biometric::{Biometric, BiometricTrait};

View File

@@ -6,7 +6,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service"
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key";
import { UserId } from "@bitwarden/common/types/guid";
import { biometrics, passwords } from "@bitwarden/desktop-napi";
import { biometrics, passwords, biometrics_v2 } from "@bitwarden/desktop-napi";
import { BiometricsStatus, BiometricStateService } from "@bitwarden/key-management";
import { WindowMain } from "../../main/window.main";
@@ -26,6 +26,8 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
private _osKeyHalf: string | null = null;
private clientKeyHalves = new Map<UserId, Uint8Array>();
private biometricsSystem = biometrics_v2.initBiometricSystem();
constructor(
private i18nService: I18nService,
private windowMain: WindowMain,
@@ -40,64 +42,16 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
}
async getBiometricKey(userId: UserId): Promise<SymmetricCryptoKey | null> {
const success = await this.authenticateBiometric();
if (!success) {
return null;
}
const value = await passwords.getPassword(SERVICE, getLookupKeyForUser(userId));
if (value == null || value == "") {
throw new Error("Biometric key not found for user");
}
let clientKeyHalfB64: string | null = null;
if (this.clientKeyHalves.has(userId)) {
clientKeyHalfB64 = Utils.fromBufferToB64(this.clientKeyHalves.get(userId)!);
}
if (!EncString.isSerializedEncString(value)) {
// Update to format encrypted with client key half
const storageDetails = await this.getStorageDetails({
clientKeyHalfB64: clientKeyHalfB64 ?? undefined,
});
await biometrics.setBiometricSecret(
SERVICE,
getLookupKeyForUser(userId),
value,
storageDetails.key_material,
storageDetails.ivB64,
);
return SymmetricCryptoKey.fromString(value);
} else {
const encValue = new EncString(value);
this.setIv(encValue.iv);
const storageDetails = await this.getStorageDetails({
clientKeyHalfB64: clientKeyHalfB64 ?? undefined,
});
return SymmetricCryptoKey.fromString(
await biometrics.getBiometricSecret(
SERVICE,
getLookupKeyForUser(userId),
storageDetails.key_material,
),
);
}
const key = await biometrics_v2.unlock(
this.biometricsSystem,
userId,
this.windowMain.win.getNativeWindowHandle(),
);
return new SymmetricCryptoKey(Uint8Array.from(key));
}
async setBiometricKey(userId: UserId, key: SymmetricCryptoKey): Promise<void> {
const clientKeyHalf = await this.getOrCreateBiometricEncryptionClientKeyHalf(userId, key);
const storageDetails = await this.getStorageDetails({
clientKeyHalfB64: Utils.fromBufferToB64(clientKeyHalf),
});
await biometrics.setBiometricSecret(
SERVICE,
getLookupKeyForUser(userId),
key.toBase64(),
storageDetails.key_material,
storageDetails.ivB64,
);
return;
}
async deleteBiometricKey(userId: UserId): Promise<void> {
@@ -199,10 +153,6 @@ export default class OsBiometricsServiceWindows implements OsBiometricService {
}
async getBiometricsFirstUnlockStatusForUser(userId: UserId): Promise<BiometricsStatus> {
if (this.clientKeyHalves.has(userId)) {
return BiometricsStatus.Available;
} else {
return BiometricsStatus.UnlockNeeded;
}
return BiometricsStatus.Available;
}
}

View File

@@ -13,6 +13,8 @@ import { isDev } from "../utils";
import { WindowMain } from "./window.main";
const LOG_MESSAGE_CONTENT = false;
export class NativeMessagingMain {
private ipcServer: ipc.IpcServer | null;
private connected: number[] = [];
@@ -97,7 +99,9 @@ export class NativeMessagingMain {
case ipc.IpcMessageType.Message:
try {
const msgJson = JSON.parse(msg.message);
this.logService.debug("Native messaging message:", msgJson);
if (LOG_MESSAGE_CONTENT) {
this.logService.debug("Native messaging message:", msgJson);
}
this.windowMain.win?.webContents.send("nativeMessaging", msgJson);
} catch (e) {
this.logService.warning("Error processing message:", e, msg.message);
@@ -124,7 +128,9 @@ export class NativeMessagingMain {
}
send(message: object) {
this.logService.debug("Native messaging reply:", message);
if (LOG_MESSAGE_CONTENT) {
this.logService.debug("Native messaging reply:", message);
}
this.ipcServer?.send(JSON.stringify(message));
}

View File

@@ -74,15 +74,24 @@ export class WindowMain {
});
ipcMain.on("window-focus", () => {
this.logService.info("Focusing window");
if (this.win != null) {
this.win.show();
this.win.focus();
this.logService.info("Showing window");
this.win.minimize();
this.show();
this.win.setSize(this.defaultWidth, this.defaultHeight);
this.win.center();
}
});
ipcMain.on("window-hide", () => {
if (this.win != null) {
this.win.hide();
if (this.win != null) {
if (isWindows()) {
// On windows, to return focus we need minimize
this.win.minimize();
} else {
this.win.hide();
}
}
});

View File

@@ -25,6 +25,9 @@ import {
import { BrowserSyncVerificationDialogComponent } from "../app/components/browser-sync-verification-dialog.component";
import { LegacyMessage, LegacyMessageWrapper } from "../models/native-messaging";
import { DesktopSettingsService } from "../platform/services/desktop-settings.service";
import { isWindows } from "../utils";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { DeviceType } from "@bitwarden/common/enums";
const MessageValidTimeout = 10 * 1000;
const HashAlgorithmForAsymmetricEncryption = "sha1";
@@ -89,6 +92,7 @@ export class BiometricMessageHandlerService {
private authService: AuthService,
private ngZone: NgZone,
private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService
) {
combineLatest([
this.desktopSettingService.browserIntegrationEnabled$,
@@ -350,6 +354,7 @@ export class BiometricMessageHandlerService {
// FIXME: Remove when updating file. Eslint update
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (e) {
this.logService.error("[Native Messaging IPC] Biometric unlock failed", e);
await this.send(
{ command: BiometricsCommands.UnlockWithBiometricsForUser, messageId, response: false },
appId,

1
package-lock.json generated
View File

@@ -403,6 +403,7 @@
"license": "GPL-3.0"
},
"libs/state-internal": {
"name": "@bitwarden/state-internal",
"version": "0.0.1",
"license": "GPL-3.0"
},