From 4aab9360d1fcfc9640e0789d0dd9cd0157f95f98 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Mon, 15 Sep 2025 14:25:59 +0200 Subject: [PATCH 01/27] Bring app bound encryption back together with admin.exe --- apps/desktop/desktop_native/Cargo.lock | 100 +++++ .../bitwarden_chromium_importer/Cargo.toml | 26 +- .../bitwarden_chromium_importer/src/abe.rs | 170 ++++++++ .../src/abe_config.rs | 1 + .../src/bin/admin.rs | 398 ++++++++++++++++++ .../src/chromium.rs | 4 + .../bitwarden_chromium_importer/src/lib.rs | 6 + .../bitwarden_chromium_importer/src/linux.rs | 4 + .../bitwarden_chromium_importer/src/macos.rs | 4 + .../src/windows.rs | 122 +++++- apps/desktop/desktop_native/build.js | 23 + apps/desktop/desktop_native/napi/index.d.ts | 5 +- apps/desktop/desktop_native/napi/src/lib.rs | 5 + libs/importer/src/metadata/importers.ts | 2 +- 14 files changed, 864 insertions(+), 6 deletions(-) create mode 100644 apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs create mode 100644 apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs create mode 100644 apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 23deda915ed..9eca6af5849 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -457,6 +457,10 @@ dependencies = [ "async-trait", "base64", "cbc", + "chacha20poly1305", + "clap", + "colog", + "env_logger", "hex", "homedir", "log", @@ -599,6 +603,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" @@ -670,12 +687,33 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "colog" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c426b7af8d5e0ad79de6713996632ce31f0d68ba84068fb0d654b396e519df0" +dependencies = [ + "colored", + "env_logger", + "log", +] + [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +[[package]] +name = "colored" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" +dependencies = [ + "lazy_static", + "windows-sys 0.59.0", +] + [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1150,6 +1188,29 @@ dependencies = [ "syn", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -1679,6 +1740,30 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jiff" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde", +] + +[[package]] +name = "jiff-static" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "keytar" version = "0.1.6" @@ -2485,6 +2570,21 @@ dependencies = [ "universal-hash", ] +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "portable-atomic-util" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.2" diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index 8512ed1b319..33b7ee14191 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -26,9 +26,26 @@ sha1 = "=0.10.6" security-framework = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +chacha20poly1305 = "=0.10.1" +clap = { version = "=4.5.40", features = ["derive"] } +colog = "=1.3.0" +env_logger = "=0.11.8" tokio = { workspace = true, features = ["full"] } winapi = { version = "=0.3.9", features = ["dpapi", "memoryapi"] } -windows = { workspace = true, features = ["Win32_Security", "Win32_Security_Cryptography", "Win32_Storage_FileSystem", "Win32_System_IO", "Win32_System_Memory", "Win32_System_Pipes", "Win32_System_Services", "Win32_System_Threading", "Win32_UI_Shell", "Win32_UI_WindowsAndMessaging"] } +windows = { workspace = true, features = [ + "Wdk_System_SystemServices", + "Win32_Security_Cryptography", + "Win32_Security", + "Win32_Storage_FileSystem", + "Win32_System_IO", + "Win32_System_Memory", + "Win32_System_Pipes", + "Win32_System_ProcessStatus", + "Win32_System_Services", + "Win32_System_Threading", + "Win32_UI_Shell", + "Win32_UI_WindowsAndMessaging" +] } [target.'cfg(target_os = "linux")'.dependencies] oo7 = { workspace = true } @@ -36,3 +53,10 @@ oo7 = { workspace = true } [lints] workspace = true +[features] +windows-binary = [] + +[[bin]] +name = "admin" +path = "src/bin/admin.rs" +required-features = ["windows-binary"] diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs new file mode 100644 index 00000000000..aab7baa0c34 --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -0,0 +1,170 @@ +use anyhow::{Result, anyhow}; +use log::debug; +use std::{ffi::OsStr, os::windows::ffi::OsStrExt}; +use tokio::{ + io::{self, AsyncReadExt, AsyncWriteExt}, + net::windows::named_pipe::{NamedPipeServer, ServerOptions}, + sync::mpsc::channel, + task::JoinHandle, +}; +use windows::{ + Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE}, + core::PCWSTR, +}; + +use crate::abe_config; + +pub fn start_tokio_named_pipe_server( + pipe_name: &'static str, + process_message: F, +) -> Result>> +where + F: Fn(&str) -> String + Send + Sync + Clone + 'static, +{ + debug!("Starting Tokio named pipe server on: {}", pipe_name); + + // The first server needs to be constructed early so that clients can be correctly + // connected. Otherwise calling .wait will cause the client to error. + // Here we also make use of `first_pipe_instance`, which will ensure that + // there are no other servers up and running already. + let mut server = ServerOptions::new() + // TODO: Try message mode + .first_pipe_instance(true) + .create(pipe_name)?; + + debug!("Named pipe server created and listening..."); + + // Spawn the server loop. + let server_task = tokio::spawn(async move { + loop { + // Wait for a client to connect. + match server.connect().await { + Ok(_) => { + debug!("Client connected to named pipe"); + let connected_client = server; + + // Construct the next server to be connected before sending the one + // we already have off to a task. This ensures that the server + // isn't closed (after it's done in the task) before a new one is + // available. Otherwise the client might error with + // `io::ErrorKind::NotFound`. + server = ServerOptions::new().create(pipe_name)?; + + // Handle the connected client in a separate task + let process_message_clone = process_message.clone(); + let _client_task = tokio::spawn(async move { + if let Err(e) = handle_client(connected_client, process_message_clone).await + { + debug!("Error handling client: {}", e); + } + }); + } + Err(e) => { + debug!("Failed to connect to client: {}", e); + continue; + } + } + } + }); + + Ok(server_task) +} + +async fn handle_client(mut client: NamedPipeServer, process_message: F) -> Result<()> +where + F: Fn(&str) -> String, +{ + debug!("Handling new client connection"); + + loop { + // Read a message from the client + let mut buffer = vec![0u8; 64 * 1024]; + match client.read(&mut buffer).await { + Ok(0) => { + debug!("Client disconnected (0 bytes read)"); + return Ok(()); + } + Ok(bytes_read) => { + let message = String::from_utf8_lossy(&buffer[..bytes_read]); + + debug!("Received from client: '{}' ({} bytes)", message, bytes_read); + + let response = process_message(&message); + + match client.write_all(response.as_bytes()).await { + Ok(_) => { + debug!("Sent response to client ({} bytes)", response.len()); + } + Err(e) => { + return Err(anyhow!("Failed to send response to client: {}", e)); + } + } + } + Err(e) => { + return Err(anyhow!("Failed to read from client: {}", e)); + } + } + } +} + +pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result { + let (tx, mut rx) = channel::(1); + + debug!( + "Starting named pipe server at '{}'...", + abe_config::ADMIN_TO_USER_PIPE_NAME + ); + + let server = match start_tokio_named_pipe_server( + abe_config::ADMIN_TO_USER_PIPE_NAME, + move |message: &str| { + let _ = tx.try_send(message.to_string()); + "ok".to_string() + }, + ) { + Ok(server) => server, + Err(e) => return Err(anyhow!("Failed to start named pipe server: {}", e)), + }; + + debug!("Launching '{}' as admin...", admin_exe); + decrypt_with_admin_internal(admin_exe, encrypted); + + // TODO: Don't wait forever, but for a reasonable time + debug!("Waiting for message from admin..."); + let message = match rx.recv().await { + Some(msg) => msg, + None => return Err(anyhow!("Failed to receive message from admin")), + }; + + debug!("Shutting down the pipe server..."); + _ = server.abort(); + + Ok(message) +} + +fn decrypt_with_admin_internal(admin_exe: &str, encrypted: &str) { + // Convert strings to wide strings for Windows API + let exe_wide = OsStr::new(admin_exe) + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let runas_wide = OsStr::new("runas") + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let parameters = OsStr::new(&format!(r#"--encrypted "{}""#, encrypted)) + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + + unsafe { + ShellExecuteW( + None, + PCWSTR(runas_wide.as_ptr()), + PCWSTR(exe_wide.as_ptr()), + PCWSTR(parameters.as_ptr()), + None, + SW_HIDE, + ); + } +} diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs new file mode 100644 index 00000000000..a1c36d700a8 --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs @@ -0,0 +1 @@ +pub const ADMIN_TO_USER_PIPE_NAME: &str = r"\\.\pipe\BitwardenEncryptionService-admin-user"; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs new file mode 100644 index 00000000000..6438e7f95b3 --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -0,0 +1,398 @@ +use anyhow::{anyhow, Result}; +use base64::{engine::general_purpose, Engine as _}; +use clap::Parser; +use env_logger::Target; +use log::{debug, error}; +use std::{ + ffi::{OsStr, OsString}, + fs::OpenOptions, + os::windows::ffi::OsStringExt as _, + path::PathBuf, + ptr, + time::Duration, +}; +use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::windows::named_pipe::ClientOptions, + time, +}; +use windows::{ + core::BOOL, + Wdk::System::SystemServices::SE_DEBUG_PRIVILEGE, + Win32::{ + Foundation::{ + CloseHandle, LocalFree, ERROR_PIPE_BUSY, HANDLE, HLOCAL, NTSTATUS, STATUS_SUCCESS, + }, + Security::{ + self, + Cryptography::{CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB}, + DuplicateToken, ImpersonateLoggedOnUser, RevertToSelf, TOKEN_DUPLICATE, TOKEN_QUERY, + }, + System::{ + ProcessStatus::{EnumProcesses, K32GetProcessImageFileNameW}, + Threading::{ + OpenProcess, OpenProcessToken, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, + }, + }, + UI::Shell::IsUserAnAdmin, + }, +}; + +use bitwarden_chromium_importer::abe_config; + +#[derive(Parser)] +#[command(name = "admin")] +#[command(about = "Admin tool for ABE service management")] +struct Args { + /// Base64 encoded encrypted data to process + #[arg(long, help = "Base64 encoded encrypted data string")] + encrypted: String, +} + +// Enable this to log to a file. Debugging a system level service in any other way is not easy. +const NEED_LOGGING: bool = true; +const LOG_FILENAME: &str = "c:\\temp\\bitwarden-abe-admin-log.txt"; + +async fn send_message_to_pipe_server(pipe_name: &'static str, message: &str) -> Result { + // TODO: Don't loop forever, but retry a few times + let mut client = loop { + match ClientOptions::new().open(pipe_name) { + Ok(client) => { + debug!("Successfully connected to the pipe!"); + break client; + } + Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => { + debug!("Pipe is busy, retrying in 50ms..."); + } + Err(e) => { + debug!("Failed to connect to pipe: {}", &e); + return Err(e.into()); + } + } + + time::sleep(Duration::from_millis(50)).await; + }; // Send multiple messages to the server + + client.write_all(message.as_bytes()).await?; + + // Try to receive a response for this message + let mut buffer = vec![0u8; 64 * 1024]; + match client.read(&mut buffer).await { + Ok(0) => Err(anyhow!( + "Server closed the connection (0 bytes read) on message" + )), + Ok(bytes_received) => { + let response = String::from_utf8_lossy(&buffer[..bytes_received]); + Ok(response.to_string()) + } + Err(e) => { + return Err(anyhow!("Failed to receive response for message: {}", e)); + } + } +} + +async fn send_error_to_user(error_message: &str) { + _ = send_to_user(&format!("!{}", error_message)).await +} + +async fn send_to_user(message: &str) { + _ = send_message_to_pipe_server(abe_config::ADMIN_TO_USER_PIPE_NAME, &message).await +} + +fn is_admin() -> bool { + unsafe { IsUserAnAdmin().as_bool() } +} + +fn decrypt_data_base64(data_base64: &str, expect_appb: bool) -> Result { + debug!("Decrypting data base64: {}", data_base64); + + let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| { + debug!("Failed to decode base64: {} APPB: {}", e, expect_appb); + e + })?; + + let decrypted = decrypt_data(&data, expect_appb)?; + let decrypted_base64 = general_purpose::STANDARD.encode(decrypted); + + Ok(decrypted_base64) +} + +fn decrypt_data(data: &[u8], expect_appb: bool) -> Result> { + if expect_appb && !data.starts_with(b"APPB") { + debug!("Decoded data does not start with 'APPB'"); + return Err(anyhow!("Decoded data does not start with 'APPB'")); + } + + let data = if expect_appb { &data[4..] } else { data }; + + let mut in_blob = CRYPT_INTEGER_BLOB { + cbData: data.len() as u32, + pbData: data.as_ptr() as *mut u8, + }; + + let mut out_blob = CRYPT_INTEGER_BLOB { + cbData: 0, + pbData: ptr::null_mut(), + }; + + let result = unsafe { + CryptUnprotectData( + &mut in_blob, + Some(ptr::null_mut()), + None, + None, + None, + CRYPTPROTECT_UI_FORBIDDEN, + &mut out_blob, + ) + }; + + if result.is_ok() && !out_blob.pbData.is_null() && out_blob.cbData > 0 { + let decrypted = unsafe { + std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize).to_vec() + }; + + // Free the memory allocated by CryptUnprotectData + unsafe { LocalFree(Some(HLOCAL(out_blob.pbData as *mut _))) }; + + Ok(decrypted) + } else { + debug!("CryptUnprotectData failed"); + Err(anyhow!("CryptUnprotectData failed")) + } +} + +// +// Impersonate a SYSTEM process +// + +struct ImpersonateGuard { + sys_token_handle: HANDLE, +} + +impl Drop for ImpersonateGuard { + fn drop(&mut self) { + _ = Self::stop(); + _ = self.close_sys_handle(); + } +} + +impl ImpersonateGuard { + pub fn start(pid: Option, sys_handle: Option) -> Result<(Self, u32)> { + Self::enable_privilege()?; + let pid = if let Some(pid) = pid { + pid + } else if let Some(pid) = Self::get_system_pid_list()?.next() { + pid + } else { + return Err(anyhow!("Cannot find system process")); + }; + let sys_token = if let Some(handle) = sys_handle { + handle + } else { + let system_handle = Self::get_process_handle(pid)?; + let sys_token = Self::get_system_token(system_handle)?; + unsafe { + CloseHandle(system_handle)?; + }; + + sys_token + }; + unsafe { + ImpersonateLoggedOnUser(sys_token)?; + }; + Ok(( + Self { + sys_token_handle: sys_token, + }, + pid, + )) + } + + pub fn stop() -> Result<()> { + unsafe { + RevertToSelf()?; + }; + Ok(()) + } + + /// stop impersonate and return sys token handle + pub fn _stop_sys_handle(self) -> Result { + unsafe { RevertToSelf() }?; + Ok(self.sys_token_handle) + } + + pub fn close_sys_handle(&self) -> Result<()> { + unsafe { CloseHandle(self.sys_token_handle) }?; + Ok(()) + } + + fn enable_privilege() -> Result<()> { + let mut previous_value = BOOL(0); + let status = unsafe { + RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value) + }; + if status != STATUS_SUCCESS { + return Err(anyhow!("Failed to adjust privilege")); + } + Ok(()) + } + + fn get_system_token(handle: HANDLE) -> Result { + let token_handle = unsafe { + let mut token_handle = HANDLE::default(); + OpenProcessToken(handle, TOKEN_DUPLICATE | TOKEN_QUERY, &mut token_handle)?; + token_handle + }; + let duplicate_token = unsafe { + let mut duplicate_token = HANDLE::default(); + DuplicateToken( + token_handle, + Security::SECURITY_IMPERSONATION_LEVEL(2), + &mut duplicate_token, + )?; + CloseHandle(token_handle)?; + duplicate_token + }; + + Ok(duplicate_token) + } + + fn process_name_is(pid: u32, name_is: F) -> Result + where + F: FnOnce(&OsStr) -> bool, + { + let hprocess = Self::get_process_handle(pid)?; + + let image_file_name = { + let mut lpimagefilename = vec![0; 260]; + let length = + unsafe { K32GetProcessImageFileNameW(hprocess, &mut lpimagefilename) } as usize; + unsafe { + CloseHandle(hprocess)?; + }; + lpimagefilename.truncate(length); + lpimagefilename + }; + + let fp = OsString::from_wide(&image_file_name); + PathBuf::from(fp) + .file_name() + .map(name_is) + .ok_or_else(|| anyhow::anyhow!("Failed to get process name")) + } + + // https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes + fn get_system_pid_list() -> Result> { + let cap = 1024; + let mut lpidprocess = Vec::with_capacity(cap); + let mut lpcbneeded = 0; + + unsafe { + EnumProcesses(lpidprocess.as_mut_ptr(), cap as u32 * 4, &mut lpcbneeded)?; + let c_processes = lpcbneeded as usize / size_of::(); + lpidprocess.set_len(c_processes); + }; + + let filter = lpidprocess.into_iter().filter(|&v| { + v != 0 + && Self::process_name_is(v, |n| n == "lsass.exe" || n == "winlogon.exe") + .unwrap_or(false) + }); + Ok(filter) + } + + fn get_process_handle(pid: u32) -> Result { + let hprocess = + unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?; + Ok(hprocess) + } +} + +#[link(name = "ntdll")] +unsafe extern "system" { + unsafe fn RtlAdjustPrivilege( + privilege: i32, + enable: BOOL, + current_thread: BOOL, + previous_value: *mut BOOL, + ) -> NTSTATUS; +} + +macro_rules! debug_and_send_error { + ($($arg:tt)*) => { + { + let error_message = format!($($arg)*); + debug!("{}", error_message); + send_error_to_user(&error_message).await; + return; + } + }; +} + +#[tokio::main] +async fn main() { + if NEED_LOGGING { + colog::default_builder() + .filter_level(log::STATIC_MAX_LEVEL) // Controlled by the feature flags in Cargo.toml + .target(Target::Pipe(Box::new( + OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILENAME) + .expect("Can't open the log file"), + ))) + .init(); + } + + debug!("Starting admin"); + + let args = match Args::try_parse() { + Ok(args) => args, + Err(e) => { + debug_and_send_error!("Failed to parse command line arguments: {}", e); + } + }; + + if !is_admin() { + error!("Expected to run with admin privileges"); + return; + } + + debug!("Running as admin"); + + // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine + let system_decrypted_base64 = { + let (_guard, pid) = ImpersonateGuard::start(None, None).unwrap(); + debug!("Impersonating system process with PID {}", pid); + + let system_decrypted = decrypt_data_base64(&args.encrypted, true); + debug!("Decrypted data1: {:?}", system_decrypted); + + if let Err(e) = system_decrypted { + let error_message = format!("Failed to decrypt data: {}", e); + error!("{}", error_message); + send_error_to_user(&error_message).await; + return; + } + + system_decrypted.unwrap() + }; + + let user_decrypted = decrypt_data_base64(&system_decrypted_base64, false); + debug!("Decrypted data2: {:?}", user_decrypted); + + if let Err(e) = user_decrypted { + let error_message = format!("Failed to decrypt data: {}", e); + error!("{}", error_message); + send_error_to_user(&error_message).await; + return; + } + + let user_decrypted_base64 = user_decrypted.unwrap(); + + debug!("Sending response back to user"); + send_to_user(&user_decrypted_base64).await; + + return; +} diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index 8179a10213d..5b72d4f888d 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -100,6 +100,10 @@ pub async fn import_logins( Ok(results) } +pub fn configure_windows_crypto_service(admin_exe_path: &String) { + platform::configure_windows_crypto_service(admin_exe_path); +} + // // Private // diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs index b0a399d6321..8702e8df903 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs @@ -1 +1,7 @@ +#[cfg(target_os = "windows")] +pub mod abe; + +#[cfg(target_os = "windows")] +pub mod abe_config; + pub mod chromium; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs index 0ead034a4b2..b520928a5d5 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs @@ -44,6 +44,10 @@ pub fn get_crypto_service( Ok(Box::new(service)) } +pub fn configure_windows_crypto_service(_admin_exe_path: &String) { + // Do nothing on Linux +} + // // Private // diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs index d9aeff68f2b..3d1c3b5bb34 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs @@ -53,6 +53,10 @@ pub fn get_crypto_service( Ok(Box::new(MacCryptoService::new(config))) } +pub fn configure_windows_crypto_service(_admin_exe_path: &String) { + // Do nothing on macOS +} + // // Private // diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index e7dffe93dba..c5b80503e12 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -1,14 +1,19 @@ +use std::sync::Mutex; + use aes_gcm::aead::Aead; use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; +use chacha20poly1305::ChaCha20Poly1305; use winapi::shared::minwindef::{BOOL, BYTE, DWORD}; use winapi::um::{dpapi::CryptUnprotectData, wincrypt::DATA_BLOB}; use windows::Win32::Foundation::{LocalFree, HLOCAL}; use crate::chromium::{BrowserConfig, CryptoService, LocalState}; +mod abe; + #[allow(dead_code)] mod util; @@ -50,12 +55,23 @@ pub fn get_crypto_service( Ok(Box::new(WindowsCryptoService::new(local_state))) } +pub fn configure_windows_crypto_service(admin_exe_path: &String) { + *ADMIN_EXE_PATH.lock().unwrap() = Some(admin_exe_path.clone()); +} + +// +// Private +// + +static ADMIN_EXE_PATH: Mutex> = Mutex::new(None); + // // CryptoService // struct WindowsCryptoService { master_key: Option>, encrypted_key: Option, + app_bound_encrypted_key: Option, } impl WindowsCryptoService { @@ -66,6 +82,10 @@ impl WindowsCryptoService { .os_crypt .as_ref() .and_then(|c| c.encrypted_key.clone()), + app_bound_encrypted_key: local_state + .os_crypt + .as_ref() + .and_then(|c| c.app_bound_encrypted_key.clone()), } } } @@ -100,7 +120,7 @@ impl CryptoService for WindowsCryptoService { } if self.master_key.is_none() { - self.master_key = Some(self.get_master_key(version)?); + self.master_key = Some(self.get_master_key(version).await?); } let key = self @@ -123,9 +143,10 @@ impl CryptoService for WindowsCryptoService { } impl WindowsCryptoService { - fn get_master_key(&mut self, version: &str) -> Result> { + async fn get_master_key(&mut self, version: &str) -> Result> { match version { "v10" => self.get_master_key_v10(), + "v20" => self.get_master_key_v20().await, _ => Err(anyhow!("Unsupported version: {}", version)), } } @@ -154,6 +175,95 @@ impl WindowsCryptoService { Ok(key) } + + async fn get_master_key_v20(&mut self) -> Result> { + if self.app_bound_encrypted_key.is_none() { + return Err(anyhow!( + "Encrypted master key is not found in the local browser state" + )); + } + + let key_base64 = abe::decrypt_with_admin( + &get_admin_exe_path()?, + &self.app_bound_encrypted_key.as_ref().unwrap(), + ) + .await?; + + if key_base64.starts_with('!') { + return Err(anyhow!( + "Failed to decrypt the master key: {}", + &key_base64[1..] + )); + } + + let key_bytes = BASE64_STANDARD.decode(&key_base64)?; + let key = unprotect_data_win(&key_bytes)?; + + if key.len() < 61 { + return Err(anyhow!("Decrypted v20 key is too short")); + } + + let key = key[key.len() - 61..].to_vec(); + + let version = key[0]; + let iv = &key[1..13]; + let ciphertext = &key[13..key.len() - 16]; + let tag = &key[key.len() - 16..]; + + match version { + 0x01 => { + // Google's fixed AES key for v20 decryption + const GOOGLE_AES_KEY: &[u8] = &[ + 0xB3, 0x1C, 0x6E, 0x24, 0x1A, 0xC8, 0x46, 0x72, 0x8D, 0xA9, 0xC1, 0xFA, 0xC4, + 0x93, 0x66, 0x51, 0xCF, 0xFB, 0x94, 0x4D, 0x14, 0x3A, 0xB8, 0x16, 0x27, 0x6B, + 0xCC, 0x6D, 0xA0, 0x28, 0x47, 0x87, + ]; + + let aes_key = Key::::from_slice(GOOGLE_AES_KEY); + let cipher = Aes256Gcm::new(aes_key); + let nonce = Nonce::from_slice(iv); + + let mut ciphertext_with_tag = Vec::new(); + ciphertext_with_tag.extend_from_slice(ciphertext); + ciphertext_with_tag.extend_from_slice(tag); + + let decrypted = cipher + .decrypt(nonce, ciphertext_with_tag.as_ref()) + .map_err(|e| anyhow!("Failed to decrypt v20 key with Google AES key: {}", e))?; + + return Ok(decrypted); + } + + 0x02 => { + // Google's fixed ChaCha20 key for v20 decryption + const GOOGLE_CHACHA20_KEY: &[u8] = &[ + 0xE9, 0x8F, 0x37, 0xD7, 0xF4, 0xE1, 0xFA, 0x43, 0x3D, 0x19, 0x30, 0x4D, 0xC2, + 0x25, 0x80, 0x42, 0x09, 0x0E, 0x2D, 0x1D, 0x7E, 0xEA, 0x76, 0x70, 0xD4, 0x1F, + 0x73, 0x8D, 0x08, 0x72, 0x96, 0x60, + ]; + + let chacha20_key = chacha20poly1305::Key::from_slice(GOOGLE_CHACHA20_KEY); + let cipher = ChaCha20Poly1305::new(chacha20_key); + let nonce = chacha20poly1305::Nonce::from_slice(iv); + + let mut ciphertext_with_tag = Vec::new(); + ciphertext_with_tag.extend_from_slice(ciphertext); + ciphertext_with_tag.extend_from_slice(tag); + + let decrypted = cipher + .decrypt(nonce, ciphertext_with_tag.as_ref()) + .map_err(|e| { + anyhow!("Failed to decrypt v20 key with Google ChaCha20 key: {}", e) + })?; + + return Ok(decrypted); + } + + _ => { + return Err(anyhow!("Unsupported v20 key version: {}", version)); + } + } + } } fn unprotect_data_win(data: &[u8]) -> Result> { @@ -203,3 +313,11 @@ fn unprotect_data_win(data: &[u8]) -> Result> { Ok(output_slice.to_vec()) } + +fn get_admin_exe_path() -> Result { + ADMIN_EXE_PATH + .lock() + .unwrap() + .clone() + .ok_or_else(|| anyhow!("admin.exe path is not set")) +} diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index 125cb1bb567..b4b2da8d93e 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -45,6 +45,26 @@ function buildProxyBin(target, release = true) { } } +function buildImporterBinaries(target, release = true) { + // These binaries are only built for Windows, so we can skip them on other platforms + if (process.platform !== "win32") { + return; + } + + ["admin"].forEach(bin => { + const targetArg = target ? `--target ${target}` : ""; + const releaseArg = release ? "--release" : ""; + child_process.execSync(`cargo build --bin ${bin} ${releaseArg} ${targetArg} --features windows-binary`, {stdio: 'inherit', cwd: path.join(__dirname, "bitwarden_chromium_importer")}); + + if (target) { + // Copy the resulting binary to the dist folder + const targetFolder = release ? "release" : "debug"; + const nodeArch = rustTargetsMap[target].nodeArch; + fs.copyFileSync(path.join(__dirname, "target", target, targetFolder, `${bin}.exe`), path.join(__dirname, "dist", `${bin}.${process.platform}-${nodeArch}.exe`)); + } + }); +} + function installTarget(target) { child_process.execSync(`rustup target add ${target}`, { stdio: 'inherit', cwd: __dirname }); } @@ -53,6 +73,7 @@ if (!crossPlatform && !target) { console.log(`Building native modules in ${mode} mode for the native architecture`); buildNapiModule(false, mode === "release"); buildProxyBin(false, mode === "release"); + buildImporterBinaries(false, mode === "release"); return; } @@ -61,6 +82,7 @@ if (target) { installTarget(target); buildNapiModule(target, mode === "release"); buildProxyBin(target, mode === "release"); + buildImporterBinaries(false, mode === "release"); return; } @@ -78,4 +100,5 @@ platformTargets.forEach(([target, _]) => { installTarget(target); buildNapiModule(target); buildProxyBin(target); + buildImporterBinaries(target); }); diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 281bfd5d69f..9e66db9f340 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -228,9 +228,10 @@ export declare namespace chromium_importer { login?: Login failure?: LoginImportFailure } - export function getInstalledBrowsers(): Promise> - export function getAvailableProfiles(browser: string): Promise> + export function getInstalledBrowsers(): Array + export function getAvailableProfiles(browser: string): Array export function importLogins(browser: string, profileId: string): Promise> + export function configureWindowsCryptoService(adminExePath: string): Promise } export declare namespace autotype { export function getForegroundWindowTitle(): string diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 0e5cdc838d7..457cd63db98 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -967,6 +967,11 @@ pub mod chromium_importer { .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) .map_err(|e| napi::Error::from_reason(e.to_string())) } + + #[napi] + pub async fn configure_windows_crypto_service(admin_exe_path: String) { + bitwarden_chromium_importer::chromium::configure_windows_crypto_service(&admin_exe_path) + } } #[napi] diff --git a/libs/importer/src/metadata/importers.ts b/libs/importer/src/metadata/importers.ts index efd5eafe7d5..6756bfd1411 100644 --- a/libs/importer/src/metadata/importers.ts +++ b/libs/importer/src/metadata/importers.ts @@ -8,7 +8,7 @@ import { ImporterMetadata } from "./types"; // FIXME: load this data from rust code const importers = [ // chromecsv import depends upon operating system, so ironically it doesn't support chromium - { id: "chromecsv", loaders: [Loader.file], instructions: Instructions.chromium }, + { id: "chromecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium }, { id: "operacsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium }, { id: "vivaldicsv", From 700b0ae70ab4aca17ae5443726ee5c0f262af06d Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Mon, 15 Sep 2025 17:18:15 +0200 Subject: [PATCH 02/27] Add to Electron --- .../src/app/tools/import/chromium-importer.service.ts | 7 +++++++ apps/desktop/src/app/tools/preload.ts | 2 ++ 2 files changed, 9 insertions(+) diff --git a/apps/desktop/src/app/tools/import/chromium-importer.service.ts b/apps/desktop/src/app/tools/import/chromium-importer.service.ts index 56f31c359db..898c8acef86 100644 --- a/apps/desktop/src/app/tools/import/chromium-importer.service.ts +++ b/apps/desktop/src/app/tools/import/chromium-importer.service.ts @@ -18,5 +18,12 @@ export class ChromiumImporterService { return await chromium_importer.importLogins(browser, profileId); }, ); + + ipcMain.handle( + "chromium_importer.configureWindowsCryptoService", + async (event, adminExePath: string) => { + return await chromium_importer.configureWindowsCryptoService(adminExePath); + }, + ); } } diff --git a/apps/desktop/src/app/tools/preload.ts b/apps/desktop/src/app/tools/preload.ts index 574c27ac9fd..16e41fe64e3 100644 --- a/apps/desktop/src/app/tools/preload.ts +++ b/apps/desktop/src/app/tools/preload.ts @@ -7,6 +7,8 @@ const chromiumImporter = { ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser), importLogins: (browser: string, profileId: string): Promise => ipcRenderer.invoke("chromium_importer.importLogins", browser, profileId), + configureWindowsCryptoService: (adminExePath: string): Promise => + ipcRenderer.invoke("chromium_importer.configureWindowsCryptoService", adminExePath), }; export default { From 7dad025a8925f9971d7bb9ade87accd06c6a66f9 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Mon, 15 Sep 2025 17:20:39 +0200 Subject: [PATCH 03/27] Move the user-decrypt from admin to user --- .../src/bin/admin.rs | 27 +++++-------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index 6438e7f95b3..fa30d7b095e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -2,7 +2,7 @@ use anyhow::{anyhow, Result}; use base64::{engine::general_purpose, Engine as _}; use clap::Parser; use env_logger::Target; -use log::{debug, error}; +use log::debug; use std::{ ffi::{OsStr, OsString}, fs::OpenOptions, @@ -325,7 +325,6 @@ macro_rules! debug_and_send_error { let error_message = format!($($arg)*); debug!("{}", error_message); send_error_to_user(&error_message).await; - return; } }; } @@ -351,11 +350,12 @@ async fn main() { Ok(args) => args, Err(e) => { debug_and_send_error!("Failed to parse command line arguments: {}", e); + return; } }; if !is_admin() { - error!("Expected to run with admin privileges"); + debug_and_send_error!("Expected to run with admin privileges"); return; } @@ -363,36 +363,23 @@ async fn main() { // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine let system_decrypted_base64 = { + // TODO: Handle errors better and report back to the user! let (_guard, pid) = ImpersonateGuard::start(None, None).unwrap(); debug!("Impersonating system process with PID {}", pid); let system_decrypted = decrypt_data_base64(&args.encrypted, true); - debug!("Decrypted data1: {:?}", system_decrypted); + debug!("Decrypted data with system: {:?}", system_decrypted); if let Err(e) = system_decrypted { - let error_message = format!("Failed to decrypt data: {}", e); - error!("{}", error_message); - send_error_to_user(&error_message).await; + debug_and_send_error!("Failed to decrypt data: {}", e); return; } system_decrypted.unwrap() }; - let user_decrypted = decrypt_data_base64(&system_decrypted_base64, false); - debug!("Decrypted data2: {:?}", user_decrypted); - - if let Err(e) = user_decrypted { - let error_message = format!("Failed to decrypt data: {}", e); - error!("{}", error_message); - send_error_to_user(&error_message).await; - return; - } - - let user_decrypted_base64 = user_decrypted.unwrap(); - debug!("Sending response back to user"); - send_to_user(&user_decrypted_base64).await; + send_to_user(&system_decrypted_base64).await; return; } From 285e2c76108898d8c45872893d8ef4abf04dbc1a Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Mon, 15 Sep 2025 21:50:46 +0200 Subject: [PATCH 04/27] Replace winapi with windows and homedir with dirs --- apps/desktop/desktop_native/Cargo.lock | 3 +- .../bitwarden_chromium_importer/Cargo.toml | 3 +- .../bitwarden_chromium_importer/src/abe.rs | 4 +-- .../src/chromium.rs | 5 ++- .../src/windows.rs | 32 +++++++++---------- 5 files changed, 22 insertions(+), 25 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 9eca6af5849..a73ba1e8b28 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -460,9 +460,9 @@ dependencies = [ "chacha20poly1305", "clap", "colog", + "dirs", "env_logger", "hex", - "homedir", "log", "oo7", "pbkdf2", @@ -473,7 +473,6 @@ dependencies = [ "serde_json", "sha1", "tokio", - "winapi", "windows 0.61.1", ] diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index 33b7ee14191..77b75d341de 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -12,8 +12,8 @@ anyhow = { workspace = true } async-trait = "=0.1.88" base64 = { workspace = true } cbc = { workspace = true, features = ["alloc"] } +dirs = { workspace = true } hex = { workspace = true } -homedir = { workspace = true } log = { workspace = true } pbkdf2 = "=0.12.2" rand = { workspace = true } @@ -31,7 +31,6 @@ clap = { version = "=4.5.40", features = ["derive"] } colog = "=1.3.0" env_logger = "=0.11.8" tokio = { workspace = true, features = ["full"] } -winapi = { version = "=0.3.9", features = ["dpapi", "memoryapi"] } windows = { workspace = true, features = [ "Wdk_System_SystemServices", "Win32_Security_Cryptography", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs index aab7baa0c34..4fa22c2787f 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -1,4 +1,4 @@ -use anyhow::{Result, anyhow}; +use anyhow::{anyhow, Result}; use log::debug; use std::{ffi::OsStr, os::windows::ffi::OsStrExt}; use tokio::{ @@ -8,8 +8,8 @@ use tokio::{ task::JoinHandle, }; use windows::{ - Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE}, core::PCWSTR, + Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE}, }; use crate::abe_config; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index 5b72d4f888d..ed638ff5335 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -3,8 +3,8 @@ use std::sync::LazyLock; use anyhow::{anyhow, Result}; use async_trait::async_trait; +use dirs; use hex::decode; -use homedir::my_home; use rusqlite::{params, Connection}; // Platform-specific code @@ -124,8 +124,7 @@ static SUPPORTED_BROWSER_MAP: LazyLock< }); fn get_browser_data_dir(config: &BrowserConfig) -> Result { - let dir = my_home() - .map_err(|_| anyhow!("Home directory not found"))? + let dir = dirs::home_dir() .ok_or_else(|| anyhow!("Home directory not found"))? .join(config.data_dir); Ok(dir) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index c5b80503e12..ed9c7cbe630 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -6,9 +6,10 @@ use anyhow::{anyhow, Result}; use async_trait::async_trait; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use chacha20poly1305::ChaCha20Poly1305; -use winapi::shared::minwindef::{BOOL, BYTE, DWORD}; -use winapi::um::{dpapi::CryptUnprotectData, wincrypt::DATA_BLOB}; -use windows::Win32::Foundation::{LocalFree, HLOCAL}; +use windows::Win32::{ + Foundation::{LocalFree, HLOCAL}, + Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB}, +}; use crate::chromium::{BrowserConfig, CryptoService, LocalState}; @@ -271,30 +272,29 @@ fn unprotect_data_win(data: &[u8]) -> Result> { return Ok(Vec::new()); } - let mut data_in = DATA_BLOB { - cbData: data.len() as DWORD, - pbData: data.as_ptr() as *mut BYTE, + let data_in = CRYPT_INTEGER_BLOB { + cbData: data.len() as u32, + pbData: data.as_ptr() as *mut u8, }; - let mut data_out = DATA_BLOB { + let mut data_out = CRYPT_INTEGER_BLOB { cbData: 0, pbData: std::ptr::null_mut(), }; - let result: BOOL = unsafe { - // BOOL from winapi (i32) + let result = unsafe { CryptUnprotectData( - &mut data_in, - std::ptr::null_mut(), // ppszDataDescr: *mut LPWSTR (*mut *mut u16) - std::ptr::null_mut(), // pOptionalEntropy: *mut DATA_BLOB - std::ptr::null_mut(), // pvReserved: LPVOID (*mut c_void) - std::ptr::null_mut(), // pPromptStruct: *mut CRYPTPROTECT_PROMPTSTRUCT - 0, // dwFlags: DWORD + &data_in, + None, // ppszDataDescr: Option<*mut PWSTR> + None, // pOptionalEntropy: Option<*const CRYPT_INTEGER_BLOB> + None, // pvReserved: Option<*const std::ffi::c_void> + None, // pPromptStruct: Option<*const CRYPTPROTECT_PROMPTSTRUCT> + 0, // dwFlags: u32 &mut data_out, ) }; - if result == 0 { + if result.is_err() { return Err(anyhow!("CryptUnprotectData failed")); } From f639df5cefd55b4adf67e395fe0d361947284cc4 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Tue, 16 Sep 2025 14:02:35 +0200 Subject: [PATCH 05/27] Replace colog and env_logger with the crates aready used in the project. And disable admin.exe logging by default. Remove hardcoded filename. --- apps/desktop/desktop_native/Cargo.lock | 86 +------------------ .../bitwarden_chromium_importer/Cargo.toml | 3 +- .../src/bin/admin.rs | 28 +++--- 3 files changed, 16 insertions(+), 101 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index a73ba1e8b28..fc2f8bb5958 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -459,9 +459,7 @@ dependencies = [ "cbc", "chacha20poly1305", "clap", - "colog", "dirs", - "env_logger", "hex", "log", "oo7", @@ -472,6 +470,7 @@ dependencies = [ "serde", "serde_json", "sha1", + "simplelog", "tokio", "windows 0.61.1", ] @@ -686,33 +685,12 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "colog" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c426b7af8d5e0ad79de6713996632ce31f0d68ba84068fb0d654b396e519df0" -dependencies = [ - "colored", - "env_logger", - "log", -] - [[package]] name = "colorchoice" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" -[[package]] -name = "colored" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" -dependencies = [ - "lazy_static", - "windows-sys 0.59.0", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -1187,29 +1165,6 @@ dependencies = [ "syn", ] -[[package]] -name = "env_filter" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c863f0904021b108aa8b2f55046443e6b1ebde8fd4a15c399893aae4fa069f" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - [[package]] name = "equivalent" version = "1.0.2" @@ -1739,30 +1694,6 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" -[[package]] -name = "jiff" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde", -] - -[[package]] -name = "jiff-static" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "keytar" version = "0.1.6" @@ -2569,21 +2500,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" - -[[package]] -name = "portable-atomic-util" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507" -dependencies = [ - "portable-atomic", -] - [[package]] name = "potential_utf" version = "0.1.2" diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index 77b75d341de..4396e9efb52 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -28,8 +28,7 @@ security-framework = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] chacha20poly1305 = "=0.10.1" clap = { version = "=4.5.40", features = ["derive"] } -colog = "=1.3.0" -env_logger = "=0.11.8" +simplelog = { workspace = true } tokio = { workspace = true, features = ["full"] } windows = { workspace = true, features = [ "Wdk_System_SystemServices", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index fa30d7b095e..205e381e030 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -1,8 +1,8 @@ use anyhow::{anyhow, Result}; use base64::{engine::general_purpose, Engine as _}; use clap::Parser; -use env_logger::Target; use log::debug; +use simplelog::*; use std::{ ffi::{OsStr, OsString}, fs::OpenOptions, @@ -49,9 +49,9 @@ struct Args { encrypted: String, } -// Enable this to log to a file. Debugging a system level service in any other way is not easy. -const NEED_LOGGING: bool = true; -const LOG_FILENAME: &str = "c:\\temp\\bitwarden-abe-admin-log.txt"; +// Enable this to log to a file. The way this executable is used, it's not easy to debug and the stdout gets lost. +const NEED_LOGGING: bool = false; +const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own async fn send_message_to_pipe_server(pipe_name: &'static str, message: &str) -> Result { // TODO: Don't loop forever, but retry a few times @@ -332,16 +332,16 @@ macro_rules! debug_and_send_error { #[tokio::main] async fn main() { if NEED_LOGGING { - colog::default_builder() - .filter_level(log::STATIC_MAX_LEVEL) // Controlled by the feature flags in Cargo.toml - .target(Target::Pipe(Box::new( - OpenOptions::new() - .create(true) - .append(true) - .open(LOG_FILENAME) - .expect("Can't open the log file"), - ))) - .init(); + WriteLogger::init( + LevelFilter::Debug, // Controlled by the feature flags in Cargo.toml + Config::default(), + OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILENAME) + .expect("Can't open the log file"), + ) + .expect("Failed to initialize logger"); } debug!("Starting admin"); From 7bc254b51277021ed4c9977555119c7ad0a925a6 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Fri, 19 Sep 2025 17:22:30 +0200 Subject: [PATCH 06/27] Parse the ABE key properly and handle multiple enctyption schemes in a browser independent way. Brave ABE is supported now. --- .../src/windows.rs | 102 +++++++++++++----- 1 file changed, 73 insertions(+), 29 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index ed9c7cbe630..c9cd50c3a5e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -200,19 +200,10 @@ impl WindowsCryptoService { let key_bytes = BASE64_STANDARD.decode(&key_base64)?; let key = unprotect_data_win(&key_bytes)?; - if key.len() < 61 { - return Err(anyhow!("Decrypted v20 key is too short")); - } + let key_data = KeyData::parse(&mut &key[..])?; - let key = key[key.len() - 61..].to_vec(); - - let version = key[0]; - let iv = &key[1..13]; - let ciphertext = &key[13..key.len() - 16]; - let tag = &key[key.len() - 16..]; - - match version { - 0x01 => { + match key_data { + KeyData::One { iv, ciphertext } => { // Google's fixed AES key for v20 decryption const GOOGLE_AES_KEY: &[u8] = &[ 0xB3, 0x1C, 0x6E, 0x24, 0x1A, 0xC8, 0x46, 0x72, 0x8D, 0xA9, 0xC1, 0xFA, 0xC4, @@ -222,20 +213,14 @@ impl WindowsCryptoService { let aes_key = Key::::from_slice(GOOGLE_AES_KEY); let cipher = Aes256Gcm::new(aes_key); - let nonce = Nonce::from_slice(iv); - - let mut ciphertext_with_tag = Vec::new(); - ciphertext_with_tag.extend_from_slice(ciphertext); - ciphertext_with_tag.extend_from_slice(tag); let decrypted = cipher - .decrypt(nonce, ciphertext_with_tag.as_ref()) + .decrypt(iv.into(), ciphertext.as_ref()) .map_err(|e| anyhow!("Failed to decrypt v20 key with Google AES key: {}", e))?; return Ok(decrypted); } - - 0x02 => { + KeyData::Two { iv, ciphertext } => { // Google's fixed ChaCha20 key for v20 decryption const GOOGLE_CHACHA20_KEY: &[u8] = &[ 0xE9, 0x8F, 0x37, 0xD7, 0xF4, 0xE1, 0xFA, 0x43, 0x3D, 0x19, 0x30, 0x4D, 0xC2, @@ -245,23 +230,21 @@ impl WindowsCryptoService { let chacha20_key = chacha20poly1305::Key::from_slice(GOOGLE_CHACHA20_KEY); let cipher = ChaCha20Poly1305::new(chacha20_key); - let nonce = chacha20poly1305::Nonce::from_slice(iv); - - let mut ciphertext_with_tag = Vec::new(); - ciphertext_with_tag.extend_from_slice(ciphertext); - ciphertext_with_tag.extend_from_slice(tag); let decrypted = cipher - .decrypt(nonce, ciphertext_with_tag.as_ref()) + .decrypt(iv.into(), ciphertext.as_ref()) .map_err(|e| { anyhow!("Failed to decrypt v20 key with Google ChaCha20 key: {}", e) })?; return Ok(decrypted); } - - _ => { - return Err(anyhow!("Unsupported v20 key version: {}", version)); + KeyData::Three { .. } => { + // There's no way to test this at the moment. This encryption scheme is not used in any of the browsers I've tested. + return Err(anyhow!("v20 version 3 is not supported yet")); + } + KeyData::Plain(key) => { + return Ok(key.to_vec()); } } } @@ -321,3 +304,64 @@ fn get_admin_exe_path() -> Result { .clone() .ok_or_else(|| anyhow!("admin.exe path is not set")) } + +// +// KeyData +// + +// Borrowed from https://github.com/saying121/tidy-browser/blob/master/crates/chromium-crypto/src/win/mod.rs +#[derive(Clone, Copy)] +enum KeyData<'k> { + One { + iv: &'k [u8], + ciphertext: &'k [u8], // with tag + }, + Two { + iv: &'k [u8], + ciphertext: &'k [u8], // with tag + }, + // TODO: Not supported yet + Three { + _enctypted_aes_key: &'k [u8], + _iv: &'k [u8], + _ciphertext: &'k [u8], // with tag + }, + Plain(&'k [u8]), +} + +impl<'k> KeyData<'k> { + fn parse<'b>(blob_data: &mut &'b [u8]) -> Result> { + let header_len = u32::from_le_bytes(blob_data[0..4].try_into()?) as usize; + // Ignore the header + + let content_len_offset = 4 + header_len; + let content_len = + u32::from_le_bytes(blob_data[content_len_offset..content_len_offset + 4].try_into()?) + as usize; + + let content_offset = content_len_offset + 4; + let content = blob_data[content_offset..content_offset + content_len].try_into()?; + + if content_len == 32 { + return Ok(KeyData::Plain(content)); + } + + let initial = content[0]; + match initial { + 1_u8 => Ok(KeyData::One { + iv: content[1..1 + 12].try_into()?, + ciphertext: content[13..13 + 48].try_into()?, + }), + 2_u8 => Ok(KeyData::Two { + iv: content[1..1 + 12].try_into()?, + ciphertext: content[13..13 + 48].try_into()?, + }), + 3_u8 => Ok(KeyData::Three { + _enctypted_aes_key: content[1..1 + 32].try_into()?, + _iv: content[33..33 + 12].try_into()?, + _ciphertext: content[45..45 + 48].try_into()?, + }), + value => Err(anyhow!("Unsupported flag: {}", value)), + } + } +} From 9704cf751d0106b54a89fff4418fcc8dc5e8893c Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Mon, 22 Sep 2025 17:11:19 +0200 Subject: [PATCH 07/27] Remove async to remove the error/warning --- apps/desktop/desktop_native/napi/index.d.ts | 2 +- apps/desktop/desktop_native/napi/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 9e66db9f340..6c53b95e0cc 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -231,7 +231,7 @@ export declare namespace chromium_importer { export function getInstalledBrowsers(): Array export function getAvailableProfiles(browser: string): Array export function importLogins(browser: string, profileId: string): Promise> - export function configureWindowsCryptoService(adminExePath: string): Promise + export function configureWindowsCryptoService(adminExePath: string): void } export declare namespace autotype { export function getForegroundWindowTitle(): string diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 457cd63db98..aafe2234ec0 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -969,7 +969,7 @@ pub mod chromium_importer { } #[napi] - pub async fn configure_windows_crypto_service(admin_exe_path: String) { + pub fn configure_windows_crypto_service(admin_exe_path: String) { bitwarden_chromium_importer::chromium::configure_windows_crypto_service(&admin_exe_path) } } From c8592fe235626b46697bd3cd41ec6c0094c6f607 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Fri, 26 Sep 2025 00:08:31 +0200 Subject: [PATCH 08/27] Update architecture description --- .../bitwarden_chromium_importer/README.md | 200 ++++++++---------- 1 file changed, 87 insertions(+), 113 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md b/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md index 498dd3ac67d..a7bf0a720c1 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md @@ -2,155 +2,129 @@ ## Overview -The Windows Application Bound Encryption (ABE) consists of three main components that work together: +The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that +work together: -- **client library** -- Library that is part of the desktop client application -- **admin.exe** -- Service launcher running as ADMINISTRATOR -- **service.exe** -- Background Windows service running as SYSTEM +- **client library** — a library that is part of the desktop client application +- **admin.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM** -_(The names of the binaries will be changed for the released product.)_ +_(The name of the binary will be changed in the released product.)_ -## The goal +See the last section for a concise summary of the entire process. -The goal of this subsystem is to decrypt the master encryption key with which the login information -is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and -Edge (untested yet) that are using the ABE/v20 encryption scheme for some of the local profiles. +## Goal -The general idea of this encryption scheme is that Chrome generates a unique random encryption key, -then encrypts it at the user level with a fixed key. It then sends it to the Windows Data Protection -API at the user level, and then, using an installed service, encrypts it with the Windows Data -Protection API at the system level on top of that. This triply encrypted key is later stored in the -`Local State` file. +The goal of this subsystem is to decrypt the master encryption key used to encrypt login information +on the local Windows system. This applies to the most recent versions of Chrome, Brave, and +(untested) Edge that use the ABE/v20 encryption scheme for some local profiles. -The next paragraphs describe what is done at each level to decrypt the key. +The general idea of this encryption scheme is as follows: -## 1. Client library +1. Chrome generates a unique random encryption key. +2. This key is first encrypted at the **user level** with a fixed key. +3. It is then encrypted at the **user level** again using the Windows **Data Protection API + (DPAPI)**. +4. Finally, it is sent to a special service that encrypts it with DPAPI at the **system level**. -This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows -(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges -by presenting the user with the UAC screen. See the `abe::decrypt_with_admin_and_service` invocation -in `windows.rs`. +This triply encrypted key is stored in the `Local State` file. -This function takes three arguments: +The following sections describe how the key is decrypted at each level. + +## 1. Client Library + +This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows +(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges, +presenting the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`. + +This function takes two arguments: 1. Absolute path to `admin.exe` -2. Absolute path to `service.exe` -3. Base64 string of the ABE key extracted from the browser's local state +2. Base64 string of the ABE key extracted from the browser's local state -It's not possible to install the service from the user-level executable. So first, we have to -elevate the privileges and run `admin.exe` as ADMINISTRATOR. This is done by calling `ShellExecute` -with the `runas` verb. Since it's not trivial to read the standard output from an application -launched in this way, a named pipe server is created at the user level, which waits for the response -from `admin.exe` after it has been launched. +First, `admin.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb. This +displays the UAC screen. If the user accepts, `admin.exe` starts with **ADMINISTRATOR** privileges. -The name of the service executable and the data to be decrypted are passed via the command line to -`admin.exe` like this: +> **The user must approve the UAC prompt or the process is aborted.** + +Because it is not possible to read the standard output of an application launched in this way, a +named pipe server is created at the user level before `admin.exe` is launched. This pipe is used to +send the decryption result from `admin.exe` back to the client. + +The data to be decrypted are passed via the command line to `admin.exe` like this: ```bat -admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..." +admin.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..." ``` -**At this point, the user must permit the action to be performed on the UAC screen.** +## 2. Admin Executable -## 2. Admin executable +Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to +**SYSTEM**. To achieve this, it uses a technique to impersonate a system-level process. -This executable receives the full path of `service.exe` and the data to be decrypted. +First, `admin.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling +`RtlAdjustPrivilege`. This allows it to enumerate running system-level processes. -First, it installs the service to run as SYSTEM and waits for it to start running. The service -creates a named pipe server that the admin-level executable communicates with (see the `service.exe` -description further down). +Next, it finds an instance of `lsass.exe` or `winlogon.exe`, which are known to run at the +**SYSTEM** level. Once a system process is found, its token is duplicated by calling +`DuplicateToken`. -It sends the base64 string to the pipe server in a raw message and waits for the answer. The answer -could be a success or a failure. In case of success, it's a base64 string decrypted at the system -level. In case of failure, it's an error message prefixed with an `!`. In either case, the response -is sent to the named pipe server created by the user. The user responds with `ok` (ignored). +With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level +process. -After that, the executable stops and uninstalls the service and then exits. +> **At this point `admin.exe` is running as SYSTEM.** -## 3. System service +The received encryption key can now be decrypted using DPAPI at the system level. -The service starts and creates a named pipe server for communication between `admin.exe` and the -system service. Please note that it is not possible to communicate between the user and the system -service directly via a named pipe. Thus, this three-layered approach is necessary. +The decrypted result is sent back to the client via the named pipe. `admin.exe` connects to the pipe +and writes the result. -Once the service is started, it waits for the incoming message via the named pipe. The expected -message is a base64 string to be decrypted. The data is decrypted via the Windows Data Protection -API `CryptUnprotectData` and sent back in response to this incoming message in base64 encoding. In -case of an error, the error message is sent back prefixed with an `!`. +The response can indicate success or failure: -The service keeps running and servicing more requests if there are any, until it's stopped and -removed from the system. Even though we send only one request, the service is designed to handle as -many clients with as many messages as needed and could be installed on the system permanently if -necessary. +- On success: a Base64-encoded string. +- On failure: an error message prefixed with `!`. -## 4. Back to client library +In either case, the response is sent to the named pipe server created by the client. The client +responds with `ok` (ignored). -The decrypted base64-encoded string comes back from the admin executable to the named pipe server at -the user level. At this point, it has been decrypted only once at the system level. +Finally, `admin.exe` exits. -In the next step, the string is decrypted at the user level with the same Windows Data Protection -API. +## 3. Back to the Client Library -And as the third step, it's decrypted with a hard-coded key found in the `elevation_service.exe` -from the Chrome installation. Based on the version of the encrypted string (encoded in the string -itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The details can be found in -`windows.rs`. +The decrypted Base64-encoded string is returned from `admin.exe` to the named pipe server at the +user level. At this point it has been decrypted only once—at the system level. -After all of these steps, we have the master key which can be used to decrypt the password -information stored in the local database. +Next, the string is decrypted at the **user level** with DPAPI. -## Summary +Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in +`elevation_service.exe` from the Chrome installation. Based on the version of the encrypted string +(encoded within the string itself), this step uses either **AES-256-GCM** or **ChaCha20-Poly1305**. +See `windows.rs` for details. -The Windows ABE decryption process involves a three-tier architecture with named pipe communication: +After these steps, the master key is available and can be used to decrypt the password information +stored in the browser’s local database. -```mermaid -sequenceDiagram - participant Client as Client Library (User) - participant Admin as admin.exe (Administrator) - participant Service as service.exe (System) +## TL;DR Steps - Client->>Client: Create named pipe server - Note over Client: \\.\pipe\BitwardenEncryptionService-admin-user +1. **Client side:** - Client->>Admin: Launch with UAC elevation - Note over Client,Admin: --service-exe c:\path\to\service.exe - Note over Client,Admin: --encrypted QVBQQgEAAADQjJ3fARXRE... + 1. Extract the encrypted key from Chrome’s settings. + 2. Create a named pipe server. + 3. Launch `admin.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted via CLI arguments. + 4. Wait for the response from `admin.exe`. - Client->>Client: Wait for response +2. **Admin side:** - Admin->>Service: Install & start service - Note over Admin,Service: c:\path\to\service.exe + 1. Start. + 2. Ensure `SE_DEBUG_PRIVILEGE` is enabled (not strictly necessary in tests). + 3. Impersonate a system process such as `lsass.exe` or `winlogon.exe`. + 4. Decrypt the key using DPAPI at the **SYSTEM** level. + 5. Send the result or error back via the named pipe. + 6. Exit. - Service->>Service: Create named pipe server - Note over Service: \\.\pipe\BitwardenEncryptionService-service-admin - - Service->>Service: Wait for message - - Admin->>Service: Send encrypted data via admin-service pipe - Note over Admin,Service: QVBQQgEAAADQjJ3fARXRE... - - Admin->>Admin: Wait for response - - Service->>Service: Decrypt with system-level DPAPI - - Service->>Admin: Return decrypted data via admin-service pipe - Note over Service,Admin: EjRWeXN0ZW0gU2VydmljZQ... - - Admin->>Client: Send result via named user-admin pipe - Note over Client,Admin: EjRWeXN0ZW0gU2VydmljZQ... - - Client->>Admin: Send ACK to admin - Note over Client,Admin: ok - - Admin->>Service: Stop & uninstall service - Service-->>Admin: Exit - - Admin-->>Client: Exit - - Client->>Client: Decrypt with user-level DPAPI - - Client->>Client: Decrypt with hardcoded key - Note over Client: AES-256-GCM or ChaCha20Poly1305 - - Client->>Client: Done -``` +3. **Back on the client side:** + 1. Receive the encryption key. + 2. Shutdown the pipe server. + 3. Decrypt it with DPAPI at the **USER** level. + 4. (For Chrome only) Decrypt again with the hard-coded key. + 5. Obtain the fully decrypted master key. + 6. Use the master key to read and decrypt stored passwords from Chrome, Brave, Edge, etc. From 86b43d80ce0f5fb9c90817bb1b7c39e14ba34f86 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Wed, 1 Oct 2025 20:45:50 +0200 Subject: [PATCH 09/27] - Verify the signature of the server process on the other end of the pipe (PoC) - Improved error handling - More logging --- apps/desktop/desktop_native/Cargo.lock | 49 ++++- .../bitwarden_chromium_importer/Cargo.toml | 2 + .../src/bin/admin.rs | 197 +++++++++++++----- 3 files changed, 185 insertions(+), 63 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 92e6f25315e..b99cb55c0b5 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -455,12 +455,14 @@ dependencies = [ "pbkdf2", "rand 0.9.1", "rusqlite", + "scopeguard", "security-framework", "serde", "serde_json", "sha1", "simplelog", "tokio", + "verifysign", "windows 0.61.1", ] @@ -1025,7 +1027,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.59.0", + "windows-sys 0.60.2", ] [[package]] @@ -1726,7 +1728,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a793df0d7afeac54f95b471d3af7f0d4fb975699f972341a4b76988d49cdf0c" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.3", ] [[package]] @@ -3632,6 +3634,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "verifysign" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ebfe12e38930c3b851aea35e93fab1a6c29279cad7e8e273f29a21678fee8c0" +dependencies = [ + "core-foundation", + "sha1", + "sha2", + "windows-sys 0.61.1", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3788,7 +3802,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.0", "windows-future", - "windows-link", + "windows-link 0.1.3", "windows-numerics", ] @@ -3821,7 +3835,7 @@ checksum = "4763c1de310c86d75a878046489e2e5ba02c649d185f21c67d4cf8a56d098980" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings", ] @@ -3833,7 +3847,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1d6bbefcb7b60acd19828e1bc965da6fcf18a7e39490c5f8be71e54a19ba32" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3886,6 +3900,12 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +[[package]] +name = "windows-link" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" + [[package]] name = "windows-numerics" version = "0.2.0" @@ -3893,7 +3913,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.0", - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3902,7 +3922,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings", ] @@ -3922,7 +3942,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3931,7 +3951,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link", + "windows-link 0.1.3", ] [[package]] @@ -3961,6 +3981,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -3998,7 +4027,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link", + "windows-link 0.1.3", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index 4396e9efb52..eda67ffa90a 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -28,8 +28,10 @@ security-framework = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] chacha20poly1305 = "=0.10.1" clap = { version = "=4.5.40", features = ["derive"] } +scopeguard = { workspace = true } simplelog = { workspace = true } tokio = { workspace = true, features = ["full"] } +verifysign = "=0.2.4" windows = { workspace = true, features = [ "Wdk_System_SystemServices", "Win32_Security_Cryptography", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index 205e381e030..d5fddd8aa38 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -1,21 +1,23 @@ use anyhow::{anyhow, Result}; use base64::{engine::general_purpose, Engine as _}; use clap::Parser; -use log::debug; +use log::{debug, error}; +use scopeguard::guard; use simplelog::*; use std::{ ffi::{OsStr, OsString}, fs::OpenOptions, - os::windows::ffi::OsStringExt as _, + os::windows::{ffi::OsStringExt as _, io::AsRawHandle}, path::PathBuf, ptr, time::Duration, }; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, - net::windows::named_pipe::ClientOptions, + net::windows::named_pipe::{ClientOptions, NamedPipeClient}, time, }; +use verifysign::CodeSignVerifier; use windows::{ core::BOOL, Wdk::System::SystemServices::SE_DEBUG_PRIVILEGE, @@ -29,9 +31,11 @@ use windows::{ DuplicateToken, ImpersonateLoggedOnUser, RevertToSelf, TOKEN_DUPLICATE, TOKEN_QUERY, }, System::{ + Pipes::GetNamedPipeServerProcessId, ProcessStatus::{EnumProcesses, K32GetProcessImageFileNameW}, Threading::{ - OpenProcess, OpenProcessToken, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, + OpenProcess, OpenProcessToken, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, + PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, }, }, UI::Shell::IsUserAnAdmin, @@ -53,9 +57,14 @@ struct Args { const NEED_LOGGING: bool = false; const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own -async fn send_message_to_pipe_server(pipe_name: &'static str, message: &str) -> Result { +// This should be enabled for production +const NEED_SERVER_SIGNATURE_VALIDATION: bool = false; +const EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT: &str = + "9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d"; + +async fn open_pipe_client(pipe_name: &'static str) -> Result { // TODO: Don't loop forever, but retry a few times - let mut client = loop { + let client = loop { match ClientOptions::new().open(pipe_name) { Ok(client) => { debug!("Successfully connected to the pipe!"); @@ -71,8 +80,12 @@ async fn send_message_to_pipe_server(pipe_name: &'static str, message: &str) -> } time::sleep(Duration::from_millis(50)).await; - }; // Send multiple messages to the server + }; + Ok(client) +} + +async fn send_message_with_client(client: &mut NamedPipeClient, message: &str) -> Result { client.write_all(message.as_bytes()).await?; // Try to receive a response for this message @@ -85,18 +98,52 @@ async fn send_message_to_pipe_server(pipe_name: &'static str, message: &str) -> let response = String::from_utf8_lossy(&buffer[..bytes_received]); Ok(response.to_string()) } - Err(e) => { - return Err(anyhow!("Failed to receive response for message: {}", e)); - } + Err(e) => Err(anyhow!("Failed to receive response for message: {}", e)), } } -async fn send_error_to_user(error_message: &str) { - _ = send_to_user(&format!("!{}", error_message)).await +fn get_named_pipe_server_pid(client: &NamedPipeClient) -> Result { + let handle = HANDLE(client.as_raw_handle() as _); + let mut pid: u32 = 0; + unsafe { GetNamedPipeServerProcessId(handle, &mut pid) }?; + Ok(pid) } -async fn send_to_user(message: &str) { - _ = send_message_to_pipe_server(abe_config::ADMIN_TO_USER_PIPE_NAME, &message).await +fn resolve_process_executable_path(pid: u32) -> Result { + debug!("Resolving process executable path for PID {}", pid); + + // Open the process handle + let hprocess = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?; + debug!("Opened process handle for PID {}", pid); + + let _guard = guard(hprocess, |_| unsafe { + debug!("Closing process handle for PID {}", pid); + _ = CloseHandle(hprocess); + }); + + let mut wide = vec![0u16; 260]; + let mut size = wide.len() as u32; + _ = unsafe { + QueryFullProcessImageNameW( + hprocess, + PROCESS_NAME_WIN32, + windows::core::PWSTR(wide.as_mut_ptr()), + &mut size, + ) + }?; + debug!("QueryFullProcessImageNameW returned {} bytes", size); + + wide.truncate(size as usize); + Ok(PathBuf::from(OsString::from_wide(&wide))) +} + +async fn send_error_to_user(client: &mut NamedPipeClient, error_message: &str) { + _ = send_to_user(client, &format!("!{}", error_message)).await +} + +async fn send_to_user(client: &mut NamedPipeClient, message: &str) -> Result<()> { + let _ = send_message_with_client(client, &message).await?; + Ok(()) } fn is_admin() -> bool { @@ -230,11 +277,16 @@ impl ImpersonateGuard { fn enable_privilege() -> Result<()> { let mut previous_value = BOOL(0); let status = unsafe { + debug!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege"); RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value) }; if status != STATUS_SUCCESS { return Err(anyhow!("Failed to adjust privilege")); } + debug!( + "SE_DEBUG_PRIVILEGE set to 1, was {} before", + previous_value.0 + ); Ok(()) } @@ -319,14 +371,68 @@ unsafe extern "system" { ) -> NTSTATUS; } -macro_rules! debug_and_send_error { - ($($arg:tt)*) => { - { - let error_message = format!($($arg)*); - debug!("{}", error_message); - send_error_to_user(&error_message).await; +async fn open_and_validate_pipe_server(pipe_name: &'static str) -> Result { + let client = open_pipe_client(pipe_name).await?; + + if NEED_SERVER_SIGNATURE_VALIDATION { + let server_pid = get_named_pipe_server_pid(&client)?; + debug!("Connected to pipe server PID {}", server_pid); + + // Validate the server end process signature + let exe_path = resolve_process_executable_path(server_pid)?; + + debug!("Pipe server executable path: {}", exe_path.display()); + + let verifier = CodeSignVerifier::for_file(exe_path.as_path()) + .map_err(|e| anyhow!("verifysign init failed for {}: {:?}", exe_path.display(), e))?; + + let signature = verifier.verify().map_err(|e| { + anyhow!( + "verifysign verify failed for {}: {:?}", + exe_path.display(), + e + ) + })?; + + debug!("Pipe server executable path: {}", exe_path.display()); + + // Dump signature fields for debugging/inspection + debug!("Signature fields:"); + debug!(" Subject Name: {:?}", signature.subject_name()); + debug!(" Issuer Name: {:?}", signature.issuer_name()); + debug!(" SHA1 Thumbprint: {:?}", signature.sha1_thumbprint()); + debug!(" SHA256 Thumbprint: {:?}", signature.sha256_thumbprint()); + debug!(" Serial Number: {:?}", signature.serial()); + + if signature.sha256_thumbprint() != EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT { + return Err(anyhow!("Pipe server signature is not valid")); } - }; + + debug!("Pipe server signature verified for PID {}", server_pid); + } + + Ok(client) +} + +async fn run() -> Result { + debug!("Starting admin.exe"); + + let args = Args::try_parse()?; + + if !is_admin() { + return Err(anyhow!("Expected to run with admin privileges")); + } + + debug!("Running as admin"); + + // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine + let (_guard, pid) = ImpersonateGuard::start(None, None)?; + debug!("Impersonating system process with PID {}", pid); + + let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; + debug!("Decrypted data with system"); + + Ok(system_decrypted_base64) } #[tokio::main] @@ -344,42 +450,27 @@ async fn main() { .expect("Failed to initialize logger"); } - debug!("Starting admin"); - - let args = match Args::try_parse() { - Ok(args) => args, + let mut client = match open_and_validate_pipe_server(abe_config::ADMIN_TO_USER_PIPE_NAME).await + { + Ok(client) => client, Err(e) => { - debug_and_send_error!("Failed to parse command line arguments: {}", e); + error!( + "Failed to open pipe {} to send result/error: {}", + abe_config::ADMIN_TO_USER_PIPE_NAME, + e + ); return; } }; - if !is_admin() { - debug_and_send_error!("Expected to run with admin privileges"); - return; + match run().await { + Ok(system_decrypted_base64) => { + debug!("Sending response back to user"); + let _ = send_to_user(&mut client, &system_decrypted_base64).await; + } + Err(e) => { + debug!("Error: {}", e); + send_error_to_user(&mut client, &format!("{}", e)).await; + } } - - debug!("Running as admin"); - - // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine - let system_decrypted_base64 = { - // TODO: Handle errors better and report back to the user! - let (_guard, pid) = ImpersonateGuard::start(None, None).unwrap(); - debug!("Impersonating system process with PID {}", pid); - - let system_decrypted = decrypt_data_base64(&args.encrypted, true); - debug!("Decrypted data with system: {:?}", system_decrypted); - - if let Err(e) = system_decrypted { - debug_and_send_error!("Failed to decrypt data: {}", e); - return; - } - - system_decrypted.unwrap() - }; - - debug!("Sending response back to user"); - send_to_user(&system_decrypted_base64).await; - - return; } From 6287ac3c100b41e7cb1f1e5b0958d085d8aaf058 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Wed, 1 Oct 2025 21:43:24 +0200 Subject: [PATCH 10/27] Verify that we're decrypting Chrome keys (sort of) --- .../src/bin/admin.rs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index d5fddd8aa38..6f027949573 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -426,11 +426,28 @@ async fn run() -> Result { debug!("Running as admin"); // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine - let (_guard, pid) = ImpersonateGuard::start(None, None)?; - debug!("Impersonating system process with PID {}", pid); + let system_decrypted_base64 = { + let (_guard, pid) = ImpersonateGuard::start(None, None)?; + debug!("Impersonating system process with PID {}", pid); - let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; - debug!("Decrypted data with system"); + let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; + debug!("Decrypted data with system"); + + system_decrypted_base64 + }; + + // This is just to check that we're decrypting Chrome keys and not something else sent to us by a malicious actor. + // Now that we're back from SYSTEM, we need to decrypt one more time just to verify. + // Chrome keys are double encrypted: once at SYSTEM level and once at USER level. + // When the decryption fails, it means that we're decrypting something unexpected. + // We don't send this result back since the library will decrypt again at USER level. + + _ = decrypt_data_base64(&system_decrypted_base64, false).map_err(|e| { + debug!("User level decryption check failed: {}", e); + e + })?; + + debug!("User level decryption check passed"); Ok(system_decrypted_base64) } From 7477c61f0033c14259f25b90adc27eb92d785328 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Tue, 21 Oct 2025 09:13:12 +0200 Subject: [PATCH 11/27] Disable one clippy warning on Linux/macOS as it makes no sense on these platforms --- .../desktop_native/bitwarden_chromium_importer/src/linux.rs | 2 ++ .../desktop_native/bitwarden_chromium_importer/src/macos.rs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs index 3e63dd28120..95816fc8432 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs @@ -44,6 +44,8 @@ pub fn get_crypto_service( Ok(Box::new(service)) } +// Need to allow this to match the signature on other platforms. On Linux this is a no-op. +#[allow(clippy::ptr_arg)] pub fn configure_windows_crypto_service(_admin_exe_path: &String) { // Do nothing on Linux } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs index ef21c578055..1d73a4ba3f6 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs @@ -53,6 +53,8 @@ pub fn get_crypto_service( Ok(Box::new(MacCryptoService::new(config))) } +// Need to allow this to match the signature on other platforms. On macOS this is a no-op. +#[allow(clippy::ptr_arg)] pub fn configure_windows_crypto_service(_admin_exe_path: &String) { // Do nothing on macOS } From 2bb00b93ec9024ad4a515e37e97ff233290c2b38 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 21 Oct 2025 09:22:39 -0400 Subject: [PATCH 12/27] Clippy fix --- .../desktop_native/bitwarden_chromium_importer/src/chromium.rs | 2 +- .../desktop_native/bitwarden_chromium_importer/src/linux.rs | 2 +- .../desktop_native/bitwarden_chromium_importer/src/macos.rs | 2 +- .../desktop_native/bitwarden_chromium_importer/src/windows.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index 4257923aaa3..86487676e9e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -108,7 +108,7 @@ pub async fn import_logins( Ok(results) } -pub fn configure_windows_crypto_service(admin_exe_path: &String) { +pub fn configure_windows_crypto_service(admin_exe_path: &str) { platform::configure_windows_crypto_service(admin_exe_path); } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs index 95816fc8432..9ffc58e76e4 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs @@ -46,7 +46,7 @@ pub fn get_crypto_service( // Need to allow this to match the signature on other platforms. On Linux this is a no-op. #[allow(clippy::ptr_arg)] -pub fn configure_windows_crypto_service(_admin_exe_path: &String) { +pub fn configure_windows_crypto_service(_admin_exe_path: &str) { // Do nothing on Linux } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs index 1d73a4ba3f6..4dd15d25de6 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs @@ -55,7 +55,7 @@ pub fn get_crypto_service( // Need to allow this to match the signature on other platforms. On macOS this is a no-op. #[allow(clippy::ptr_arg)] -pub fn configure_windows_crypto_service(_admin_exe_path: &String) { +pub fn configure_windows_crypto_service(_admin_exe_path: &str) { // Do nothing on macOS } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index ec8e95c5640..f7c4c019506 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -48,7 +48,7 @@ pub fn get_crypto_service( Ok(Box::new(WindowsCryptoService::new(local_state))) } -pub fn configure_windows_crypto_service(admin_exe_path: &String) { +pub fn configure_windows_crypto_service(admin_exe_path: &str) { *ADMIN_EXE_PATH.lock().unwrap() = Some(admin_exe_path.clone()); } From 8c2493a3ccf5fd18e271d58220c597878ae0fe16 Mon Sep 17 00:00:00 2001 From: adudek-bw Date: Tue, 21 Oct 2025 09:48:41 -0400 Subject: [PATCH 13/27] String fix --- .../desktop_native/bitwarden_chromium_importer/src/windows.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index f7c4c019506..bca6d6c5dfa 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -49,7 +49,7 @@ pub fn get_crypto_service( } pub fn configure_windows_crypto_service(admin_exe_path: &str) { - *ADMIN_EXE_PATH.lock().unwrap() = Some(admin_exe_path.clone()); + *ADMIN_EXE_PATH.lock().unwrap() = Some(admin_exe_path.to_string()); } // From 76ac318bcb9a09a96637c436db5e3b67153e5a59 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Wed, 22 Oct 2025 11:08:38 +0200 Subject: [PATCH 14/27] Remove unnecessary clippy config attr and stale comments --- .../desktop_native/bitwarden_chromium_importer/src/linux.rs | 2 -- .../desktop_native/bitwarden_chromium_importer/src/macos.rs | 2 -- 2 files changed, 4 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs index 9ffc58e76e4..ebacc3bfe12 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs @@ -44,8 +44,6 @@ pub fn get_crypto_service( Ok(Box::new(service)) } -// Need to allow this to match the signature on other platforms. On Linux this is a no-op. -#[allow(clippy::ptr_arg)] pub fn configure_windows_crypto_service(_admin_exe_path: &str) { // Do nothing on Linux } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs index 4dd15d25de6..61d79042064 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs @@ -53,8 +53,6 @@ pub fn get_crypto_service( Ok(Box::new(MacCryptoService::new(config))) } -// Need to allow this to match the signature on other platforms. On macOS this is a no-op. -#[allow(clippy::ptr_arg)] pub fn configure_windows_crypto_service(_admin_exe_path: &str) { // Do nothing on macOS } From 7b692f0e57459841b0457064fb02d1d678ade433 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Wed, 22 Oct 2025 13:07:13 +0200 Subject: [PATCH 15/27] Move all admin.exe code into an inline module and hide it behind cfg windows to fix clippy warning --- .../src/bin/admin.rs | 920 +++++++++--------- 1 file changed, 469 insertions(+), 451 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index 6f027949573..055f4fa9a2a 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -1,493 +1,511 @@ -use anyhow::{anyhow, Result}; -use base64::{engine::general_purpose, Engine as _}; -use clap::Parser; -use log::{debug, error}; -use scopeguard::guard; -use simplelog::*; -use std::{ - ffi::{OsStr, OsString}, - fs::OpenOptions, - os::windows::{ffi::OsStringExt as _, io::AsRawHandle}, - path::PathBuf, - ptr, - time::Duration, -}; -use tokio::{ - io::{AsyncReadExt, AsyncWriteExt}, - net::windows::named_pipe::{ClientOptions, NamedPipeClient}, - time, -}; -use verifysign::CodeSignVerifier; -use windows::{ - core::BOOL, - Wdk::System::SystemServices::SE_DEBUG_PRIVILEGE, - Win32::{ - Foundation::{ - CloseHandle, LocalFree, ERROR_PIPE_BUSY, HANDLE, HLOCAL, NTSTATUS, STATUS_SUCCESS, - }, - Security::{ - self, - Cryptography::{CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB}, - DuplicateToken, ImpersonateLoggedOnUser, RevertToSelf, TOKEN_DUPLICATE, TOKEN_QUERY, - }, - System::{ - Pipes::GetNamedPipeServerProcessId, - ProcessStatus::{EnumProcesses, K32GetProcessImageFileNameW}, - Threading::{ - OpenProcess, OpenProcessToken, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, - PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, +use napi::tokio; + +// Hide everything inside a platform specific module to avoid clippy errors on other platforms +#[cfg(target_os = "windows")] +mod windows_binary { + use anyhow::{anyhow, Result}; + use base64::{engine::general_purpose, Engine as _}; + use clap::Parser; + use log::{debug, error}; + use scopeguard::guard; + use simplelog::*; + use std::{ + ffi::{OsStr, OsString}, + fs::OpenOptions, + os::windows::{ffi::OsStringExt as _, io::AsRawHandle}, + path::PathBuf, + ptr, + time::Duration, + }; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::windows::named_pipe::{ClientOptions, NamedPipeClient}, + time, + }; + use verifysign::CodeSignVerifier; + use windows::{ + core::BOOL, + Wdk::System::SystemServices::SE_DEBUG_PRIVILEGE, + Win32::{ + Foundation::{ + CloseHandle, LocalFree, ERROR_PIPE_BUSY, HANDLE, HLOCAL, NTSTATUS, STATUS_SUCCESS, }, - }, - UI::Shell::IsUserAnAdmin, - }, -}; - -use bitwarden_chromium_importer::abe_config; - -#[derive(Parser)] -#[command(name = "admin")] -#[command(about = "Admin tool for ABE service management")] -struct Args { - /// Base64 encoded encrypted data to process - #[arg(long, help = "Base64 encoded encrypted data string")] - encrypted: String, -} - -// Enable this to log to a file. The way this executable is used, it's not easy to debug and the stdout gets lost. -const NEED_LOGGING: bool = false; -const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own - -// This should be enabled for production -const NEED_SERVER_SIGNATURE_VALIDATION: bool = false; -const EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT: &str = - "9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d"; - -async fn open_pipe_client(pipe_name: &'static str) -> Result { - // TODO: Don't loop forever, but retry a few times - let client = loop { - match ClientOptions::new().open(pipe_name) { - Ok(client) => { - debug!("Successfully connected to the pipe!"); - break client; - } - Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => { - debug!("Pipe is busy, retrying in 50ms..."); - } - Err(e) => { - debug!("Failed to connect to pipe: {}", &e); - return Err(e.into()); - } - } - - time::sleep(Duration::from_millis(50)).await; - }; - - Ok(client) -} - -async fn send_message_with_client(client: &mut NamedPipeClient, message: &str) -> Result { - client.write_all(message.as_bytes()).await?; - - // Try to receive a response for this message - let mut buffer = vec![0u8; 64 * 1024]; - match client.read(&mut buffer).await { - Ok(0) => Err(anyhow!( - "Server closed the connection (0 bytes read) on message" - )), - Ok(bytes_received) => { - let response = String::from_utf8_lossy(&buffer[..bytes_received]); - Ok(response.to_string()) - } - Err(e) => Err(anyhow!("Failed to receive response for message: {}", e)), - } -} - -fn get_named_pipe_server_pid(client: &NamedPipeClient) -> Result { - let handle = HANDLE(client.as_raw_handle() as _); - let mut pid: u32 = 0; - unsafe { GetNamedPipeServerProcessId(handle, &mut pid) }?; - Ok(pid) -} - -fn resolve_process_executable_path(pid: u32) -> Result { - debug!("Resolving process executable path for PID {}", pid); - - // Open the process handle - let hprocess = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?; - debug!("Opened process handle for PID {}", pid); - - let _guard = guard(hprocess, |_| unsafe { - debug!("Closing process handle for PID {}", pid); - _ = CloseHandle(hprocess); - }); - - let mut wide = vec![0u16; 260]; - let mut size = wide.len() as u32; - _ = unsafe { - QueryFullProcessImageNameW( - hprocess, - PROCESS_NAME_WIN32, - windows::core::PWSTR(wide.as_mut_ptr()), - &mut size, - ) - }?; - debug!("QueryFullProcessImageNameW returned {} bytes", size); - - wide.truncate(size as usize); - Ok(PathBuf::from(OsString::from_wide(&wide))) -} - -async fn send_error_to_user(client: &mut NamedPipeClient, error_message: &str) { - _ = send_to_user(client, &format!("!{}", error_message)).await -} - -async fn send_to_user(client: &mut NamedPipeClient, message: &str) -> Result<()> { - let _ = send_message_with_client(client, &message).await?; - Ok(()) -} - -fn is_admin() -> bool { - unsafe { IsUserAnAdmin().as_bool() } -} - -fn decrypt_data_base64(data_base64: &str, expect_appb: bool) -> Result { - debug!("Decrypting data base64: {}", data_base64); - - let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| { - debug!("Failed to decode base64: {} APPB: {}", e, expect_appb); - e - })?; - - let decrypted = decrypt_data(&data, expect_appb)?; - let decrypted_base64 = general_purpose::STANDARD.encode(decrypted); - - Ok(decrypted_base64) -} - -fn decrypt_data(data: &[u8], expect_appb: bool) -> Result> { - if expect_appb && !data.starts_with(b"APPB") { - debug!("Decoded data does not start with 'APPB'"); - return Err(anyhow!("Decoded data does not start with 'APPB'")); - } - - let data = if expect_appb { &data[4..] } else { data }; - - let mut in_blob = CRYPT_INTEGER_BLOB { - cbData: data.len() as u32, - pbData: data.as_ptr() as *mut u8, - }; - - let mut out_blob = CRYPT_INTEGER_BLOB { - cbData: 0, - pbData: ptr::null_mut(), - }; - - let result = unsafe { - CryptUnprotectData( - &mut in_blob, - Some(ptr::null_mut()), - None, - None, - None, - CRYPTPROTECT_UI_FORBIDDEN, - &mut out_blob, - ) - }; - - if result.is_ok() && !out_blob.pbData.is_null() && out_blob.cbData > 0 { - let decrypted = unsafe { - std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize).to_vec() - }; - - // Free the memory allocated by CryptUnprotectData - unsafe { LocalFree(Some(HLOCAL(out_blob.pbData as *mut _))) }; - - Ok(decrypted) - } else { - debug!("CryptUnprotectData failed"); - Err(anyhow!("CryptUnprotectData failed")) - } -} - -// -// Impersonate a SYSTEM process -// - -struct ImpersonateGuard { - sys_token_handle: HANDLE, -} - -impl Drop for ImpersonateGuard { - fn drop(&mut self) { - _ = Self::stop(); - _ = self.close_sys_handle(); - } -} - -impl ImpersonateGuard { - pub fn start(pid: Option, sys_handle: Option) -> Result<(Self, u32)> { - Self::enable_privilege()?; - let pid = if let Some(pid) = pid { - pid - } else if let Some(pid) = Self::get_system_pid_list()?.next() { - pid - } else { - return Err(anyhow!("Cannot find system process")); - }; - let sys_token = if let Some(handle) = sys_handle { - handle - } else { - let system_handle = Self::get_process_handle(pid)?; - let sys_token = Self::get_system_token(system_handle)?; - unsafe { - CloseHandle(system_handle)?; - }; - - sys_token - }; - unsafe { - ImpersonateLoggedOnUser(sys_token)?; - }; - Ok(( - Self { - sys_token_handle: sys_token, + Security::{ + self, + Cryptography::{CryptUnprotectData, CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB}, + DuplicateToken, ImpersonateLoggedOnUser, RevertToSelf, TOKEN_DUPLICATE, + TOKEN_QUERY, }, - pid, - )) + System::{ + Pipes::GetNamedPipeServerProcessId, + ProcessStatus::{EnumProcesses, K32GetProcessImageFileNameW}, + Threading::{ + OpenProcess, OpenProcessToken, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, + PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, + }, + }, + UI::Shell::IsUserAnAdmin, + }, + }; + + use bitwarden_chromium_importer::abe_config; + + #[derive(Parser)] + #[command(name = "admin")] + #[command(about = "Admin tool for ABE service management")] + struct Args { + /// Base64 encoded encrypted data to process + #[arg(long, help = "Base64 encoded encrypted data string")] + encrypted: String, } - pub fn stop() -> Result<()> { - unsafe { - RevertToSelf()?; + // Enable this to log to a file. The way this executable is used, it's not easy to debug and the stdout gets lost. + const NEED_LOGGING: bool = false; + const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own + + // This should be enabled for production + const NEED_SERVER_SIGNATURE_VALIDATION: bool = false; + const EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT: &str = + "9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d"; + + async fn open_pipe_client(pipe_name: &'static str) -> Result { + // TODO: Don't loop forever, but retry a few times + let client = loop { + match ClientOptions::new().open(pipe_name) { + Ok(client) => { + debug!("Successfully connected to the pipe!"); + break client; + } + Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => { + debug!("Pipe is busy, retrying in 50ms..."); + } + Err(e) => { + debug!("Failed to connect to pipe: {}", &e); + return Err(e.into()); + } + } + + time::sleep(Duration::from_millis(50)).await; }; - Ok(()) + + Ok(client) } - /// stop impersonate and return sys token handle - pub fn _stop_sys_handle(self) -> Result { - unsafe { RevertToSelf() }?; - Ok(self.sys_token_handle) - } + async fn send_message_with_client( + client: &mut NamedPipeClient, + message: &str, + ) -> Result { + client.write_all(message.as_bytes()).await?; - pub fn close_sys_handle(&self) -> Result<()> { - unsafe { CloseHandle(self.sys_token_handle) }?; - Ok(()) - } - - fn enable_privilege() -> Result<()> { - let mut previous_value = BOOL(0); - let status = unsafe { - debug!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege"); - RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value) - }; - if status != STATUS_SUCCESS { - return Err(anyhow!("Failed to adjust privilege")); + // Try to receive a response for this message + let mut buffer = vec![0u8; 64 * 1024]; + match client.read(&mut buffer).await { + Ok(0) => Err(anyhow!( + "Server closed the connection (0 bytes read) on message" + )), + Ok(bytes_received) => { + let response = String::from_utf8_lossy(&buffer[..bytes_received]); + Ok(response.to_string()) + } + Err(e) => Err(anyhow!("Failed to receive response for message: {}", e)), } - debug!( - "SE_DEBUG_PRIVILEGE set to 1, was {} before", - previous_value.0 - ); - Ok(()) } - fn get_system_token(handle: HANDLE) -> Result { - let token_handle = unsafe { - let mut token_handle = HANDLE::default(); - OpenProcessToken(handle, TOKEN_DUPLICATE | TOKEN_QUERY, &mut token_handle)?; - token_handle - }; - let duplicate_token = unsafe { - let mut duplicate_token = HANDLE::default(); - DuplicateToken( - token_handle, - Security::SECURITY_IMPERSONATION_LEVEL(2), - &mut duplicate_token, - )?; - CloseHandle(token_handle)?; - duplicate_token - }; - - Ok(duplicate_token) + fn get_named_pipe_server_pid(client: &NamedPipeClient) -> Result { + let handle = HANDLE(client.as_raw_handle() as _); + let mut pid: u32 = 0; + unsafe { GetNamedPipeServerProcessId(handle, &mut pid) }?; + Ok(pid) } - fn process_name_is(pid: u32, name_is: F) -> Result - where - F: FnOnce(&OsStr) -> bool, - { - let hprocess = Self::get_process_handle(pid)?; + fn resolve_process_executable_path(pid: u32) -> Result { + debug!("Resolving process executable path for PID {}", pid); - let image_file_name = { - let mut lpimagefilename = vec![0; 260]; - let length = - unsafe { K32GetProcessImageFileNameW(hprocess, &mut lpimagefilename) } as usize; - unsafe { - CloseHandle(hprocess)?; - }; - lpimagefilename.truncate(length); - lpimagefilename - }; - - let fp = OsString::from_wide(&image_file_name); - PathBuf::from(fp) - .file_name() - .map(name_is) - .ok_or_else(|| anyhow::anyhow!("Failed to get process name")) - } - - // https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes - fn get_system_pid_list() -> Result> { - let cap = 1024; - let mut lpidprocess = Vec::with_capacity(cap); - let mut lpcbneeded = 0; - - unsafe { - EnumProcesses(lpidprocess.as_mut_ptr(), cap as u32 * 4, &mut lpcbneeded)?; - let c_processes = lpcbneeded as usize / size_of::(); - lpidprocess.set_len(c_processes); - }; - - let filter = lpidprocess.into_iter().filter(|&v| { - v != 0 - && Self::process_name_is(v, |n| n == "lsass.exe" || n == "winlogon.exe") - .unwrap_or(false) - }); - Ok(filter) - } - - fn get_process_handle(pid: u32) -> Result { + // Open the process handle let hprocess = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?; - Ok(hprocess) - } -} + debug!("Opened process handle for PID {}", pid); -#[link(name = "ntdll")] -unsafe extern "system" { - unsafe fn RtlAdjustPrivilege( - privilege: i32, - enable: BOOL, - current_thread: BOOL, - previous_value: *mut BOOL, - ) -> NTSTATUS; -} + let _guard = guard(hprocess, |_| unsafe { + debug!("Closing process handle for PID {}", pid); + _ = CloseHandle(hprocess); + }); -async fn open_and_validate_pipe_server(pipe_name: &'static str) -> Result { - let client = open_pipe_client(pipe_name).await?; - - if NEED_SERVER_SIGNATURE_VALIDATION { - let server_pid = get_named_pipe_server_pid(&client)?; - debug!("Connected to pipe server PID {}", server_pid); - - // Validate the server end process signature - let exe_path = resolve_process_executable_path(server_pid)?; - - debug!("Pipe server executable path: {}", exe_path.display()); - - let verifier = CodeSignVerifier::for_file(exe_path.as_path()) - .map_err(|e| anyhow!("verifysign init failed for {}: {:?}", exe_path.display(), e))?; - - let signature = verifier.verify().map_err(|e| { - anyhow!( - "verifysign verify failed for {}: {:?}", - exe_path.display(), - e + let mut wide = vec![0u16; 260]; + let mut size = wide.len() as u32; + _ = unsafe { + QueryFullProcessImageNameW( + hprocess, + PROCESS_NAME_WIN32, + windows::core::PWSTR(wide.as_mut_ptr()), + &mut size, ) + }?; + debug!("QueryFullProcessImageNameW returned {} bytes", size); + + wide.truncate(size as usize); + Ok(PathBuf::from(OsString::from_wide(&wide))) + } + + async fn send_error_to_user(client: &mut NamedPipeClient, error_message: &str) { + _ = send_to_user(client, &format!("!{}", error_message)).await + } + + async fn send_to_user(client: &mut NamedPipeClient, message: &str) -> Result<()> { + let _ = send_message_with_client(client, &message).await?; + Ok(()) + } + + fn is_admin() -> bool { + unsafe { IsUserAnAdmin().as_bool() } + } + + fn decrypt_data_base64(data_base64: &str, expect_appb: bool) -> Result { + debug!("Decrypting data base64: {}", data_base64); + + let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| { + debug!("Failed to decode base64: {} APPB: {}", e, expect_appb); + e })?; - debug!("Pipe server executable path: {}", exe_path.display()); + let decrypted = decrypt_data(&data, expect_appb)?; + let decrypted_base64 = general_purpose::STANDARD.encode(decrypted); - // Dump signature fields for debugging/inspection - debug!("Signature fields:"); - debug!(" Subject Name: {:?}", signature.subject_name()); - debug!(" Issuer Name: {:?}", signature.issuer_name()); - debug!(" SHA1 Thumbprint: {:?}", signature.sha1_thumbprint()); - debug!(" SHA256 Thumbprint: {:?}", signature.sha256_thumbprint()); - debug!(" Serial Number: {:?}", signature.serial()); + Ok(decrypted_base64) + } - if signature.sha256_thumbprint() != EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT { - return Err(anyhow!("Pipe server signature is not valid")); + fn decrypt_data(data: &[u8], expect_appb: bool) -> Result> { + if expect_appb && !data.starts_with(b"APPB") { + debug!("Decoded data does not start with 'APPB'"); + return Err(anyhow!("Decoded data does not start with 'APPB'")); } - debug!("Pipe server signature verified for PID {}", server_pid); + let data = if expect_appb { &data[4..] } else { data }; + + let mut in_blob = CRYPT_INTEGER_BLOB { + cbData: data.len() as u32, + pbData: data.as_ptr() as *mut u8, + }; + + let mut out_blob = CRYPT_INTEGER_BLOB { + cbData: 0, + pbData: ptr::null_mut(), + }; + + let result = unsafe { + CryptUnprotectData( + &mut in_blob, + Some(ptr::null_mut()), + None, + None, + None, + CRYPTPROTECT_UI_FORBIDDEN, + &mut out_blob, + ) + }; + + if result.is_ok() && !out_blob.pbData.is_null() && out_blob.cbData > 0 { + let decrypted = unsafe { + std::slice::from_raw_parts(out_blob.pbData, out_blob.cbData as usize).to_vec() + }; + + // Free the memory allocated by CryptUnprotectData + unsafe { LocalFree(Some(HLOCAL(out_blob.pbData as *mut _))) }; + + Ok(decrypted) + } else { + debug!("CryptUnprotectData failed"); + Err(anyhow!("CryptUnprotectData failed")) + } } - Ok(client) -} + // + // Impersonate a SYSTEM process + // -async fn run() -> Result { - debug!("Starting admin.exe"); - - let args = Args::try_parse()?; - - if !is_admin() { - return Err(anyhow!("Expected to run with admin privileges")); + struct ImpersonateGuard { + sys_token_handle: HANDLE, } - debug!("Running as admin"); + impl Drop for ImpersonateGuard { + fn drop(&mut self) { + _ = Self::stop(); + _ = self.close_sys_handle(); + } + } - // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine - let system_decrypted_base64 = { - let (_guard, pid) = ImpersonateGuard::start(None, None)?; - debug!("Impersonating system process with PID {}", pid); + impl ImpersonateGuard { + pub fn start(pid: Option, sys_handle: Option) -> Result<(Self, u32)> { + Self::enable_privilege()?; + let pid = if let Some(pid) = pid { + pid + } else if let Some(pid) = Self::get_system_pid_list()?.next() { + pid + } else { + return Err(anyhow!("Cannot find system process")); + }; + let sys_token = if let Some(handle) = sys_handle { + handle + } else { + let system_handle = Self::get_process_handle(pid)?; + let sys_token = Self::get_system_token(system_handle)?; + unsafe { + CloseHandle(system_handle)?; + }; - let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; - debug!("Decrypted data with system"); + sys_token + }; + unsafe { + ImpersonateLoggedOnUser(sys_token)?; + }; + Ok(( + Self { + sys_token_handle: sys_token, + }, + pid, + )) + } - system_decrypted_base64 - }; + pub fn stop() -> Result<()> { + unsafe { + RevertToSelf()?; + }; + Ok(()) + } - // This is just to check that we're decrypting Chrome keys and not something else sent to us by a malicious actor. - // Now that we're back from SYSTEM, we need to decrypt one more time just to verify. - // Chrome keys are double encrypted: once at SYSTEM level and once at USER level. - // When the decryption fails, it means that we're decrypting something unexpected. - // We don't send this result back since the library will decrypt again at USER level. + /// stop impersonate and return sys token handle + pub fn _stop_sys_handle(self) -> Result { + unsafe { RevertToSelf() }?; + Ok(self.sys_token_handle) + } - _ = decrypt_data_base64(&system_decrypted_base64, false).map_err(|e| { - debug!("User level decryption check failed: {}", e); - e - })?; + pub fn close_sys_handle(&self) -> Result<()> { + unsafe { CloseHandle(self.sys_token_handle) }?; + Ok(()) + } - debug!("User level decryption check passed"); + fn enable_privilege() -> Result<()> { + let mut previous_value = BOOL(0); + let status = unsafe { + debug!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege"); + RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value) + }; + if status != STATUS_SUCCESS { + return Err(anyhow!("Failed to adjust privilege")); + } + debug!( + "SE_DEBUG_PRIVILEGE set to 1, was {} before", + previous_value.0 + ); + Ok(()) + } - Ok(system_decrypted_base64) + fn get_system_token(handle: HANDLE) -> Result { + let token_handle = unsafe { + let mut token_handle = HANDLE::default(); + OpenProcessToken(handle, TOKEN_DUPLICATE | TOKEN_QUERY, &mut token_handle)?; + token_handle + }; + let duplicate_token = unsafe { + let mut duplicate_token = HANDLE::default(); + DuplicateToken( + token_handle, + Security::SECURITY_IMPERSONATION_LEVEL(2), + &mut duplicate_token, + )?; + CloseHandle(token_handle)?; + duplicate_token + }; + + Ok(duplicate_token) + } + + fn process_name_is(pid: u32, name_is: F) -> Result + where + F: FnOnce(&OsStr) -> bool, + { + let hprocess = Self::get_process_handle(pid)?; + + let image_file_name = { + let mut lpimagefilename = vec![0; 260]; + let length = + unsafe { K32GetProcessImageFileNameW(hprocess, &mut lpimagefilename) } as usize; + unsafe { + CloseHandle(hprocess)?; + }; + lpimagefilename.truncate(length); + lpimagefilename + }; + + let fp = OsString::from_wide(&image_file_name); + PathBuf::from(fp) + .file_name() + .map(name_is) + .ok_or_else(|| anyhow::anyhow!("Failed to get process name")) + } + + // https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes + fn get_system_pid_list() -> Result> { + let cap = 1024; + let mut lpidprocess = Vec::with_capacity(cap); + let mut lpcbneeded = 0; + + unsafe { + EnumProcesses(lpidprocess.as_mut_ptr(), cap as u32 * 4, &mut lpcbneeded)?; + let c_processes = lpcbneeded as usize / size_of::(); + lpidprocess.set_len(c_processes); + }; + + let filter = lpidprocess.into_iter().filter(|&v| { + v != 0 + && Self::process_name_is(v, |n| n == "lsass.exe" || n == "winlogon.exe") + .unwrap_or(false) + }); + Ok(filter) + } + + fn get_process_handle(pid: u32) -> Result { + let hprocess = + unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?; + Ok(hprocess) + } + } + + #[link(name = "ntdll")] + unsafe extern "system" { + unsafe fn RtlAdjustPrivilege( + privilege: i32, + enable: BOOL, + current_thread: BOOL, + previous_value: *mut BOOL, + ) -> NTSTATUS; + } + + async fn open_and_validate_pipe_server(pipe_name: &'static str) -> Result { + let client = open_pipe_client(pipe_name).await?; + + if NEED_SERVER_SIGNATURE_VALIDATION { + let server_pid = get_named_pipe_server_pid(&client)?; + debug!("Connected to pipe server PID {}", server_pid); + + // Validate the server end process signature + let exe_path = resolve_process_executable_path(server_pid)?; + + debug!("Pipe server executable path: {}", exe_path.display()); + + let verifier = CodeSignVerifier::for_file(exe_path.as_path()).map_err(|e| { + anyhow!("verifysign init failed for {}: {:?}", exe_path.display(), e) + })?; + + let signature = verifier.verify().map_err(|e| { + anyhow!( + "verifysign verify failed for {}: {:?}", + exe_path.display(), + e + ) + })?; + + debug!("Pipe server executable path: {}", exe_path.display()); + + // Dump signature fields for debugging/inspection + debug!("Signature fields:"); + debug!(" Subject Name: {:?}", signature.subject_name()); + debug!(" Issuer Name: {:?}", signature.issuer_name()); + debug!(" SHA1 Thumbprint: {:?}", signature.sha1_thumbprint()); + debug!(" SHA256 Thumbprint: {:?}", signature.sha256_thumbprint()); + debug!(" Serial Number: {:?}", signature.serial()); + + if signature.sha256_thumbprint() != EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT { + return Err(anyhow!("Pipe server signature is not valid")); + } + + debug!("Pipe server signature verified for PID {}", server_pid); + } + + Ok(client) + } + + async fn run() -> Result { + debug!("Starting admin.exe"); + + let args = Args::try_parse()?; + + if !is_admin() { + return Err(anyhow!("Expected to run with admin privileges")); + } + + debug!("Running as admin"); + + // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine + let system_decrypted_base64 = { + let (_guard, pid) = ImpersonateGuard::start(None, None)?; + debug!("Impersonating system process with PID {}", pid); + + let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; + debug!("Decrypted data with system"); + + system_decrypted_base64 + }; + + // This is just to check that we're decrypting Chrome keys and not something else sent to us by a malicious actor. + // Now that we're back from SYSTEM, we need to decrypt one more time just to verify. + // Chrome keys are double encrypted: once at SYSTEM level and once at USER level. + // When the decryption fails, it means that we're decrypting something unexpected. + // We don't send this result back since the library will decrypt again at USER level. + + _ = decrypt_data_base64(&system_decrypted_base64, false).map_err(|e| { + debug!("User level decryption check failed: {}", e); + e + })?; + + debug!("User level decryption check passed"); + + Ok(system_decrypted_base64) + } + + #[tokio::main] + async fn main() { + if NEED_LOGGING { + WriteLogger::init( + LevelFilter::Debug, // Controlled by the feature flags in Cargo.toml + Config::default(), + OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILENAME) + .expect("Can't open the log file"), + ) + .expect("Failed to initialize logger"); + } + + let mut client = + match open_and_validate_pipe_server(abe_config::ADMIN_TO_USER_PIPE_NAME).await { + Ok(client) => client, + Err(e) => { + error!( + "Failed to open pipe {} to send result/error: {}", + abe_config::ADMIN_TO_USER_PIPE_NAME, + e + ); + return; + } + }; + + match run().await { + Ok(system_decrypted_base64) => { + debug!("Sending response back to user"); + let _ = send_to_user(&mut client, &system_decrypted_base64).await; + } + Err(e) => { + debug!("Error: {}", e); + send_error_to_user(&mut client, &format!("{}", e)).await; + } + } + } } #[tokio::main] async fn main() { - if NEED_LOGGING { - WriteLogger::init( - LevelFilter::Debug, // Controlled by the feature flags in Cargo.toml - Config::default(), - OpenOptions::new() - .create(true) - .append(true) - .open(LOG_FILENAME) - .expect("Can't open the log file"), - ) - .expect("Failed to initialize logger"); - } - - let mut client = match open_and_validate_pipe_server(abe_config::ADMIN_TO_USER_PIPE_NAME).await - { - Ok(client) => client, - Err(e) => { - error!( - "Failed to open pipe {} to send result/error: {}", - abe_config::ADMIN_TO_USER_PIPE_NAME, - e - ); - return; - } - }; - - match run().await { - Ok(system_decrypted_base64) => { - debug!("Sending response back to user"); - let _ = send_to_user(&mut client, &system_decrypted_base64).await; - } - Err(e) => { - debug!("Error: {}", e); - send_error_to_user(&mut client, &format!("{}", e)).await; - } - } + #[cfg(target_os = "windows")] + windows_binary::main().await; } From 7c35a363f4885921a7dfe742fe18dbe3a5807348 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Wed, 22 Oct 2025 13:27:19 +0200 Subject: [PATCH 16/27] A bunch of clippy warnings fixed on Windows --- .../bitwarden_chromium_importer/src/abe.rs | 2 +- .../src/bin/admin.rs | 17 ++++++------ .../bitwarden_chromium_importer/src/lib.rs | 3 --- .../src/windows.rs | 26 ++++++++++--------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs index 4fa22c2787f..ee3d7c0868c 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -137,7 +137,7 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result Result<()> { - let _ = send_message_with_client(client, &message).await?; + let _ = send_message_with_client(client, message).await?; Ok(()) } @@ -182,7 +183,7 @@ mod windows_binary { let data = if expect_appb { &data[4..] } else { data }; - let mut in_blob = CRYPT_INTEGER_BLOB { + let in_blob = CRYPT_INTEGER_BLOB { cbData: data.len() as u32, pbData: data.as_ptr() as *mut u8, }; @@ -194,7 +195,7 @@ mod windows_binary { let result = unsafe { CryptUnprotectData( - &mut in_blob, + &in_blob, Some(ptr::null_mut()), None, None, @@ -425,7 +426,7 @@ mod windows_binary { Ok(client) } - async fn run() -> Result { + fn run() -> Result { debug!("Starting admin.exe"); let args = Args::try_parse()?; @@ -464,7 +465,7 @@ mod windows_binary { } #[tokio::main] - async fn main() { + pub async fn main() { if NEED_LOGGING { WriteLogger::init( LevelFilter::Debug, // Controlled by the feature flags in Cargo.toml @@ -491,7 +492,7 @@ mod windows_binary { } }; - match run().await { + match run() { Ok(system_decrypted_base64) => { debug!("Sending response back to user"); let _ = send_to_user(&mut client, &system_decrypted_base64).await; @@ -507,5 +508,5 @@ mod windows_binary { #[tokio::main] async fn main() { #[cfg(target_os = "windows")] - windows_binary::main().await; + windows_binary::main(); } diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs index aafddde7201..36ffd3cc187 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs @@ -1,6 +1,3 @@ -#[cfg(target_os = "windows")] -pub mod abe; - #[cfg(target_os = "windows")] pub mod abe_config; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index fff60b5988a..87f86ef888c 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -57,7 +57,9 @@ pub fn get_crypto_service( } pub fn configure_windows_crypto_service(admin_exe_path: &str) { - *ADMIN_EXE_PATH.lock().unwrap() = Some(admin_exe_path.to_string()); + *ADMIN_EXE_PATH + .lock() + .expect("Failed to acquire lock on ADMIN_EXE_PATH") = Some(admin_exe_path.to_string()); } // @@ -186,14 +188,16 @@ impl WindowsCryptoService { let key_base64 = abe::decrypt_with_admin( &get_admin_exe_path()?, - &self.app_bound_encrypted_key.as_ref().unwrap(), + self.app_bound_encrypted_key + .as_ref() + .expect("app_bound_encrypted_key should not be None"), ) .await?; - if key_base64.starts_with('!') { + if let Some(error_message) = key_base64.strip_prefix('!') { return Err(anyhow!( "Failed to decrypt the master key: {}", - &key_base64[1..] + error_message )); } @@ -218,7 +222,7 @@ impl WindowsCryptoService { .decrypt(iv.into(), ciphertext.as_ref()) .map_err(|e| anyhow!("Failed to decrypt v20 key with Google AES key: {}", e))?; - return Ok(decrypted); + Ok(decrypted) } KeyData::Two { iv, ciphertext } => { // Google's fixed ChaCha20 key for v20 decryption @@ -237,15 +241,13 @@ impl WindowsCryptoService { anyhow!("Failed to decrypt v20 key with Google ChaCha20 key: {}", e) })?; - return Ok(decrypted); + Ok(decrypted) } KeyData::Three { .. } => { // There's no way to test this at the moment. This encryption scheme is not used in any of the browsers I've tested. - return Err(anyhow!("v20 version 3 is not supported yet")); - } - KeyData::Plain(key) => { - return Ok(key.to_vec()); + Err(anyhow!("v20 version 3 is not supported yet")) } + KeyData::Plain(key) => Ok(key.to_vec()), } } } @@ -300,7 +302,7 @@ fn unprotect_data_win(data: &[u8]) -> Result> { fn get_admin_exe_path() -> Result { ADMIN_EXE_PATH .lock() - .unwrap() + .expect("Failed to acquire lock on ADMIN_EXE_PATH") .clone() .ok_or_else(|| anyhow!("admin.exe path is not set")) } @@ -329,7 +331,7 @@ enum KeyData<'k> { Plain(&'k [u8]), } -impl<'k> KeyData<'k> { +impl KeyData<'_> { fn parse<'b>(blob_data: &mut &'b [u8]) -> Result> { let header_len = u32::from_le_bytes(blob_data[0..4].try_into()?) as usize; // Ignore the header From 535c21a0e50b3c5dc997e9f03d0bcb4ee8a844f3 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Thu, 23 Oct 2025 22:16:30 +0200 Subject: [PATCH 17/27] Use services.exe instead of lsass.exe and winlogon.exe to elevate to SYSTEM --- .../bitwarden_chromium_importer/src/bin/admin.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index 71c7417a39a..38b6a581c58 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -358,9 +358,7 @@ mod windows_binary { }; let filter = lpidprocess.into_iter().filter(|&v| { - v != 0 - && Self::process_name_is(v, |n| n == "lsass.exe" || n == "winlogon.exe") - .unwrap_or(false) + v != 0 && Self::process_name_is(v, |n| n == "services.exe").unwrap_or(false) }); Ok(filter) } From 4968cae17a804c25a5aa6aba4c85a1529400d305 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Fri, 24 Oct 2025 22:53:01 +0200 Subject: [PATCH 18/27] Fix admin.exe to run again, some tokio weirdness --- .../bitwarden_chromium_importer/src/bin/admin.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index 38b6a581c58..c1c520c1271 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -462,7 +462,6 @@ mod windows_binary { Ok(system_decrypted_base64) } - #[tokio::main] pub async fn main() { if NEED_LOGGING { WriteLogger::init( @@ -506,5 +505,5 @@ mod windows_binary { #[tokio::main] async fn main() { #[cfg(target_os = "windows")] - windows_binary::main(); + windows_binary::main().await; } From 20b6c5a680bf2e2c2fcc48796076687c946fabfa Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Fri, 24 Oct 2025 22:54:10 +0200 Subject: [PATCH 19/27] Look for admin.exe in runtime, don't need to confiture windows crypto service anymore --- .../src/windows.rs | 86 +++++++++++++++---- 1 file changed, 68 insertions(+), 18 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index 87f86ef888c..5dbedd0ba7e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -1,21 +1,18 @@ -use std::sync::Mutex; - use aes_gcm::aead::Aead; use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use chacha20poly1305::ChaCha20Poly1305; +use std::path::PathBuf; use windows::Win32::{ Foundation::{LocalFree, HLOCAL}, Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB}, }; use crate::chromium::{BrowserConfig, CryptoService, LocalState}; - -mod abe; - use crate::util; +mod abe; // // Public API @@ -56,18 +53,14 @@ pub fn get_crypto_service( Ok(Box::new(WindowsCryptoService::new(local_state))) } -pub fn configure_windows_crypto_service(admin_exe_path: &str) { - *ADMIN_EXE_PATH - .lock() - .expect("Failed to acquire lock on ADMIN_EXE_PATH") = Some(admin_exe_path.to_string()); +pub fn configure_windows_crypto_service(_admin_exe_path: &str) { + // No-op for now } // // Private // -static ADMIN_EXE_PATH: Mutex> = Mutex::new(None); - // // CryptoService // @@ -186,8 +179,13 @@ impl WindowsCryptoService { )); } + let admin_exe_path = get_admin_exe_path()?; + let admin_exe_str = admin_exe_path + .to_str() + .ok_or_else(|| anyhow!("Failed to convert admin.exe path to string"))?; + let key_base64 = abe::decrypt_with_admin( - &get_admin_exe_path()?, + &admin_exe_str, self.app_bound_encrypted_key .as_ref() .expect("app_bound_encrypted_key should not be None"), @@ -299,12 +297,64 @@ fn unprotect_data_win(data: &[u8]) -> Result> { Ok(output_slice.to_vec()) } -fn get_admin_exe_path() -> Result { - ADMIN_EXE_PATH - .lock() - .expect("Failed to acquire lock on ADMIN_EXE_PATH") - .clone() - .ok_or_else(|| anyhow!("admin.exe path is not set")) +fn get_admin_exe_path() -> Result { + let current_exe_full_path = std::env::current_exe() + .map_err(|e| anyhow!("Failed to get current executable path: {}", e))?; + + let exe_name = current_exe_full_path + .file_name() + .ok_or_else(|| anyhow!("Failed to get file name from current executable path"))?; + + let admin_exe_full_path = if exe_name.to_ascii_lowercase() == "electron.exe" { + get_debug_admin_exe_path()? + } else { + get_dist_admin_exe_path(¤t_exe_full_path)? + }; + + // check if admin.exe exists + if !admin_exe_full_path.exists() { + return Err(anyhow!( + "admin.exe not found at path: {:?}", + admin_exe_full_path + )); + } + + Ok(admin_exe_full_path) +} + +fn get_dist_admin_exe_path(current_exe_full_path: &PathBuf) -> Result { + let admin_exe = current_exe_full_path + .parent() + .map(|p| p.join("admin.exe")) + .ok_or_else(|| anyhow!("Failed to get parent directory of current executable"))?; + + Ok(admin_exe) +} + +// Try to find admin.exe in debug build folders. This might not cover all the cases. +// Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native. +fn get_debug_admin_exe_path() -> Result { + let current_dir = std::env::current_dir()?; + let folder_name = current_dir + .file_name() + .ok_or_else(|| anyhow!("Failed to get folder name from current directory"))?; + match folder_name.to_str() { + Some("desktop") => Ok(get_target_admin_exe_path( + current_dir.join("desktop_native"), + )), + Some("desktop_native") => Ok(get_target_admin_exe_path(current_dir)), + _ => Err(anyhow!( + "Cannot determine admin.exe path from current directory: {}", + current_dir.display() + )), + } +} + +fn get_target_admin_exe_path(desktop_native_dir: PathBuf) -> PathBuf { + desktop_native_dir + .join("target") + .join("debug") + .join("admin.exe") } // From b9ca1ae57d6825af1f9fb79788b683c332ababdd Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 17:52:10 +0200 Subject: [PATCH 20/27] Rework SYSTEM process discovery to use sysinfo, iterate through multiple processes --- apps/desktop/desktop_native/Cargo.lock | 1 + .../bitwarden_chromium_importer/Cargo.toml | 3 +- .../src/bin/admin.rs | 125 ++++++++---------- 3 files changed, 58 insertions(+), 71 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index f6391350d14..9996756d8e1 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -465,6 +465,7 @@ dependencies = [ "serde_json", "sha1", "simplelog", + "sysinfo", "tokio", "verifysign", "windows 0.61.1", diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index f08d299e502..1fbf7cb0e7e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -23,6 +23,7 @@ rusqlite = { version = "=0.37.0", features = ["bundled"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha1 = "=0.10.6" +sysinfo = { workspace = true, optional = true } [target.'cfg(target_os = "macos")'.dependencies] security-framework = { workspace = true } @@ -56,7 +57,7 @@ oo7 = { workspace = true } workspace = true [features] -windows-binary = [] +windows-binary = ["sysinfo"] [[bin]] name = "admin" diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs index c1c520c1271..8564cd11342 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/admin.rs @@ -11,13 +11,14 @@ mod windows_binary { use scopeguard::guard; use simplelog::*; use std::{ - ffi::{OsStr, OsString}, + ffi::OsString, fs::OpenOptions, os::windows::{ffi::OsStringExt as _, io::AsRawHandle}, path::PathBuf, ptr, time::Duration, }; + use sysinfo::System; use tokio::{ io::{AsyncReadExt, AsyncWriteExt}, net::windows::named_pipe::{ClientOptions, NamedPipeClient}, @@ -39,7 +40,6 @@ mod windows_binary { }, System::{ Pipes::GetNamedPipeServerProcessId, - ProcessStatus::{EnumProcesses, K32GetProcessImageFileNameW}, Threading::{ OpenProcess, OpenProcessToken, QueryFullProcessImageNameW, PROCESS_NAME_WIN32, PROCESS_QUERY_INFORMATION, PROCESS_VM_READ, @@ -69,6 +69,9 @@ mod windows_binary { const EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT: &str = "9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d"; + // List of SYSTEM process names to try to impersonate + const SYSTEM_PROCESS_NAMES: [&'static str; 2] = ["services.exe", "winlogon.exe"]; + async fn open_pipe_client(pipe_name: &'static str) -> Result { // TODO: Don't loop forever, but retry a few times let client = loop { @@ -236,35 +239,22 @@ mod windows_binary { } impl ImpersonateGuard { - pub fn start(pid: Option, sys_handle: Option) -> Result<(Self, u32)> { + pub fn start() -> Result { Self::enable_privilege()?; - let pid = if let Some(pid) = pid { - pid - } else if let Some(pid) = Self::get_system_pid_list()?.next() { - pid - } else { - return Err(anyhow!("Cannot find system process")); - }; - let sys_token = if let Some(handle) = sys_handle { - handle - } else { - let system_handle = Self::get_process_handle(pid)?; - let sys_token = Self::get_system_token(system_handle)?; - unsafe { - CloseHandle(system_handle)?; - }; - sys_token - }; + // Find a SYSTEM process and get its token. Not every SYSTEM process allows token duplication, so try several. + let (sys_token, pid, name) = + Self::find_system_process_with_token(Self::get_system_pid_list())?; + + // Impersonate the SYSTEM process unsafe { ImpersonateLoggedOnUser(sys_token)?; }; - Ok(( - Self { - sys_token_handle: sys_token, - }, - pid, - )) + debug!("Impersonating system process '{}' (PID: {})", name, pid); + + Ok(Self { + sys_token_handle: sys_token, + }) } pub fn stop() -> Result<()> { @@ -301,6 +291,35 @@ mod windows_binary { Ok(()) } + fn find_system_process_with_token( + pids: Vec<(u32, &'static str)>, + ) -> Result<(HANDLE, u32, &'static str)> { + for (pid, name) in pids { + match Self::get_system_token_from_pid(pid) { + Err(_) => { + debug!( + "Failed to open process handle '{}' (PID: {}), skipping", + name, pid + ); + continue; + } + Ok(system_handle) => { + return Ok((system_handle, pid, name)); + } + } + } + Err(anyhow!("Failed to get system token from any process")) + } + + fn get_system_token_from_pid(pid: u32) -> Result { + let handle = Self::get_process_handle(pid)?; + let token = Self::get_system_token(handle)?; + unsafe { + CloseHandle(handle)?; + }; + Ok(token) + } + fn get_system_token(handle: HANDLE) -> Result { let token_handle = unsafe { let mut token_handle = HANDLE::default(); @@ -321,46 +340,15 @@ mod windows_binary { Ok(duplicate_token) } - fn process_name_is(pid: u32, name_is: F) -> Result - where - F: FnOnce(&OsStr) -> bool, - { - let hprocess = Self::get_process_handle(pid)?; - - let image_file_name = { - let mut lpimagefilename = vec![0; 260]; - let length = - unsafe { K32GetProcessImageFileNameW(hprocess, &mut lpimagefilename) } as usize; - unsafe { - CloseHandle(hprocess)?; - }; - lpimagefilename.truncate(length); - lpimagefilename - }; - - let fp = OsString::from_wide(&image_file_name); - PathBuf::from(fp) - .file_name() - .map(name_is) - .ok_or_else(|| anyhow::anyhow!("Failed to get process name")) - } - - // https://learn.microsoft.com/en-us/windows/win32/psapi/enumerating-all-processes - fn get_system_pid_list() -> Result> { - let cap = 1024; - let mut lpidprocess = Vec::with_capacity(cap); - let mut lpcbneeded = 0; - - unsafe { - EnumProcesses(lpidprocess.as_mut_ptr(), cap as u32 * 4, &mut lpcbneeded)?; - let c_processes = lpcbneeded as usize / size_of::(); - lpidprocess.set_len(c_processes); - }; - - let filter = lpidprocess.into_iter().filter(|&v| { - v != 0 && Self::process_name_is(v, |n| n == "services.exe").unwrap_or(false) - }); - Ok(filter) + fn get_system_pid_list() -> Vec<(u32, &'static str)> { + let mut pids = Vec::new(); + let sys = System::new_all(); + for name in SYSTEM_PROCESS_NAMES { + for process in sys.processes_by_exact_name(name.as_ref()) { + pids.push((process.pid().as_u32(), name)); + } + } + pids } fn get_process_handle(pid: u32) -> Result { @@ -437,12 +425,9 @@ mod windows_binary { // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine let system_decrypted_base64 = { - let (_guard, pid) = ImpersonateGuard::start(None, None)?; - debug!("Impersonating system process with PID {}", pid); - + let _guard = ImpersonateGuard::start()?; let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; debug!("Decrypted data with system"); - system_decrypted_base64 }; From ca21df5f8e34030db489e76cbbdd00d2e02cdff1 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 18:11:31 +0200 Subject: [PATCH 21/27] Rename admin.exe to bitwarden_chromium_import_helper.exe --- .../bitwarden_chromium_importer/Cargo.toml | 4 +- .../bitwarden_chromium_importer/README.md | 85 +++++++++---------- .../bitwarden_chromium_importer/src/abe.rs | 10 +-- ...rs => bitwarden_chromium_import_helper.rs} | 10 +-- .../src/windows.rs | 20 +++-- apps/desktop/desktop_native/build.js | 2 +- 6 files changed, 66 insertions(+), 65 deletions(-) rename apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/{admin.rs => bitwarden_chromium_import_helper.rs} (98%) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index 1fbf7cb0e7e..2f3ca776138 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -60,6 +60,6 @@ workspace = true windows-binary = ["sysinfo"] [[bin]] -name = "admin" -path = "src/bin/admin.rs" +name = "bitwarden_chromium_import_helper" +path = "src/bin/bitwarden_chromium_import_helper.rs" required-features = ["windows-binary"] diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md b/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md index a7bf0a720c1..8e088c4c25e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md @@ -2,11 +2,10 @@ ## Overview -The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that -work together: +The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that work together: - **client library** — a library that is part of the desktop client application -- **admin.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM** +- **bitwarden_chromium_import_helper.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM** _(The name of the binary will be changed in the released product.)_ @@ -14,16 +13,15 @@ See the last section for a concise summary of the entire process. ## Goal -The goal of this subsystem is to decrypt the master encryption key used to encrypt login information -on the local Windows system. This applies to the most recent versions of Chrome, Brave, and -(untested) Edge that use the ABE/v20 encryption scheme for some local profiles. +The goal of this subsystem is to decrypt the master encryption key used to encrypt login information on the local +Windows system. This applies to the most recent versions of Chrome, Brave, and (untested) Edge that use the ABE/v20 +encryption scheme for some local profiles. The general idea of this encryption scheme is as follows: 1. Chrome generates a unique random encryption key. 2. This key is first encrypted at the **user level** with a fixed key. -3. It is then encrypted at the **user level** again using the Windows **Data Protection API - (DPAPI)**. +3. It is then encrypted at the **user level** again using the Windows **Data Protection API (DPAPI)**. 4. Finally, it is sent to a special service that encrypts it with DPAPI at the **system level**. This triply encrypted key is stored in the `Local State` file. @@ -32,76 +30,74 @@ The following sections describe how the key is decrypted at each level. ## 1. Client Library -This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows -(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges, -presenting the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`. +This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows (see `abe.rs` and +`abe_config.rs`). Its main task is to launch `bitwarden_chromium_import_helper.exe` with elevated privileges, presenting +the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`. This function takes two arguments: -1. Absolute path to `admin.exe` +1. Absolute path to `bitwarden_chromium_import_helper.exe` 2. Base64 string of the ABE key extracted from the browser's local state -First, `admin.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb. This -displays the UAC screen. If the user accepts, `admin.exe` starts with **ADMINISTRATOR** privileges. +First, `bitwarden_chromium_import_helper.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb. +This displays the UAC screen. If the user accepts, `bitwarden_chromium_import_helper.exe` starts with **ADMINISTRATOR** +privileges. > **The user must approve the UAC prompt or the process is aborted.** -Because it is not possible to read the standard output of an application launched in this way, a -named pipe server is created at the user level before `admin.exe` is launched. This pipe is used to -send the decryption result from `admin.exe` back to the client. +Because it is not possible to read the standard output of an application launched in this way, a named pipe server is +created at the user level before `bitwarden_chromium_import_helper.exe` is launched. This pipe is used to send the +decryption result from `bitwarden_chromium_import_helper.exe` back to the client. -The data to be decrypted are passed via the command line to `admin.exe` like this: +The data to be decrypted are passed via the command line to `bitwarden_chromium_import_helper.exe` like this: ```bat -admin.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..." +bitwarden_chromium_import_helper.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..." ``` ## 2. Admin Executable -Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to -**SYSTEM**. To achieve this, it uses a technique to impersonate a system-level process. +Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to **SYSTEM**. To achieve +this, it uses a technique to impersonate a system-level process. -First, `admin.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling +First, `bitwarden_chromium_import_helper.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling `RtlAdjustPrivilege`. This allows it to enumerate running system-level processes. -Next, it finds an instance of `lsass.exe` or `winlogon.exe`, which are known to run at the -**SYSTEM** level. Once a system process is found, its token is duplicated by calling -`DuplicateToken`. +Next, it finds an instance of `services.exe` or `winlogon.exe`, which are known to run at the **SYSTEM** level. Once a +system process is found, its token is duplicated by calling `DuplicateToken`. -With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level -process. +With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level process. -> **At this point `admin.exe` is running as SYSTEM.** +> **At this point `bitwarden_chromium_import_helper.exe` is running as SYSTEM.** The received encryption key can now be decrypted using DPAPI at the system level. -The decrypted result is sent back to the client via the named pipe. `admin.exe` connects to the pipe -and writes the result. +The decrypted result is sent back to the client via the named pipe. `bitwarden_chromium_import_helper.exe` connects to +the pipe and writes the result. The response can indicate success or failure: - On success: a Base64-encoded string. - On failure: an error message prefixed with `!`. -In either case, the response is sent to the named pipe server created by the client. The client -responds with `ok` (ignored). +In either case, the response is sent to the named pipe server created by the client. The client responds with `ok` +(ignored). -Finally, `admin.exe` exits. +Finally, `bitwarden_chromium_import_helper.exe` exits. ## 3. Back to the Client Library -The decrypted Base64-encoded string is returned from `admin.exe` to the named pipe server at the -user level. At this point it has been decrypted only once—at the system level. +The decrypted Base64-encoded string is returned from `bitwarden_chromium_import_helper.exe` to the named pipe server at +the user level. At this point it has been decrypted only once—at the system level. Next, the string is decrypted at the **user level** with DPAPI. -Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in -`elevation_service.exe` from the Chrome installation. Based on the version of the encrypted string -(encoded within the string itself), this step uses either **AES-256-GCM** or **ChaCha20-Poly1305**. -See `windows.rs` for details. +Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in `elevation_service.exe` +from the Chrome installation. Based on the version of the encrypted string (encoded within the string itself), this step +uses either **AES-256-GCM** or **ChaCha20-Poly1305**. See `windows.rs` for details. -After these steps, the master key is available and can be used to decrypt the password information -stored in the browser’s local database. +After these steps, the master key is available and can be used to decrypt the password information stored in the +browser’s local database. ## TL;DR Steps @@ -109,14 +105,15 @@ stored in the browser’s local database. 1. Extract the encrypted key from Chrome’s settings. 2. Create a named pipe server. - 3. Launch `admin.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted via CLI arguments. - 4. Wait for the response from `admin.exe`. + 3. Launch `bitwarden_chromium_import_helper.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted + via CLI arguments. + 4. Wait for the response from `bitwarden_chromium_import_helper.exe`. 2. **Admin side:** 1. Start. 2. Ensure `SE_DEBUG_PRIVILEGE` is enabled (not strictly necessary in tests). - 3. Impersonate a system process such as `lsass.exe` or `winlogon.exe`. + 3. Impersonate a system process such as `services.exe` or `winlogon.exe`. 4. Decrypt the key using DPAPI at the **SYSTEM** level. 5. Send the result or error back via the named pipe. 6. Exit. diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs index ee3d7c0868c..71a0c9c1b80 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -107,7 +107,7 @@ where } } -pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result { +pub async fn decrypt_with_admin_exe(admin_exe: &str, encrypted: &str) -> Result { let (tx, mut rx) = channel::(1); debug!( @@ -126,11 +126,11 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result return Err(anyhow!("Failed to start named pipe server: {}", e)), }; - debug!("Launching '{}' as admin...", admin_exe); - decrypt_with_admin_internal(admin_exe, encrypted); + debug!("Launching '{}' as ADMINISTRATOR...", admin_exe); + decrypt_with_admin_exe_internal(admin_exe, encrypted); // TODO: Don't wait forever, but for a reasonable time - debug!("Waiting for message from admin..."); + debug!("Waiting for message from {}...", admin_exe); let message = match rx.recv().await { Some(msg) => msg, None => return Err(anyhow!("Failed to receive message from admin")), @@ -142,7 +142,7 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result Result { - debug!("Starting admin.exe"); + debug!("Starting bitwarden_chromium_import_helper.exe"); let args = Args::try_parse()?; @@ -421,7 +421,7 @@ mod windows_binary { return Err(anyhow!("Expected to run with admin privileges")); } - debug!("Running as admin"); + debug!("Running as ADMINISTRATOR"); // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine let system_decrypted_base64 = { diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index 5dbedd0ba7e..3b4a3388365 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -61,6 +61,8 @@ pub fn configure_windows_crypto_service(_admin_exe_path: &str) { // Private // +const ADMIN_EXE_FILENAME: &'static str = "bitwarden_chromium_import_helper.exe"; + // // CryptoService // @@ -182,9 +184,9 @@ impl WindowsCryptoService { let admin_exe_path = get_admin_exe_path()?; let admin_exe_str = admin_exe_path .to_str() - .ok_or_else(|| anyhow!("Failed to convert admin.exe path to string"))?; + .ok_or_else(|| anyhow!("Failed to convert {} path to string", ADMIN_EXE_FILENAME))?; - let key_base64 = abe::decrypt_with_admin( + let key_base64 = abe::decrypt_with_admin_exe( &admin_exe_str, self.app_bound_encrypted_key .as_ref() @@ -311,10 +313,11 @@ fn get_admin_exe_path() -> Result { get_dist_admin_exe_path(¤t_exe_full_path)? }; - // check if admin.exe exists + // check if bitwarden_chromium_import_helper.exe exists if !admin_exe_full_path.exists() { return Err(anyhow!( - "admin.exe not found at path: {:?}", + "{} not found at path: {:?}", + ADMIN_EXE_FILENAME, admin_exe_full_path )); } @@ -325,13 +328,13 @@ fn get_admin_exe_path() -> Result { fn get_dist_admin_exe_path(current_exe_full_path: &PathBuf) -> Result { let admin_exe = current_exe_full_path .parent() - .map(|p| p.join("admin.exe")) + .map(|p| p.join(ADMIN_EXE_FILENAME)) .ok_or_else(|| anyhow!("Failed to get parent directory of current executable"))?; Ok(admin_exe) } -// Try to find admin.exe in debug build folders. This might not cover all the cases. +// Try to find bitwarden_chromium_import_helper.exe in debug build folders. This might not cover all the cases. // Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native. fn get_debug_admin_exe_path() -> Result { let current_dir = std::env::current_dir()?; @@ -344,7 +347,8 @@ fn get_debug_admin_exe_path() -> Result { )), Some("desktop_native") => Ok(get_target_admin_exe_path(current_dir)), _ => Err(anyhow!( - "Cannot determine admin.exe path from current directory: {}", + "Cannot determine {} path from current directory: {}", + ADMIN_EXE_FILENAME, current_dir.display() )), } @@ -354,7 +358,7 @@ fn get_target_admin_exe_path(desktop_native_dir: PathBuf) -> PathBuf { desktop_native_dir .join("target") .join("debug") - .join("admin.exe") + .join(ADMIN_EXE_FILENAME) } // diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index 2790833ebfe..5df033a1634 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -51,7 +51,7 @@ function buildImporterBinaries(target, release = true) { return; } - ["admin"].forEach(bin => { + ["bitwarden_chromium_import_helper"].forEach(bin => { const targetArg = target ? `--target ${target}` : ""; const releaseArg = release ? "--release" : ""; child_process.execSync(`cargo build --bin ${bin} ${releaseArg} ${targetArg} --features windows-binary`, {stdio: 'inherit', cwd: path.join(__dirname, "bitwarden_chromium_importer")}); From 889e02543cf10d2b810471ecdedb9d1c31df60ee Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 18:12:01 +0200 Subject: [PATCH 22/27] Add bitwarden_chromium_import_helper.exe to the electron build scripts --- apps/desktop/electron-builder.beta.json | 4 ++++ apps/desktop/electron-builder.json | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 5b792097623..630a956560d 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -36,6 +36,10 @@ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "to": "desktop_proxy.exe" + }, + { + "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "to": "bitwarden_chromium_import_helper.exe" } ] }, diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index f7dcfb65044..0b18786863d 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -96,6 +96,10 @@ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "to": "desktop_proxy.exe" + }, + { + "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "to": "bitwarden_chromium_import_helper.exe" } ] }, From 953468268a3970f7c285807c662c52631d9c1525 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 19:26:59 +0200 Subject: [PATCH 23/27] Remove configure_windows_crypto_service --- .../bitwarden_chromium_importer/src/chromium.rs | 4 ---- .../bitwarden_chromium_importer/src/linux.rs | 4 ---- .../bitwarden_chromium_importer/src/macos.rs | 4 ---- .../bitwarden_chromium_importer/src/windows.rs | 4 ---- apps/desktop/desktop_native/napi/index.d.ts | 1 - apps/desktop/desktop_native/napi/src/lib.rs | 5 ----- .../src/app/tools/import/chromium-importer.service.ts | 7 ------- apps/desktop/src/app/tools/preload.ts | 2 -- 8 files changed, 31 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index 86487676e9e..d27647db445 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -108,10 +108,6 @@ pub async fn import_logins( Ok(results) } -pub fn configure_windows_crypto_service(admin_exe_path: &str) { - platform::configure_windows_crypto_service(admin_exe_path); -} - // // Private // diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs index ebacc3bfe12..be3bcdb1e1d 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/linux.rs @@ -44,10 +44,6 @@ pub fn get_crypto_service( Ok(Box::new(service)) } -pub fn configure_windows_crypto_service(_admin_exe_path: &str) { - // Do nothing on Linux -} - // // Private // diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs index 61d79042064..bcb2c005000 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/macos.rs @@ -53,10 +53,6 @@ pub fn get_crypto_service( Ok(Box::new(MacCryptoService::new(config))) } -pub fn configure_windows_crypto_service(_admin_exe_path: &str) { - // Do nothing on macOS -} - // // Private // diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index 3b4a3388365..c31574c5e92 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -53,10 +53,6 @@ pub fn get_crypto_service( Ok(Box::new(WindowsCryptoService::new(local_state))) } -pub fn configure_windows_crypto_service(_admin_exe_path: &str) { - // No-op for now -} - // // Private // diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index ef705ae4c34..59751cd3246 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -254,7 +254,6 @@ export declare namespace chromium_importer { export function getInstalledBrowsers(): Array export function getAvailableProfiles(browser: string): Array export function importLogins(browser: string, profileId: string): Promise> - export function configureWindowsCryptoService(adminExePath: string): void } export declare namespace autotype { export function getForegroundWindowTitle(): string diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 16369076596..a193e44d6df 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -1158,11 +1158,6 @@ pub mod chromium_importer { .map(|logins| logins.into_iter().map(LoginImportResult::from).collect()) .map_err(|e| napi::Error::from_reason(e.to_string())) } - - #[napi] - pub fn configure_windows_crypto_service(admin_exe_path: String) { - bitwarden_chromium_importer::chromium::configure_windows_crypto_service(&admin_exe_path) - } } #[napi] diff --git a/apps/desktop/src/app/tools/import/chromium-importer.service.ts b/apps/desktop/src/app/tools/import/chromium-importer.service.ts index ecc8d1cddbb..5273eef4b54 100644 --- a/apps/desktop/src/app/tools/import/chromium-importer.service.ts +++ b/apps/desktop/src/app/tools/import/chromium-importer.service.ts @@ -22,12 +22,5 @@ export class ChromiumImporterService { return await chromium_importer.importLogins(browser, profileId); }, ); - - ipcMain.handle( - "chromium_importer.configureWindowsCryptoService", - async (event, adminExePath: string) => { - return await chromium_importer.configureWindowsCryptoService(adminExePath); - }, - ); } } diff --git a/apps/desktop/src/app/tools/preload.ts b/apps/desktop/src/app/tools/preload.ts index 1a917085614..4d629c992ad 100644 --- a/apps/desktop/src/app/tools/preload.ts +++ b/apps/desktop/src/app/tools/preload.ts @@ -11,8 +11,6 @@ const chromiumImporter = { ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser), importLogins: (browser: string, profileId: string): Promise => ipcRenderer.invoke("chromium_importer.importLogins", browser, profileId), - configureWindowsCryptoService: (adminExePath: string): Promise => - ipcRenderer.invoke("chromium_importer.configureWindowsCryptoService", adminExePath), }; export default { From 6fddb44dc20a2d527ca488989cf902d4dbfbb1c3 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 19:34:14 +0200 Subject: [PATCH 24/27] Fix clippy warnings --- .../src/bin/bitwarden_chromium_import_helper.rs | 2 +- .../bitwarden_chromium_importer/src/windows.rs | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs index c84fddeb974..764aacbead1 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs @@ -70,7 +70,7 @@ mod windows_binary { "9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d"; // List of SYSTEM process names to try to impersonate - const SYSTEM_PROCESS_NAMES: [&'static str; 2] = ["services.exe", "winlogon.exe"]; + const SYSTEM_PROCESS_NAMES: [&str; 2] = ["services.exe", "winlogon.exe"]; async fn open_pipe_client(pipe_name: &'static str) -> Result { // TODO: Don't loop forever, but retry a few times diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs index c31574c5e92..b3d6eff0613 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -1,10 +1,9 @@ -use aes_gcm::aead::Aead; -use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; +use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit, Nonce}; use anyhow::{anyhow, Result}; use async_trait::async_trait; use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _}; use chacha20poly1305::ChaCha20Poly1305; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use windows::Win32::{ Foundation::{LocalFree, HLOCAL}, Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB}, @@ -57,7 +56,7 @@ pub fn get_crypto_service( // Private // -const ADMIN_EXE_FILENAME: &'static str = "bitwarden_chromium_import_helper.exe"; +const ADMIN_EXE_FILENAME: &str = "bitwarden_chromium_import_helper.exe"; // // CryptoService @@ -183,7 +182,7 @@ impl WindowsCryptoService { .ok_or_else(|| anyhow!("Failed to convert {} path to string", ADMIN_EXE_FILENAME))?; let key_base64 = abe::decrypt_with_admin_exe( - &admin_exe_str, + admin_exe_str, self.app_bound_encrypted_key .as_ref() .expect("app_bound_encrypted_key should not be None"), @@ -303,7 +302,7 @@ fn get_admin_exe_path() -> Result { .file_name() .ok_or_else(|| anyhow!("Failed to get file name from current executable path"))?; - let admin_exe_full_path = if exe_name.to_ascii_lowercase() == "electron.exe" { + let admin_exe_full_path = if exe_name.eq_ignore_ascii_case("electron.exe") { get_debug_admin_exe_path()? } else { get_dist_admin_exe_path(¤t_exe_full_path)? @@ -321,7 +320,7 @@ fn get_admin_exe_path() -> Result { Ok(admin_exe_full_path) } -fn get_dist_admin_exe_path(current_exe_full_path: &PathBuf) -> Result { +fn get_dist_admin_exe_path(current_exe_full_path: &Path) -> Result { let admin_exe = current_exe_full_path .parent() .map(|p| p.join(ADMIN_EXE_FILENAME)) From 129531a320e246ba0b15d33c500c792bcfe6b0cd Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 19:35:45 +0200 Subject: [PATCH 25/27] Disable logging, oops... --- .../src/bin/bitwarden_chromium_import_helper.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs index 764aacbead1..dc8b4ef5fd2 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs @@ -61,8 +61,8 @@ mod windows_binary { } // Enable this to log to a file. The way this executable is used, it's not easy to debug and the stdout gets lost. - const NEED_LOGGING: bool = true; - const LOG_FILENAME: &str = "c:\\temp\\admin-log.txt"; // This is an example filename, replace it with you own + const NEED_LOGGING: bool = false; + const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own // This should be enabled for production const NEED_SERVER_SIGNATURE_VALIDATION: bool = false; From b6fec3b17eaf1bd3de7decb5f8db456620094c47 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 23:14:51 +0200 Subject: [PATCH 26/27] Fix and remove some TODOs --- .../bitwarden_chromium_importer/src/abe.rs | 17 ++++++++++++----- .../bitwarden_chromium_importer/src/chromium.rs | 2 -- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs index 71a0c9c1b80..9508e0bb776 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -6,6 +6,7 @@ use tokio::{ net::windows::named_pipe::{NamedPipeServer, ServerOptions}, sync::mpsc::channel, task::JoinHandle, + time::{timeout, Duration}, }; use windows::{ core::PCWSTR, @@ -14,6 +15,8 @@ use windows::{ use crate::abe_config; +const WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS: u64 = 30; + pub fn start_tokio_named_pipe_server( pipe_name: &'static str, process_message: F, @@ -28,7 +31,6 @@ where // Here we also make use of `first_pipe_instance`, which will ensure that // there are no other servers up and running already. let mut server = ServerOptions::new() - // TODO: Try message mode .first_pipe_instance(true) .create(pipe_name)?; @@ -129,11 +131,16 @@ pub async fn decrypt_with_admin_exe(admin_exe: &str, encrypted: &str) -> Result< debug!("Launching '{}' as ADMINISTRATOR...", admin_exe); decrypt_with_admin_exe_internal(admin_exe, encrypted); - // TODO: Don't wait forever, but for a reasonable time debug!("Waiting for message from {}...", admin_exe); - let message = match rx.recv().await { - Some(msg) => msg, - None => return Err(anyhow!("Failed to receive message from admin")), + let message = match timeout( + Duration::from_secs(WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS), + rx.recv(), + ) + .await + { + Ok(Some(msg)) => msg, + Ok(None) => return Err(anyhow!("Channel closed without message from {}", admin_exe)), + Err(_) => return Err(anyhow!("Timeout waiting for message from {}", admin_exe)), }; debug!("Shutting down the pipe server..."); diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index d27647db445..f067ede1bbf 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -57,7 +57,6 @@ pub trait InstalledBrowserRetriever { pub struct DefaultInstalledBrowserRetriever {} impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever { - // TODO: Make thus async fn get_installed_browsers() -> Result> { let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len()); @@ -72,7 +71,6 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever { } } -// TODO: Make thus async pub fn get_available_profiles(browser_name: &String) -> Result> { let (_, local_state) = load_local_state_for_browser(browser_name)?; Ok(get_profile_info(&local_state)) From 3e26ace4189749aba49a6f65d3f76577d9b179a2 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Sat, 25 Oct 2025 23:43:56 +0200 Subject: [PATCH 27/27] Remove sensitive info from debug! and wrap debug! in a new macro that is controlled by a compile time const to make sure nothing sensitive is dumped in release --- .../bitwarden_chromium_importer/src/abe.rs | 8 +- .../bin/bitwarden_chromium_import_helper.rs | 76 +++++++++++-------- 2 files changed, 48 insertions(+), 36 deletions(-) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs index 9508e0bb776..9081a86188e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -88,11 +88,13 @@ where } Ok(bytes_read) => { let message = String::from_utf8_lossy(&buffer[..bytes_read]); - - debug!("Received from client: '{}' ({} bytes)", message, bytes_read); + let preview = message.chars().take(16).collect::(); + debug!( + "Received from client: '{}...' ({} bytes)", + preview, bytes_read, + ); let response = process_message(&message); - match client.write_all(response.as_bytes()).await { Ok(_) => { debug!("Sent response to client ({} bytes)", response.len()); diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs index dc8b4ef5fd2..26d317e52c0 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs @@ -72,19 +72,28 @@ mod windows_binary { // List of SYSTEM process names to try to impersonate const SYSTEM_PROCESS_NAMES: [&str; 2] = ["services.exe", "winlogon.exe"]; + // Macro wrapper around debug! that compiles to no-op when NEED_LOGGING is false + macro_rules! dbg_log { + ($($arg:tt)*) => { + if NEED_LOGGING { + debug!($($arg)*); + } + }; + } + async fn open_pipe_client(pipe_name: &'static str) -> Result { // TODO: Don't loop forever, but retry a few times let client = loop { match ClientOptions::new().open(pipe_name) { Ok(client) => { - debug!("Successfully connected to the pipe!"); + dbg_log!("Successfully connected to the pipe!"); break client; } Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => { - debug!("Pipe is busy, retrying in 50ms..."); + dbg_log!("Pipe is busy, retrying in 50ms..."); } Err(e) => { - debug!("Failed to connect to pipe: {}", &e); + dbg_log!("Failed to connect to pipe: {}", &e); return Err(e.into()); } } @@ -123,15 +132,15 @@ mod windows_binary { } fn resolve_process_executable_path(pid: u32) -> Result { - debug!("Resolving process executable path for PID {}", pid); + dbg_log!("Resolving process executable path for PID {}", pid); // Open the process handle let hprocess = unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?; - debug!("Opened process handle for PID {}", pid); + dbg_log!("Opened process handle for PID {}", pid); let _guard = guard(hprocess, |_| unsafe { - debug!("Closing process handle for PID {}", pid); + dbg_log!("Closing process handle for PID {}", pid); _ = CloseHandle(hprocess); }); @@ -145,7 +154,7 @@ mod windows_binary { &mut size, ) }?; - debug!("QueryFullProcessImageNameW returned {} bytes", size); + dbg_log!("QueryFullProcessImageNameW returned {} bytes", size); wide.truncate(size as usize); Ok(PathBuf::from(OsString::from_wide(&wide))) @@ -165,10 +174,10 @@ mod windows_binary { } fn decrypt_data_base64(data_base64: &str, expect_appb: bool) -> Result { - debug!("Decrypting data base64: {}", data_base64); + dbg_log!("Decrypting data base64: {}", data_base64); let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| { - debug!("Failed to decode base64: {} APPB: {}", e, expect_appb); + dbg_log!("Failed to decode base64: {} APPB: {}", e, expect_appb); e })?; @@ -180,7 +189,7 @@ mod windows_binary { fn decrypt_data(data: &[u8], expect_appb: bool) -> Result> { if expect_appb && !data.starts_with(b"APPB") { - debug!("Decoded data does not start with 'APPB'"); + dbg_log!("Decoded data does not start with 'APPB'"); return Err(anyhow!("Decoded data does not start with 'APPB'")); } @@ -218,7 +227,7 @@ mod windows_binary { Ok(decrypted) } else { - debug!("CryptUnprotectData failed"); + dbg_log!("CryptUnprotectData failed"); Err(anyhow!("CryptUnprotectData failed")) } } @@ -250,7 +259,7 @@ mod windows_binary { unsafe { ImpersonateLoggedOnUser(sys_token)?; }; - debug!("Impersonating system process '{}' (PID: {})", name, pid); + dbg_log!("Impersonating system process '{}' (PID: {})", name, pid); Ok(Self { sys_token_handle: sys_token, @@ -278,13 +287,13 @@ mod windows_binary { fn enable_privilege() -> Result<()> { let mut previous_value = BOOL(0); let status = unsafe { - debug!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege"); + dbg_log!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege"); RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value) }; if status != STATUS_SUCCESS { return Err(anyhow!("Failed to adjust privilege")); } - debug!( + dbg_log!( "SE_DEBUG_PRIVILEGE set to 1, was {} before", previous_value.0 ); @@ -297,9 +306,10 @@ mod windows_binary { for (pid, name) in pids { match Self::get_system_token_from_pid(pid) { Err(_) => { - debug!( + dbg_log!( "Failed to open process handle '{}' (PID: {}), skipping", - name, pid + name, + pid ); continue; } @@ -373,12 +383,12 @@ mod windows_binary { if NEED_SERVER_SIGNATURE_VALIDATION { let server_pid = get_named_pipe_server_pid(&client)?; - debug!("Connected to pipe server PID {}", server_pid); + dbg_log!("Connected to pipe server PID {}", server_pid); // Validate the server end process signature let exe_path = resolve_process_executable_path(server_pid)?; - debug!("Pipe server executable path: {}", exe_path.display()); + dbg_log!("Pipe server executable path: {}", exe_path.display()); let verifier = CodeSignVerifier::for_file(exe_path.as_path()).map_err(|e| { anyhow!("verifysign init failed for {}: {:?}", exe_path.display(), e) @@ -392,28 +402,28 @@ mod windows_binary { ) })?; - debug!("Pipe server executable path: {}", exe_path.display()); + dbg_log!("Pipe server executable path: {}", exe_path.display()); // Dump signature fields for debugging/inspection - debug!("Signature fields:"); - debug!(" Subject Name: {:?}", signature.subject_name()); - debug!(" Issuer Name: {:?}", signature.issuer_name()); - debug!(" SHA1 Thumbprint: {:?}", signature.sha1_thumbprint()); - debug!(" SHA256 Thumbprint: {:?}", signature.sha256_thumbprint()); - debug!(" Serial Number: {:?}", signature.serial()); + dbg_log!("Signature fields:"); + dbg_log!(" Subject Name: {:?}", signature.subject_name()); + dbg_log!(" Issuer Name: {:?}", signature.issuer_name()); + dbg_log!(" SHA1 Thumbprint: {:?}", signature.sha1_thumbprint()); + dbg_log!(" SHA256 Thumbprint: {:?}", signature.sha256_thumbprint()); + dbg_log!(" Serial Number: {:?}", signature.serial()); if signature.sha256_thumbprint() != EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT { return Err(anyhow!("Pipe server signature is not valid")); } - debug!("Pipe server signature verified for PID {}", server_pid); + dbg_log!("Pipe server signature verified for PID {}", server_pid); } Ok(client) } fn run() -> Result { - debug!("Starting bitwarden_chromium_import_helper.exe"); + dbg_log!("Starting bitwarden_chromium_import_helper.exe"); let args = Args::try_parse()?; @@ -421,13 +431,13 @@ mod windows_binary { return Err(anyhow!("Expected to run with admin privileges")); } - debug!("Running as ADMINISTRATOR"); + dbg_log!("Running as ADMINISTRATOR"); // Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine let system_decrypted_base64 = { let _guard = ImpersonateGuard::start()?; let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?; - debug!("Decrypted data with system"); + dbg_log!("Decrypted data with system"); system_decrypted_base64 }; @@ -438,11 +448,11 @@ mod windows_binary { // We don't send this result back since the library will decrypt again at USER level. _ = decrypt_data_base64(&system_decrypted_base64, false).map_err(|e| { - debug!("User level decryption check failed: {}", e); + dbg_log!("User level decryption check failed: {}", e); e })?; - debug!("User level decryption check passed"); + dbg_log!("User level decryption check passed"); Ok(system_decrypted_base64) } @@ -476,11 +486,11 @@ mod windows_binary { match run() { Ok(system_decrypted_base64) => { - debug!("Sending response back to user"); + dbg_log!("Sending response back to user"); let _ = send_to_user(&mut client, &system_decrypted_base64).await; } Err(e) => { - debug!("Error: {}", e); + dbg_log!("Error: {}", e); send_error_to_user(&mut client, &format!("{}", e)).await; } }