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