mirror of
https://github.com/bitwarden/browser
synced 2026-02-03 02:03:53 +00:00
Rename admin.exe to bitwarden_chromium_import_helper.exe
This commit is contained in:
@@ -60,6 +60,6 @@ workspace = true
|
||||
windows-binary = ["sysinfo"]
|
||||
|
||||
[[bin]]
|
||||
name = "admin"
|
||||
path = "src/bin/admin.rs"
|
||||
name = "bitwarden_chromium_import_helper"
|
||||
path = "src/bin/bitwarden_chromium_import_helper.rs"
|
||||
required-features = ["windows-binary"]
|
||||
|
||||
@@ -2,11 +2,10 @@
|
||||
|
||||
## Overview
|
||||
|
||||
The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that
|
||||
work together:
|
||||
The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that work together:
|
||||
|
||||
- **client library** — a library that is part of the desktop client application
|
||||
- **admin.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM**
|
||||
- **bitwarden_chromium_import_helper.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM**
|
||||
|
||||
_(The name of the binary will be changed in the released product.)_
|
||||
|
||||
@@ -14,16 +13,15 @@ See the last section for a concise summary of the entire process.
|
||||
|
||||
## Goal
|
||||
|
||||
The goal of this subsystem is to decrypt the master encryption key used to encrypt login information
|
||||
on the local Windows system. This applies to the most recent versions of Chrome, Brave, and
|
||||
(untested) Edge that use the ABE/v20 encryption scheme for some local profiles.
|
||||
The goal of this subsystem is to decrypt the master encryption key used to encrypt login information on the local
|
||||
Windows system. This applies to the most recent versions of Chrome, Brave, and (untested) Edge that use the ABE/v20
|
||||
encryption scheme for some local profiles.
|
||||
|
||||
The general idea of this encryption scheme is as follows:
|
||||
|
||||
1. Chrome generates a unique random encryption key.
|
||||
2. This key is first encrypted at the **user level** with a fixed key.
|
||||
3. It is then encrypted at the **user level** again using the Windows **Data Protection API
|
||||
(DPAPI)**.
|
||||
3. It is then encrypted at the **user level** again using the Windows **Data Protection API (DPAPI)**.
|
||||
4. Finally, it is sent to a special service that encrypts it with DPAPI at the **system level**.
|
||||
|
||||
This triply encrypted key is stored in the `Local State` file.
|
||||
@@ -32,76 +30,74 @@ The following sections describe how the key is decrypted at each level.
|
||||
|
||||
## 1. Client Library
|
||||
|
||||
This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows
|
||||
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges,
|
||||
presenting the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`.
|
||||
This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows (see `abe.rs` and
|
||||
`abe_config.rs`). Its main task is to launch `bitwarden_chromium_import_helper.exe` with elevated privileges, presenting
|
||||
the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`.
|
||||
|
||||
This function takes two arguments:
|
||||
|
||||
1. Absolute path to `admin.exe`
|
||||
1. Absolute path to `bitwarden_chromium_import_helper.exe`
|
||||
2. Base64 string of the ABE key extracted from the browser's local state
|
||||
|
||||
First, `admin.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb. This
|
||||
displays the UAC screen. If the user accepts, `admin.exe` starts with **ADMINISTRATOR** privileges.
|
||||
First, `bitwarden_chromium_import_helper.exe` is launched by calling a variant of `ShellExecute` with the `runas` verb.
|
||||
This displays the UAC screen. If the user accepts, `bitwarden_chromium_import_helper.exe` starts with **ADMINISTRATOR**
|
||||
privileges.
|
||||
|
||||
> **The user must approve the UAC prompt or the process is aborted.**
|
||||
|
||||
Because it is not possible to read the standard output of an application launched in this way, a
|
||||
named pipe server is created at the user level before `admin.exe` is launched. This pipe is used to
|
||||
send the decryption result from `admin.exe` back to the client.
|
||||
Because it is not possible to read the standard output of an application launched in this way, a named pipe server is
|
||||
created at the user level before `bitwarden_chromium_import_helper.exe` is launched. This pipe is used to send the
|
||||
decryption result from `bitwarden_chromium_import_helper.exe` back to the client.
|
||||
|
||||
The data to be decrypted are passed via the command line to `admin.exe` like this:
|
||||
The data to be decrypted are passed via the command line to `bitwarden_chromium_import_helper.exe` like this:
|
||||
|
||||
```bat
|
||||
admin.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
|
||||
bitwarden_chromium_import_helper.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
|
||||
```
|
||||
|
||||
## 2. Admin Executable
|
||||
|
||||
Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to
|
||||
**SYSTEM**. To achieve this, it uses a technique to impersonate a system-level process.
|
||||
Although the process starts with **ADMINISTRATOR** privileges, its ultimate goal is to elevate to **SYSTEM**. To achieve
|
||||
this, it uses a technique to impersonate a system-level process.
|
||||
|
||||
First, `admin.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling
|
||||
First, `bitwarden_chromium_import_helper.exe` ensures that the `SE_DEBUG_PRIVILEGE` privilege is enabled by calling
|
||||
`RtlAdjustPrivilege`. This allows it to enumerate running system-level processes.
|
||||
|
||||
Next, it finds an instance of `lsass.exe` or `winlogon.exe`, which are known to run at the
|
||||
**SYSTEM** level. Once a system process is found, its token is duplicated by calling
|
||||
`DuplicateToken`.
|
||||
Next, it finds an instance of `services.exe` or `winlogon.exe`, which are known to run at the **SYSTEM** level. Once a
|
||||
system process is found, its token is duplicated by calling `DuplicateToken`.
|
||||
|
||||
With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level
|
||||
process.
|
||||
With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level process.
|
||||
|
||||
> **At this point `admin.exe` is running as SYSTEM.**
|
||||
> **At this point `bitwarden_chromium_import_helper.exe` is running as SYSTEM.**
|
||||
|
||||
The received encryption key can now be decrypted using DPAPI at the system level.
|
||||
|
||||
The decrypted result is sent back to the client via the named pipe. `admin.exe` connects to the pipe
|
||||
and writes the result.
|
||||
The decrypted result is sent back to the client via the named pipe. `bitwarden_chromium_import_helper.exe` connects to
|
||||
the pipe and writes the result.
|
||||
|
||||
The response can indicate success or failure:
|
||||
|
||||
- On success: a Base64-encoded string.
|
||||
- On failure: an error message prefixed with `!`.
|
||||
|
||||
In either case, the response is sent to the named pipe server created by the client. The client
|
||||
responds with `ok` (ignored).
|
||||
In either case, the response is sent to the named pipe server created by the client. The client responds with `ok`
|
||||
(ignored).
|
||||
|
||||
Finally, `admin.exe` exits.
|
||||
Finally, `bitwarden_chromium_import_helper.exe` exits.
|
||||
|
||||
## 3. Back to the Client Library
|
||||
|
||||
The decrypted Base64-encoded string is returned from `admin.exe` to the named pipe server at the
|
||||
user level. At this point it has been decrypted only once—at the system level.
|
||||
The decrypted Base64-encoded string is returned from `bitwarden_chromium_import_helper.exe` to the named pipe server at
|
||||
the user level. At this point it has been decrypted only once—at the system level.
|
||||
|
||||
Next, the string is decrypted at the **user level** with DPAPI.
|
||||
|
||||
Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in
|
||||
`elevation_service.exe` from the Chrome installation. Based on the version of the encrypted string
|
||||
(encoded within the string itself), this step uses either **AES-256-GCM** or **ChaCha20-Poly1305**.
|
||||
See `windows.rs` for details.
|
||||
Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in `elevation_service.exe`
|
||||
from the Chrome installation. Based on the version of the encrypted string (encoded within the string itself), this step
|
||||
uses either **AES-256-GCM** or **ChaCha20-Poly1305**. See `windows.rs` for details.
|
||||
|
||||
After these steps, the master key is available and can be used to decrypt the password information
|
||||
stored in the browser’s local database.
|
||||
After these steps, the master key is available and can be used to decrypt the password information stored in the
|
||||
browser’s local database.
|
||||
|
||||
## TL;DR Steps
|
||||
|
||||
@@ -109,14 +105,15 @@ stored in the browser’s local database.
|
||||
|
||||
1. Extract the encrypted key from Chrome’s settings.
|
||||
2. Create a named pipe server.
|
||||
3. Launch `admin.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted via CLI arguments.
|
||||
4. Wait for the response from `admin.exe`.
|
||||
3. Launch `bitwarden_chromium_import_helper.exe` with **ADMINISTRATOR** privileges, passing the key to be decrypted
|
||||
via CLI arguments.
|
||||
4. Wait for the response from `bitwarden_chromium_import_helper.exe`.
|
||||
|
||||
2. **Admin side:**
|
||||
|
||||
1. Start.
|
||||
2. Ensure `SE_DEBUG_PRIVILEGE` is enabled (not strictly necessary in tests).
|
||||
3. Impersonate a system process such as `lsass.exe` or `winlogon.exe`.
|
||||
3. Impersonate a system process such as `services.exe` or `winlogon.exe`.
|
||||
4. Decrypt the key using DPAPI at the **SYSTEM** level.
|
||||
5. Send the result or error back via the named pipe.
|
||||
6. Exit.
|
||||
|
||||
@@ -107,7 +107,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result<String> {
|
||||
pub async fn decrypt_with_admin_exe(admin_exe: &str, encrypted: &str) -> Result<String> {
|
||||
let (tx, mut rx) = channel::<String>(1);
|
||||
|
||||
debug!(
|
||||
@@ -126,11 +126,11 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result<Stri
|
||||
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);
|
||||
debug!("Launching '{}' as ADMINISTRATOR...", admin_exe);
|
||||
decrypt_with_admin_exe_internal(admin_exe, encrypted);
|
||||
|
||||
// TODO: Don't wait forever, but for a reasonable time
|
||||
debug!("Waiting for message from admin...");
|
||||
debug!("Waiting for message from {}...", admin_exe);
|
||||
let message = match rx.recv().await {
|
||||
Some(msg) => msg,
|
||||
None => return Err(anyhow!("Failed to receive message from admin")),
|
||||
@@ -142,7 +142,7 @@ pub async fn decrypt_with_admin(admin_exe: &str, encrypted: &str) -> Result<Stri
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
fn decrypt_with_admin_internal(admin_exe: &str, encrypted: &str) {
|
||||
fn decrypt_with_admin_exe_internal(admin_exe: &str, encrypted: &str) {
|
||||
// Convert strings to wide strings for Windows API
|
||||
let exe_wide = OsStr::new(admin_exe)
|
||||
.encode_wide()
|
||||
|
||||
@@ -52,7 +52,7 @@ mod windows_binary {
|
||||
use bitwarden_chromium_importer::abe_config;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[command(name = "admin")]
|
||||
#[command(name = "bitwarden_chromium_import_helper")]
|
||||
#[command(about = "Admin tool for ABE service management")]
|
||||
struct Args {
|
||||
/// Base64 encoded encrypted data to process
|
||||
@@ -61,8 +61,8 @@ mod windows_binary {
|
||||
}
|
||||
|
||||
// Enable this to log to a file. The way this executable is used, it's not easy to debug and the stdout gets lost.
|
||||
const NEED_LOGGING: bool = false;
|
||||
const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own
|
||||
const NEED_LOGGING: bool = true;
|
||||
const LOG_FILENAME: &str = "c:\\temp\\admin-log.txt"; // This is an example filename, replace it with you own
|
||||
|
||||
// This should be enabled for production
|
||||
const NEED_SERVER_SIGNATURE_VALIDATION: bool = false;
|
||||
@@ -413,7 +413,7 @@ mod windows_binary {
|
||||
}
|
||||
|
||||
fn run() -> Result<String> {
|
||||
debug!("Starting admin.exe");
|
||||
debug!("Starting bitwarden_chromium_import_helper.exe");
|
||||
|
||||
let args = Args::try_parse()?;
|
||||
|
||||
@@ -421,7 +421,7 @@ mod windows_binary {
|
||||
return Err(anyhow!("Expected to run with admin privileges"));
|
||||
}
|
||||
|
||||
debug!("Running as admin");
|
||||
debug!("Running as ADMINISTRATOR");
|
||||
|
||||
// Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine
|
||||
let system_decrypted_base64 = {
|
||||
@@ -61,6 +61,8 @@ pub fn configure_windows_crypto_service(_admin_exe_path: &str) {
|
||||
// Private
|
||||
//
|
||||
|
||||
const ADMIN_EXE_FILENAME: &'static str = "bitwarden_chromium_import_helper.exe";
|
||||
|
||||
//
|
||||
// CryptoService
|
||||
//
|
||||
@@ -182,9 +184,9 @@ impl WindowsCryptoService {
|
||||
let admin_exe_path = get_admin_exe_path()?;
|
||||
let admin_exe_str = admin_exe_path
|
||||
.to_str()
|
||||
.ok_or_else(|| anyhow!("Failed to convert admin.exe path to string"))?;
|
||||
.ok_or_else(|| anyhow!("Failed to convert {} path to string", ADMIN_EXE_FILENAME))?;
|
||||
|
||||
let key_base64 = abe::decrypt_with_admin(
|
||||
let key_base64 = abe::decrypt_with_admin_exe(
|
||||
&admin_exe_str,
|
||||
self.app_bound_encrypted_key
|
||||
.as_ref()
|
||||
@@ -311,10 +313,11 @@ fn get_admin_exe_path() -> Result<PathBuf> {
|
||||
get_dist_admin_exe_path(¤t_exe_full_path)?
|
||||
};
|
||||
|
||||
// check if admin.exe exists
|
||||
// check if bitwarden_chromium_import_helper.exe exists
|
||||
if !admin_exe_full_path.exists() {
|
||||
return Err(anyhow!(
|
||||
"admin.exe not found at path: {:?}",
|
||||
"{} not found at path: {:?}",
|
||||
ADMIN_EXE_FILENAME,
|
||||
admin_exe_full_path
|
||||
));
|
||||
}
|
||||
@@ -325,13 +328,13 @@ fn get_admin_exe_path() -> Result<PathBuf> {
|
||||
fn get_dist_admin_exe_path(current_exe_full_path: &PathBuf) -> Result<PathBuf> {
|
||||
let admin_exe = current_exe_full_path
|
||||
.parent()
|
||||
.map(|p| p.join("admin.exe"))
|
||||
.map(|p| p.join(ADMIN_EXE_FILENAME))
|
||||
.ok_or_else(|| anyhow!("Failed to get parent directory of current executable"))?;
|
||||
|
||||
Ok(admin_exe)
|
||||
}
|
||||
|
||||
// Try to find admin.exe in debug build folders. This might not cover all the cases.
|
||||
// Try to find bitwarden_chromium_import_helper.exe in debug build folders. This might not cover all the cases.
|
||||
// Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native.
|
||||
fn get_debug_admin_exe_path() -> Result<PathBuf> {
|
||||
let current_dir = std::env::current_dir()?;
|
||||
@@ -344,7 +347,8 @@ fn get_debug_admin_exe_path() -> Result<PathBuf> {
|
||||
)),
|
||||
Some("desktop_native") => Ok(get_target_admin_exe_path(current_dir)),
|
||||
_ => Err(anyhow!(
|
||||
"Cannot determine admin.exe path from current directory: {}",
|
||||
"Cannot determine {} path from current directory: {}",
|
||||
ADMIN_EXE_FILENAME,
|
||||
current_dir.display()
|
||||
)),
|
||||
}
|
||||
@@ -354,7 +358,7 @@ fn get_target_admin_exe_path(desktop_native_dir: PathBuf) -> PathBuf {
|
||||
desktop_native_dir
|
||||
.join("target")
|
||||
.join("debug")
|
||||
.join("admin.exe")
|
||||
.join(ADMIN_EXE_FILENAME)
|
||||
}
|
||||
|
||||
//
|
||||
|
||||
@@ -51,7 +51,7 @@ function buildImporterBinaries(target, release = true) {
|
||||
return;
|
||||
}
|
||||
|
||||
["admin"].forEach(bin => {
|
||||
["bitwarden_chromium_import_helper"].forEach(bin => {
|
||||
const targetArg = target ? `--target ${target}` : "";
|
||||
const releaseArg = release ? "--release" : "";
|
||||
child_process.execSync(`cargo build --bin ${bin} ${releaseArg} ${targetArg} --features windows-binary`, {stdio: 'inherit', cwd: path.join(__dirname, "bitwarden_chromium_importer")});
|
||||
|
||||
Reference in New Issue
Block a user