diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 5e658546671..4b816303b90 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -450,20 +450,26 @@ dependencies = [ "async-trait", "base64", "cbc", + "chacha20poly1305", + "clap", + "dirs", "hex", - "homedir", + "log", "napi", "napi-derive", "oo7", "pbkdf2", "rand 0.9.1", "rusqlite", + "scopeguard", "security-framework", "serde", "serde_json", "sha1", + "simplelog", + "sysinfo", "tokio", - "winapi", + "verifysign", "windows 0.61.1", ] @@ -906,6 +912,15 @@ dependencies = [ "zeroize", ] +[[package]] +name = "deranged" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41953f86f8a05768a6cda24def994fd2f424b04ec5c719cf89989779f199071" +dependencies = [ + "powerfmt", +] + [[package]] name = "desktop_core" version = "0.0.0" @@ -1042,7 +1057,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.61.1", ] [[package]] @@ -2145,6 +2160,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + [[package]] name = "num-integer" version = "0.1.46" @@ -2186,6 +2207,15 @@ dependencies = [ "libm", ] +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + [[package]] name = "objc2" version = "0.6.1" @@ -2606,6 +2636,12 @@ dependencies = [ "zerovec", ] +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "ppv-lite86" version = "0.2.21" @@ -3186,6 +3222,17 @@ dependencies = [ "rand_core 0.6.4", ] +[[package]] +name = "simplelog" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0" +dependencies = [ + "log", + "termcolor", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -3433,6 +3480,39 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.8.1" @@ -3826,6 +3906,18 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" 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.1", +] + [[package]] name = "version_check" version = "0.9.5" @@ -4082,9 +4174,9 @@ checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-link" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "45e46c0661abb7180e7b9c281db115305d49ca1709ab8242adf09666d2173c65" [[package]] name = "windows-numerics" @@ -4102,9 +4194,9 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", + "windows-link 0.1.3", + "windows-result 0.3.4", + "windows-strings", ] [[package]] @@ -4125,15 +4217,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -4143,15 +4226,6 @@ dependencies = [ "windows-link 0.1.3", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-sys" version = "0.45.0" @@ -4188,6 +4262,15 @@ dependencies = [ "windows-targets 0.53.3", ] +[[package]] +name = "windows-sys" +version = "0.61.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f109e41dd4a3c848907eb83d5a42ea98b3769495597450cf6d153507b166f0f" +dependencies = [ + "windows-link 0.2.0", +] + [[package]] name = "windows-targets" version = "0.42.2" diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml index 656c3ad1504..2f3ca776138 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml @@ -12,8 +12,9 @@ anyhow = { workspace = true } async-trait = "=0.1.88" base64 = { workspace = true } cbc = { workspace = true, features = ["alloc"] } +dirs = { workspace = true } hex = { workspace = true } -homedir = { workspace = true } +log = { workspace = true } napi = { workspace = true } napi-derive = { workspace = true } pbkdf2 = "=0.12.2" @@ -22,14 +23,32 @@ rusqlite = { version = "=0.37.0", features = ["bundled"] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } sha1 = "=0.10.6" +sysinfo = { workspace = true, optional = true } [target.'cfg(target_os = "macos")'.dependencies] security-framework = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] +chacha20poly1305 = "=0.10.1" +clap = { version = "=4.5.40", features = ["derive"] } +scopeguard = { workspace = true } +simplelog = { workspace = true } 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"] } +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] oo7 = { workspace = true } @@ -37,3 +56,10 @@ oo7 = { workspace = true } [lints] workspace = true +[features] +windows-binary = ["sysinfo"] + +[[bin]] +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 498dd3ac67d..8e088c4c25e 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/README.md @@ -2,155 +2,126 @@ ## 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 -- **admin.exe** -- Service launcher running as ADMINISTRATOR -- **service.exe** -- Background Windows service running as SYSTEM +- **client library** — a library that is part of the desktop client application +- **bitwarden_chromium_import_helper.exe** — a password decryptor running as **ADMINISTRATOR** and later 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 -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. +## Goal -The general idea of this encryption scheme is that Chrome generates a unique random encryption key, -then encrypts it at the user level with a fixed key. It then sends it to the Windows Data Protection -API at the user level, and then, using an installed service, encrypts it with the Windows Data -Protection API at the system level on top of that. This triply encrypted key is later stored in the -`Local State` file. +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 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 -(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 triply encrypted key is stored in the `Local State` file. -This function takes three arguments: +The following sections describe how the key is decrypted at each level. -1. Absolute path to `admin.exe` -2. Absolute path to `service.exe` -3. Base64 string of the ABE key extracted from the browser's local state +## 1. Client Library -It's not possible to install the service from the user-level executable. So first, we have to -elevate the privileges and run `admin.exe` as ADMINISTRATOR. This is done by calling `ShellExecute` -with the `runas` verb. Since it's not trivial to read the standard output from an application -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. +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`. -The name of the service executable and the data to be decrypted are passed via the command line to -`admin.exe` like this: +This function takes two arguments: + +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 -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 -creates a named pipe server that the admin-level executable communicates with (see the `service.exe` -description further down). +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`. -It sends the base64 string to the pipe server in a raw message and waits for the answer. The answer -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). +With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a system-level process. -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 -system service. Please note that it is not possible to communicate between the user and the system -service directly via a named pipe. Thus, this three-layered approach is necessary. +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. -Once the service is started, it waits for the incoming message via the named pipe. The expected -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 response can indicate success or failure: -The service keeps running and servicing more requests if there are any, until it's stopped and -removed from the system. Even though we send only one request, the service is designed to handle as -many clients with as many messages as needed and could be installed on the system permanently if -necessary. +- On success: a Base64-encoded string. +- On failure: an error message prefixed with `!`. -## 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 -the user level. At this point, it has been decrypted only once at the system level. +Finally, `bitwarden_chromium_import_helper.exe` exits. -In the next step, the string is decrypted at the user level with the same Windows Data Protection -API. +## 3. Back to the Client Library -And as the third step, it's decrypted with a hard-coded key found in the `elevation_service.exe` -from the Chrome installation. Based on the version of the encrypted string (encoded in the string -itself), it's either AES-256-GCM or ChaCha20Poly1305 encryption scheme. The details can be found in -`windows.rs`. +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. -After all of these steps, we have the master key which can be used to decrypt the password -information stored in the local database. +Next, the string is decrypted at the **user level** with DPAPI. -## 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 +browser’s local database. -```mermaid -sequenceDiagram - participant Client as Client Library (User) - participant Admin as admin.exe (Administrator) - participant Service as service.exe (System) +## TL;DR Steps - Client->>Client: Create named pipe server - Note over Client: \\.\pipe\BitwardenEncryptionService-admin-user +1. **Client side:** - Client->>Admin: Launch with UAC elevation - Note over Client,Admin: --service-exe c:\path\to\service.exe - Note over Client,Admin: --encrypted QVBQQgEAAADQjJ3fARXRE... + 1. Extract the encrypted key from Chrome’s settings. + 2. Create a named pipe server. + 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 - Note over Admin,Service: c:\path\to\service.exe + 1. Start. + 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 - Note over Service: \\.\pipe\BitwardenEncryptionService-service-admin - - Service->>Service: Wait for message - - Admin->>Service: Send encrypted data via admin-service pipe - Note over Admin,Service: QVBQQgEAAADQjJ3fARXRE... - - 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 -``` +3. **Back on the client side:** + 1. Receive the encryption key. + 2. Shutdown the pipe server. + 3. Decrypt it with DPAPI at the **USER** level. + 4. (For Chrome only) Decrypt again with the hard-coded key. + 5. Obtain the fully decrypted master key. + 6. Use the master key to read and decrypt stored passwords from Chrome, Brave, Edge, etc. diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs new file mode 100644 index 00000000000..9081a86188e --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe.rs @@ -0,0 +1,179 @@ +use anyhow::{anyhow, Result}; +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, + time::{timeout, Duration}, +}; +use windows::{ + core::PCWSTR, + Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE}, +}; + +use crate::abe_config; + +const WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS: u64 = 30; + +pub fn start_tokio_named_pipe_server( + pipe_name: &'static str, + process_message: F, +) -> Result>> +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(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::(); + 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 async fn decrypt_with_admin_exe(admin_exe: &str, encrypted: &str) -> Result { + let (tx, mut rx) = channel::(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::>(); + let runas_wide = OsStr::new("runas") + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + let parameters = OsStr::new(&format!(r#"--encrypted "{}""#, encrypted)) + .encode_wide() + .chain(std::iter::once(0)) + .collect::>(); + + unsafe { + ShellExecuteW( + None, + PCWSTR(runas_wide.as_ptr()), + PCWSTR(exe_wide.as_ptr()), + PCWSTR(parameters.as_ptr()), + None, + SW_HIDE, + ); + } +} diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs new file mode 100644 index 00000000000..a1c36d700a8 --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/abe_config.rs @@ -0,0 +1 @@ +pub const ADMIN_TO_USER_PIPE_NAME: &str = r"\\.\pipe\BitwardenEncryptionService-admin-user"; diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs new file mode 100644 index 00000000000..26d317e52c0 --- /dev/null +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/bin/bitwarden_chromium_import_helper.rs @@ -0,0 +1,504 @@ +#[cfg(not(target_os = "windows"))] +use napi::tokio; + +// 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 log::{debug, error}; + use scopeguard::guard; + use simplelog::*; + use std::{ + ffi::OsString, + fs::OpenOptions, + os::windows::{ffi::OsStringExt as _, io::AsRawHandle}, + path::PathBuf, + ptr, + time::Duration, + }; + use sysinfo::System; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::windows::named_pipe::{ClientOptions, NamedPipeClient}, + time, + }; + 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 bitwarden_chromium_importer::abe_config; + + #[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. + const NEED_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 NEED_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 NEED_LOGGING is false + macro_rules! dbg_log { + ($($arg:tt)*) => { + if NEED_LOGGING { + debug!($($arg)*); + } + }; + } + + async fn open_pipe_client(pipe_name: &'static str) -> Result { + // TODO: Don't loop forever, but retry a few times + let client = loop { + match ClientOptions::new().open(pipe_name) { + Ok(client) => { + dbg_log!("Successfully connected to the pipe!"); + break 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; + }; + + Ok(client) + } + + async fn send_message_with_client( + client: &mut NamedPipeClient, + message: &str, + ) -> Result { + 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 { + 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 { + 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); + + let _guard = guard(hprocess, |_| unsafe { + dbg_log!("Closing process handle for PID {}", pid); + _ = CloseHandle(hprocess); + }); + + let mut wide = vec![0u16; 260]; + let mut size = wide.len() as u32; + unsafe { + QueryFullProcessImageNameW( + hprocess, + PROCESS_NAME_WIN32, + windows::core::PWSTR(wide.as_mut_ptr()), + &mut size, + ) + }?; + dbg_log!("QueryFullProcessImageNameW returned {} bytes", size); + + wide.truncate(size as usize); + Ok(PathBuf::from(OsString::from_wide(&wide))) + } + + 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 { + 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> { + 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, + 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 { + dbg_log!("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() -> Result { + Self::enable_privilege()?; + + // Find a SYSTEM process and get its token. Not every SYSTEM process allows token duplication, so try several. + let (sys_token, pid, name) = + Self::find_system_process_with_token(Self::get_system_pid_list())?; + + // Impersonate the SYSTEM process + unsafe { + ImpersonateLoggedOnUser(sys_token)?; + }; + dbg_log!("Impersonating system process '{}' (PID: {})", name, pid); + + Ok(Self { + sys_token_handle: sys_token, + }) + } + + pub fn stop() -> Result<()> { + unsafe { + RevertToSelf()?; + }; + Ok(()) + } + + /// stop impersonate and return sys token handle + pub fn _stop_sys_handle(self) -> Result { + 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 { + dbg_log!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege"); + RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value) + }; + if status != STATUS_SUCCESS { + return Err(anyhow!("Failed to adjust privilege")); + } + dbg_log!( + "SE_DEBUG_PRIVILEGE set to 1, was {} before", + previous_value.0 + ); + Ok(()) + } + + fn find_system_process_with_token( + pids: Vec<(u32, &'static str)>, + ) -> Result<(HANDLE, u32, &'static str)> { + for (pid, name) in pids { + match Self::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 { + let handle = Self::get_process_handle(pid)?; + let token = Self::get_system_token(handle)?; + unsafe { + CloseHandle(handle)?; + }; + Ok(token) + } + + fn get_system_token(handle: HANDLE) -> Result { + 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 mut pids = Vec::new(); + let sys = System::new_all(); + for name in SYSTEM_PROCESS_NAMES { + for process in sys.processes_by_exact_name(name.as_ref()) { + pids.push((process.pid().as_u32(), name)); + } + } + pids + } + + fn get_process_handle(pid: u32) -> Result { + 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; + } + + async fn open_and_validate_pipe_server(pipe_name: &'static str) -> Result { + let client = open_pipe_client(pipe_name).await?; + + if NEED_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 { + 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 _guard = ImpersonateGuard::start()?; + 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) + } + + pub async fn main() { + if NEED_LOGGING { + WriteLogger::init( + LevelFilter::Debug, // Controlled by the feature flags in Cargo.toml + Config::default(), + OpenOptions::new() + .create(true) + .append(true) + .open(LOG_FILENAME) + .expect("Can't open the log file"), + ) + .expect("Failed to initialize logger"); + } + + let mut client = + match open_and_validate_pipe_server(abe_config::ADMIN_TO_USER_PIPE_NAME).await { + Ok(client) => client, + Err(e) => { + error!( + "Failed to open pipe {} to send result/error: {}", + abe_config::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; +} diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs index 094500e6d42..f067ede1bbf 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/chromium.rs @@ -3,8 +3,8 @@ use std::sync::LazyLock; use anyhow::{anyhow, Result}; use async_trait::async_trait; +use dirs; use hex::decode; -use homedir::my_home; use rusqlite::{params, Connection}; // Platform-specific code @@ -57,7 +57,6 @@ pub trait InstalledBrowserRetriever { pub struct DefaultInstalledBrowserRetriever {} impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever { - // TODO: Make thus async fn get_installed_browsers() -> Result> { let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len()); @@ -72,7 +71,6 @@ impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever { } } -// TODO: Make thus async pub fn get_available_profiles(browser_name: &String) -> Result> { let (_, local_state) = load_local_state_for_browser(browser_name)?; Ok(get_profile_info(&local_state)) @@ -128,8 +126,7 @@ pub static SUPPORTED_BROWSER_MAP: LazyLock< }); fn get_browser_data_dir(config: &BrowserConfig) -> Result { - let dir = my_home() - .map_err(|_| anyhow!("Home directory not found"))? + let dir = dirs::home_dir() .ok_or_else(|| anyhow!("Home directory not found"))? .join(config.data_dir); Ok(dir) diff --git a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs index 84f140d2341..36ffd3cc187 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/lib.rs @@ -1,3 +1,6 @@ +#[cfg(target_os = "windows")] +pub mod abe_config; + #[macro_use] extern crate napi_derive; 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 096808aafb6..b3d6eff0613 100644 --- a/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs +++ b/apps/desktop/desktop_native/bitwarden_chromium_importer/src/windows.rs @@ -1,15 +1,17 @@ -use aes_gcm::aead::Aead; -use aes_gcm::{Aes256Gcm, Key, KeyInit, Nonce}; +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 winapi::shared::minwindef::{BOOL, BYTE, DWORD}; -use winapi::um::{dpapi::CryptUnprotectData, wincrypt::DATA_BLOB}; -use windows::Win32::Foundation::{LocalFree, HLOCAL}; +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; // // Public API @@ -50,12 +52,19 @@ pub fn get_crypto_service( Ok(Box::new(WindowsCryptoService::new(local_state))) } +// +// Private +// + +const ADMIN_EXE_FILENAME: &str = "bitwarden_chromium_import_helper.exe"; + // // CryptoService // struct WindowsCryptoService { master_key: Option>, encrypted_key: Option, + app_bound_encrypted_key: Option, } impl WindowsCryptoService { @@ -66,6 +75,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 +113,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 +136,10 @@ impl CryptoService for WindowsCryptoService { } impl WindowsCryptoService { - fn get_master_key(&mut self, version: &str) -> Result> { + async fn get_master_key(&mut self, version: &str) -> Result> { match version { "v10" => self.get_master_key_v10(), + "v20" => self.get_master_key_v20().await, _ => Err(anyhow!("Unsupported version: {}", version)), } } @@ -154,6 +168,83 @@ impl WindowsCryptoService { Ok(key) } + + async fn get_master_key_v20(&mut self) -> Result> { + 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)?; + + let key_data = KeyData::parse(&mut &key[..])?; + + match key_data { + KeyData::One { iv, ciphertext } => { + // 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::::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) + } + KeyData::Two { iv, ciphertext } => { + // 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 decrypted = cipher + .decrypt(iv.into(), ciphertext.as_ref()) + .map_err(|e| { + anyhow!("Failed to decrypt v20 key with Google ChaCha20 key: {}", e) + })?; + + Ok(decrypted) + } + KeyData::Three { .. } => { + // 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!("v20 version 3 is not supported yet")) + } + KeyData::Plain(key) => Ok(key.to_vec()), + } + } } fn unprotect_data_win(data: &[u8]) -> Result> { @@ -161,30 +252,29 @@ fn unprotect_data_win(data: &[u8]) -> Result> { return Ok(Vec::new()); } - let mut data_in = DATA_BLOB { - cbData: data.len() as DWORD, - pbData: data.as_ptr() as *mut BYTE, + let data_in = CRYPT_INTEGER_BLOB { + cbData: data.len() as u32, + pbData: data.as_ptr() as *mut u8, }; - let mut data_out = DATA_BLOB { + let mut data_out = CRYPT_INTEGER_BLOB { cbData: 0, pbData: std::ptr::null_mut(), }; - let result: BOOL = unsafe { - // BOOL from winapi (i32) + let result = unsafe { 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 + &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 == 0 { + if result.is_err() { return Err(anyhow!("CryptUnprotectData failed")); } @@ -203,3 +293,126 @@ fn unprotect_data_win(data: &[u8]) -> Result> { Ok(output_slice.to_vec()) } + +fn get_admin_exe_path() -> Result { + 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(¤t_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 { + 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 { + 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) +} + +// +// KeyData +// + +// Borrowed from https://github.com/saying121/tidy-browser/blob/master/crates/chromium-crypto/src/win/mod.rs +#[derive(Clone, Copy)] +enum KeyData<'k> { + One { + iv: &'k [u8], + ciphertext: &'k [u8], // with tag + }, + Two { + iv: &'k [u8], + ciphertext: &'k [u8], // with tag + }, + // TODO: Not supported yet + Three { + _enctypted_aes_key: &'k [u8], + _iv: &'k [u8], + _ciphertext: &'k [u8], // with tag + }, + Plain(&'k [u8]), +} + +impl KeyData<'_> { + fn parse<'b>(blob_data: &mut &'b [u8]) -> Result> { + 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; + + let content_offset = content_len_offset + 4; + let content = blob_data[content_offset..content_offset + content_len].try_into()?; + + if content_len == 32 { + return Ok(KeyData::Plain(content)); + } + + let initial = content[0]; + match initial { + 1_u8 => Ok(KeyData::One { + iv: content[1..1 + 12].try_into()?, + ciphertext: content[13..13 + 48].try_into()?, + }), + 2_u8 => Ok(KeyData::Two { + iv: content[1..1 + 12].try_into()?, + ciphertext: content[13..13 + 48].try_into()?, + }), + 3_u8 => Ok(KeyData::Three { + _enctypted_aes_key: content[1..1 + 32].try_into()?, + _iv: content[33..33 + 12].try_into()?, + _ciphertext: content[45..45 + 48].try_into()?, + }), + value => Err(anyhow!("Unsupported flag: {}", value)), + } + } +} diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index 8b13fcc6eb3..5df033a1634 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -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; + } + + ["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")}); + + 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() { if (process.platform !== "linux") { return; @@ -67,6 +87,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"); buildProcessIsolation(); return; } @@ -76,6 +97,7 @@ if (target) { installTarget(target); buildNapiModule(target, mode === "release"); buildProxyBin(target, mode === "release"); + buildImporterBinaries(false, mode === "release"); buildProcessIsolation(); return; } @@ -94,5 +116,6 @@ platformTargets.forEach(([target, _]) => { installTarget(target); buildNapiModule(target); buildProxyBin(target); + buildImporterBinaries(target); buildProcessIsolation(); }); diff --git a/apps/desktop/electron-builder.beta.json b/apps/desktop/electron-builder.beta.json index 5b792097623..630a956560d 100644 --- a/apps/desktop/electron-builder.beta.json +++ b/apps/desktop/electron-builder.beta.json @@ -36,6 +36,10 @@ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "to": "desktop_proxy.exe" + }, + { + "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "to": "bitwarden_chromium_import_helper.exe" } ] }, diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index f7dcfb65044..0b18786863d 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -96,6 +96,10 @@ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}.exe", "to": "desktop_proxy.exe" + }, + { + "from": "desktop_native/dist/bitwarden_chromium_import_helper.${platform}-${arch}.exe", + "to": "bitwarden_chromium_import_helper.exe" } ] },