1
0
mirror of https://github.com/bitwarden/browser synced 2026-01-06 18:43:25 +00:00

[PM-27786] Chrome application bound encryption v3 support (#17205)

* Update cargo.lock on windows

* Move ABE key decoding to helper.exe

* Safe slice operations (no panics)

* Refactor CNG code a bit

* Refactor CNG code a bit more

* Update README to match the new flow

* DRY up v1 and v2 decryption

* Remove all the crates and windows features that are not needed

* helper.exe split into a bunch of files

* Refator mod windows

* Minor cleanup

---------

Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
Dmitry Yakimenko
2025-11-06 10:20:23 +01:00
committed by GitHub
parent 57b8f18cdd
commit 5c2215401c
13 changed files with 777 additions and 642 deletions

View File

@@ -444,8 +444,10 @@ dependencies = [
name = "bitwarden_chromium_import_helper"
version = "0.0.0"
dependencies = [
"aes-gcm",
"anyhow",
"base64",
"chacha20poly1305",
"chromium_importer",
"clap",
"embed-resource",
@@ -606,7 +608,6 @@ dependencies = [
"async-trait",
"base64",
"cbc",
"chacha20poly1305",
"dirs",
"hex",
"oo7",

View File

@@ -20,6 +20,7 @@ publish = false
[workspace.dependencies]
aes = "=0.8.4"
aes-gcm = "=0.10.3"
anyhow = "=1.0.94"
arboard = { version = "=3.6.0", default-features = false }
ashpd = "=0.11.0"

View File

@@ -8,23 +8,14 @@ publish.workspace = true
[dependencies]
[target.'cfg(target_os = "windows")'.dependencies]
aes-gcm = { workspace = true }
chacha20poly1305 = { workspace = true }
chromium_importer = { path = "../chromium_importer" }
clap = { version = "=4.5.40", features = ["derive"] }
scopeguard = { workspace = true }
sysinfo = { workspace = true }
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",
] }
anyhow = { workspace = true }
base64 = { workspace = true }

View File

@@ -1,482 +0,0 @@
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 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_LIMITED_INFORMATION,
},
},
UI::Shell::IsUserAnAdmin,
},
};
use chromium_importer::chromium::{verify_signature, 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 = true;
// 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_LIMITED_INFORMATION, 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_LIMITED_INFORMATION, 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());
if !verify_signature(&exe_path)? {
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(crate) 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;
}
}
}
}
pub(crate) use windows_binary::*;

View File

@@ -0,0 +1,11 @@
// 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.
pub(crate) const ENABLE_DEVELOPER_LOGGING: bool = false;
pub(crate) 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
pub(crate) const ENABLE_SERVER_SIGNATURE_VALIDATION: bool = true;
// List of SYSTEM process names to try to impersonate
pub(crate) const SYSTEM_PROCESS_NAMES: [&str; 2] = ["services.exe", "winlogon.exe"];

View File

@@ -0,0 +1,313 @@
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit};
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose, Engine as _};
use chacha20poly1305::ChaCha20Poly1305;
use scopeguard::defer;
use tracing::debug;
use windows::{
core::w,
Win32::{
Foundation::{LocalFree, HLOCAL},
Security::Cryptography::{
self, CryptUnprotectData, NCryptOpenKey, NCryptOpenStorageProvider, CERT_KEY_SPEC,
CRYPTPROTECT_UI_FORBIDDEN, CRYPT_INTEGER_BLOB, NCRYPT_FLAGS, NCRYPT_KEY_HANDLE,
NCRYPT_PROV_HANDLE, NCRYPT_SILENT_FLAG,
},
},
};
use super::impersonate::{start_impersonating, stop_impersonating};
use crate::dbg_log;
//
// Base64
//
pub(crate) fn decode_base64(data_base64: &str) -> Result<Vec<u8>> {
dbg_log!("Decoding base64 data: {}", data_base64);
let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| {
dbg_log!("Failed to decode base64: {}", e);
e
})?;
Ok(data)
}
pub(crate) fn encode_base64(data: &[u8]) -> String {
general_purpose::STANDARD.encode(data)
}
//
// DPAPI decryption
//
pub(crate) fn decrypt_with_dpapi_as_system(encrypted: &[u8]) -> Result<Vec<u8>> {
// Impersonate a SYSTEM process to be able to decrypt data encrypted for the machine
let system_token = start_impersonating()?;
defer! {
dbg_log!("Stopping impersonation");
_ = stop_impersonating(system_token);
}
decrypt_with_dpapi_as_user(encrypted, true)
}
pub(crate) fn decrypt_with_dpapi_as_user(encrypted: &[u8], expect_appb: bool) -> Result<Vec<u8>> {
let system_decrypted = decrypt_with_dpapi(encrypted, expect_appb)?;
dbg_log!(
"Decrypted data with SYSTEM {} bytes",
system_decrypted.len()
);
Ok(system_decrypted)
}
fn decrypt_with_dpapi(data: &[u8], expect_appb: bool) -> Result<Vec<u8>> {
if expect_appb && (data.len() < 5 || !data.starts_with(b"APPB")) {
const ERR_MSG: &str = "Ciphertext is too short or does not start with 'APPB'";
dbg_log!("{}", ERR_MSG);
return Err(anyhow!(ERR_MSG));
}
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::default();
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"))
}
}
//
// Chromium key decoding
//
pub(crate) fn decode_abe_key_blob(blob_data: &[u8]) -> Result<Vec<u8>> {
// Parse and skip the header
let header_len = u32::from_le_bytes(get_safe(blob_data, 0, 4)?.try_into()?) as usize;
debug!("ABE key blob header length: {}", header_len);
// Parse content length
let content_len_offset = 4 + header_len;
let content_len =
u32::from_le_bytes(get_safe(blob_data, content_len_offset, 4)?.try_into()?) as usize;
debug!("ABE key blob content length: {}", content_len);
if content_len < 32 {
return Err(anyhow!(
"Corrupted ABE key blob: content length is less than 32"
));
}
let content_offset = content_len_offset + 4;
let content = get_safe(blob_data, 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];
debug!("ABE key blob version: {}", version);
let key_blob = &content[1..];
match version {
// Google Chrome v1 key encrypted with a hardcoded AES key
1_u8 => decrypt_abe_key_blob_chrome_aes(key_blob),
// Google Chrome v2 key encrypted with a hardcoded ChaCha20 key
2_u8 => decrypt_abe_key_blob_chrome_chacha20(key_blob),
// Google Chrome v3 key encrypted with CNG APIs
3_u8 => decrypt_abe_key_blob_chrome_cng(key_blob),
v => Err(anyhow!("Unsupported ABE key blob version: {}", v)),
}
}
fn get_safe(data: &[u8], start: usize, len: usize) -> Result<&[u8]> {
let end = start + len;
data.get(start..end).ok_or_else(|| {
anyhow!(
"Corrupted ABE key blob: expected bytes {}..{}, got {}",
start,
end,
data.len()
)
})
}
fn decrypt_abe_key_blob_chrome_aes(blob: &[u8]) -> Result<Vec<u8>> {
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);
decrypt_abe_key_blob_with_aead(blob, &cipher, "v1 (AES flavor)")
}
fn decrypt_abe_key_blob_chrome_chacha20(blob: &[u8]) -> Result<Vec<u8>> {
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);
decrypt_abe_key_blob_with_aead(blob, &cipher, "v2 (ChaCha20 flavor)")
}
fn decrypt_abe_key_blob_with_aead<C>(blob: &[u8], cipher: &C, version: &str) -> Result<Vec<u8>>
where
C: Aead,
{
if blob.len() < 60 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 60 bytes, got {} bytes",
blob.len()
));
}
let iv = &blob[0..12];
let ciphertext = &blob[12..12 + 48];
debug!("Google ABE {} detected: {:?} {:?}", version, iv, ciphertext);
let decrypted = cipher
.decrypt(iv.into(), ciphertext)
.map_err(|e| anyhow!("Failed to decrypt v20 key with {}: {}", version, 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()?;
debug!(
"Google ABE v3 (CNG flavor) detected: {:?} {:?} {:?}",
encrypted_aes_key, iv, ciphertext
);
// First, decrypt the AES key with CNG API
let decrypted_aes_key: Vec<u8> = {
let system_token = start_impersonating()?;
defer! {
dbg_log!("Stopping impersonation");
_ = stop_impersonating(system_token);
}
decrypt_with_cng(&encrypted_aes_key)?
};
const GOOGLE_XOR_KEY: [u8; 32] = [
0xCC, 0xF8, 0xA1, 0xCE, 0xC5, 0x66, 0x05, 0xB8, 0x51, 0x75, 0x52, 0xBA, 0x1A, 0x2D, 0x06,
0x1C, 0x03, 0xA2, 0x9E, 0x90, 0x27, 0x4F, 0xB2, 0xFC, 0xF5, 0x9B, 0xA4, 0xB7, 0x5C, 0x39,
0x23, 0x90,
];
// XOR the decrypted AES key with the hardcoded key
let aes_key: Vec<u8> = decrypted_aes_key
.into_iter()
.zip(GOOGLE_XOR_KEY)
.map(|(a, b)| a ^ b)
.collect();
// Decrypt the actual ABE key with the decrypted AES key
let cipher = Aes256Gcm::new(aes_key.as_slice().into());
let key = cipher
.decrypt((&iv).into(), ciphertext.as_ref())
.map_err(|e| anyhow!("Failed to decrypt v20 key with AES-GCM: {}", e))?;
Ok(key)
}
fn decrypt_with_cng(ciphertext: &[u8]) -> Result<Vec<u8>> {
// 1. Open the cryptographic provider
let mut provider = NCRYPT_PROV_HANDLE::default();
unsafe {
NCryptOpenStorageProvider(
&mut provider,
w!("Microsoft Software Key Storage Provider"),
0,
)?;
};
// Don't forget to free the provider
defer!(unsafe {
_ = Cryptography::NCryptFreeObject(provider.into());
});
// 2. Open the key
let mut key = NCRYPT_KEY_HANDLE::default();
unsafe {
NCryptOpenKey(
provider,
&mut key,
w!("Google Chromekey1"),
CERT_KEY_SPEC::default(),
NCRYPT_FLAGS::default(),
)?;
};
// Don't forget to free the key
defer!(unsafe {
_ = Cryptography::NCryptFreeObject(key.into());
});
// 3. Decrypt the data (assume the plaintext is not larger than the ciphertext)
let mut plaintext = vec![0; ciphertext.len()];
let mut plaintext_len = 0;
unsafe {
Cryptography::NCryptDecrypt(
key,
ciphertext.into(),
None,
Some(&mut plaintext),
&mut plaintext_len,
NCRYPT_SILENT_FLAG,
)?;
};
// In case the plaintext is smaller than the ciphertext
plaintext.truncate(plaintext_len as usize);
Ok(plaintext)
}

View File

@@ -0,0 +1,140 @@
use anyhow::{anyhow, Result};
use sysinfo::System;
use windows::{
core::BOOL,
Wdk::System::SystemServices::SE_DEBUG_PRIVILEGE,
Win32::{
Foundation::{CloseHandle, HANDLE, NTSTATUS, STATUS_SUCCESS},
Security::{
self, DuplicateToken, ImpersonateLoggedOnUser, RevertToSelf, TOKEN_DUPLICATE,
TOKEN_QUERY,
},
System::Threading::{OpenProcess, OpenProcessToken, PROCESS_QUERY_LIMITED_INFORMATION},
},
};
use super::config::SYSTEM_PROCESS_NAMES;
use crate::dbg_log;
#[link(name = "ntdll")]
unsafe extern "system" {
unsafe fn RtlAdjustPrivilege(
privilege: i32,
enable: BOOL,
current_thread: BOOL,
previous_value: *mut BOOL,
) -> NTSTATUS;
}
pub(crate) 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)
}
pub(crate) 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_LIMITED_INFORMATION, false, pid) }?;
Ok(hprocess)
}
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"))
}
}
}

View File

@@ -0,0 +1,39 @@
use tracing::{error, level_filters::LevelFilter};
use tracing_subscriber::{
fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, Layer as _,
};
use super::config::{ENABLE_DEVELOPER_LOGGING, LOG_FILENAME};
// Macro wrapper around debug! that compiles to no-op when ENABLE_DEVELOPER_LOGGING is false
#[macro_export]
macro_rules! dbg_log {
($($arg:tt)*) => {
if $crate::windows::config::ENABLE_DEVELOPER_LOGGING {
tracing::debug!($($arg)*);
}
};
}
pub(crate) fn init_logging() {
if ENABLE_DEVELOPER_LOGGING {
// 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_FILENAME) {
Ok(file) => {
let file_filter = EnvFilter::builder()
.with_default_directive(LevelFilter::DEBUG.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_FILENAME, "Could not create log file.");
}
}
}
}

View File

@@ -0,0 +1,229 @@
use anyhow::{anyhow, Result};
use clap::Parser;
use scopeguard::defer;
use std::{
ffi::OsString,
os::windows::{ffi::OsStringExt as _, io::AsRawHandle},
path::PathBuf,
time::Duration,
};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::windows::named_pipe::{ClientOptions, NamedPipeClient},
time,
};
use tracing::error;
use windows::Win32::{
Foundation::{CloseHandle, ERROR_PIPE_BUSY, HANDLE},
System::{
Pipes::GetNamedPipeServerProcessId,
Threading::{
OpenProcess, QueryFullProcessImageNameW, PROCESS_NAME_WIN32,
PROCESS_QUERY_LIMITED_INFORMATION,
},
},
UI::Shell::IsUserAnAdmin,
};
use chromium_importer::chromium::{verify_signature, ADMIN_TO_USER_PIPE_NAME};
use super::{
config::ENABLE_SERVER_SIGNATURE_VALIDATION,
crypto::{
decode_abe_key_blob, decode_base64, decrypt_with_dpapi_as_system,
decrypt_with_dpapi_as_user, encode_base64,
},
log::init_logging,
};
use crate::dbg_log;
#[derive(Parser)]
#[command(name = "bitwarden_chromium_import_helper")]
#[command(about = "Admin tool for ABE service management")]
struct Args {
#[arg(long, help = "Base64 encoded encrypted data string")]
encrypted: String,
}
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_LIMITED_INFORMATION, 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() }
}
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());
if !verify_signature(&exe_path)? {
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");
let encrypted = decode_base64(&args.encrypted)?;
dbg_log!(
"Decoded encrypted data [{}] {:?}",
encrypted.len(),
encrypted
);
let system_decrypted = decrypt_with_dpapi_as_system(&encrypted)?;
dbg_log!(
"Decrypted data with DPAPI as SYSTEM {} {:?}",
system_decrypted.len(),
system_decrypted
);
let user_decrypted = decrypt_with_dpapi_as_user(&system_decrypted, false)?;
dbg_log!(
"Decrypted data with DPAPI as USER {} {:?}",
user_decrypted.len(),
user_decrypted
);
let key = decode_abe_key_blob(&user_decrypted)?;
dbg_log!("Decoded ABE key blob {} {:?}", key.len(), key);
Ok(encode_base64(&key))
}
pub(crate) async fn main() {
init_logging();
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;
}
}
}

View File

@@ -0,0 +1,7 @@
mod config;
mod crypto;
mod impersonate;
mod log;
mod main;
pub(crate) use main::main;

View File

@@ -7,7 +7,7 @@ publish = { workspace = true }
[dependencies]
aes = { workspace = true }
aes-gcm = "=0.10.3"
aes-gcm = { workspace = true }
anyhow = { workspace = true }
async-trait = "=0.1.88"
base64 = { workspace = true }
@@ -22,24 +22,13 @@ serde_json = { workspace = true }
sha1 = "=0.10.6"
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
security-framework = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
chacha20poly1305 = { workspace = true }
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",
] }

View File

@@ -4,7 +4,7 @@ A rust library that allows you to directly import credentials from Chromium-base
## Windows ABE Architecture
On Windows chrome has additional protection measurements which needs to be circumvented in order to
On Windows Chrome has additional protection measurements which needs to be circumvented in order to
get access to the passwords.
### Overview
@@ -25,7 +25,9 @@ encryption scheme for some local profiles.
The general idea of this encryption scheme is as follows:
1. Chrome generates a unique random encryption key.
2. This key is first encrypted at the **user level** with a fixed key.
2. This key is first encrypted at the **user level** with a fixed key for v1/v2 of ABE. For ABE v3 a more complicated
scheme is used that encrypts the key with a combination of a fixed key and a randomly generated key at the **system
level** via Windows CNG API.
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**.
@@ -37,7 +39,7 @@ The following sections describe how the key is decrypted at each level.
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 user with the UAC prompt. See the `abe::decrypt_with_admin` call in `platform/windows/mod.rs`.
This function takes two arguments:
@@ -75,10 +77,26 @@ With the duplicated token, `ImpersonateLoggedOnUser` is called to impersonate a
> **At this point `bitwarden_chromium_import_helper.exe` is running as SYSTEM.**
The received encryption key can now be decrypted using DPAPI at the system level.
The received encryption key can now be decrypted using DPAPI at the **system level**.
The decrypted result is sent back to the client via the named pipe. `bitwarden_chromium_import_helper.exe` connects to
the pipe and writes the result.
Next, the impersonation is stopped and the feshly decrypted key is decrypted at the **user level** with DPAPI one more
time.
At this point, for browsers not using the custom encryption/obfuscation layer like unbranded Chromium, the twice
decrypted key is the actual encryption key that could be used to decrypt the stored passwords.
For other browsers like Google Chrome, some additional processing is required. The decrypted key is actually a blob of structured data that could take multiple forms:
1. exactly 32 bytes: plain key, nothing to be done more in this case
2. blob starts with 0x01: the key is encrypted with a fixed AES key found in Google Chrome binary, a random IV is stored
in the blob as well
3. blob starts with 0x02: the key is encrypted with a fixed ChaCha20 key found in Google Chrome binary, a random IV is
stored in the blob as well
4. blob starts with 0x03: the blob contains a random key, encrypted with CNG API with a random key stored in the
**system keychain** under the name `Google Chromekey1`. After that key is decryped (under **system level** impersonation again), the key is xor'ed with a fixed key from the Chrome binary and the it is used to decrypt the key from the last DPAPI decryption stage.
The decrypted key is sent back to the client via the named pipe. `bitwarden_chromium_import_helper.exe` connects to the
pipe and writes the result.
The response can indicate success or failure:
@@ -92,17 +110,8 @@ Finally, `bitwarden_chromium_import_helper.exe` exits.
### 3. Back to the Client Library
The decrypted Base64-encoded string is returned from `bitwarden_chromium_import_helper.exe` to the named pipe server at
the user level. At this point it has been decrypted only once—at the system level.
Next, the string is decrypted at the **user level** with DPAPI.
Finally, for Google Chrome (but not Brave), it is decrypted again with a hard-coded key found in `elevation_service.exe`
from the Chrome installation. Based on the version of the encrypted string (encoded within the string itself), this step
uses either **AES-256-GCM** or **ChaCha20-Poly1305**. See `windows.rs` for details.
After these steps, the master key is available and can be used to decrypt the password information stored in the
browsers local database.
The decrypted Base64-encoded key is returned from `bitwarden_chromium_import_helper.exe` to the named pipe server at the
user level. The key is used to decrypt the stored passwords and notes.
### TL;DR Steps
@@ -120,13 +129,12 @@ browsers local database.
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. Decrypt it again with DPAPI at the **USER** level.
6. (For Chrome only) Decrypt again with the hard-coded key, possibly at the **system level** again (see above).
5. Send the result or error back via the named pipe.
6. Exit.
3. **Back on the client side:**
1. Receive the encryption key.
1. Receive the master 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.
3. Use the master key to read and decrypt stored passwords from Chrome, Brave, Edge, etc.

View File

@@ -2,7 +2,6 @@ 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},
@@ -208,119 +207,8 @@ impl WindowsCryptoService {
));
}
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"))
let key = BASE64_STANDARD.decode(&key_base64)?;
Ok(key)
}
}