1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-04 10:43:47 +00:00

Bring app bound encryption back together with admin.exe

This commit is contained in:
Dmitry Yakimenko
2025-09-15 14:25:59 +02:00
parent f20ed9f0e9
commit 4aab9360d1
14 changed files with 864 additions and 6 deletions

View File

@@ -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"

View File

@@ -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"]

View File

@@ -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<F>(
pipe_name: &'static str,
process_message: F,
) -> Result<JoinHandle<Result<(), io::Error>>>
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<F>(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<String> {
let (tx, mut rx) = channel::<String>(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::<Vec<u16>>();
let runas_wide = OsStr::new("runas")
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<u16>>();
let parameters = OsStr::new(&format!(r#"--encrypted "{}""#, encrypted))
.encode_wide()
.chain(std::iter::once(0))
.collect::<Vec<u16>>();
unsafe {
ShellExecuteW(
None,
PCWSTR(runas_wide.as_ptr()),
PCWSTR(exe_wide.as_ptr()),
PCWSTR(parameters.as_ptr()),
None,
SW_HIDE,
);
}
}

View File

@@ -0,0 +1 @@
pub const ADMIN_TO_USER_PIPE_NAME: &str = r"\\.\pipe\BitwardenEncryptionService-admin-user";

View File

@@ -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<String> {
// 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<String> {
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<Vec<u8>> {
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<u32>, sys_handle: Option<HANDLE>) -> 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<HANDLE> {
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<HANDLE> {
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<F>(pid: u32, name_is: F) -> Result<bool>
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<impl Iterator<Item = u32>> {
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::<u32>();
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<HANDLE> {
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;
}

View File

@@ -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
//

View File

@@ -1 +1,7 @@
#[cfg(target_os = "windows")]
pub mod abe;
#[cfg(target_os = "windows")]
pub mod abe_config;
pub mod chromium;

View File

@@ -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
//

View File

@@ -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
//

View File

@@ -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<Option<String>> = Mutex::new(None);
//
// CryptoService
//
struct WindowsCryptoService {
master_key: Option<Vec<u8>>,
encrypted_key: Option<String>,
app_bound_encrypted_key: Option<String>,
}
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<Vec<u8>> {
async fn get_master_key(&mut self, version: &str) -> Result<Vec<u8>> {
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<Vec<u8>> {
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::<Aes256Gcm>::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<Vec<u8>> {
@@ -203,3 +313,11 @@ fn unprotect_data_win(data: &[u8]) -> Result<Vec<u8>> {
Ok(output_slice.to_vec())
}
fn get_admin_exe_path() -> Result<String> {
ADMIN_EXE_PATH
.lock()
.unwrap()
.clone()
.ok_or_else(|| anyhow!("admin.exe path is not set"))
}

View File

@@ -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);
});

View File

@@ -228,9 +228,10 @@ export declare namespace chromium_importer {
login?: Login
failure?: LoginImportFailure
}
export function getInstalledBrowsers(): Promise<Array<string>>
export function getAvailableProfiles(browser: string): Promise<Array<ProfileInfo>>
export function getInstalledBrowsers(): Array<string>
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
export function configureWindowsCryptoService(adminExePath: string): Promise<void>
}
export declare namespace autotype {
export function getForegroundWindowTitle(): string

View File

@@ -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]

View File

@@ -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",