From 4aab9360d1fcfc9640e0789d0dd9cd0157f95f98 Mon Sep 17 00:00:00 2001 From: Dmitry Yakimenko Date: Mon, 15 Sep 2025 14:25:59 +0200 Subject: [PATCH] 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",