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:
100
apps/desktop/desktop_native/Cargo.lock
generated
100
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
pub const ADMIN_TO_USER_PIPE_NAME: &str = r"\\.\pipe\BitwardenEncryptionService-admin-user";
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -1 +1,7 @@
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod abe;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub mod abe_config;
|
||||
|
||||
pub mod chromium;
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
5
apps/desktop/desktop_native/napi/index.d.ts
vendored
5
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user