1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 00:33:44 +00:00

[PM-25855][PM-24948][PM-24947] Chromium import functionality with application bound encryption on Windows (#16429)

Adds application bound encryption in order to support chrome imports on windows.

---------

Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com>
Co-authored-by: adudek-bw <adudek@bitwarden.com>
Co-authored-by: Hinton <hinton@users.noreply.github.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Dmitry Yakimenko
2025-10-30 13:18:30 +01:00
committed by GitHub
parent e41680df41
commit dcf8c1d83b
13 changed files with 1303 additions and 331 deletions

View File

@@ -589,18 +589,24 @@ dependencies = [
"async-trait", "async-trait",
"base64", "base64",
"cbc", "cbc",
"chacha20poly1305",
"clap",
"dirs",
"hex", "hex",
"homedir",
"oo7", "oo7",
"pbkdf2", "pbkdf2",
"rand 0.9.1", "rand 0.9.1",
"rusqlite", "rusqlite",
"scopeguard",
"security-framework", "security-framework",
"serde", "serde",
"serde_json", "serde_json",
"sha1", "sha1",
"sysinfo",
"tokio", "tokio",
"winapi", "tracing",
"tracing-subscriber",
"verifysign",
"windows 0.61.1", "windows 0.61.1",
] ]
@@ -3824,6 +3830,18 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "verifysign"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ebfe12e38930c3b851aea35e93fab1a6c29279cad7e8e273f29a21678fee8c0"
dependencies = [
"core-foundation",
"sha1",
"sha2",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "version_check" name = "version_check"
version = "0.9.5" version = "0.9.5"
@@ -4186,6 +4204,15 @@ dependencies = [
"windows-targets 0.53.3", "windows-targets 0.53.3",
] ]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link 0.2.1",
]
[[package]] [[package]]
name = "windows-targets" name = "windows-targets"
version = "0.42.2" version = "0.42.2"

View File

@@ -45,6 +45,25 @@ 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;
}
const bin = "bitwarden_chromium_import_helper";
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, "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 buildProcessIsolation() { function buildProcessIsolation() {
if (process.platform !== "linux") { if (process.platform !== "linux") {
return; return;
@@ -67,6 +86,7 @@ if (!crossPlatform && !target) {
console.log(`Building native modules in ${mode} mode for the native architecture`); console.log(`Building native modules in ${mode} mode for the native architecture`);
buildNapiModule(false, mode === "release"); buildNapiModule(false, mode === "release");
buildProxyBin(false, mode === "release"); buildProxyBin(false, mode === "release");
buildImporterBinaries(false, mode === "release");
buildProcessIsolation(); buildProcessIsolation();
return; return;
} }
@@ -76,6 +96,7 @@ if (target) {
installTarget(target); installTarget(target);
buildNapiModule(target, mode === "release"); buildNapiModule(target, mode === "release");
buildProxyBin(target, mode === "release"); buildProxyBin(target, mode === "release");
buildImporterBinaries(false, mode === "release");
buildProcessIsolation(); buildProcessIsolation();
return; return;
} }
@@ -94,5 +115,6 @@ platformTargets.forEach(([target, _]) => {
installTarget(target); installTarget(target);
buildNapiModule(target); buildNapiModule(target);
buildProxyBin(target); buildProxyBin(target);
buildImporterBinaries(target);
buildProcessIsolation(); buildProcessIsolation();
}); });

View File

@@ -12,25 +12,52 @@ anyhow = { workspace = true }
async-trait = "=0.1.88" async-trait = "=0.1.88"
base64 = { workspace = true } base64 = { workspace = true }
cbc = { workspace = true, features = ["alloc"] } cbc = { workspace = true, features = ["alloc"] }
dirs = { workspace = true }
hex = { workspace = true } hex = { workspace = true }
homedir = { workspace = true }
pbkdf2 = "=0.12.2" pbkdf2 = "=0.12.2"
rand = { workspace = true } rand = { workspace = true }
rusqlite = { version = "=0.37.0", features = ["bundled"] } rusqlite = { version = "=0.37.0", features = ["bundled"] }
serde = { workspace = true, features = ["derive"] } serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true } serde_json = { workspace = true }
sha1 = "=0.10.6" sha1 = "=0.10.6"
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
security-framework = { workspace = true } security-framework = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies] [target.'cfg(target_os = "windows")'.dependencies]
tokio = { workspace = true, features = ["full"] } chacha20poly1305 = { workspace = true }
winapi = { version = "=0.3.9", features = ["dpapi", "memoryapi"] } clap = { version = "=4.5.40", features = ["derive"] }
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"] } scopeguard = { workspace = true }
sysinfo = { workspace = true, optional = true }
verifysign = "=0.2.4"
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] [target.'cfg(target_os = "linux")'.dependencies]
oo7 = { workspace = true } oo7 = { workspace = true }
[lints] [lints]
workspace = true workspace = true
[features]
windows-binary = ["dep:sysinfo"]
[[bin]]
name = "bitwarden_chromium_import_helper"
path = "src/bin/bitwarden_chromium_import_helper.rs"
required-features = ["windows-binary"]

View File

@@ -9,155 +9,126 @@ get access to the passwords.
### Overview ### Overview
The Windows Application Bound Encryption (ABE) consists of three main components that work together: The Windows **Application Bound Encryption (ABE)** subsystem consists of two main components that work together:
- **client library** -- Library that is part of the desktop client application - **client library** — a library that is part of the desktop client application
- **admin.exe** -- Service launcher running as ADMINISTRATOR - **bitwarden_chromium_import_helper.exe** — a password decryptor running as **ADMINISTRATOR** and later as **SYSTEM**
- **service.exe** -- Background Windows service running as SYSTEM
_(The names of the binaries will be changed for the released product.)_ _(The name of the binary will be changed in the released product.)_
### The goal See the last section for a concise summary of the entire process.
The goal of this subsystem is to decrypt the master encryption key with which the login information ### Goal
is encrypted on the local system in Windows. This applies to the most recent versions of Chrome and
Edge (untested yet) that are using the ABE/v20 encryption scheme for some of the local profiles.
The general idea of this encryption scheme is that Chrome generates a unique random encryption key, The goal of this subsystem is to decrypt the master encryption key used to encrypt login information on the local
then encrypts it at the user level with a fixed key. It then sends it to the Windows Data Protection Windows system. This applies to the most recent versions of Chrome, Brave, and (untested) Edge that use the ABE/v20
API at the user level, and then, using an installed service, encrypts it with the Windows Data encryption scheme for some local profiles.
Protection API at the system level on top of that. This triply encrypted key is later stored in the
`Local State` file.
The next paragraphs describe what is done at each level to decrypt the key. The general idea of this encryption scheme is as follows:
### 1. Client library 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)**.
4. Finally, it is sent to a special service that encrypts it with DPAPI at the **system level**.
This is a Rust module that is part of the Chromium importer. It only compiles and runs on Windows This triply encrypted key is stored in the `Local State` file.
(see `abe.rs` and `abe_config.rs`). Its main task is to launch `admin.exe` with elevated privileges
by presenting the user with the UAC screen. See the `abe::decrypt_with_admin_and_service` invocation
in `windows.rs`.
This function takes three arguments: The following sections describe how the key is decrypted at each level.
1. Absolute path to `admin.exe` ### 1. Client Library
2. Absolute path to `service.exe`
3. Base64 string of the ABE key extracted from the browser's local state
It's not possible to install the service from the user-level executable. So first, we have to This is a Rust module that is part of the Chromium importer. It compiles and runs only on Windows (see `abe.rs` and
elevate the privileges and run `admin.exe` as ADMINISTRATOR. This is done by calling `ShellExecute` `abe_config.rs`). Its main task is to launch `bitwarden_chromium_import_helper.exe` with elevated privileges, presenting
with the `runas` verb. Since it's not trivial to read the standard output from an application the user with the UAC prompt. See the `abe::decrypt_with_admin` call in `windows.rs`.
launched in this way, a named pipe server is created at the user level, which waits for the response
from `admin.exe` after it has been launched.
The name of the service executable and the data to be decrypted are passed via the command line to This function takes two arguments:
`admin.exe` like this:
1. Absolute path to `bitwarden_chromium_import_helper.exe`
2. Base64 string of the ABE key extracted from the browser's local state
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 `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 `bitwarden_chromium_import_helper.exe` like this:
```bat ```bat
admin.exe --service-exe "c:\temp\service.exe" --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..." bitwarden_chromium_import_helper.exe --encrypted "QVBQQgEAAADQjJ3fARXREYx6AMBPwpfrAQAAA..."
``` ```
**At this point, the user must permit the action to be performed on the UAC screen.** ### 2. Admin Executable
### 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.
This executable receives the full path of `service.exe` and the data to be decrypted. 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.
First, it installs the service to run as SYSTEM and waits for it to start running. The service Next, it finds an instance of `services.exe` or `winlogon.exe`, which are known to run at the **SYSTEM** level. Once a
creates a named pipe server that the admin-level executable communicates with (see the `service.exe` system process is found, its token is duplicated by calling `DuplicateToken`.
description further down).
It sends the base64 string to the pipe server in a raw message and waits for the answer. The answer With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level process.
could be a success or a failure. In case of success, it's a base64 string decrypted at the system
level. In case of failure, it's an error message prefixed with an `!`. In either case, the response
is sent to the named pipe server created by the user. The user responds with `ok` (ignored).
After that, the executable stops and uninstalls the service and then exits. > **At this point `bitwarden_chromium_import_helper.exe` is running as SYSTEM.**
### 3. System service The received encryption key can now be decrypted using DPAPI at the system level.
The service starts and creates a named pipe server for communication between `admin.exe` and the The decrypted result is sent back to the client via the named pipe. `bitwarden_chromium_import_helper.exe` connects to
system service. Please note that it is not possible to communicate between the user and the system the pipe and writes the result.
service directly via a named pipe. Thus, this three-layered approach is necessary.
Once the service is started, it waits for the incoming message via the named pipe. The expected The response can indicate success or failure:
message is a base64 string to be decrypted. The data is decrypted via the Windows Data Protection
API `CryptUnprotectData` and sent back in response to this incoming message in base64 encoding. In
case of an error, the error message is sent back prefixed with an `!`.
The service keeps running and servicing more requests if there are any, until it's stopped and - On success: a Base64-encoded string.
removed from the system. Even though we send only one request, the service is designed to handle as - On failure: an error message prefixed with `!`.
many clients with as many messages as needed and could be installed on the system permanently if
necessary.
### 4. Back to client library In either case, the response is sent to the named pipe server created by the client. The client responds with `ok`
(ignored).
The decrypted base64-encoded string comes back from the admin executable to the named pipe server at Finally, `bitwarden_chromium_import_helper.exe` exits.
the user level. At this point, it has been decrypted only once at the system level.
In the next step, the string is decrypted at the user level with the same Windows Data Protection ### 3. Back to the Client Library
API.
And as the third step, it's decrypted with a hard-coded key found in the `elevation_service.exe` The decrypted Base64-encoded string is returned from `bitwarden_chromium_import_helper.exe` to the named pipe server at
from the Chrome installation. Based on the version of the encrypted string (encoded in the string the user level. At this point it has been decrypted only once—at the system level.
itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The details can be found in
`windows.rs`.
After all of these steps, we have the master key which can be used to decrypt the password Next, the string is decrypted at the **user level** with DPAPI.
information stored in the local database.
### Summary 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.
The Windows ABE decryption process involves a three-tier architecture with named pipe communication: After these steps, the master key is available and can be used to decrypt the password information stored in the
browsers local database.
```mermaid ### TL;DR Steps
sequenceDiagram
participant Client as Client Library (User)
participant Admin as admin.exe (Administrator)
participant Service as service.exe (System)
Client->>Client: Create named pipe server 1. **Client side:**
Note over Client: \\.\pipe\BitwardenEncryptionService-admin-user
Client->>Admin: Launch with UAC elevation 1. Extract the encrypted key from Chromes settings.
Note over Client,Admin: --service-exe c:\path\to\service.exe 2. Create a named pipe server.
Note over Client,Admin: --encrypted QVBQQgEAAADQjJ3fARXRE... 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`.
Client->>Client: Wait for response 2. **Admin side:**
Admin->>Service: Install & start service 1. Start.
Note over Admin,Service: c:\path\to\service.exe 2. Ensure `SE_DEBUG_PRIVILEGE` is enabled (not strictly necessary in tests).
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.
Service->>Service: Create named pipe server 3. **Back on the client side:**
Note over Service: \\.\pipe\BitwardenEncryptionService-service-admin 1. Receive the encryption key.
2. Shutdown the pipe server.
Service->>Service: Wait for message 3. Decrypt it with DPAPI at the **USER** level.
4. (For Chrome only) Decrypt again with the hard-coded key.
Admin->>Service: Send encrypted data via admin-service pipe 5. Obtain the fully decrypted master key.
Note over Admin,Service: QVBQQgEAAADQjJ3fARXRE... 6. Use the master key to read and decrypt stored passwords from Chrome, Brave, Edge, etc.
Admin->>Admin: Wait for response
Service->>Service: Decrypt with system-level DPAPI
Service->>Admin: Return decrypted data via admin-service pipe
Note over Service,Admin: EjRWeXN0ZW0gU2VydmljZQ...
Admin->>Client: Send result via named user-admin pipe
Note over Client,Admin: EjRWeXN0ZW0gU2VydmljZQ...
Client->>Admin: Send ACK to admin
Note over Client,Admin: ok
Admin->>Service: Stop & uninstall service
Service-->>Admin: Exit
Admin-->>Client: Exit
Client->>Client: Decrypt with user-level DPAPI
Client->>Client: Decrypt with hardcoded key
Note over Client: AES-256-GCM or ChaCha20Poly1305
Client->>Client: Done
```

View File

@@ -0,0 +1,515 @@
// Hide everything inside a platform specific module to avoid clippy errors on other platforms
#[cfg(target_os = "windows")]
mod windows_binary {
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose, Engine as _};
use clap::Parser;
use scopeguard::defer;
use std::{
ffi::OsString,
os::windows::{ffi::OsStringExt as _, io::AsRawHandle},
path::{Path, PathBuf},
ptr,
time::Duration,
};
use sysinfo::System;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::windows::named_pipe::{ClientOptions, NamedPipeClient},
time,
};
use tracing::{debug, error, level_filters::LevelFilter};
use tracing_subscriber::{
fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, Layer as _,
};
use verifysign::CodeSignVerifier;
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::{
Pipes::GetNamedPipeServerProcessId,
Threading::{
OpenProcess, OpenProcessToken, QueryFullProcessImageNameW, PROCESS_NAME_WIN32,
PROCESS_QUERY_INFORMATION, PROCESS_VM_READ,
},
},
UI::Shell::IsUserAnAdmin,
},
};
use chromium_importer::chromium::ADMIN_TO_USER_PIPE_NAME;
#[derive(Parser)]
#[command(name = "bitwarden_chromium_import_helper")]
#[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. The way this executable is used, it's not easy to debug and the stdout gets lost.
// This is intended for development time only. All the logging is wrapped in `dbg_log!`` macro that compiles to
// no-op when logging is disabled. This is needed to avoid any sensitive data being logged in production. Normally
// all the logging code is present in the release build and could be enabled via RUST_LOG environment variable.
// We don't want that!
const ENABLE_DEVELOPER_LOGGING: bool = false;
const LOG_FILENAME: &str = "c:\\path\\to\\log.txt"; // This is an example filename, replace it with you own
// This should be enabled for production
const ENABLE_SERVER_SIGNATURE_VALIDATION: bool = false;
const EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT: &str =
"9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d";
// List of SYSTEM process names to try to impersonate
const SYSTEM_PROCESS_NAMES: [&str; 2] = ["services.exe", "winlogon.exe"];
// Macro wrapper around debug! that compiles to no-op when ENABLE_DEVELOPER_LOGGING is false
macro_rules! dbg_log {
($($arg:tt)*) => {
if ENABLE_DEVELOPER_LOGGING {
debug!($($arg)*);
}
};
}
async fn open_pipe_client(pipe_name: &'static str) -> Result<NamedPipeClient> {
let max_attempts = 5;
for _ in 0..max_attempts {
match ClientOptions::new().open(pipe_name) {
Ok(client) => {
dbg_log!("Successfully connected to the pipe!");
return Ok(client);
}
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => {
dbg_log!("Pipe is busy, retrying in 50ms...");
}
Err(e) => {
dbg_log!("Failed to connect to pipe: {}", &e);
return Err(e.into());
}
}
time::sleep(Duration::from_millis(50)).await;
}
Err(anyhow!(
"Failed to connect to pipe after {} attempts",
max_attempts
))
}
async fn send_message_with_client(
client: &mut NamedPipeClient,
message: &str,
) -> Result<String> {
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) => Err(anyhow!("Failed to receive response for message: {}", e)),
}
}
fn get_named_pipe_server_pid(client: &NamedPipeClient) -> Result<u32> {
let handle = HANDLE(client.as_raw_handle() as _);
let mut pid: u32 = 0;
unsafe { GetNamedPipeServerProcessId(handle, &mut pid) }?;
Ok(pid)
}
fn resolve_process_executable_path(pid: u32) -> Result<PathBuf> {
dbg_log!("Resolving process executable path for PID {}", pid);
// Open the process handle
let hprocess =
unsafe { OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, false, pid) }?;
dbg_log!("Opened process handle for PID {}", pid);
// Close when no longer needed
defer! {
dbg_log!("Closing process handle for PID {}", pid);
unsafe {
_ = CloseHandle(hprocess);
}
};
let mut exe_name = vec![0u16; 32 * 1024];
let mut exe_name_length = exe_name.len() as u32;
unsafe {
QueryFullProcessImageNameW(
hprocess,
PROCESS_NAME_WIN32,
windows::core::PWSTR(exe_name.as_mut_ptr()),
&mut exe_name_length,
)
}?;
dbg_log!(
"QueryFullProcessImageNameW returned {} bytes",
exe_name_length
);
exe_name.truncate(exe_name_length as usize);
Ok(PathBuf::from(OsString::from_wide(&exe_name)))
}
async fn send_error_to_user(client: &mut NamedPipeClient, error_message: &str) {
_ = send_to_user(client, &format!("!{}", error_message)).await
}
async fn send_to_user(client: &mut NamedPipeClient, message: &str) -> Result<()> {
let _ = send_message_with_client(client, message).await?;
Ok(())
}
fn is_admin() -> bool {
unsafe { IsUserAnAdmin().as_bool() }
}
fn decrypt_data_base64(data_base64: &str, expect_appb: bool) -> Result<String> {
dbg_log!("Decrypting data base64: {}", data_base64);
let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| {
dbg_log!("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") {
dbg_log!("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 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(
&in_blob,
None,
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 {
dbg_log!("CryptUnprotectData failed");
Err(anyhow!("CryptUnprotectData failed"))
}
}
//
// Impersonate a SYSTEM process
//
fn start_impersonating() -> Result<HANDLE> {
// Need to enable SE_DEBUG_PRIVILEGE to enumerate and open SYSTEM processes
enable_debug_privilege()?;
// Find a SYSTEM process and get its token. Not every SYSTEM process allows token duplication, so try several.
let (token, pid, name) = find_system_process_with_token(get_system_pid_list())?;
// Impersonate the SYSTEM process
unsafe {
ImpersonateLoggedOnUser(token)?;
};
dbg_log!("Impersonating system process '{}' (PID: {})", name, pid);
Ok(token)
}
fn stop_impersonating(token: HANDLE) -> Result<()> {
unsafe {
RevertToSelf()?;
CloseHandle(token)?;
};
Ok(())
}
fn find_system_process_with_token(
pids: Vec<(u32, &'static str)>,
) -> Result<(HANDLE, u32, &'static str)> {
for (pid, name) in pids {
match get_system_token_from_pid(pid) {
Err(_) => {
dbg_log!(
"Failed to open process handle '{}' (PID: {}), skipping",
name,
pid
);
continue;
}
Ok(system_handle) => {
return Ok((system_handle, pid, name));
}
}
}
Err(anyhow!("Failed to get system token from any process"))
}
fn get_system_token_from_pid(pid: u32) -> Result<HANDLE> {
let handle = get_process_handle(pid)?;
let token = get_system_token(handle)?;
unsafe {
CloseHandle(handle)?;
};
Ok(token)
}
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 get_system_pid_list() -> Vec<(u32, &'static str)> {
let sys = System::new_all();
SYSTEM_PROCESS_NAMES
.iter()
.flat_map(|&name| {
sys.processes_by_exact_name(name.as_ref())
.map(move |process| (process.pid().as_u32(), name))
})
.collect()
}
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;
}
fn enable_debug_privilege() -> Result<()> {
let mut previous_value = BOOL(0);
let status = unsafe {
dbg_log!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege");
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value)
};
match status {
STATUS_SUCCESS => {
dbg_log!(
"SE_DEBUG_PRIVILEGE set to 1, was {} before",
previous_value.as_bool()
);
Ok(())
}
_ => {
dbg_log!("RtlAdjustPrivilege failed with status: 0x{:X}", status.0);
Err(anyhow!("Failed to adjust privilege"))
}
}
}
//
// Pipe
//
async fn open_and_validate_pipe_server(pipe_name: &'static str) -> Result<NamedPipeClient> {
let client = open_pipe_client(pipe_name).await?;
if ENABLE_SERVER_SIGNATURE_VALIDATION {
let server_pid = get_named_pipe_server_pid(&client)?;
dbg_log!("Connected to pipe server PID {}", server_pid);
// Validate the server end process signature
let exe_path = resolve_process_executable_path(server_pid)?;
dbg_log!("Pipe server executable path: {}", exe_path.display());
let verifier = CodeSignVerifier::for_file(exe_path.as_path()).map_err(|e| {
anyhow!("verifysign init failed for {}: {:?}", exe_path.display(), e)
})?;
let signature = verifier.verify().map_err(|e| {
anyhow!(
"verifysign verify failed for {}: {:?}",
exe_path.display(),
e
)
})?;
dbg_log!("Pipe server executable path: {}", exe_path.display());
// Dump signature fields for debugging/inspection
dbg_log!("Signature fields:");
dbg_log!(" Subject Name: {:?}", signature.subject_name());
dbg_log!(" Issuer Name: {:?}", signature.issuer_name());
dbg_log!(" SHA1 Thumbprint: {:?}", signature.sha1_thumbprint());
dbg_log!(" SHA256 Thumbprint: {:?}", signature.sha256_thumbprint());
dbg_log!(" Serial Number: {:?}", signature.serial());
if signature.sha256_thumbprint() != EXPECTED_SERVER_SIGNATURE_SHA256_THUMBPRINT {
return Err(anyhow!("Pipe server signature is not valid"));
}
dbg_log!("Pipe server signature verified for PID {}", server_pid);
}
Ok(client)
}
fn run() -> Result<String> {
dbg_log!("Starting bitwarden_chromium_import_helper.exe");
let args = Args::try_parse()?;
if !is_admin() {
return Err(anyhow!("Expected to run with admin privileges"));
}
dbg_log!("Running as ADMINISTRATOR");
// Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine
let system_decrypted_base64 = {
let system_token = start_impersonating()?;
defer! {
dbg_log!("Stopping impersonation");
_ = stop_impersonating(system_token);
}
let system_decrypted_base64 = decrypt_data_base64(&args.encrypted, true)?;
dbg_log!("Decrypted data with system");
system_decrypted_base64
};
// This is just to check that we're decrypting Chrome keys and not something else sent to us by a malicious actor.
// Now that we're back from SYSTEM, we need to decrypt one more time just to verify.
// Chrome keys are double encrypted: once at SYSTEM level and once at USER level.
// When the decryption fails, it means that we're decrypting something unexpected.
// We don't send this result back since the library will decrypt again at USER level.
_ = decrypt_data_base64(&system_decrypted_base64, false).map_err(|e| {
dbg_log!("User level decryption check failed: {}", e);
e
})?;
dbg_log!("User level decryption check passed");
Ok(system_decrypted_base64)
}
fn init_logging(log_path: &Path, file_level: LevelFilter) {
// We only log to a file. It's impossible to see stdout/stderr when this exe is launched from ShellExecuteW.
match std::fs::File::create(log_path) {
Ok(file) => {
let file_filter = EnvFilter::builder()
.with_default_directive(file_level.into())
.from_env_lossy();
let file_layer = fmt::layer()
.with_writer(file)
.with_ansi(false)
.with_filter(file_filter);
tracing_subscriber::registry().with(file_layer).init();
}
Err(error) => {
error!(%error, ?log_path, "Could not create log file.");
}
}
}
pub async fn main() {
if ENABLE_DEVELOPER_LOGGING {
init_logging(LOG_FILENAME.as_ref(), LevelFilter::DEBUG);
}
let mut client = match open_and_validate_pipe_server(ADMIN_TO_USER_PIPE_NAME).await {
Ok(client) => client,
Err(e) => {
error!(
"Failed to open pipe {} to send result/error: {}",
ADMIN_TO_USER_PIPE_NAME, e
);
return;
}
};
match run() {
Ok(system_decrypted_base64) => {
dbg_log!("Sending response back to user");
let _ = send_to_user(&mut client, &system_decrypted_base64).await;
}
Err(e) => {
dbg_log!("Error: {}", e);
send_error_to_user(&mut client, &format!("{}", e)).await;
}
}
}
}
#[tokio::main]
async fn main() {
#[cfg(target_os = "windows")]
windows_binary::main().await;
}

View File

@@ -3,12 +3,15 @@ use std::sync::LazyLock;
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use async_trait::async_trait; use async_trait::async_trait;
use dirs;
use hex::decode; use hex::decode;
use homedir::my_home;
use rusqlite::{params, Connection}; use rusqlite::{params, Connection};
mod platform; mod platform;
#[cfg(target_os = "windows")]
pub use platform::ADMIN_TO_USER_PIPE_NAME;
pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS; pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
// //
@@ -52,7 +55,6 @@ pub trait InstalledBrowserRetriever {
pub struct DefaultInstalledBrowserRetriever {} pub struct DefaultInstalledBrowserRetriever {}
impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever { impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
// TODO: Make thus async
fn get_installed_browsers() -> Result<Vec<String>> { fn get_installed_browsers() -> Result<Vec<String>> {
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len()); let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
@@ -67,7 +69,6 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
} }
} }
// TODO: Make thus async
pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>> { pub fn get_available_profiles(browser_name: &String) -> Result<Vec<ProfileInfo>> {
let (_, local_state) = load_local_state_for_browser(browser_name)?; let (_, local_state) = load_local_state_for_browser(browser_name)?;
Ok(get_profile_info(&local_state)) Ok(get_profile_info(&local_state))
@@ -123,8 +124,7 @@ pub(crate) static SUPPORTED_BROWSER_MAP: LazyLock<
}); });
fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> { fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
let dir = my_home() let dir = dirs::home_dir()
.map_err(|_| anyhow!("Home directory not found"))?
.ok_or_else(|| anyhow!("Home directory not found"))? .ok_or_else(|| anyhow!("Home directory not found"))?
.join(config.data_dir); .join(config.data_dir);
Ok(dir) Ok(dir)

View File

@@ -1,7 +1,9 @@
// Platform-specific code // Platform-specific code
#[cfg_attr(target_os = "linux", path = "linux.rs")] #[cfg_attr(target_os = "linux", path = "linux.rs")]
#[cfg_attr(target_os = "windows", path = "windows.rs")] #[cfg_attr(target_os = "windows", path = "windows/mod.rs")]
#[cfg_attr(target_os = "macos", path = "macos.rs")] #[cfg_attr(target_os = "macos", path = "macos.rs")]
mod native; mod native;
pub(crate) use native::*; // Windows exposes public const
#[allow(unused_imports)]
pub use native::*;

View File

@@ -1,204 +0,0 @@
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 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};
use crate::util;
//
// Public API
//
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Brave",
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
},
BrowserConfig {
name: "Chrome",
data_dir: "AppData/Local/Google/Chrome/User Data",
},
BrowserConfig {
name: "Chromium",
data_dir: "AppData/Local/Chromium/User Data",
},
BrowserConfig {
name: "Microsoft Edge",
data_dir: "AppData/Local/Microsoft/Edge/User Data",
},
BrowserConfig {
name: "Opera",
data_dir: "AppData/Roaming/Opera Software/Opera Stable",
},
BrowserConfig {
name: "Vivaldi",
data_dir: "AppData/Local/Vivaldi/User Data",
},
];
pub(crate) fn get_crypto_service(
_browser_name: &str,
local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {
Ok(Box::new(WindowsCryptoService::new(local_state)))
}
//
// CryptoService
//
struct WindowsCryptoService {
master_key: Option<Vec<u8>>,
encrypted_key: Option<String>,
}
impl WindowsCryptoService {
pub(crate) fn new(local_state: &LocalState) -> Self {
Self {
master_key: None,
encrypted_key: local_state
.os_crypt
.as_ref()
.and_then(|c| c.encrypted_key.clone()),
}
}
}
#[async_trait]
impl CryptoService for WindowsCryptoService {
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
if encrypted.is_empty() {
return Ok(String::new());
}
// On Windows only v10 and v20 are supported at the moment
let (version, no_prefix) =
util::split_encrypted_string_and_validate(encrypted, &["v10", "v20"])?;
// v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes auth tag]
const IV_SIZE: usize = 12;
const TAG_SIZE: usize = 16;
const MIN_LENGTH: usize = IV_SIZE + TAG_SIZE;
if no_prefix.len() < MIN_LENGTH {
return Err(anyhow!(
"Corrupted entry: expected at least {} bytes, got {} bytes",
MIN_LENGTH,
no_prefix.len()
));
}
// Allow empty passwords
if no_prefix.len() == MIN_LENGTH {
return Ok(String::new());
}
if self.master_key.is_none() {
self.master_key = Some(self.get_master_key(version)?);
}
let key = self
.master_key
.as_ref()
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
let key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(&no_prefix[..IV_SIZE]);
let decrypted_bytes = cipher
.decrypt(nonce, no_prefix[IV_SIZE..].as_ref())
.map_err(|e| anyhow!("Decryption failed: {}", e))?;
let plaintext = String::from_utf8(decrypted_bytes)
.map_err(|e| anyhow!("Failed to convert decrypted data to UTF-8: {}", e))?;
Ok(plaintext)
}
}
impl WindowsCryptoService {
fn get_master_key(&mut self, version: &str) -> Result<Vec<u8>> {
match version {
"v10" => self.get_master_key_v10(),
_ => Err(anyhow!("Unsupported version: {}", version)),
}
}
fn get_master_key_v10(&mut self) -> Result<Vec<u8>> {
if self.encrypted_key.is_none() {
return Err(anyhow!(
"Encrypted master key is not found in the local browser state"
));
}
let key = self
.encrypted_key
.as_ref()
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
let key_bytes = BASE64_STANDARD
.decode(key)
.map_err(|e| anyhow!("Encrypted master key is not a valid base64 string: {}", e))?;
if key_bytes.len() <= 5 || &key_bytes[..5] != b"DPAPI" {
return Err(anyhow!("Encrypted master key is not encrypted with DPAPI"));
}
let key = unprotect_data_win(&key_bytes[5..])
.map_err(|e| anyhow!("Failed to unprotect the master key: {}", e))?;
Ok(key)
}
}
fn unprotect_data_win(data: &[u8]) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(Vec::new());
}
let mut data_in = DATA_BLOB {
cbData: data.len() as DWORD,
pbData: data.as_ptr() as *mut BYTE,
};
let mut data_out = DATA_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let result: BOOL = unsafe {
// BOOL from winapi (i32)
CryptUnprotectData(
&mut data_in,
std::ptr::null_mut(), // ppszDataDescr: *mut LPWSTR (*mut *mut u16)
std::ptr::null_mut(), // pOptionalEntropy: *mut DATA_BLOB
std::ptr::null_mut(), // pvReserved: LPVOID (*mut c_void)
std::ptr::null_mut(), // pPromptStruct: *mut CRYPTPROTECT_PROMPTSTRUCT
0, // dwFlags: DWORD
&mut data_out,
)
};
if result == 0 {
return Err(anyhow!("CryptUnprotectData failed"));
}
if data_out.pbData.is_null() || data_out.cbData == 0 {
return Ok(Vec::new());
}
let output_slice =
unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) };
unsafe {
if !data_out.pbData.is_null() {
LocalFree(Some(HLOCAL(data_out.pbData as *mut std::ffi::c_void)));
}
}
Ok(output_slice.to_vec())
}

View File

@@ -0,0 +1,178 @@
use super::abe_config;
use anyhow::{anyhow, Result};
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,
time::{timeout, Duration},
};
use tracing::debug;
use windows::{
core::PCWSTR,
Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE},
};
const WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS: u64 = 30;
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()
.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]);
let preview = message.chars().take(16).collect::<String>();
debug!(
"Received from client: '{}...' ({} bytes)",
preview, 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(crate) async fn decrypt_with_admin_exe(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 ADMINISTRATOR...", admin_exe);
decrypt_with_admin_exe_internal(admin_exe, encrypted);
debug!("Waiting for message from {}...", admin_exe);
let message = match timeout(
Duration::from_secs(WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS),
rx.recv(),
)
.await
{
Ok(Some(msg)) => msg,
Ok(None) => return Err(anyhow!("Channel closed without message from {}", admin_exe)),
Err(_) => return Err(anyhow!("Timeout waiting for message from {}", admin_exe)),
};
debug!("Shutting down the pipe server...");
server.abort();
Ok(message)
}
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()
.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,2 @@
pub const ADMIN_TO_USER_PIPE_NAME: &str =
r"\\.\pipe\bitwarden-to-bitwarden-chromium-importer-helper";

View File

@@ -0,0 +1,424 @@
use aes_gcm::{aead::Aead, 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 std::path::{Path, PathBuf};
use windows::Win32::{
Foundation::{LocalFree, HLOCAL},
Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB},
};
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
use crate::util;
mod abe;
mod abe_config;
pub use abe_config::ADMIN_TO_USER_PIPE_NAME;
//
// Public API
//
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Brave",
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
},
BrowserConfig {
name: "Chrome",
data_dir: "AppData/Local/Google/Chrome/User Data",
},
BrowserConfig {
name: "Chromium",
data_dir: "AppData/Local/Chromium/User Data",
},
BrowserConfig {
name: "Microsoft Edge",
data_dir: "AppData/Local/Microsoft/Edge/User Data",
},
BrowserConfig {
name: "Opera",
data_dir: "AppData/Roaming/Opera Software/Opera Stable",
},
BrowserConfig {
name: "Vivaldi",
data_dir: "AppData/Local/Vivaldi/User Data",
},
];
pub(crate) fn get_crypto_service(
_browser_name: &str,
local_state: &LocalState,
) -> Result<Box<dyn CryptoService>> {
Ok(Box::new(WindowsCryptoService::new(local_state)))
}
//
// Private
//
const ADMIN_EXE_FILENAME: &str = "bitwarden_chromium_import_helper.exe";
//
// CryptoService
//
struct WindowsCryptoService {
master_key: Option<Vec<u8>>,
encrypted_key: Option<String>,
app_bound_encrypted_key: Option<String>,
}
impl WindowsCryptoService {
pub(crate) fn new(local_state: &LocalState) -> Self {
Self {
master_key: None,
encrypted_key: local_state
.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()),
}
}
}
#[async_trait]
impl CryptoService for WindowsCryptoService {
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String> {
if encrypted.is_empty() {
return Ok(String::new());
}
// On Windows only v10 and v20 are supported at the moment
let (version, no_prefix) =
util::split_encrypted_string_and_validate(encrypted, &["v10", "v20"])?;
// v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes auth tag]
const IV_SIZE: usize = 12;
const TAG_SIZE: usize = 16;
const MIN_LENGTH: usize = IV_SIZE + TAG_SIZE;
if no_prefix.len() < MIN_LENGTH {
return Err(anyhow!(
"Corrupted entry: expected at least {} bytes, got {} bytes",
MIN_LENGTH,
no_prefix.len()
));
}
// Allow empty passwords
if no_prefix.len() == MIN_LENGTH {
return Ok(String::new());
}
if self.master_key.is_none() {
self.master_key = Some(self.get_master_key(version).await?);
}
let key = self
.master_key
.as_ref()
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
let key = Key::<Aes256Gcm>::from_slice(key);
let cipher = Aes256Gcm::new(key);
let nonce = Nonce::from_slice(&no_prefix[..IV_SIZE]);
let decrypted_bytes = cipher
.decrypt(nonce, no_prefix[IV_SIZE..].as_ref())
.map_err(|e| anyhow!("Decryption failed: {}", e))?;
let plaintext = String::from_utf8(decrypted_bytes)
.map_err(|e| anyhow!("Failed to convert decrypted data to UTF-8: {}", e))?;
Ok(plaintext)
}
}
impl WindowsCryptoService {
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)),
}
}
fn get_master_key_v10(&mut self) -> Result<Vec<u8>> {
if self.encrypted_key.is_none() {
return Err(anyhow!(
"Encrypted master key is not found in the local browser state"
));
}
let key = self
.encrypted_key
.as_ref()
.ok_or_else(|| anyhow!("Failed to retrieve key"))?;
let key_bytes = BASE64_STANDARD
.decode(key)
.map_err(|e| anyhow!("Encrypted master key is not a valid base64 string: {}", e))?;
if key_bytes.len() <= 5 || &key_bytes[..5] != b"DPAPI" {
return Err(anyhow!("Encrypted master key is not encrypted with DPAPI"));
}
let key = unprotect_data_win(&key_bytes[5..])
.map_err(|e| anyhow!("Failed to unprotect the master key: {}", e))?;
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 admin_exe_path = get_admin_exe_path()?;
let admin_exe_str = admin_exe_path
.to_str()
.ok_or_else(|| anyhow!("Failed to convert {} path to string", ADMIN_EXE_FILENAME))?;
let key_base64 = abe::decrypt_with_admin_exe(
admin_exe_str,
self.app_bound_encrypted_key
.as_ref()
.expect("app_bound_encrypted_key should not be None"),
)
.await?;
if let Some(error_message) = key_base64.strip_prefix('!') {
return Err(anyhow!(
"Failed to decrypt the master key: {}",
error_message
));
}
let key_bytes = BASE64_STANDARD.decode(&key_base64)?;
let key = unprotect_data_win(&key_bytes)?;
Self::decode_abe_key_blob(key.as_slice())
}
fn decode_abe_key_blob(blob_data: &[u8]) -> Result<Vec<u8>> {
let header_len = u32::from_le_bytes(blob_data[0..4].try_into()?) as usize;
// Ignore the header
let content_len_offset = 4 + header_len;
let content_len =
u32::from_le_bytes(blob_data[content_len_offset..content_len_offset + 4].try_into()?)
as usize;
if content_len < 1 {
return Err(anyhow!(
"Corrupted ABE key blob: content length is less than 1"
));
}
let content_offset = content_len_offset + 4;
let content = &blob_data[content_offset..content_offset + content_len];
// When the size is exactly 32 bytes, it's a plain key. It's used in unbranded Chromium builds, Brave, possibly Edge
if content_len == 32 {
return Ok(content.to_vec());
}
let version = content[0];
let key_blob = &content[1..];
match version {
// Google Chrome v1 key encrypted with a hardcoded AES key
1_u8 => Self::decrypt_abe_key_blob_chrome_aes(key_blob),
// Google Chrome v2 key encrypted with a hardcoded ChaCha20 key
2_u8 => Self::decrypt_abe_key_blob_chrome_chacha20(key_blob),
// Google Chrome v3 key encrypted with CNG APIs
3_u8 => Self::decrypt_abe_key_blob_chrome_cng(key_blob),
v => Err(anyhow!("Unsupported ABE key blob version: {}", v)),
}
}
// TODO: DRY up with decrypt_abe_key_blob_chrome_chacha20
fn decrypt_abe_key_blob_chrome_aes(blob: &[u8]) -> Result<Vec<u8>> {
if blob.len() < 60 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 60 bytes, got {} bytes",
blob.len()
));
}
let iv: [u8; 12] = blob[0..12].try_into()?;
let ciphertext: [u8; 48] = blob[12..12 + 48].try_into()?;
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 decrypted = cipher
.decrypt((&iv).into(), ciphertext.as_ref())
.map_err(|e| anyhow!("Failed to decrypt v20 key with Google AES key: {}", e))?;
Ok(decrypted)
}
fn decrypt_abe_key_blob_chrome_chacha20(blob: &[u8]) -> Result<Vec<u8>> {
if blob.len() < 60 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 60 bytes, got {} bytes",
blob.len()
));
}
let chacha20_key = chacha20poly1305::Key::from_slice(GOOGLE_CHACHA20_KEY);
let cipher = ChaCha20Poly1305::new(chacha20_key);
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 iv: [u8; 12] = blob[0..12].try_into()?;
let ciphertext: [u8; 48] = blob[12..12 + 48].try_into()?;
let decrypted = cipher
.decrypt((&iv).into(), ciphertext.as_ref())
.map_err(|e| anyhow!("Failed to decrypt v20 key with Google ChaCha20 key: {}", e))?;
Ok(decrypted)
}
fn decrypt_abe_key_blob_chrome_cng(blob: &[u8]) -> Result<Vec<u8>> {
if blob.len() < 92 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 92 bytes, got {} bytes",
blob.len()
));
}
let _encrypted_aes_key: [u8; 32] = blob[0..32].try_into()?;
let _iv: [u8; 12] = blob[32..32 + 12].try_into()?;
let _ciphertext: [u8; 48] = blob[44..44 + 48].try_into()?;
// TODO: Decrypt the AES key using CNG APIs
// TODO: Implement this in the future once we run into a browser that uses this scheme
// There's no way to test this at the moment. This encryption scheme is not used in any of the browsers I've tested.
Err(anyhow!("Google ABE CNG flavor is not supported yet"))
}
}
fn unprotect_data_win(data: &[u8]) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(Vec::new());
}
let data_in = CRYPT_INTEGER_BLOB {
cbData: data.len() as u32,
pbData: data.as_ptr() as *mut u8,
};
let mut data_out = CRYPT_INTEGER_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let result = unsafe {
CryptUnprotectData(
&data_in,
None, // ppszDataDescr: Option<*mut PWSTR>
None, // pOptionalEntropy: Option<*const CRYPT_INTEGER_BLOB>
None, // pvReserved: Option<*const std::ffi::c_void>
None, // pPromptStruct: Option<*const CRYPTPROTECT_PROMPTSTRUCT>
0, // dwFlags: u32
&mut data_out,
)
};
if result.is_err() {
return Err(anyhow!("CryptUnprotectData failed"));
}
if data_out.pbData.is_null() || data_out.cbData == 0 {
return Ok(Vec::new());
}
let output_slice =
unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) };
unsafe {
if !data_out.pbData.is_null() {
LocalFree(Some(HLOCAL(data_out.pbData as *mut std::ffi::c_void)));
}
}
Ok(output_slice.to_vec())
}
fn get_admin_exe_path() -> Result<PathBuf> {
let current_exe_full_path = std::env::current_exe()
.map_err(|e| anyhow!("Failed to get current executable path: {}", e))?;
let exe_name = current_exe_full_path
.file_name()
.ok_or_else(|| anyhow!("Failed to get file name from current executable path"))?;
let admin_exe_full_path = if exe_name.eq_ignore_ascii_case("electron.exe") {
get_debug_admin_exe_path()?
} else {
get_dist_admin_exe_path(&current_exe_full_path)?
};
// check if bitwarden_chromium_import_helper.exe exists
if !admin_exe_full_path.exists() {
return Err(anyhow!(
"{} not found at path: {:?}",
ADMIN_EXE_FILENAME,
admin_exe_full_path
));
}
Ok(admin_exe_full_path)
}
fn get_dist_admin_exe_path(current_exe_full_path: &Path) -> Result<PathBuf> {
let admin_exe = current_exe_full_path
.parent()
.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 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()?;
let folder_name = current_dir
.file_name()
.ok_or_else(|| anyhow!("Failed to get folder name from current directory"))?;
match folder_name.to_str() {
Some("desktop") => Ok(get_target_admin_exe_path(
current_dir.join("desktop_native"),
)),
Some("desktop_native") => Ok(get_target_admin_exe_path(current_dir)),
_ => Err(anyhow!(
"Cannot determine {} path from current directory: {}",
ADMIN_EXE_FILENAME,
current_dir.display()
)),
}
}
fn get_target_admin_exe_path(desktop_native_dir: PathBuf) -> PathBuf {
desktop_native_dir
.join("target")
.join("debug")
.join(ADMIN_EXE_FILENAME)
}

View File

@@ -36,6 +36,10 @@
{ {
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
"to": "desktop_proxy.exe" "to": "desktop_proxy.exe"
},
{
"from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe",
"to": "bitwarden_chromium_import_helper.exe"
} }
] ]
}, },

View File

@@ -96,6 +96,10 @@
{ {
"from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe",
"to": "desktop_proxy.exe" "to": "desktop_proxy.exe"
},
{
"from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe",
"to": "bitwarden_chromium_import_helper.exe"
} }
] ]
}, },