1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-28 10:33:31 +00:00

Merge branch 'main' into km/auto-kdf

This commit is contained in:
Bernd Schoolmann
2025-12-01 14:08:37 +01:00
1027 changed files with 43220 additions and 14561 deletions

View File

@@ -120,9 +120,9 @@ checksum = "c1fd03a028ef38ba2276dce7e33fcd6369c158a1bca17946c4b1b701891c1ff7"
[[package]]
name = "arboard"
version = "3.6.0"
version = "3.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55f533f8e0af236ffe5eb979b99381df3258853f00ba2e44b6e1955292c75227"
checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf"
dependencies = [
"clipboard-win",
"log",
@@ -131,6 +131,7 @@ dependencies = [
"objc2-foundation",
"parking_lot",
"percent-encoding",
"windows-sys 0.60.2",
"wl-clipboard-rs",
"x11rb",
]
@@ -317,9 +318,9 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]]
name = "async-trait"
version = "0.1.88"
version = "0.1.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5"
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [
"proc-macro2",
"quote",
@@ -444,8 +445,10 @@ dependencies = [
name = "bitwarden_chromium_import_helper"
version = "0.0.0"
dependencies = [
"aes-gcm",
"anyhow",
"base64",
"chacha20poly1305",
"chromium_importer",
"clap",
"embed-resource",
@@ -553,10 +556,11 @@ dependencies = [
[[package]]
name = "cc"
version = "1.2.4"
version = "1.2.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9157bbaa6b165880c27a4293a474c91cdcf265cc68cc829bf10be0964a391caf"
checksum = "b97463e1064cb1b1c1384ad0a0b9c8abd0988e2a91f52606c80ef14aadb63e36"
dependencies = [
"find-msvc-tools",
"shlex",
]
@@ -606,7 +610,6 @@ dependencies = [
"async-trait",
"base64",
"cbc",
"chacha20poly1305",
"dirs",
"hex",
"oo7",
@@ -619,7 +622,6 @@ dependencies = [
"sha1",
"tokio",
"tracing",
"tracing-subscriber",
"verifysign",
"windows 0.61.1",
]
@@ -637,9 +639,9 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.40"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f"
checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5"
dependencies = [
"clap_builder",
"clap_derive",
@@ -647,9 +649,9 @@ dependencies = [
[[package]]
name = "clap_builder"
version = "4.5.40"
version = "4.5.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e"
checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a"
dependencies = [
"anstream",
"anstyle",
@@ -659,9 +661,9 @@ dependencies = [
[[package]]
name = "clap_derive"
version = "4.5.40"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [
"heck",
"proc-macro2",
@@ -684,17 +686,6 @@ dependencies = [
"error-code",
]
[[package]]
name = "codespan-reporting"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe6d2e5af09e8c8ad56c969f2157a3d4238cebc7c55f0a517728c38f7b200f81"
dependencies = [
"serde",
"termcolor",
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.3"
@@ -841,78 +832,6 @@ dependencies = [
"syn",
]
[[package]]
name = "cxx"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a71ea7f29c73f7ffa64c50b83c9fe4d3a6d4be89a86b009eb80d5a6d3429d741"
dependencies = [
"cc",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36a8232661d66dcf713394726157d3cfe0a89bfc85f52d6e9f9bbc2306797fe7"
dependencies = [
"cc",
"codespan-reporting",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f44296c8693e9ea226a48f6a122727f77aa9e9e338380cb021accaeeb7ee279"
dependencies = [
"clap",
"codespan-reporting",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f69c181c176981ae44ba9876e2ea41ce8e574c296b38d06925ce9214fb8e4"
[[package]]
name = "cxxbridge-macro"
version = "1.0.158"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8faff5d4467e0709448187df29ccbf3b0982cc426ee444a193f87b11afb565a8"
dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "dashmap"
version = "5.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
dependencies = [
"cfg-if",
"hashbrown 0.14.5",
"lock_api",
"once_cell",
"parking_lot_core",
]
[[package]]
name = "der"
version = "0.7.10"
@@ -934,27 +853,21 @@ dependencies = [
"ashpd",
"base64",
"bitwarden-russh",
"byteorder",
"bytes",
"cbc",
"chacha20poly1305",
"core-foundation",
"desktop_objc",
"dirs",
"ed25519",
"futures",
"homedir",
"interprocess",
"keytar",
"libc",
"linux-keyutils",
"memsec",
"oo7",
"pin-project",
"pkcs8",
"rand 0.9.1",
"rsa",
"russh-cryptovec",
"scopeguard",
"secmem-proc",
"security-framework",
@@ -962,12 +875,10 @@ dependencies = [
"serde",
"serde_json",
"sha2",
"ssh-encoding",
"ssh-key",
"sysinfo",
"thiserror 2.0.12",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"typenum",
@@ -985,18 +896,14 @@ version = "0.0.0"
dependencies = [
"anyhow",
"autotype",
"base64",
"chromium_importer",
"desktop_core",
"hex",
"napi",
"napi-build",
"napi-derive",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"tracing-subscriber",
"windows-registry",
@@ -1009,9 +916,7 @@ version = "0.0.0"
dependencies = [
"anyhow",
"cc",
"core-foundation",
"glob",
"thiserror 2.0.12",
"tokio",
"tracing",
]
@@ -1020,7 +925,6 @@ dependencies = [
name = "desktop_proxy"
version = "0.0.0"
dependencies = [
"anyhow",
"desktop_core",
"embed_plist",
"futures",
@@ -1301,6 +1205,12 @@ version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
[[package]]
name = "find-msvc-tools"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
[[package]]
name = "fixedbitset"
version = "0.4.2"
@@ -1533,12 +1443,6 @@ dependencies = [
"subtle",
]
[[package]]
name = "hashbrown"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
[[package]]
name = "hashbrown"
version = "0.15.3"
@@ -1554,7 +1458,7 @@ version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
dependencies = [
"hashbrown 0.15.3",
"hashbrown",
]
[[package]]
@@ -1719,7 +1623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown 0.15.3",
"hashbrown",
]
[[package]]
@@ -1759,27 +1663,6 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "keytar"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d361c55fba09829ac620b040f5425bf239b1030c3d6820a84acac8da867dca4d"
dependencies = [
"keytar-sys",
]
[[package]]
name = "keytar-sys"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe908c6896705a1cb516cd6a5d956c63f08d95ace81b93253a98cd93e1e6a65a"
dependencies = [
"cc",
"cxx",
"cxx-build",
"pkg-config",
]
[[package]]
name = "lazy_static"
version = "1.5.0"
@@ -1791,9 +1674,9 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.172"
version = "0.2.177"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa"
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
[[package]]
name = "libloading"
@@ -1832,15 +1715,6 @@ dependencies = [
"vcpkg",
]
[[package]]
name = "link-cplusplus"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212"
dependencies = [
"cc",
]
[[package]]
name = "linux-keyutils"
version = "0.2.4"
@@ -1891,11 +1765,9 @@ version = "0.0.0"
dependencies = [
"desktop_core",
"futures",
"oslog",
"serde",
"serde_json",
"tokio",
"tokio-util",
"tracing",
"tracing-oslog",
"tracing-subscriber",
@@ -2379,17 +2251,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "oslog"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80d2043d1f61d77cb2f4b1f7b7b2295f40507f5f8e9d1c8bf10a1ca5f97a3969"
dependencies = [
"cc",
"dashmap",
"log",
]
[[package]]
name = "p256"
version = "0.13.2"
@@ -2552,21 +2413,6 @@ dependencies = [
"spki",
]
[[package]]
name = "pkcs5"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6"
dependencies = [
"aes",
"cbc",
"der",
"pbkdf2",
"scrypt",
"sha2",
"spki",
]
[[package]]
name = "pkcs8"
version = "0.10.2"
@@ -2574,8 +2420,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
dependencies = [
"der",
"pkcs5",
"rand_core 0.6.4",
"spki",
]
@@ -2954,27 +2798,12 @@ dependencies = [
"rustix 1.0.7",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "salsa20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213"
dependencies = [
"cipher",
]
[[package]]
name = "scc"
version = "2.4.0"
@@ -2990,12 +2819,6 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "scratch"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52"
[[package]]
name = "scroll"
version = "0.12.0"
@@ -3016,17 +2839,6 @@ dependencies = [
"syn",
]
[[package]]
name = "scrypt"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f"
dependencies = [
"pbkdf2",
"salsa20",
"sha2",
]
[[package]]
name = "sdd"
version = "3.0.10"
@@ -3401,15 +3213,6 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "termtree"
version = "0.5.1"
@@ -3514,17 +3317,6 @@ dependencies = [
"syn",
]
[[package]]
name = "tokio-stream"
version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af"
dependencies = [
"futures-core",
"pin-project-lite",
"tokio",
]
[[package]]
name = "tokio-util"
version = "0.7.13"
@@ -3691,9 +3483,9 @@ dependencies = [
[[package]]
name = "typenum"
version = "1.18.0"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
[[package]]
name = "uds_windows"
@@ -3724,12 +3516,6 @@ version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
[[package]]
name = "unicode-width"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
[[package]]
name = "uniffi"
version = "0.28.3"
@@ -4060,15 +3846,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"

View File

@@ -20,8 +20,9 @@ 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 }
arboard = { version = "=3.6.1", default-features = false }
ashpd = "=0.11.0"
base64 = "=0.22.1"
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", rev = "a641316227227f8777fdf56ac9fa2d6b5f7fe662" }
@@ -38,16 +39,13 @@ futures = "=0.3.31"
hex = "=0.4.3"
homedir = "=0.3.4"
interprocess = "=2.2.1"
keytar = "=0.1.6"
libc = "=0.2.172"
libc = "=0.2.177"
linux-keyutils = "=0.2.4"
log = "=0.4.25"
memsec = "=0.7.0"
napi = "=2.16.17"
napi-build = "=2.2.0"
napi-derive = "=2.16.13"
oo7 = "=0.4.3"
oslog = "=0.2.0"
pin-project = "=1.1.10"
pkcs8 = "=0.10.2"
rand = "=0.9.1"
@@ -60,13 +58,11 @@ security-framework-sys = "=2.15.0"
serde = "=1.0.209"
serde_json = "=1.0.127"
sha2 = "=0.10.8"
simplelog = "=0.12.2"
ssh-encoding = "=0.2.0"
ssh-key = { version = "=0.6.7", default-features = false }
sysinfo = "=0.35.0"
thiserror = "=2.0.12"
tokio = "=1.45.0"
tokio-stream = "=0.1.15"
tokio-util = "=0.7.13"
tracing = "=0.1.41"
tracing-subscriber = { version = "=0.3.20", features = [
@@ -74,7 +70,7 @@ tracing-subscriber = { version = "=0.3.20", features = [
"env-filter",
"tracing-log",
] }
typenum = "=1.18.0"
typenum = "=1.19.0"
uniffi = "=0.28.3"
widestring = "=1.2.0"
windows = "=0.61.1"

View File

@@ -33,7 +33,8 @@ impl InputOperations for Win32InputOperations {
/// Attempts to type the input text wherever the user's cursor is.
///
/// `input` must be a vector of utf-16 encoded characters to insert.
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super, Shift, letters a - Z
/// `keyboard_shortcut` must be a vector of Strings, where valid shortcut keys: Control, Alt, Super,
/// Shift, letters a - Z
///
/// https://learn.microsoft.com/en-in/windows/win32/api/winuser/nf-winuser-sendinput
pub(super) fn type_input(input: Vec<u16>, keyboard_shortcut: Vec<String>) -> Result<()> {
@@ -234,16 +235,16 @@ where
#[cfg(test)]
mod tests {
//! For the mocking of the traits that are static methods, we need to use the `serial_test` crate
//! in order to mock those, since the mock expectations set have to be global in absence of a `self`.
//! More info: https://docs.rs/mockall/latest/mockall/#static-methods
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
//! crate in order to mock those, since the mock expectations set have to be global in
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
use super::*;
use crate::windowing::MockErrorOperations;
use serial_test::serial;
use windows::Win32::Foundation::WIN32_ERROR;
use super::*;
use crate::windowing::MockErrorOperations;
#[test]
fn get_alphabetic_hot_key_succeeds() {
for c in ('a'..='z').chain('A'..='Z') {

View File

@@ -127,8 +127,8 @@ where
///
/// # Errors
///
/// - If the actual window title length (what the win32 API declares was written into the
/// buffer), is length zero and GetLastError() != 0 , return the GetLastError() message.
/// - If the actual window title length (what the win32 API declares was written into the buffer),
/// is length zero and GetLastError() != 0 , return the GetLastError() message.
fn get_window_title<H, E>(window_handle: &H, expected_title_length: usize) -> Result<String>
where
H: WindowHandleOperations,
@@ -169,17 +169,17 @@ where
#[cfg(test)]
mod tests {
//! For the mocking of the traits that are static methods, we need to use the `serial_test` crate
//! in order to mock those, since the mock expectations set have to be global in absence of a `self`.
//! More info: https://docs.rs/mockall/latest/mockall/#static-methods
//! For the mocking of the traits that are static methods, we need to use the `serial_test`
//! crate in order to mock those, since the mock expectations set have to be global in
//! absence of a `self`. More info: https://docs.rs/mockall/latest/mockall/#static-methods
use super::*;
use crate::windowing::MockErrorOperations;
use mockall::predicate;
use serial_test::serial;
use windows::Win32::Foundation::WIN32_ERROR;
use super::*;
use crate::windowing::MockErrorOperations;
#[test]
#[serial]
fn get_window_title_length_can_be_zero() {

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"] }
clap = { version = "=4.5.51", 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,2 @@
// 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,279 @@
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit};
use anyhow::{anyhow, Result};
use base64::{engine::general_purpose, Engine as _};
use chacha20poly1305::ChaCha20Poly1305;
use chromium_importer::chromium::crypt_unprotect_data;
use scopeguard::defer;
use tracing::debug;
use windows::{
core::w,
Win32::Security::Cryptography::{
self, NCryptOpenKey, NCryptOpenStorageProvider, CERT_KEY_SPEC, CRYPTPROTECT_UI_FORBIDDEN,
NCRYPT_FLAGS, NCRYPT_KEY_HANDLE, NCRYPT_PROV_HANDLE, NCRYPT_SILENT_FLAG,
},
};
use super::impersonate::{start_impersonating, stop_impersonating};
//
// Base64
//
pub(crate) fn decode_base64(data_base64: &str) -> Result<Vec<u8>> {
debug!("Decoding base64 data: {}", data_base64);
let data = general_purpose::STANDARD.decode(data_base64).map_err(|e| {
debug!("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! {
debug!("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)?;
debug!(
"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'";
debug!("{}", ERR_MSG);
return Err(anyhow!(ERR_MSG));
}
let data = if expect_appb { &data[4..] } else { data };
crypt_unprotect_data(data, CRYPTPROTECT_UI_FORBIDDEN)
}
//
// 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! {
debug!("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 tracing::debug;
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;
#[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)?;
};
debug!("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(_) => {
debug!(
"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 {
debug!("Setting SE_DEBUG_PRIVILEGE to 1 via RtlAdjustPrivilege");
RtlAdjustPrivilege(SE_DEBUG_PRIVILEGE, BOOL(1), BOOL(0), &mut previous_value)
};
match status {
STATUS_SUCCESS => {
debug!(
"SE_DEBUG_PRIVILEGE set to 1, was {} before",
previous_value.as_bool()
);
Ok(())
}
_ => {
debug!("RtlAdjustPrivilege failed with status: 0x{:X}", status.0);
Err(anyhow!("Failed to adjust privilege"))
}
}
}

View File

@@ -0,0 +1,29 @@
use chromium_importer::config::{ENABLE_DEVELOPER_LOGGING, LOG_FILENAME};
use tracing::{error, level_filters::LevelFilter};
use tracing_subscriber::{
fmt, layer::SubscriberExt as _, util::SubscriberInitExt as _, EnvFilter, Layer as _,
};
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,225 @@
use std::{
ffi::OsString,
os::windows::{ffi::OsStringExt as _, io::AsRawHandle},
path::PathBuf,
time::Duration,
};
use anyhow::{anyhow, Result};
use chromium_importer::chromium::{verify_signature, ADMIN_TO_USER_PIPE_NAME};
use clap::Parser;
use scopeguard::defer;
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::windows::named_pipe::{ClientOptions, NamedPipeClient},
time,
};
use tracing::{debug, 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 super::{
crypto::{
decode_abe_key_blob, decode_base64, decrypt_with_dpapi_as_system,
decrypt_with_dpapi_as_user, encode_base64,
},
log::init_logging,
};
#[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) => {
debug!("Successfully connected to the pipe!");
return Ok(client);
}
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY.0 as i32) => {
debug!("Pipe is busy, retrying in 50ms...");
}
Err(e) => {
debug!("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> {
debug!("Resolving process executable path for PID {}", pid);
// Open the process handle
let hprocess = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }?;
debug!("Opened process handle for PID {}", pid);
// Close when no longer needed
defer! {
debug!("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,
)
}?;
debug!(
"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?;
let server_pid = get_named_pipe_server_pid(&client)?;
debug!("Connected to pipe server PID {}", server_pid);
// Validate the server end process signature
let exe_path = resolve_process_executable_path(server_pid)?;
debug!("Pipe server executable path: {}", exe_path.display());
if !verify_signature(&exe_path)? {
return Err(anyhow!("Pipe server signature is not valid"));
}
debug!("Pipe server signature verified for PID {}", server_pid);
Ok(client)
}
fn run() -> Result<String> {
debug!("Starting bitwarden_chromium_import_helper.exe");
let args = Args::try_parse()?;
if !is_admin() {
return Err(anyhow!("Expected to run with admin privileges"));
}
debug!("Running as ADMINISTRATOR");
let encrypted = decode_base64(&args.encrypted)?;
debug!(
"Decoded encrypted data [{}] {:?}",
encrypted.len(),
encrypted
);
let system_decrypted = decrypt_with_dpapi_as_system(&encrypted)?;
debug!(
"Decrypted data with DPAPI as SYSTEM {} {:?}",
system_decrypted.len(),
system_decrypted
);
let user_decrypted = decrypt_with_dpapi_as_user(&system_decrypted, false)?;
debug!(
"Decrypted data with DPAPI as USER {} {:?}",
user_decrypted.len(),
user_decrypted
);
let key = decode_abe_key_blob(&user_decrypted)?;
debug!("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) => {
debug!("Sending response back to user");
let _ = send_to_user(&mut client, &system_decrypted_base64).await;
}
Err(e) => {
debug!("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,46 +7,38 @@ publish = { workspace = true }
[dependencies]
aes = { workspace = true }
aes-gcm = "=0.10.3"
anyhow = { workspace = true }
async-trait = "=0.1.88"
base64 = { workspace = true }
cbc = { workspace = true, features = ["alloc"] }
async-trait = "=0.1.89"
dirs = { workspace = true }
hex = { workspace = true }
pbkdf2 = "=0.12.2"
rand = { workspace = true }
rusqlite = { version = "=0.37.0", features = ["bundled"] }
serde = { workspace = true, features = ["derive"] }
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]
cbc = { workspace = true, features = ["alloc"] }
pbkdf2 = "=0.12.2"
security-framework = { workspace = true }
sha1 = "=0.10.6"
[target.'cfg(target_os = "windows")'.dependencies]
chacha20poly1305 = { workspace = true }
aes-gcm = { workspace = true }
base64 = { 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",
] }
verifysign = "=0.2.4"
tokio = { workspace = true, features = ["full"] }
tracing = { workspace = true }
[target.'cfg(target_os = "linux")'.dependencies]
cbc = { workspace = true, features = ["alloc"] }
oo7 = { workspace = true }
pbkdf2 = "=0.12.2"
sha1 = "=0.10.6"
[lints]
workspace = true

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

@@ -0,0 +1,15 @@
include!("config_constants.rs");
fn main() {
println!("cargo:rerun-if-changed=config_constants.rs");
if cfg!(not(debug_assertions)) {
if ENABLE_DEVELOPER_LOGGING {
panic!("ENABLE_DEVELOPER_LOGGING must be false in release builds");
}
if !ENABLE_SIGNATURE_VALIDATION {
panic!("ENABLE_SIGNATURE_VALIDATION must be true in release builds");
}
}
}

View File

@@ -0,0 +1,12 @@
// 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.
pub const ENABLE_DEVELOPER_LOGGING: bool = false;
// The absolute path to log file when developer logging is enabled
// Change this to a suitable path for your environment
pub const LOG_FILENAME: &str = "c:\\path\\to\\log.txt";
/// Ensure the signature of the helper and main binary is validated in production builds
///
/// This must be true in release builds but may be disabled in debug builds for testing.
pub const ENABLE_SIGNATURE_VALIDATION: bool = true;

View File

@@ -1,5 +1,8 @@
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use std::{
collections::HashMap,
path::{Path, PathBuf},
sync::LazyLock,
};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
@@ -9,12 +12,9 @@ use rusqlite::{params, Connection};
mod platform;
#[cfg(target_os = "windows")]
pub use platform::{
verify_signature, ADMIN_TO_USER_PIPE_NAME, EXPECTED_SIGNATURE_SHA256_THUMBPRINT,
};
pub(crate) use platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
#[cfg(target_os = "windows")]
pub use platform::*;
//
// Public API
@@ -88,14 +88,15 @@ pub async fn import_logins(
let local_logins = get_logins(&data_dir, profile_id, "Login Data")
.map_err(|e| anyhow!("Failed to query logins: {}", e))?;
// This is not available in all browsers, but there's no harm in trying. If the file doesn't exist we just get an empty vector.
// This is not available in all browsers, but there's no harm in trying. If the file doesn't
// exist we just get an empty vector.
let account_logins = get_logins(&data_dir, profile_id, "Login Data For Account")
.map_err(|e| anyhow!("Failed to query logins: {}", e))?;
// TODO: Do we need a better merge strategy? Maybe ignore duplicates at least?
// TODO: Should we also ignore an error from one of the two imports? If one is successful and the other fails,
// should we still return the successful ones? At the moment it doesn't fail for a missing file, only when
// something goes really wrong.
// TODO: Should we also ignore an error from one of the two imports? If one is successful and
// the other fails, should we still return the successful ones? At the moment it
// doesn't fail for a missing file, only when something goes really wrong.
let all_logins = local_logins
.into_iter()
.chain(account_logins.into_iter())
@@ -150,13 +151,13 @@ pub(crate) struct LocalState {
#[derive(serde::Deserialize, Clone)]
struct AllProfiles {
info_cache: std::collections::HashMap<String, OneProfile>,
info_cache: HashMap<String, OneProfile>,
}
#[derive(serde::Deserialize, Clone)]
struct OneProfile {
name: String,
gaia_name: Option<String>,
gaia_id: Option<String>,
user_name: Option<String>,
}
@@ -199,10 +200,14 @@ fn get_profile_info(local_state: &LocalState) -> Vec<ProfileInfo> {
.profile
.info_cache
.iter()
.map(|(name, info)| ProfileInfo {
name: info.name.clone(),
folder: name.clone(),
account_name: info.gaia_name.clone(),
.map(|(folder, info)| ProfileInfo {
name: if !info.name.trim().is_empty() {
info.name.clone()
} else {
folder.clone()
},
folder: folder.clone(),
account_name: info.gaia_id.clone(),
account_email: info.user_name.clone(),
})
.collect()
@@ -350,3 +355,111 @@ async fn decrypt_login(
}),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn make_local_state(profiles: Vec<(&str, &str, Option<&str>, Option<&str>)>) -> LocalState {
let info_cache = profiles
.into_iter()
.map(|(folder, name, gaia_id, user_name)| {
(
folder.to_string(),
OneProfile {
name: name.to_string(),
gaia_id: gaia_id.map(|s| s.to_string()),
user_name: user_name.map(|s| s.to_string()),
},
)
})
.collect::<HashMap<_, _>>();
LocalState {
profile: AllProfiles { info_cache },
os_crypt: None,
}
}
#[test]
fn test_get_profile_info_basic() {
let local_state = make_local_state(vec![
(
"Profile 1",
"User 1",
Some("Account 1"),
Some("email1@example.com"),
),
(
"Profile 2",
"User 2",
Some("Account 2"),
Some("email2@example.com"),
),
]);
let infos = get_profile_info(&local_state);
assert_eq!(infos.len(), 2);
let profile1 = infos.iter().find(|p| p.folder == "Profile 1").unwrap();
assert_eq!(profile1.name, "User 1");
assert_eq!(profile1.account_name.as_deref(), Some("Account 1"));
assert_eq!(
profile1.account_email.as_deref(),
Some("email1@example.com")
);
let profile2 = infos.iter().find(|p| p.folder == "Profile 2").unwrap();
assert_eq!(profile2.name, "User 2");
assert_eq!(profile2.account_name.as_deref(), Some("Account 2"));
assert_eq!(
profile2.account_email.as_deref(),
Some("email2@example.com")
);
}
#[test]
fn test_get_profile_info_empty_name() {
let local_state = make_local_state(vec![(
"ProfileX",
"",
Some("AccountX"),
Some("emailx@example.com"),
)]);
let infos = get_profile_info(&local_state);
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].name, "ProfileX");
assert_eq!(infos[0].folder, "ProfileX");
}
#[test]
fn test_get_profile_info_none_fields() {
let local_state = make_local_state(vec![("ProfileY", "NameY", None, None)]);
let infos = get_profile_info(&local_state);
assert_eq!(infos.len(), 1);
assert_eq!(infos[0].name, "NameY");
assert_eq!(infos[0].account_name, None);
assert_eq!(infos[0].account_email, None);
}
#[test]
fn test_get_profile_info_multiple_profiles() {
let local_state = make_local_state(vec![
("P1", "N1", Some("A1"), Some("E1")),
("P2", "", None, None),
("P3", "N3", Some("A3"), None),
]);
let infos = get_profile_info(&local_state);
assert_eq!(infos.len(), 3);
let p1 = infos.iter().find(|p| p.folder == "P1").unwrap();
assert_eq!(p1.name, "N1");
let p2 = infos.iter().find(|p| p.folder == "P2").unwrap();
assert_eq!(p2.name, "P2");
let p3 = infos.iter().find(|p| p.folder == "P3").unwrap();
assert_eq!(p3.name, "N3");
assert_eq!(p3.account_name.as_deref(), Some("A3"));
assert_eq!(p3.account_email, None);
}
}

View File

@@ -4,15 +4,17 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait;
use oo7::XDG_SCHEMA_ATTRIBUTE;
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
use crate::util;
use crate::{
chromium::{BrowserConfig, CryptoService, LocalState},
util,
};
//
// Public API
//
// TODO: It's possible that there might be multiple possible data directories, depending on the installation method (e.g., snap, flatpak, etc.).
// TODO: It's possible that there might be multiple possible data directories, depending on the
// installation method (e.g., snap, flatpak, etc.).
pub(crate) const SUPPORTED_BROWSERS: &[BrowserConfig] = &[
BrowserConfig {
name: "Chrome",

View File

@@ -2,9 +2,10 @@ use anyhow::{anyhow, Result};
use async_trait::async_trait;
use security_framework::passwords::get_generic_password;
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
use crate::util;
use crate::{
chromium::{BrowserConfig, CryptoService, LocalState},
util,
};
//
// Public API

View File

@@ -1,6 +1,6 @@
use super::abe_config;
use anyhow::{anyhow, Result};
use std::{ffi::OsStr, os::windows::ffi::OsStrExt};
use anyhow::{anyhow, Result};
use tokio::{
io::{self, AsyncReadExt, AsyncWriteExt},
net::windows::named_pipe::{NamedPipeServer, ServerOptions},
@@ -14,6 +14,8 @@ use windows::{
Win32::UI::{Shell::ShellExecuteW, WindowsAndMessaging::SW_HIDE},
};
use super::abe_config;
const WAIT_FOR_ADMIN_MESSAGE_TIMEOUT_SECS: u64 = 30;
fn start_tokio_named_pipe_server<F>(

View File

@@ -0,0 +1,54 @@
use anyhow::{anyhow, Result};
use windows::Win32::{
Foundation::{LocalFree, HLOCAL},
Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB},
};
/// Rust friendly wrapper around CryptUnprotectData
///
/// Decrypts the data passed in using the `CryptUnprotectData` api.
pub fn crypt_unprotect_data(data: &[u8], flags: u32) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(Vec::new());
}
let data_in = CRYPT_INTEGER_BLOB {
cbData: data.len() as u32,
pbData: data.as_ptr() as *mut u8,
};
let mut data_out = CRYPT_INTEGER_BLOB::default();
let result = unsafe {
CryptUnprotectData(
&data_in,
None, // ppszDataDescr: Option<*mut PWSTR>
None, // pOptionalEntropy: Option<*const CRYPT_INTEGER_BLOB>
None, // pvReserved: Option<*const std::ffi::c_void>
None, // pPromptStruct: Option<*const CRYPTPROTECT_PROMPTSTRUCT>
flags, // dwFlags: u32
&mut data_out,
)
};
if result.is_err() {
return Err(anyhow!("CryptUnprotectData failed"));
}
if data_out.pbData.is_null() || data_out.cbData == 0 {
return Ok(Vec::new());
}
let output_slice =
unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) };
// SAFETY: Must copy data before calling LocalFree() below.
// Calling to_vec() after LocalFree() causes use-after-free bugs.
let output = output_slice.to_vec();
unsafe {
LocalFree(Some(HLOCAL(data_out.pbData as *mut _)));
}
Ok(output)
}

View File

@@ -1,21 +1,21 @@
use std::path::{Path, PathBuf};
use aes_gcm::{aead::Aead, Aes256Gcm, Key, KeyInit, Nonce};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use base64::{engine::general_purpose::STANDARD as BASE64_STANDARD, Engine as _};
use chacha20poly1305::ChaCha20Poly1305;
use std::path::{Path, PathBuf};
use windows::Win32::{
Foundation::{LocalFree, HLOCAL},
Security::Cryptography::{CryptUnprotectData, CRYPT_INTEGER_BLOB},
};
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
use crate::util;
use crate::{
chromium::{BrowserConfig, CryptoService, LocalState},
util,
};
mod abe;
mod abe_config;
mod crypto;
mod signature;
pub use abe_config::ADMIN_TO_USER_PIPE_NAME;
pub use crypto::*;
pub use signature::*;
//
@@ -62,9 +62,6 @@ pub(crate) fn get_crypto_service(
const ADMIN_EXE_FILENAME: &str = "bitwarden_chromium_import_helper.exe";
// This should be enabled for production
const ENABLE_SIGNATURE_VALIDATION: bool = true;
//
// CryptoService
//
@@ -101,7 +98,8 @@ impl CryptoService for WindowsCryptoService {
let (version, no_prefix) =
util::split_encrypted_string_and_validate(encrypted, &["v10", "v20"])?;
// v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes auth tag]
// v10 is already stripped; Windows Chrome uses AES-GCM: [12 bytes IV][ciphertext][16 bytes
// auth tag]
const IV_SIZE: usize = 12;
const TAG_SIZE: usize = 16;
const MIN_LENGTH: usize = IV_SIZE + TAG_SIZE;
@@ -170,7 +168,7 @@ impl WindowsCryptoService {
return Err(anyhow!("Encrypted master key is not encrypted with DPAPI"));
}
let key = unprotect_data_win(&key_bytes[5..])
let key = crypt_unprotect_data(&key_bytes[5..], 0)
.map_err(|e| anyhow!("Failed to unprotect the master key: {}", e))?;
Ok(key)
@@ -185,7 +183,7 @@ impl WindowsCryptoService {
let admin_exe_path = get_admin_exe_path()?;
if ENABLE_SIGNATURE_VALIDATION && !verify_signature(&admin_exe_path)? {
if !verify_signature(&admin_exe_path)? {
return Err(anyhow!("Helper executable signature is not valid"));
}
@@ -208,167 +206,9 @@ 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())
let key = BASE64_STANDARD.decode(&key_base64)?;
Ok(key)
}
fn decode_abe_key_blob(blob_data: &[u8]) -> Result<Vec<u8>> {
let header_len = u32::from_le_bytes(blob_data[0..4].try_into()?) as usize;
// Ignore the header
let content_len_offset = 4 + header_len;
let content_len =
u32::from_le_bytes(blob_data[content_len_offset..content_len_offset + 4].try_into()?)
as usize;
if content_len < 1 {
return Err(anyhow!(
"Corrupted ABE key blob: content length is less than 1"
));
}
let content_offset = content_len_offset + 4;
let content = &blob_data[content_offset..content_offset + content_len];
// When the size is exactly 32 bytes, it's a plain key. It's used in unbranded Chromium builds, Brave, possibly Edge
if content_len == 32 {
return Ok(content.to_vec());
}
let version = content[0];
let key_blob = &content[1..];
match version {
// Google Chrome v1 key encrypted with a hardcoded AES key
1_u8 => Self::decrypt_abe_key_blob_chrome_aes(key_blob),
// Google Chrome v2 key encrypted with a hardcoded ChaCha20 key
2_u8 => Self::decrypt_abe_key_blob_chrome_chacha20(key_blob),
// Google Chrome v3 key encrypted with CNG APIs
3_u8 => Self::decrypt_abe_key_blob_chrome_cng(key_blob),
v => Err(anyhow!("Unsupported ABE key blob version: {}", v)),
}
}
// TODO: DRY up with decrypt_abe_key_blob_chrome_chacha20
fn decrypt_abe_key_blob_chrome_aes(blob: &[u8]) -> Result<Vec<u8>> {
if blob.len() < 60 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 60 bytes, got {} bytes",
blob.len()
));
}
let iv: [u8; 12] = blob[0..12].try_into()?;
let ciphertext: [u8; 48] = blob[12..12 + 48].try_into()?;
const GOOGLE_AES_KEY: &[u8] = &[
0xB3, 0x1C, 0x6E, 0x24, 0x1A, 0xC8, 0x46, 0x72, 0x8D, 0xA9, 0xC1, 0xFA, 0xC4, 0x93,
0x66, 0x51, 0xCF, 0xFB, 0x94, 0x4D, 0x14, 0x3A, 0xB8, 0x16, 0x27, 0x6B, 0xCC, 0x6D,
0xA0, 0x28, 0x47, 0x87,
];
let aes_key = Key::<Aes256Gcm>::from_slice(GOOGLE_AES_KEY);
let cipher = Aes256Gcm::new(aes_key);
let decrypted = cipher
.decrypt((&iv).into(), ciphertext.as_ref())
.map_err(|e| anyhow!("Failed to decrypt v20 key with Google AES key: {}", e))?;
Ok(decrypted)
}
fn decrypt_abe_key_blob_chrome_chacha20(blob: &[u8]) -> Result<Vec<u8>> {
if blob.len() < 60 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 60 bytes, got {} bytes",
blob.len()
));
}
let chacha20_key = chacha20poly1305::Key::from_slice(GOOGLE_CHACHA20_KEY);
let cipher = ChaCha20Poly1305::new(chacha20_key);
const GOOGLE_CHACHA20_KEY: &[u8] = &[
0xE9, 0x8F, 0x37, 0xD7, 0xF4, 0xE1, 0xFA, 0x43, 0x3D, 0x19, 0x30, 0x4D, 0xC2, 0x25,
0x80, 0x42, 0x09, 0x0E, 0x2D, 0x1D, 0x7E, 0xEA, 0x76, 0x70, 0xD4, 0x1F, 0x73, 0x8D,
0x08, 0x72, 0x96, 0x60,
];
let iv: [u8; 12] = blob[0..12].try_into()?;
let ciphertext: [u8; 48] = blob[12..12 + 48].try_into()?;
let decrypted = cipher
.decrypt((&iv).into(), ciphertext.as_ref())
.map_err(|e| anyhow!("Failed to decrypt v20 key with Google ChaCha20 key: {}", e))?;
Ok(decrypted)
}
fn decrypt_abe_key_blob_chrome_cng(blob: &[u8]) -> Result<Vec<u8>> {
if blob.len() < 92 {
return Err(anyhow!(
"Corrupted ABE key blob: expected at least 92 bytes, got {} bytes",
blob.len()
));
}
let _encrypted_aes_key: [u8; 32] = blob[0..32].try_into()?;
let _iv: [u8; 12] = blob[32..32 + 12].try_into()?;
let _ciphertext: [u8; 48] = blob[44..44 + 48].try_into()?;
// TODO: Decrypt the AES key using CNG APIs
// TODO: Implement this in the future once we run into a browser that uses this scheme
// There's no way to test this at the moment. This encryption scheme is not used in any of the browsers I've tested.
Err(anyhow!("Google ABE CNG flavor is not supported yet"))
}
}
fn unprotect_data_win(data: &[u8]) -> Result<Vec<u8>> {
if data.is_empty() {
return Ok(Vec::new());
}
let data_in = CRYPT_INTEGER_BLOB {
cbData: data.len() as u32,
pbData: data.as_ptr() as *mut u8,
};
let mut data_out = CRYPT_INTEGER_BLOB {
cbData: 0,
pbData: std::ptr::null_mut(),
};
let result = unsafe {
CryptUnprotectData(
&data_in,
None, // ppszDataDescr: Option<*mut PWSTR>
None, // pOptionalEntropy: Option<*const CRYPT_INTEGER_BLOB>
None, // pvReserved: Option<*const std::ffi::c_void>
None, // pPromptStruct: Option<*const CRYPTPROTECT_PROMPTSTRUCT>
0, // dwFlags: u32
&mut data_out,
)
};
if result.is_err() {
return Err(anyhow!("CryptUnprotectData failed"));
}
if data_out.pbData.is_null() || data_out.cbData == 0 {
return Ok(Vec::new());
}
let output_slice =
unsafe { std::slice::from_raw_parts(data_out.pbData, data_out.cbData as usize) };
unsafe {
if !data_out.pbData.is_null() {
LocalFree(Some(HLOCAL(data_out.pbData as *mut std::ffi::c_void)));
}
}
Ok(output_slice.to_vec())
}
fn get_admin_exe_path() -> Result<PathBuf> {
@@ -406,8 +246,8 @@ fn get_dist_admin_exe_path(current_exe_full_path: &Path) -> Result<PathBuf> {
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.
// Try to find bitwarden_chromium_import_helper.exe in debug build folders. This might not cover all
// the cases. Tested on `npm run electron` from apps/desktop and apps/desktop/desktop_native.
fn get_debug_admin_exe_path() -> Result<PathBuf> {
let current_dir = std::env::current_dir()?;
let folder_name = current_dir

View File

@@ -1,12 +1,23 @@
use anyhow::{anyhow, Result};
use std::path::Path;
use anyhow::{anyhow, Result};
use tracing::{debug, info};
use verifysign::CodeSignVerifier;
use crate::config::ENABLE_SIGNATURE_VALIDATION;
pub const EXPECTED_SIGNATURE_SHA256_THUMBPRINT: &str =
"9f6680c4720dbf66d1cb8ed6e328f58e42523badc60d138c7a04e63af14ea40d";
pub fn verify_signature(path: &Path) -> Result<bool> {
if !ENABLE_SIGNATURE_VALIDATION {
info!(
"Signature validation is disabled. Skipping verification for: {}",
path.display()
);
return Ok(true);
}
info!("verifying signature of: {}", path.display());
let verifier = CodeSignVerifier::for_file(path)

View File

@@ -1,5 +1,9 @@
#![doc = include_str!("../README.md")]
pub mod config {
include!("../config_constants.rs");
}
pub mod chromium;
pub mod metadata;
mod util;

View File

@@ -59,9 +59,9 @@ pub fn get_supported_importers<T: InstalledBrowserRetriever>(
// Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
#[cfg(test)]
mod tests {
use super::*;
use std::collections::HashSet;
use super::*;
use crate::chromium::{InstalledBrowserRetriever, SUPPORTED_BROWSER_MAP};
pub struct MockInstalledBrowserRetriever {}

View File

@@ -32,7 +32,7 @@ pub(crate) fn split_encrypted_string_and_validate<'a>(
}
/// Decrypt using AES-128 in CBC mode.
#[cfg(any(target_os = "linux", target_os = "macos", test))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> Result<Vec<u8>> {
use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
@@ -41,7 +41,8 @@ pub(crate) fn decrypt_aes_128_cbc(key: &[u8], iv: &[u8], ciphertext: &[u8]) -> R
.map_err(|e| anyhow!("Failed to decrypt: {}", e))
}
/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration count.
/// Derives a PBKDF2 key from the static "saltysalt" salt with the given password and iteration
/// count.
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u8>> {
use pbkdf2::{hmac::Hmac, pbkdf2};
@@ -55,27 +56,9 @@ pub(crate) fn derive_saltysalt(password: &[u8], iterations: u32) -> Result<Vec<u
#[cfg(test)]
mod tests {
use aes::cipher::{
block_padding::Pkcs7,
generic_array::{sequence::GenericSequence, GenericArray},
ArrayLength, BlockEncryptMut, KeyIvInit,
};
const LENGTH16: usize = 16;
const LENGTH10: usize = 10;
const LENGTH0: usize = 0;
fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
(0..length).map(|i| offset + i as u8 * increment).collect()
}
fn generate_generic_array<N: ArrayLength<u8>>(
offset: u8,
increment: u8,
) -> GenericArray<u8, N> {
GenericArray::generate(|i| offset + i as u8 * increment)
}
fn run_split_encrypted_string_test<'a, const N: usize>(
successfully_split: bool,
plaintext_to_encrypt: &'a str,
@@ -144,8 +127,28 @@ mod tests {
run_split_encrypted_string_and_validate_test(false, "v10EncryptMe!", &[]);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_decrypt_aes_128_cbc() {
use aes::cipher::{
block_padding::Pkcs7,
generic_array::{sequence::GenericSequence, GenericArray},
ArrayLength, BlockEncryptMut, KeyIvInit,
};
const LENGTH16: usize = 16;
fn generate_generic_array<N: ArrayLength<u8>>(
offset: u8,
increment: u8,
) -> GenericArray<u8, N> {
GenericArray::generate(|i| offset + i as u8 * increment)
}
fn generate_vec(length: usize, offset: u8, increment: u8) -> Vec<u8> {
(0..length).map(|i| offset + i as u8 * increment).collect()
}
let offset = 0;
let increment = 1;

View File

@@ -23,27 +23,15 @@ anyhow = { workspace = true }
arboard = { workspace = true, features = ["wayland-data-control"] }
base64 = { workspace = true }
bitwarden-russh = { workspace = true }
byteorder = { workspace = true }
bytes = { workspace = true }
cbc = { workspace = true, features = ["alloc"] }
chacha20poly1305 = { workspace = true }
dirs = { workspace = true }
ed25519 = { workspace = true, features = ["pkcs8"] }
futures = { workspace = true }
homedir = { workspace = true }
interprocess = { workspace = true, features = ["tokio"] }
memsec = { workspace = true, features = ["alloc_ext"] }
pin-project = { workspace = true }
pkcs8 = { workspace = true, features = ["alloc", "encryption", "pem"] }
rand = { workspace = true }
rsa = { workspace = true }
russh-cryptovec = { workspace = true }
scopeguard = { workspace = true }
secmem-proc = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sha2 = { workspace = true }
ssh-encoding = { workspace = true }
ssh-key = { workspace = true, features = [
"encryption",
"ed25519",
@@ -53,13 +41,17 @@ ssh-key = { workspace = true, features = [
sysinfo = { workspace = true, features = ["windows"] }
thiserror = { workspace = true }
tokio = { workspace = true, features = ["io-util", "sync", "macros", "net"] }
tokio-stream = { workspace = true, features = ["net"] }
tokio-util = { workspace = true, features = ["codec"] }
tracing = { workspace = true }
typenum = { workspace = true }
zeroizing-alloc = { workspace = true }
[target.'cfg(windows)'.dependencies]
pin-project = { workspace = true }
scopeguard = { workspace = true }
secmem-proc = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
widestring = { workspace = true, optional = true }
windows = { workspace = true, features = [
"Foundation",
@@ -76,21 +68,20 @@ windows = { workspace = true, features = [
], optional = true }
windows-future = { workspace = true }
[target.'cfg(windows)'.dev-dependencies]
keytar = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = { workspace = true, optional = true }
homedir = { workspace = true }
secmem-proc = { workspace = true }
security-framework = { workspace = true, optional = true }
security-framework-sys = { workspace = true, optional = true }
desktop_objc = { path = "../objc" }
[target.'cfg(target_os = "linux")'.dependencies]
oo7 = { workspace = true }
ashpd = { workspace = true }
homedir = { workspace = true }
libc = { workspace = true }
linux-keyutils = { workspace = true }
ashpd = { workspace = true }
oo7 = { workspace = true }
zbus = { workspace = true, optional = true }
zbus_polkit = { workspace = true, optional = true }

View File

@@ -86,11 +86,15 @@ impl KeyMaterial {
#[cfg(test)]
mod tests {
use crate::biometric::{decrypt, encrypt, KeyMaterial};
use crate::crypto::CipherString;
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
use std::str::FromStr;
use base64::{engine::general_purpose::STANDARD as base64_engine, Engine};
use crate::{
biometric::{decrypt, encrypt, KeyMaterial},
crypto::CipherString,
};
fn key_material() -> KeyMaterial {
KeyMaterial {
os_key_part_b64: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=".to_owned(),

View File

@@ -1,18 +1,18 @@
use std::str::FromStr;
use anyhow::Result;
use anyhow::{anyhow, Result};
use base64::Engine;
use rand::RngCore;
use sha2::{Digest, Sha256};
use tracing::error;
use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey};
use zbus::Connection;
use zbus_polkit::policykit1::*;
use super::{decrypt, encrypt};
use crate::crypto::CipherString;
use anyhow::anyhow;
use crate::{
biometric::{base64_engine, KeyMaterial, OsDerivedKey},
crypto::CipherString,
};
/// The Unix implementation of the biometric trait.
pub struct Biometric {}

View File

@@ -16,13 +16,12 @@ use windows::{
};
use windows_future::IAsyncOperation;
use super::{decrypt, encrypt, windows_focus::set_focus};
use crate::{
biometric::{KeyMaterial, OsDerivedKey},
crypto::CipherString,
};
use super::{decrypt, encrypt, windows_focus::set_focus};
/// The Windows OS implementation of the biometric trait.
pub struct Biometric {}
@@ -61,7 +60,8 @@ impl super::BiometricTrait for Biometric {
match ucv_available {
UserConsentVerifierAvailability::Available => Ok(true),
UserConsentVerifierAvailability::DeviceBusy => Ok(true), // TODO: Look into removing this and making the check more ad-hoc
// TODO: look into removing this and making the check more ad-hoc
UserConsentVerifierAvailability::DeviceBusy => Ok(true),
_ => Ok(false),
}
}
@@ -133,7 +133,6 @@ fn random_challenge() -> [u8; 16] {
#[cfg(test)]
mod tests {
use super::*;
use crate::biometric::BiometricTrait;
#[test]

View File

@@ -1,17 +1,19 @@
//! This file implements Polkit based system unlock.
//!
//! # Security
//! This section describes the assumed security model and security guarantees achieved. In the required security
//! guarantee is that a locked vault - a running app - cannot be unlocked when the device (user-space)
//! is compromised in this state.
//! This section describes the assumed security model and security guarantees achieved. In the
//! required security guarantee is that a locked vault - a running app - cannot be unlocked when the
//! device (user-space) is compromised in this state.
//!
//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory,
//! protected by memfd_secret. This makes it inaccessible to other processes, even if they compromise root, a kernel compromise
//! has circumventable best-effort protections. While the app is running this key is held in memory, even if locked.
//! When unlocking, the app will prompt the user via `polkit` to get a yes/no decision on whether to release the key to the app.
//! When first unlocking the app, the app sends the user-key to this module, which holds it in
//! secure memory, protected by memfd_secret. This makes it inaccessible to other processes, even if
//! they compromise root, a kernel compromise has circumventable best-effort protections. While the
//! app is running this key is held in memory, even if locked. When unlocking, the app will prompt
//! the user via `polkit` to get a yes/no decision on whether to release the key to the app.
use std::sync::Arc;
use anyhow::{anyhow, Result};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::{debug, warn};
use zbus::Connection;
@@ -20,8 +22,8 @@ use zbus_polkit::policykit1::{AuthorityProxy, CheckAuthorizationFlags, Subject};
use crate::secure_memory::*;
pub struct BiometricLockSystem {
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure
// locked vaults cannot be unlocked
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to
// ensure locked vaults cannot be unlocked
secure_memory: Arc<Mutex<crate::secure_memory::encrypted_memory_store::EncryptedMemoryStore>>,
}
@@ -88,8 +90,9 @@ impl super::BiometricTrait for BiometricLockSystem {
}
}
/// Perform a polkit authorization against the bitwarden unlock policy. Note: This relies on no custom
/// rules in the system skipping the authorization check, in which case this counts as UV / authentication.
/// Perform a polkit authorization against the bitwarden unlock policy. Note: This relies on no
/// custom rules in the system skipping the authorization check, in which case this counts as UV /
/// authentication.
async fn polkit_authenticate_bitwarden_policy() -> Result<bool> {
debug!("[Polkit] Authenticating / performing UV");

View File

@@ -17,8 +17,8 @@ pub trait BiometricTrait: Send + Sync {
async fn authenticate(&self, hwnd: Vec<u8>, message: String) -> Result<bool>;
/// Check if biometric authentication is available
async fn authenticate_available(&self) -> Result<bool>;
/// Enroll a key for persistent unlock. If the implementation does not support persistent enrollment,
/// this function should do nothing.
/// Enroll a key for persistent unlock. If the implementation does not support persistent
/// enrollment, this function should do nothing.
async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()>;
/// Clear the persistent and ephemeral keys
async fn unenroll(&self, user_id: &str) -> Result<()>;
@@ -28,6 +28,7 @@ pub trait BiometricTrait: Send + Sync {
async fn provide_key(&self, user_id: &str, key: &[u8]);
/// Perform biometric unlock and return the key
async fn unlock(&self, user_id: &str, hwnd: Vec<u8>) -> Result<Vec<u8>>;
/// Check if biometric unlock is available based on whether a key is present and whether authentication is possible
/// Check if biometric unlock is available based on whether a key is present and whether
/// authentication is possible
async fn unlock_available(&self, user_id: &str) -> Result<bool>;
}

View File

@@ -2,38 +2,40 @@
//!
//! There are two paths implemented here.
//! The former via UV + ephemerally (but protected) keys. This only works after first unlock.
//! The latter via a signing API, that deterministically signs a challenge, from which a windows hello key is derived. This key
//! is used to encrypt the protected key.
//! The latter via a signing API, that deterministically signs a challenge, from which a windows
//! hello key is derived. This key is used to encrypt the protected key.
//!
//! # Security
//! The security goal is that a locked vault - a running app - cannot be unlocked when the device (user-space)
//! is compromised in this state.
//! The security goal is that a locked vault - a running app - cannot be unlocked when the device
//! (user-space) is compromised in this state.
//!
//! ## UV path
//! When first unlocking the app, the app sends the user-key to this module, which holds it in secure memory,
//! protected by DPAPI. This makes it inaccessible to other processes, unless they compromise the system administrator, or kernel.
//! While the app is running this key is held in memory, even if locked. When unlocking, the app will prompt the user via
//! When first unlocking the app, the app sends the user-key to this module, which holds it in
//! secure memory, protected by DPAPI. This makes it inaccessible to other processes, unless they
//! compromise the system administrator, or kernel. While the app is running this key is held in
//! memory, even if locked. When unlocking, the app will prompt the user via
//! `windows_hello_authenticate` to get a yes/no decision on whether to release the key to the app.
//! Note: Further process isolation is needed here so that code cannot be injected into the running process, which may
//! circumvent DPAPI.
//! Note: Further process isolation is needed here so that code cannot be injected into the running
//! process, which may circumvent DPAPI.
//!
//! ## Sign path
//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the windows hello key
//! with the Windows Hello prompt. This is done by signing a per-user challenge, which produces a deterministic
//! signature which is hashed to obtain a key. This key is used to encrypt and persist the vault unlock key (user key).
//! In this scenario, when enrolling, the app sends the user-key to this module, which derives the
//! windows hello key with the Windows Hello prompt. This is done by signing a per-user challenge,
//! which produces a deterministic signature which is hashed to obtain a key. This key is used to
//! encrypt and persist the vault unlock key (user key).
//!
//! Since the keychain can be accessed by all user-space processes, the challenge is known to all userspace processes.
//! Therefore, to circumvent the security measure, the attacker would need to create a fake Windows-Hello prompt, and
//! get the user to confirm it.
//! Since the keychain can be accessed by all user-space processes, the challenge is known to all
//! userspace processes. Therefore, to circumvent the security measure, the attacker would need to
//! create a fake Windows-Hello prompt, and get the user to confirm it.
use std::sync::{atomic::AtomicBool, Arc};
use tracing::{debug, warn};
use aes::cipher::KeyInit;
use anyhow::{anyhow, Result};
use chacha20poly1305::{aead::Aead, XChaCha20Poly1305, XNonce};
use sha2::{Digest, Sha256};
use tokio::sync::Mutex;
use tracing::{debug, warn};
use windows::{
core::{factory, h, Interface, HSTRING},
Security::{
@@ -74,8 +76,8 @@ struct WindowsHelloKeychainEntry {
/// The Windows OS implementation of the biometric trait.
pub struct BiometricLockSystem {
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to ensure
// locked vaults cannot be unlocked
// The userkeys that are held in memory MUST be protected from memory dumping attacks, to
// ensure locked vaults cannot be unlocked
secure_memory: Arc<Mutex<crate::secure_memory::dpapi::DpapiSecretKVStore>>,
}
@@ -114,12 +116,14 @@ impl super::BiometricTrait for BiometricLockSystem {
}
async fn enroll_persistent(&self, user_id: &str, key: &[u8]) -> Result<()> {
// Enrollment works by first generating a random challenge unique to the user / enrollment. Then,
// with the challenge and a Windows-Hello prompt, the "windows hello key" is derived. The windows
// hello key is used to encrypt the key to store with XChaCha20Poly1305. The bundle of nonce,
// challenge and wrapped-key are stored to the keychain
// Enrollment works by first generating a random challenge unique to the user / enrollment.
// Then, with the challenge and a Windows-Hello prompt, the "windows hello key" is
// derived. The windows hello key is used to encrypt the key to store with
// XChaCha20Poly1305. The bundle of nonce, challenge and wrapped-key are stored to
// the keychain
// Each enrollment (per user) has a unique challenge, so that the windows-hello key is unique
// Each enrollment (per user) has a unique challenge, so that the windows-hello key is
// unique
let challenge: [u8; CHALLENGE_LENGTH] = rand::random();
// This key is unique to the challenge
@@ -155,8 +159,8 @@ impl super::BiometricTrait for BiometricLockSystem {
});
let mut secure_memory = self.secure_memory.lock().await;
// If the key is held ephemerally, always use UV API. Only use signing API if the key is not held
// ephemerally but the keychain holds it persistently.
// If the key is held ephemerally, always use UV API. Only use signing API if the key is not
// held ephemerally but the keychain holds it persistently.
if secure_memory.has(user_id) {
if windows_hello_authenticate("Unlock your vault".to_string()).await? {
secure_memory
@@ -175,7 +179,8 @@ impl super::BiometricTrait for BiometricLockSystem {
&keychain_entry.wrapped_key,
&keychain_entry.nonce,
)?;
// The first unlock already sets the key for subsequent unlocks. The key may again be set externally after unlock finishes.
// The first unlock already sets the key for subsequent unlocks. The key may again be
// set externally after unlock finishes.
secure_memory.put(user_id.to_string(), &decrypted_key.clone());
Ok(decrypted_key)
}
@@ -231,8 +236,8 @@ async fn windows_hello_authenticate_with_crypto(
) -> Result<[u8; XCHACHA20POLY1305_KEY_LENGTH]> {
debug!("[Windows Hello] Authenticating to sign challenge");
// Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a new API.
// This is unreliable, and if it does not work, the operation may fail
// Ugly hack: We need to focus the window via window focusing APIs until Microsoft releases a
// new API. This is unreliable, and if it does not work, the operation may fail
let stop_focusing = Arc::new(AtomicBool::new(false));
let stop_focusing_clone = stop_focusing.clone();
let _ = std::thread::spawn(move || loop {
@@ -243,8 +248,8 @@ async fn windows_hello_authenticate_with_crypto(
break;
}
});
// Only stop focusing once this function exits. The focus MUST run both during the initial creation
// with RequestCreateAsync, and also with the subsequent use with RequestSignAsync.
// Only stop focusing once this function exits. The focus MUST run both during the initial
// creation with RequestCreateAsync, and also with the subsequent use with RequestSignAsync.
let _guard = scopeguard::guard((), |_| {
stop_focusing.store(true, std::sync::atomic::Ordering::Relaxed);
});
@@ -283,8 +288,8 @@ async fn windows_hello_authenticate_with_crypto(
let signature_buffer = signature.Result()?;
let signature_value = unsafe { as_mut_bytes(&signature_buffer)? };
// The signature is deterministic based on the challenge and keychain key. Thus, it can be hashed to a key.
// It is unclear what entropy this key provides.
// The signature is deterministic based on the challenge and keychain key. Thus, it can be
// hashed to a key. It is unclear what entropy this key provides.
let windows_hello_key = Sha256::digest(signature_value).into();
Ok(windows_hello_key)
}

View File

@@ -34,23 +34,25 @@ pub fn focus_security_prompt() {
/// Sets focus to a window using a few unstable methods
fn set_focus(hwnd: HWND) {
unsafe {
// Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello bugs.
// The windows hello signing prompt NEEDS to be focused instantly, or it will error, but it does
// not focus itself.
// Windows REALLY does not like apps stealing focus, even if it is for fixing Windows-Hello
// bugs. The windows hello signing prompt NEEDS to be focused instantly, or it will
// error, but it does not focus itself.
// This function implements forced focusing of windows using a few hacks.
// The conditions to successfully foreground a window are:
// All of the following conditions are true:
// The calling process belongs to a desktop application, not a UWP app or a Windows Store app designed for Windows 8 or 8.1.
// The foreground process has not disabled calls to SetForegroundWindow by a previous call to the LockSetForegroundWindow function.
// The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in SystemParametersInfo).
// No menus are active.
// - The calling process belongs to a desktop application, not a UWP app or a Windows
// Store app designed for Windows 8 or 8.1.
// - The foreground process has not disabled calls to SetForegroundWindow by a previous
// call to the LockSetForegroundWindow function.
// - The foreground lock time-out has expired (see SPI_GETFOREGROUNDLOCKTIMEOUT in
// SystemParametersInfo). No menus are active.
// Additionally, at least one of the following conditions is true:
// The calling process is the foreground process.
// The calling process was started by the foreground process.
// There is currently no foreground window, and thus no foreground process.
// The calling process received the last input event.
// Either the foreground process or the calling process is being debugged.
// - The calling process is the foreground process.
// - The calling process was started by the foreground process.
// - There is currently no foreground window, and thus no foreground process.
// - The calling process received the last input event.
// - Either the foreground process or the calling process is being debugged.
// Update the foreground lock timeout temporarily
let mut old_timeout = 0;
@@ -75,7 +77,8 @@ fn set_focus(hwnd: HWND) {
);
});
// Attach to the foreground thread once attached, we can foreground, even if in the background
// Attach to the foreground thread once attached, we can foreground, even if in the
// background
let dw_current_thread = GetCurrentThreadId();
let dw_fg_thread = GetWindowThreadProcessId(GetForegroundWindow(), None);
@@ -91,7 +94,8 @@ fn set_focus(hwnd: HWND) {
}
}
/// When restoring focus to the application window, we need a less aggressive method so the electron window doesn't get frozen.
/// When restoring focus to the application window, we need a less aggressive method so the electron
/// window doesn't get frozen.
pub(crate) fn restore_focus(hwnd: HWND) {
unsafe {
let _ = SetForegroundWindow(hwnd);

View File

@@ -5,9 +5,8 @@ use aes::cipher::{
BlockEncryptMut, KeyIvInit,
};
use crate::error::{CryptoError, Result};
use super::CipherString;
use crate::error::{CryptoError, Result};
pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) -> Result<Vec<u8>> {
let iv = GenericArray::from_slice(iv);
@@ -16,7 +15,8 @@ pub fn decrypt_aes256(iv: &[u8; 16], data: &[u8], key: GenericArray<u8, U32>) ->
.decrypt_padded_mut::<Pkcs7>(&mut data)
.map_err(|_| CryptoError::KeyDecrypt)?;
// Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it, we truncate to the subslice length
// Data is decrypted in place and returns a subslice of the original Vec, to avoid cloning it,
// we truncate to the subslice length
let decrypted_len = decrypted_key_slice.len();
data.truncate(decrypted_len);

View File

@@ -35,15 +35,4 @@ pub enum KdfParamError {
InvalidParams(String),
}
// Ensure that the error messages implement Send and Sync
#[cfg(test)]
const _: () = {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
fn assert_all() {
assert_send::<Error>();
assert_sync::<Error>();
}
};
pub type Result<T, E = Error> = std::result::Result<T, E>;

View File

@@ -49,7 +49,8 @@ pub fn path(name: &str) -> std::path::PathBuf {
#[cfg(target_os = "macos")]
{
// When running in an unsandboxed environment, path is: /Users/<user>/
// While running sandboxed, it's different: /Users/<user>/Library/Containers/com.bitwarden.desktop/Data
// While running sandboxed, it's different:
// /Users/<user>/Library/Containers/com.bitwarden.desktop/Data
let mut home = dirs::home_dir().unwrap();
// Check if the app is sandboxed by looking for the Containers directory
@@ -59,8 +60,9 @@ pub fn path(name: &str) -> std::path::PathBuf {
// If the app is sanboxed, we need to use the App Group directory
if let Some(position) = containers_position {
// We want to use App Groups in /Users/<user>/Library/Group Containers/LTZ2PFU5D6.com.bitwarden.desktop,
// so we need to remove all the components after the user. We can use the previous position to do this.
// We want to use App Groups in /Users/<user>/Library/Group
// Containers/LTZ2PFU5D6.com.bitwarden.desktop, so we need to remove all the
// components after the user. We can use the previous position to do this.
while home.components().count() > position - 1 {
home.pop();
}

View File

@@ -3,9 +3,8 @@ use std::{
path::{Path, PathBuf},
};
use futures::{SinkExt, StreamExt, TryFutureExt};
use anyhow::Result;
use futures::{SinkExt, StreamExt, TryFutureExt};
use interprocess::local_socket::{tokio::prelude::*, GenericFilePath, ListenerOptions};
use tokio::{
io::{AsyncRead, AsyncWrite},
@@ -42,14 +41,17 @@ impl Server {
///
/// # Parameters
///
/// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// - `client_to_server_send`: This [`mpsc::Sender<Message>`] will receive all the [`Message`]'s that the clients send to this server.
/// - `name`: The endpoint name to listen on. This name uniquely identifies the IPC connection
/// and must be the same for both the server and client.
/// - `client_to_server_send`: This [`mpsc::Sender<Message>`] will receive all the [`Message`]'s
/// that the clients send to this server.
pub fn start(
path: &Path,
client_to_server_send: mpsc::Sender<Message>,
) -> Result<Self, Box<dyn Error>> {
// If the unix socket file already exists, we get an error when trying to bind to it. So we remove it first.
// Any processes that were using the old socket should remain connected to it but any new connections will use the new socket.
// If the unix socket file already exists, we get an error when trying to bind to it. So we
// remove it first. Any processes that were using the old socket should remain
// connected to it but any new connections will use the new socket.
if !cfg!(windows) {
let _ = std::fs::remove_file(path);
}
@@ -58,8 +60,9 @@ impl Server {
let opts = ListenerOptions::new().name(name);
let listener = opts.create_tokio()?;
// This broadcast channel is used for sending messages to all connected clients, and so the sender
// will be stored in the server while the receiver will be cloned and passed to each client handler.
// This broadcast channel is used for sending messages to all connected clients, and so the
// sender will be stored in the server while the receiver will be cloned and passed
// to each client handler.
let (server_to_clients_send, server_to_clients_recv) =
broadcast::channel::<String>(MESSAGE_CHANNEL_BUFFER);

View File

@@ -1,9 +1,10 @@
use crate::password::PASSWORD_NOT_FOUND;
use anyhow::Result;
use security_framework::passwords::{
delete_generic_password, get_generic_password, set_generic_password,
};
use crate::password::PASSWORD_NOT_FOUND;
#[allow(clippy::unused_async)]
pub async fn get_password(service: &str, account: &str) -> Result<String> {
let password = get_generic_password(service, account).map_err(convert_error)?;

View File

@@ -1,9 +1,11 @@
use crate::password::PASSWORD_NOT_FOUND;
use std::collections::HashMap;
use anyhow::{anyhow, Result};
use oo7::dbus::{self};
use std::collections::HashMap;
use tracing::info;
use crate::password::PASSWORD_NOT_FOUND;
pub async fn get_password(service: &str, account: &str) -> Result<String> {
match get_password_new(service, account).await {
Ok(res) => Ok(res),

View File

@@ -1,4 +1,3 @@
use crate::password::PASSWORD_NOT_FOUND;
use anyhow::{anyhow, Result};
use widestring::{U16CString, U16String};
use windows::{
@@ -12,6 +11,8 @@ use windows::{
},
};
use crate::password::PASSWORD_NOT_FOUND;
const CRED_FLAGS_NONE: u32 = 0;
#[allow(clippy::unused_async)]

View File

@@ -4,15 +4,15 @@ use libc::c_uint;
use libc::{self, c_int};
use tracing::info;
// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on crashes
// https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20
// RLIMIT_CORE is the maximum size of a core dump file. Setting both to 0 disables core dumps, on
// crashes https://github.com/torvalds/linux/blob/1613e604df0cd359cf2a7fbd9be7a0bcfacfabd0/include/uapi/asm-generic/resource.h#L20
#[cfg(target_env = "musl")]
const RLIMIT_CORE: c_int = 4;
#[cfg(target_env = "gnu")]
const RLIMIT_CORE: c_uint = 4;
// PR_SET_DUMPABLE makes it so no other running process (root or same user) can dump the memory of this process
// or attach a debugger to it.
// PR_SET_DUMPABLE makes it so no other running process (root or same user) can dump the memory of
// this process or attach a debugger to it.
// https://github.com/torvalds/linux/blob/a38297e3fb012ddfa7ce0321a7e5a8daeb1872b6/include/uapi/linux/prctl.h#L14
const PR_SET_DUMPABLE: c_int = 4;

View File

@@ -29,8 +29,9 @@ impl SecureMemoryStore for DpapiSecretKVStore {
fn put(&mut self, key: String, value: &[u8]) {
let length_header_len = std::mem::size_of::<usize>();
// The allocated data has to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE, so we pad it and write the length in front
// We are storing LENGTH|DATA|00..00, where LENGTH is the length of DATA, the total length is a multiple
// The allocated data has to be a multiple of CRYPTPROTECTMEMORY_BLOCK_SIZE, so we pad it
// and write the length in front We are storing LENGTH|DATA|00..00, where LENGTH is
// the length of DATA, the total length is a multiple
// of CRYPTPROTECTMEMORY_BLOCK_SIZE, and the padding is filled with zeros.
let data_len = value.len();

View File

@@ -10,8 +10,8 @@ use crate::secure_memory::{
/// allows circumventing length and amount limitations on platform specific secure memory APIs since
/// only a single short item needs to be protected.
///
/// The key is briefly in process memory during encryption and decryption, in memory that is protected
/// from swapping to disk via mlock, and then zeroed out immediately after use.
/// The key is briefly in process memory during encryption and decryption, in memory that is
/// protected from swapping to disk via mlock, and then zeroed out immediately after use.
#[allow(unused)]
pub(crate) struct EncryptedMemoryStore {
map: std::collections::HashMap<String, EncryptedMemory>,

View File

@@ -6,9 +6,9 @@ use rand::{rng, Rng};
pub(super) const KEY_SIZE: usize = 32;
pub(super) const NONCE_SIZE: usize = 24;
/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts will result
/// in a decryption failure and panic. The key's memory contents are protected from being swapped to disk
/// via mlock.
/// The encryption performed here is xchacha-poly1305. Any tampering with the key or the ciphertexts
/// will result in a decryption failure and panic. The key's memory contents are protected from
/// being swapped to disk via mlock.
pub(super) struct MemoryEncryptionKey(NonNull<[u8]>);
/// An encrypted memory blob that must be decrypted using the same key that it was encrypted with.

View File

@@ -1,10 +1,13 @@
use super::crypto::{MemoryEncryptionKey, KEY_SIZE};
use super::SecureKeyContainer;
use windows::Win32::Security::Cryptography::{
CryptProtectMemory, CryptUnprotectMemory, CRYPTPROTECTMEMORY_BLOCK_SIZE,
CRYPTPROTECTMEMORY_SAME_PROCESS,
};
use super::{
crypto::{MemoryEncryptionKey, KEY_SIZE},
SecureKeyContainer,
};
/// https://learn.microsoft.com/en-us/windows/win32/api/dpapi/nf-dpapi-cryptprotectdata
/// The DPAPI store encrypts data using the Windows Data Protection API (DPAPI). The key is bound
/// to the current process, and cannot be decrypted by other user-mode processes.

View File

@@ -1,9 +1,8 @@
use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey;
use super::crypto::KEY_SIZE;
use super::SecureKeyContainer;
use linux_keyutils::{KeyRing, KeyRingIdentifier};
use super::{crypto::KEY_SIZE, SecureKeyContainer};
use crate::secure_memory::secure_key::crypto::MemoryEncryptionKey;
/// The keys are bound to the process keyring.
const KEY_RING_IDENTIFIER: KeyRingIdentifier = KeyRingIdentifier::Process;
/// This is an atomic global counter used to help generate unique key IDs
@@ -26,9 +25,9 @@ pub(super) struct KeyctlSecureKeyContainer {
id: String,
}
// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on drop.
// Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the key
// is accessible across threads within the same process bound.
// SAFETY: The key id is fully owned by this struct and not exposed or cloned, and cleaned up on
// drop. Further, since we use `KeyRingIdentifier::Process` and not `KeyRingIdentifier::Thread`, the
// key is accessible across threads within the same process bound.
unsafe impl Send for KeyctlSecureKeyContainer {}
// SAFETY: The container is non-mutable and thus safe to share between threads.
unsafe impl Sync for KeyctlSecureKeyContainer {}

View File

@@ -1,8 +1,9 @@
use std::{ptr::NonNull, sync::LazyLock};
use super::crypto::MemoryEncryptionKey;
use super::crypto::KEY_SIZE;
use super::SecureKeyContainer;
use super::{
crypto::{MemoryEncryptionKey, KEY_SIZE},
SecureKeyContainer,
};
/// https://man.archlinux.org/man/memfd_secret.2.en
/// The memfd_secret store protects the data using the `memfd_secret` syscall. The
@@ -15,8 +16,8 @@ pub(super) struct MemfdSecretSecureKeyContainer {
// SAFETY: The pointers in this struct are allocated by `memfd_secret`, and we have full ownership.
// They are never exposed outside or cloned, and are cleaned up by drop.
unsafe impl Send for MemfdSecretSecureKeyContainer {}
// SAFETY: The container is non-mutable and thus safe to share between threads. Further, memfd-secret
// is accessible across threads within the same process bound.
// SAFETY: The container is non-mutable and thus safe to share between threads. Further,
// memfd-secret is accessible across threads within the same process bound.
unsafe impl Sync for MemfdSecretSecureKeyContainer {}
impl SecureKeyContainer for MemfdSecretSecureKeyContainer {

View File

@@ -1,8 +1,9 @@
use std::ptr::NonNull;
use super::crypto::MemoryEncryptionKey;
use super::crypto::KEY_SIZE;
use super::SecureKeyContainer;
use super::{
crypto::{MemoryEncryptionKey, KEY_SIZE},
SecureKeyContainer,
};
/// A SecureKeyContainer that uses mlock to prevent the memory from being swapped to disk.
/// This does not provide as strong protections as other methods, but is always supported.

View File

@@ -1,9 +1,12 @@
//! This module provides hardened storage for single cryptographic keys. These are meant for encrypting large amounts of memory.
//! Some platforms restrict how many keys can be protected by their APIs, which necessitates this layer of indirection. This significantly
//! reduces the complexity of each platform specific implementation, since all that's needed is implementing protecting a single fixed sized key
//! instead of protecting many arbitrarily sized secrets. This significantly lowers the effort to maintain each implementation.
//! This module provides hardened storage for single cryptographic keys. These are meant for
//! encrypting large amounts of memory. Some platforms restrict how many keys can be protected by
//! their APIs, which necessitates this layer of indirection. This significantly reduces the
//! complexity of each platform specific implementation, since all that's needed is implementing
//! protecting a single fixed sized key instead of protecting many arbitrarily sized secrets. This
//! significantly lowers the effort to maintain each implementation.
//!
//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux, and a fallback implementation using mlock.
//! The implementations include DPAPI on Windows, `keyctl` on Linux, and `memfd_secret` on Linux,
//! and a fallback implementation using mlock.
use tracing::info;
@@ -20,12 +23,13 @@ pub use crypto::EncryptedMemory;
use crate::secure_memory::secure_key::crypto::DecryptionError;
/// An ephemeral key that is protected using a platform mechanism. It is generated on construction freshly, and can be used
/// to encrypt and decrypt segments of memory. Since the key is ephemeral, persistent data cannot be encrypted with this key.
/// On Linux and Windows, in most cases the protection mechanisms prevent memory dumps/debuggers from reading the key.
/// An ephemeral key that is protected using a platform mechanism. It is generated on construction
/// freshly, and can be used to encrypt and decrypt segments of memory. Since the key is ephemeral,
/// persistent data cannot be encrypted with this key. On Linux and Windows, in most cases the
/// protection mechanisms prevent memory dumps/debuggers from reading the key.
///
/// Note: This can be circumvented if code can be injected into the process and is only effective in combination with the
/// memory isolation provided in `process_isolation`.
/// Note: This can be circumvented if code can be injected into the process and is only effective in
/// combination with the memory isolation provided in `process_isolation`.
/// - https://github.com/zer1t0/keydump
#[allow(unused)]
pub(crate) struct SecureMemoryEncryptionKey(CrossPlatformSecureKeyContainer);
@@ -55,7 +59,8 @@ impl SecureMemoryEncryptionKey {
/// from memory attacks.
#[allow(unused)]
trait SecureKeyContainer: Sync + Send {
/// Returns the key as a byte slice. This slice does not have additional memory protections applied.
/// Returns the key as a byte slice. This slice does not have additional memory protections
/// applied.
fn as_key(&self) -> crypto::MemoryEncryptionKey;
/// Creates a new SecureKeyContainer from the provided key.
fn from_key(key: crypto::MemoryEncryptionKey) -> Self;

View File

@@ -7,13 +7,12 @@ use std::{
};
use base64::{engine::general_purpose::STANDARD, Engine as _};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use bitwarden_russh::{
session_bind::SessionBindResult,
ssh_agent::{self, SshKey},
};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use tracing::{error, info};
#[cfg_attr(target_os = "windows", path = "windows.rs")]
@@ -34,7 +33,8 @@ pub struct BitwardenDesktopAgent {
show_ui_request_tx: tokio::sync::mpsc::Sender<SshAgentUIRequest>,
get_ui_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
request_id: Arc<AtomicU32>,
/// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys
/// before first unlock, or after account switching, listing keys should require an unlock to
/// get a list of public keys
needs_unlock: Arc<AtomicBool>,
is_running: Arc<AtomicBool>,
}

View File

@@ -1,7 +1,6 @@
use futures::Stream;
use std::os::windows::prelude::AsRawHandle as _;
use std::{
io,
os::windows::prelude::AsRawHandle as _,
pin::Pin,
sync::{
atomic::{AtomicBool, Ordering},
@@ -9,6 +8,8 @@ use std::{
},
task::{Context, Poll},
};
use futures::Stream;
use tokio::{
net::windows::named_pipe::{NamedPipeServer, ServerOptions},
select,

View File

@@ -1,11 +1,13 @@
use std::{
io,
pin::Pin,
task::{Context, Poll},
};
use futures::Stream;
use std::io;
use std::pin::Pin;
use std::task::{Context, Poll};
use tokio::net::{UnixListener, UnixStream};
use super::peerinfo;
use super::peerinfo::models::PeerInfo;
use super::{peerinfo, peerinfo::models::PeerInfo};
#[derive(Debug)]
pub struct PeercredUnixListenerStream {

View File

@@ -1,9 +1,10 @@
use std::sync::{atomic::AtomicBool, Arc, Mutex};
/**
* Peerinfo represents the information of a peer process connecting over a socket.
* This can be later extended to include more information (icon, app name) for the corresponding application.
*/
* Peerinfo represents the information of a peer process connecting over a socket.
* This can be later extended to include more information (icon, app name) for the corresponding
* application.
*/
#[derive(Debug, Clone)]
pub struct PeerInfo {
uid: u32,

View File

@@ -6,9 +6,8 @@ use homedir::my_home;
use tokio::{net::UnixListener, sync::Mutex};
use tracing::{error, info};
use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream;
use super::{BitwardenDesktopAgent, SshAgentUIRequest};
use crate::ssh_agent::peercred_unix_listener_stream::PeercredUnixListenerStream;
/// User can override the default socket path with this env var
const ENV_BITWARDEN_SSH_AUTH_SOCK: &str = "BITWARDEN_SSH_AUTH_SOCK";

View File

@@ -2,6 +2,7 @@ use bitwarden_russh::ssh_agent;
pub mod named_pipe_listener_stream;
use std::sync::Arc;
use tokio::sync::Mutex;
use super::{BitwardenDesktopAgent, SshAgentUIRequest};

View File

@@ -14,19 +14,17 @@ crate-type = ["staticlib", "cdylib"]
bench = false
[dependencies]
uniffi = { workspace = true, features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
desktop_core = { path = "../core" }
futures = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true, features = ["sync"] }
tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-oslog = "0.3.0"
tracing-subscriber = { workspace = true }
uniffi = { workspace = true, features = ["cli"] }
[target.'cfg(target_os = "macos")'.dependencies]
oslog = { workspace = true }
tracing-oslog = "0.3.0"
[build-dependencies]
uniffi = { workspace = true, features = ["build"] }

View File

@@ -16,17 +16,13 @@ manual_test = []
[dependencies]
anyhow = { workspace = true }
autotype = { path = "../autotype" }
base64 = { workspace = true }
chromium_importer = { path = "../chromium_importer" }
desktop_core = { path = "../core" }
hex = { workspace = true }
napi = { workspace = true, features = ["async"] }
napi-derive = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
tokio-util = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }

View File

@@ -11,7 +11,10 @@ export declare namespace passwords {
* Throws {@link Error} with message {@link PASSWORD_NOT_FOUND} if the password does not exist.
*/
export function getPassword(service: string, account: string): Promise<string>
/** Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry. */
/**
* Save the password to the keychain. Adds an entry if none exists otherwise updates the
* existing entry.
*/
export function setPassword(service: string, account: string, password: string): Promise<void>
/**
* Delete the stored password from the keychain.
@@ -35,7 +38,8 @@ export declare namespace biometrics {
* base64 encoded key and the base64 encoded challenge used to create it
* separated by a `|` character.
*
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated.
* If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
* be generated.
*
* `format!("<key_base64>|<iv_base64>")`
*/
@@ -119,8 +123,9 @@ export declare namespace ipc {
/**
* Create and start the IPC server without blocking.
*
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
* @param callback This function will be called whenever a message is received from a client.
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
* connection and must be the same for both the server and client. @param callback
* This function will be called whenever a message is received from a client.
*/
static listen(name: string, callback: (error: null | Error, message: IpcMessage) => void): Promise<IpcServer>
/** Return the path to the IPC server. */
@@ -130,8 +135,9 @@ export declare namespace ipc {
/**
* Send a message over the IPC server to all the connected clients
*
* @return The number of clients that the message was sent to. Note that the number of messages
* actually received may be less, as some clients could disconnect before receiving the message.
* @return The number of clients that the message was sent to. Note that the number of
* messages actually received may be less, as some clients could disconnect before
* receiving the message.
*/
send(message: string): number
}
@@ -194,8 +200,9 @@ export declare namespace autofill {
/**
* Create and start the IPC server without blocking.
*
* @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
* @param callback This function will be called whenever a message is received from a client.
* @param name The endpoint name to listen on. This name uniquely identifies the IPC
* connection and must be the same for both the server and client. @param callback
* This function will be called whenever a message is received from a client.
*/
static listen(name: string, registrationCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void, assertionCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void, assertionWithoutUserInterfaceCallback: (error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void): Promise<IpcServer>
/** Return the path to the IPC server. */

View File

@@ -78,12 +78,6 @@ switch (platform) {
throw new Error(`Unsupported architecture on macOS: ${arch}`);
}
break;
case "freebsd":
nativeBinding = loadFirstAvailable(
["desktop_napi.freebsd-x64.node"],
"@bitwarden/desktop-napi-freebsd-x64",
);
break;
case "linux":
switch (arch) {
case "x64":

View File

@@ -3,7 +3,7 @@
"version": "0.1.0",
"description": "",
"scripts": {
"build": "napi build --platform --js false",
"build": "node scripts/build.js",
"test": "cargo test"
},
"author": "",

View File

@@ -0,0 +1,14 @@
/* eslint-disable @typescript-eslint/no-var-requires */
const { execSync } = require('child_process');
const args = process.argv.slice(2);
const isRelease = args.includes('--release');
if (isRelease) {
console.log('Building release mode.');
} else {
console.log('Building debug mode.');
process.env.RUST_LOG = 'debug';
}
execSync(`napi build --platform --js false`, { stdio: 'inherit', env: process.env });

View File

@@ -19,7 +19,8 @@ pub mod passwords {
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry.
/// Save the password to the keychain. Adds an entry if none exists otherwise updates the
/// existing entry.
#[napi]
pub async fn set_password(
service: String,
@@ -107,7 +108,8 @@ pub mod biometrics {
/// base64 encoded key and the base64 encoded challenge used to create it
/// separated by a `|` character.
///
/// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated.
/// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will
/// be generated.
///
/// `format!("<key_base64>|<iv_base64>")`
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
@@ -556,8 +558,9 @@ pub mod ipc {
impl IpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// @param callback This function will be called whenever a message is received from a client.
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
/// connection and must be the same for both the server and client. @param callback
/// This function will be called whenever a message is received from a client.
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
#[napi(factory)]
pub async fn listen(
@@ -598,8 +601,9 @@ pub mod ipc {
/// Send a message over the IPC server to all the connected clients
///
/// @return The number of clients that the message was sent to. Note that the number of messages
/// actually received may be less, as some clients could disconnect before receiving the message.
/// @return The number of clients that the message was sent to. Note that the number of
/// messages actually received may be less, as some clients could disconnect before
/// receiving the message.
#[napi]
pub fn send(&self, message: String) -> napi::Result<u32> {
self.server
@@ -743,8 +747,9 @@ pub mod autofill {
impl IpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// @param callback This function will be called whenever a message is received from a client.
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC
/// connection and must be the same for both the server and client. @param callback
/// This function will be called whenever a message is received from a client.
#[allow(clippy::unused_async)] // FIXME: Remove unused async!
#[napi(factory)]
pub async fn listen(
@@ -946,18 +951,18 @@ pub mod logging {
//!
//! # Example
//!
//! [Elec] 14:34:03.517 [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock}
//! [Elec] 14:34:03.517 [NAPI] [INFO] desktop_core::ssh_agent::platform_ssh_agent: Starting
//! SSH Agent server {socket=/Users/foo/.bitwarden-ssh-agent.sock}
use std::fmt::Write;
use std::sync::OnceLock;
use std::{fmt::Write, sync::OnceLock};
use napi::threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use tracing::Level;
use tracing_subscriber::fmt::format::{DefaultVisitor, Writer};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
filter::EnvFilter,
fmt::format::{DefaultVisitor, Writer},
layer::SubscriberExt,
util::SubscriberInitExt,
Layer,
@@ -1044,9 +1049,17 @@ pub mod logging {
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
let _ = JS_LOGGER.0.set(js_log_fn);
// the log level hierarchy is determined by:
// - if RUST_LOG is detected at runtime
// - if RUST_LOG is provided at compile time
// - default to INFO
let filter = EnvFilter::builder()
// set the default log level to INFO.
.with_default_directive(LevelFilter::INFO.into())
.with_default_directive(
option_env!("RUST_LOG")
.unwrap_or("info")
.parse()
.expect("should provide valid log level at compile time."),
)
// parse directives from the RUST_LOG environment variable,
// overriding the default directive for matching targets.
.from_env_lossy();
@@ -1064,6 +1077,8 @@ pub mod logging {
#[napi]
pub mod chromium_importer {
use std::collections::HashMap;
use chromium_importer::{
chromium::{
DefaultInstalledBrowserRetriever, LoginImportResult as _LoginImportResult,
@@ -1071,7 +1086,6 @@ pub mod chromium_importer {
},
metadata::NativeImporterMetadata as _NativeImporterMetadata,
};
use std::collections::HashMap;
#[napi(object)]
pub struct ProfileInfo {

View File

@@ -8,17 +8,13 @@ publish = { workspace = true }
[features]
default = []
[dependencies]
[target.'cfg(target_os = "macos")'.dependencies]
anyhow = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
core-foundation = "=0.10.1"
[build-dependencies]
cc = "=1.2.4"
[target.'cfg(target_os = "macos")'.build-dependencies]
cc = "=1.2.46"
glob = "=0.3.2"
[lints]

View File

@@ -8,7 +8,7 @@ publish = { workspace = true }
[lib]
crate-type = ["cdylib"]
[dependencies]
[target.'cfg(target_os = "linux")'.dependencies]
ctor = { workspace = true }
desktop_core = { path = "../core" }
libc = { workspace = true }

View File

@@ -5,8 +5,9 @@
//! On Linux, this is PR_SET_DUMPABLE to prevent debuggers from attaching, the env
//! from being read and the memory from being stolen.
use desktop_core::process_isolation;
use std::{ffi::c_char, sync::LazyLock};
use desktop_core::process_isolation;
use tracing::info;
static ORIGINAL_UNSETENV: LazyLock<unsafe extern "C" fn(*const c_char) -> i32> =

View File

@@ -6,7 +6,6 @@ version = { workspace = true }
publish = { workspace = true }
[dependencies]
anyhow = { workspace = true }
desktop_core = { path = "../core" }
futures = { workspace = true }
tokio = { workspace = true, features = ["io-std", "io-util", "macros", "rt"] }

View File

@@ -60,7 +60,6 @@ fn init_logging(log_path: &Path, console_level: LevelFilter, file_level: LevelFi
/// a stable communication channel between the proxy and the running desktop application.
///
/// Browser extension <-[native messaging]-> proxy <-[ipc]-> desktop
///
// FIXME: Remove unwraps! They panic and terminate the whole application.
#[allow(clippy::unwrap_used)]
#[tokio::main(flavor = "current_thread")]
@@ -83,8 +82,10 @@ async fn main() {
// Different browsers send different arguments when the app starts:
//
// Firefox:
// - The complete path to the app manifest. (in the form `/Users/<user>/Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`)
// - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in the form `{[UUID]}`).
// - The complete path to the app manifest. (in the form
// `/Users/<user>/Library/.../Mozilla/NativeMessagingHosts/com.8bit.bitwarden.json`)
// - (in Firefox 55+) the ID (as given in the manifest.json) of the add-on that started it (in
// the form `{[UUID]}`).
//
// Chrome on Windows:
// - Origin of the extension that started it (in the form `chrome-extension://[ID]`).
@@ -96,7 +97,8 @@ async fn main() {
let args: Vec<_> = std::env::args().skip(1).collect();
info!(?args, "Process args");
// Setup two channels, one for sending messages to the desktop application (`out`) and one for receiving messages from the desktop application (`in`)
// Setup two channels, one for sending messages to the desktop application (`out`) and one for
// receiving messages from the desktop application (`in`)
let (in_send, in_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);
let (out_send, mut out_recv) = tokio::sync::mpsc::channel(MESSAGE_CHANNEL_BUFFER);

View File

@@ -0,0 +1,7 @@
# Wrap comments and increase the width of comments to 100
comment_width = 100
wrap_comments = true
# Sort and group imports
group_imports = "StdExternalCrate"
imports_granularity = "Crate"

View File

@@ -2,11 +2,12 @@
#![allow(non_snake_case)]
#![allow(non_camel_case_types)]
use std::ffi::c_uchar;
use std::ptr;
use windows::Win32::Foundation::*;
use windows::Win32::System::Com::*;
use windows::Win32::System::LibraryLoader::*;
use std::{ffi::c_uchar, ptr};
use windows::Win32::{
Foundation::*,
System::{Com::*, LibraryLoader::*},
};
use windows_core::*;
mod pluginauthenticator;

View File

@@ -116,7 +116,7 @@
"to": "libprocess_isolation.so"
}
],
"target": ["deb", "freebsd", "rpm", "AppImage", "snap"],
"target": ["deb", "rpm", "AppImage", "snap"],
"desktop": {
"entry": {
"Name": "Bitwarden",
@@ -252,9 +252,6 @@
"artifactName": "${productName}-${version}-${arch}.${ext}",
"fpm": ["--rpm-rpmbuild-define", "_build_id_links none"]
},
"freebsd": {
"artifactName": "${productName}-${version}-${arch}.${ext}"
},
"snap": {
"summary": "Bitwarden is a secure and free password manager for all of your devices.",
"description": "Password Manager\n**Installation**\nBitwarden requires access to the `password-manager-service`. Please enable it through permissions or by running `sudo snap connect bitwarden:password-manager-service` after installation. See https://btwrdn.com/install-snap for details.",

View File

@@ -21,11 +21,13 @@ platform :mac do
.split('.')
.map(&:strip)
.reject(&:empty?)
.map { |item| "• #{item}" }
.map { |item| "• #{item.gsub(/\A(?:•|\u2022)\s*/, '')}" }
.join("\n")
UI.message("Original changelog: #{changelog[0,100]}#{changelog.length > 100 ? '...' : ''}")
UI.message("Formatted changelog: #{formatted_changelog[0,100]}#{formatted_changelog.length > 100 ? '...' : ''}")
UI.message("Original changelog: ")
UI.message("#{changelog}")
UI.message("Formatted changelog: ")
UI.message("#{formatted_changelog}")
# Create release notes directories and files for all locales
APP_CONFIG[:locales].each do |locale|

View File

@@ -19,7 +19,7 @@
"yargs": "18.0.0"
},
"devDependencies": {
"@types/node": "22.18.11",
"@types/node": "22.19.1",
"typescript": "5.4.2"
}
},
@@ -117,9 +117,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
"version": "22.18.11",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.11.tgz",
"integrity": "sha512-Gd33J2XIrXurb+eT2ktze3rJAfAp9ZNjlBdh4SVgyrKEOADwCbdUDaK7QgJno8Ue4kcajscsKqu6n8OBG3hhCQ==",
"version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"license": "MIT",
"peer": true,
"dependencies": {

View File

@@ -24,7 +24,7 @@
"yargs": "18.0.0"
},
"devDependencies": {
"@types/node": "22.18.11",
"@types/node": "22.19.1",
"typescript": "5.4.2"
},
"_moduleAliases": {

View File

@@ -1,7 +1,7 @@
{
"name": "@bitwarden/desktop",
"description": "A secure and free password manager for all of your devices.",
"version": "2025.11.0",
"version": "2025.11.3",
"keywords": [
"bitwarden",
"password",
@@ -40,7 +40,7 @@
"pack:dir": "npm run clean:dist && electron-builder --dir -p never",
"pack:lin:flatpak": "flatpak-builder --repo=../../.flatpak-repo ../../.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ../../.flatpak-repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop",
"pack:lin": "npm run clean:dist && electron-builder --linux --x64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/",
"pack:lin:arm64": "npm run clean:dist && electron-builder --dir -p never && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .",
"pack:lin:arm64": "npm run clean:dist && electron-builder --linux --arm64 -p never && export SNAP_FILE=$(realpath ./dist/bitwarden_*.snap) && unsquashfs -d ./dist/tmp-snap/ $SNAP_FILE && mkdir -p ./dist/tmp-snap/meta/polkit/ && cp ./resources/com.bitwarden.desktop.policy ./dist/tmp-snap/meta/polkit/polkit.com.bitwarden.desktop.policy && rm $SNAP_FILE && snap pack --compression=lzo ./dist/tmp-snap/ && mv ./*.snap ./dist/ && rm -rf ./dist/tmp-snap/ && tar -czvf ./dist/bitwarden_desktop_arm64.tar.gz -C ./dist/linux-arm64-unpacked/ .",
"pack:mac": "npm run clean:dist && electron-builder --mac --universal -p never",
"pack:mac:with-extension": "npm run clean:dist && npm run build:macos-extension:mac && electron-builder --mac --universal -p never",
"pack:mac:arm64": "npm run clean:dist && electron-builder --mac --arm64 -p never",

View File

@@ -31,36 +31,50 @@
</h2>
<ng-container *ngIf="showSecurity">
<bit-section disableMargin>
<bit-section-header>
<h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
</bit-section-header>
@if (consolidatedSessionTimeoutComponent$ | async) {
<bit-section-header>
<h2 bitTypography="h6">{{ "sessionTimeoutHeader" | i18n }}</h2>
</bit-section-header>
<auth-vault-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</auth-vault-timeout-input>
<bit-session-timeout-settings
[refreshTimeoutActionSettings]="refreshTimeoutSettings$"
/>
} @else {
<bit-section-header>
<h2 bitTypography="h6">{{ "vaultTimeoutHeader" | i18n }}</h2>
</bit-section-header>
<bit-form-field disableMargin>
<bit-label for="vaultTimeoutAction">{{ "vaultTimeoutAction1" | i18n }}</bit-label>
<bit-select id="vaultTimeoutAction" formControlName="vaultTimeoutAction">
<bit-option
*ngFor="let action of availableVaultTimeoutActions"
[value]="action"
[label]="action | i18n"
<bit-session-timeout-input
[vaultTimeoutOptions]="vaultTimeoutOptions"
[formControl]="form.controls.vaultTimeout"
ngDefaultControl
>
</bit-session-timeout-input>
<bit-form-field disableMargin>
<bit-label for="vaultTimeoutAction">{{
"vaultTimeoutAction1" | i18n
}}</bit-label>
<bit-select id="vaultTimeoutAction" formControlName="vaultTimeoutAction">
<bit-option
*ngFor="let action of availableVaultTimeoutActions"
[value]="action"
[label]="action | i18n"
>
</bit-option>
</bit-select>
<bit-hint
*ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)"
>
</bit-option>
</bit-select>
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}<br />
</bit-hint>
</bit-form-field>
<bit-hint *ngIf="!availableVaultTimeoutActions.includes(VaultTimeoutAction.Lock)">
{{ "unlockMethodNeededToChangeTimeoutActionDesc" | i18n }}<br />
<bit-hint *ngIf="hasVaultTimeoutPolicy" class="tw-mt-4">
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
</bit-hint>
</bit-form-field>
<bit-hint *ngIf="hasVaultTimeoutPolicy" class="tw-mt-4">
{{ "vaultTimeoutPolicyAffectingOptions" | i18n }}
</bit-hint>
}
</bit-section>
<div class="form-group tw-mt-4" *ngIf="(pinEnabled$ | async) || this.form.value.pin">
<div class="checkbox">

View File

@@ -191,7 +191,7 @@ describe("SettingsComponent", () => {
desktopAutotypeService.autotypeEnabledUserSetting$ = of(false);
desktopAutotypeService.autotypeKeyboardShortcut$ = of(["Control", "Shift", "B"]);
billingAccountProfileStateService.hasPremiumFromAnySource$.mockReturnValue(of(false));
configService.getFeatureFlag$.mockReturnValue(of(true));
configService.getFeatureFlag$.mockReturnValue(of(false));
});
afterEach(() => {

View File

@@ -9,7 +9,6 @@ import { concatMap, map, pairwise, startWith, switchMap, takeUntil, timeout } fr
import { PremiumBadgeComponent } from "@bitwarden/angular/billing/components/premium-badge";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { VaultTimeoutInputComponent } from "@bitwarden/auth/angular";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { getFirstPolicy } from "@bitwarden/common/admin-console/services/policy/default-policy.service";
@@ -55,6 +54,10 @@ import {
TypographyModule,
} from "@bitwarden/components";
import { KeyService, BiometricStateService, BiometricsStatus } from "@bitwarden/key-management";
import {
SessionTimeoutInputComponent,
SessionTimeoutSettingsComponent,
} from "@bitwarden/key-management-ui";
import { PermitCipherDetailsPopoverComponent } from "@bitwarden/vault";
import { SetPinComponent } from "../../auth/components/set-pin.component";
@@ -94,7 +97,8 @@ import { NativeMessagingManifestService } from "../services/native-messaging-man
SectionHeaderComponent,
SelectModule,
TypographyModule,
VaultTimeoutInputComponent,
SessionTimeoutInputComponent,
SessionTimeoutSettingsComponent,
PermitCipherDetailsPopoverComponent,
PremiumBadgeComponent,
],
@@ -146,6 +150,8 @@ export class SettingsComponent implements OnInit, OnDestroy {
pinEnabled$: Observable<boolean> = of(true);
isWindowsV2BiometricsEnabled: boolean = false;
consolidatedSessionTimeoutComponent$: Observable<boolean>;
form = this.formBuilder.group({
// Security
vaultTimeout: [null as VaultTimeout | null],
@@ -184,7 +190,7 @@ export class SettingsComponent implements OnInit, OnDestroy {
locale: [null as string | null],
});
private refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
protected refreshTimeoutSettings$ = new BehaviorSubject<void>(undefined);
private destroy$ = new Subject<void>();
constructor(
@@ -282,12 +288,17 @@ export class SettingsComponent implements OnInit, OnDestroy {
value: SshAgentPromptType.RememberUntilLock,
},
];
this.consolidatedSessionTimeoutComponent$ = this.configService.getFeatureFlag$(
FeatureFlag.ConsolidatedSessionTimeoutComponent,
);
}
async ngOnInit() {
this.vaultTimeoutOptions = await this.generateVaultTimeoutOptions();
this.isWindowsV2BiometricsEnabled = await this.biometricsService.isWindowsV2BiometricsEnabled();
this.vaultTimeoutOptions = await this.generateVaultTimeoutOptions();
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
// Autotype is for Windows initially
@@ -828,22 +839,6 @@ export class SettingsComponent implements OnInit, OnDestroy {
ipc.platform.allowBrowserintegrationOverride || ipc.platform.isDev;
if (!skipSupportedPlatformCheck) {
if (
ipc.platform.deviceType === DeviceType.MacOsDesktop &&
!this.platformUtilsService.isMacAppStore()
) {
await this.dialogService.openSimpleDialog({
title: { key: "browserIntegrationUnsupportedTitle" },
content: { key: "browserIntegrationMasOnlyDesc" },
acceptButtonText: { key: "ok" },
cancelButtonText: null,
type: "warning",
});
this.form.controls.enableBrowserIntegration.setValue(false);
return;
}
if (ipc.platform.isWindowsStore) {
await this.dialogService.openSimpleDialog({
title: { key: "browserIntegrationUnsupportedTitle" },

View File

@@ -32,6 +32,7 @@ import { ModalService } from "@bitwarden/angular/services/modal.service";
import { FingerprintDialogComponent } from "@bitwarden/auth/angular";
import {
DESKTOP_SSO_CALLBACK,
LockService,
LogoutReason,
UserDecryptionOptionsServiceAbstraction,
} from "@bitwarden/auth/common";
@@ -195,6 +196,7 @@ export class AppComponent implements OnInit, OnDestroy {
private pinService: PinServiceAbstraction,
private readonly tokenService: TokenService,
private desktopAutotypeDefaultSettingPolicy: DesktopAutotypeDefaultSettingPolicy,
private readonly lockService: LockService,
) {
this.deviceTrustToastService.setupListeners$.pipe(takeUntilDestroyed()).subscribe();
@@ -245,7 +247,7 @@ export class AppComponent implements OnInit, OnDestroy {
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.processReloadService.startProcessReload(this.authService);
await this.processReloadService.startProcessReload();
break;
case "authBlocked":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
@@ -258,21 +260,10 @@ export class AppComponent implements OnInit, OnDestroy {
this.loading = false;
break;
case "lockVault":
await this.vaultTimeoutService.lock(message.userId);
await this.lockService.lock(message.userId);
break;
case "lockAllVaults": {
const currentUser = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a.id)),
);
const accounts = await firstValueFrom(this.accountService.accounts$);
await this.vaultTimeoutService.lock(currentUser);
for (const account of Object.keys(accounts)) {
if (account === currentUser) {
continue;
}
await this.vaultTimeoutService.lock(account);
}
await this.lockService.lockAll();
break;
}
case "locked":
@@ -286,12 +277,12 @@ export class AppComponent implements OnInit, OnDestroy {
}
await this.updateAppMenu();
await this.systemService.clearPendingClipboard();
await this.processReloadService.startProcessReload(this.authService);
await this.processReloadService.startProcessReload();
break;
case "startProcessReload":
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.processReloadService.startProcessReload(this.authService);
this.processReloadService.startProcessReload();
break;
case "cancelProcessReload":
this.processReloadService.cancelProcessReload();
@@ -736,8 +727,6 @@ export class AppComponent implements OnInit, OnDestroy {
}
}
await this.updateAppMenu();
// This must come last otherwise the logout will prematurely trigger
// a process reload before all the state service user data can be cleaned up
this.authService.logOut(async () => {}, userBeingLoggedOut);
@@ -814,11 +803,9 @@ export class AppComponent implements OnInit, OnDestroy {
}
const options = await this.getVaultTimeoutOptions(userId);
if (options[0] === timeout) {
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises
options[1] === "logOut"
? this.logOut("vaultTimeout", userId as UserId)
: await this.vaultTimeoutService.lock(userId);
? await this.logOut("vaultTimeout", userId as UserId)
: await this.lockService.lock(userId as UserId);
}
}
}

View File

@@ -1,7 +1,13 @@
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
DIALOG_DATA,
ButtonModule,
DialogModule,
DialogService,
CenterPositionStrategy,
} from "@bitwarden/components";
export type BrowserSyncVerificationDialogParams = {
fingerprint: string[];
@@ -19,6 +25,7 @@ export class BrowserSyncVerificationDialogComponent {
static open(dialogService: DialogService, data: BrowserSyncVerificationDialogParams) {
return dialogService.open(BrowserSyncVerificationDialogComponent, {
data,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -1,7 +1,13 @@
import { Component, Inject } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DIALOG_DATA, ButtonModule, DialogModule, DialogService } from "@bitwarden/components";
import {
DIALOG_DATA,
ButtonModule,
DialogModule,
DialogService,
CenterPositionStrategy,
} from "@bitwarden/components";
export type VerifyNativeMessagingDialogData = {
applicationName: string;
@@ -19,6 +25,7 @@ export class VerifyNativeMessagingDialogComponent {
static open(dialogService: DialogService, data: VerifyNativeMessagingDialogData) {
return dialogService.open<boolean>(VerifyNativeMessagingDialogComponent, {
data,
positionStrategy: new CenterPositionStrategy(),
});
}
}

View File

@@ -6,7 +6,7 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { WINDOW } from "@bitwarden/angular/services/injection-tokens";
import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TwoFactorService as TwoFactorServiceAbstraction } from "@bitwarden/common/auth/abstractions/two-factor.service";
import { TwoFactorService } from "@bitwarden/common/auth/two-factor";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { DefaultVaultTimeoutService } from "@bitwarden/common/key-management/vault-timeout";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -39,7 +39,7 @@ export class InitService {
private vaultTimeoutService: DefaultVaultTimeoutService,
private i18nService: I18nServiceAbstraction,
private eventUploadService: EventUploadServiceAbstraction,
private twoFactorService: TwoFactorServiceAbstraction,
private twoFactorService: TwoFactorService,
private notificationsService: ServerNotificationsService,
private platformUtilsService: PlatformUtilsServiceAbstraction,
private stateService: StateServiceAbstraction,

View File

@@ -77,7 +77,10 @@ import {
LogService as LogServiceAbstraction,
} from "@bitwarden/common/platform/abstractions/log.service";
import { MessagingService as MessagingServiceAbstraction } from "@bitwarden/common/platform/abstractions/messaging.service";
import { PlatformUtilsService as PlatformUtilsServiceAbstraction } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import {
PlatformUtilsService,
PlatformUtilsService as PlatformUtilsServiceAbstraction,
} from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { SdkClientFactory } from "@bitwarden/common/platform/abstractions/sdk/sdk-client-factory";
import { SdkLoadService } from "@bitwarden/common/platform/abstractions/sdk/sdk-load.service";
import { StateService as StateServiceAbstraction } from "@bitwarden/common/platform/abstractions/state.service";
@@ -106,7 +109,10 @@ import {
BiometricStateService,
BiometricsService,
} from "@bitwarden/key-management";
import { LockComponentService } from "@bitwarden/key-management-ui";
import {
LockComponentService,
SessionTimeoutSettingsComponentService,
} from "@bitwarden/key-management-ui";
import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { DefaultSshImportPromptService, SshImportPromptService } from "@bitwarden/vault";
@@ -122,6 +128,7 @@ import { DesktopBiometricsService } from "../../key-management/biometrics/deskto
import { RendererBiometricsService } from "../../key-management/biometrics/renderer-biometrics.service";
import { ElectronKeyService } from "../../key-management/electron-key.service";
import { DesktopLockComponentService } from "../../key-management/lock/services/desktop-lock-component.service";
import { DesktopSessionTimeoutSettingsComponentService } from "../../key-management/session-timeout/services/desktop-session-timeout-settings-component.service";
import { flagEnabled } from "../../platform/flags";
import { DesktopSettingsService } from "../../platform/services/desktop-settings.service";
import { ElectronLogRendererService } from "../../platform/services/electron-log.renderer.service";
@@ -262,6 +269,7 @@ const safeProviders: SafeProvider[] = [
BiometricStateService,
AccountServiceAbstraction,
LogService,
AuthServiceAbstraction,
],
}),
safeProvider({
@@ -336,6 +344,7 @@ const safeProviders: SafeProvider[] = [
ConfigService,
Fido2AuthenticatorServiceAbstraction,
AccountService,
PlatformUtilsService,
],
}),
safeProvider({
@@ -475,6 +484,11 @@ const safeProviders: SafeProvider[] = [
useClass: DesktopAutotypeDefaultSettingPolicy,
deps: [AccountServiceAbstraction, AuthServiceAbstraction, InternalPolicyService, ConfigService],
}),
safeProvider({
provide: SessionTimeoutSettingsComponentService,
useClass: DesktopSessionTimeoutSettingsComponentService,
deps: [I18nServiceAbstraction],
}),
];
@NgModule({

View File

@@ -119,7 +119,9 @@ describe("DesktopSetInitialPasswordService", () => {
userDecryptionOptions = new UserDecryptionOptions({ hasMasterPassword: true });
userDecryptionOptionsSubject = new BehaviorSubject(userDecryptionOptions);
userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject;
userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue(
userDecryptionOptionsSubject,
);
setPasswordRequest = new SetPasswordRequest(
credentials.newServerMasterKeyHash,

View File

@@ -3,6 +3,7 @@ import { Component } from "@angular/core";
import { JslibModule } from "@bitwarden/angular/jslib.module";
import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
import type { chromium_importer } from "@bitwarden/desktop-napi";
import { ImportMetadataServiceAbstraction } from "@bitwarden/importer-core";
import {
ImportComponent,
@@ -47,11 +48,14 @@ export class ImportDesktopComponent {
this.dialogRef.close();
}
protected onLoadProfilesFromBrowser(browser: string): Promise<any[]> {
protected onLoadProfilesFromBrowser(browser: string): Promise<chromium_importer.ProfileInfo[]> {
return ipc.tools.chromiumImporter.getAvailableProfiles(browser);
}
protected onImportFromBrowser(browser: string, profile: string): Promise<any[]> {
protected onImportFromBrowser(
browser: string,
profile: string,
): Promise<chromium_importer.LoginImportResult[]> {
return ipc.tools.chromiumImporter.importLogins(browser, profile);
}
}

View File

@@ -5,9 +5,12 @@ import type { chromium_importer } from "@bitwarden/desktop-napi";
const chromiumImporter = {
getMetadata: (): Promise<Record<string, chromium_importer.NativeImporterMetadata>> =>
ipcRenderer.invoke("chromium_importer.getMetadata"),
getAvailableProfiles: (browser: string): Promise<any[]> =>
getAvailableProfiles: (browser: string): Promise<chromium_importer.ProfileInfo[]> =>
ipcRenderer.invoke("chromium_importer.getAvailableProfiles", browser),
importLogins: (browser: string, profileId: string): Promise<any[]> =>
importLogins: (
browser: string,
profileId: string,
): Promise<chromium_importer.LoginImportResult[]> =>
ipcRenderer.invoke("chromium_importer.importLogins", browser, profileId),
};

View File

@@ -1,6 +1,6 @@
<form [bitSubmit]="submit" [formGroup]="setPinForm">
<bit-dialog>
<div class="tw-font-semibold" bitDialogTitle>
<div class="tw-font-medium" bitDialogTitle>
{{ "unlockWithPin" | i18n }}
</div>
<div bitDialogContent>

View File

@@ -1,6 +1,6 @@
<form [bitSubmit]="submit" [formGroup]="setShortcutForm">
<bit-dialog>
<div class="tw-font-semibold" bitDialogTitle>
<div class="tw-font-medium" bitDialogTitle>
{{ "typeShortcut" | i18n }}
</div>
<div bitDialogContent>

View File

@@ -5,6 +5,8 @@ import { LogService } from "@bitwarden/logging";
import { WindowMain } from "../../main/window.main";
import { stringIsNotUndefinedNullAndEmpty } from "../../utils";
import { AutotypeMatchError } from "../models/autotype-errors";
import { AutotypeVaultData } from "../models/autotype-vault-data";
import { AutotypeKeyboardShortcut } from "../models/main-autotype-keyboard-shortcut";
export class MainDesktopAutotypeService {
@@ -47,20 +49,22 @@ export class MainDesktopAutotypeService {
}
});
ipcMain.on("autofill.completeAutotypeRequest", (event, data) => {
const { response } = data;
ipcMain.on("autofill.completeAutotypeRequest", (_event, vaultData: AutotypeVaultData) => {
if (
stringIsNotUndefinedNullAndEmpty(response.username) &&
stringIsNotUndefinedNullAndEmpty(response.password)
stringIsNotUndefinedNullAndEmpty(vaultData.username) &&
stringIsNotUndefinedNullAndEmpty(vaultData.password)
) {
this.doAutotype(
response.username,
response.password,
this.autotypeKeyboardShortcut.getArrayFormat(),
);
this.doAutotype(vaultData, this.autotypeKeyboardShortcut.getArrayFormat());
}
});
ipcMain.on("autofill.completeAutotypeError", (_event, matchError: AutotypeMatchError) => {
this.logService.debug(
"autofill.completeAutotypeError",
"No match for window: " + matchError.windowTitle,
);
this.logService.error("autofill.completeAutotypeError", matchError.errorMessage);
});
}
disableAutotype() {
@@ -89,8 +93,9 @@ export class MainDesktopAutotypeService {
: this.logService.info("Enabling autotype failed.");
}
private doAutotype(username: string, password: string, keyboardShortcut: string[]) {
const inputPattern = username + "\t" + password;
private doAutotype(vaultData: AutotypeVaultData, keyboardShortcut: string[]) {
const TAB = "\t";
const inputPattern = vaultData.username + TAB + vaultData.password;
const inputArray = new Array<number>(inputPattern.length);
for (let i = 0; i < inputPattern.length; i++) {

View File

@@ -0,0 +1,8 @@
/**
* This error is surfaced when there is no matching
* vault item found.
*/
export interface AutotypeMatchError {
windowTitle: string;
errorMessage: string;
}

View File

@@ -0,0 +1,8 @@
/**
* Vault data used in autotype operations.
* `username` and `password` are guaranteed to be not null/undefined.
*/
export interface AutotypeVaultData {
username: string;
password: string;
}

View File

@@ -5,6 +5,9 @@ import type { autofill } from "@bitwarden/desktop-napi";
import { Command } from "../platform/main/autofill/command";
import { RunCommandParams, RunCommandResult } from "../platform/main/autofill/native-autofill.main";
import { AutotypeMatchError } from "./models/autotype-errors";
import { AutotypeVaultData } from "./models/autotype-vault-data";
export default {
runCommand: <C extends Command>(params: RunCommandParams<C>): Promise<RunCommandResult<C>> =>
ipcRenderer.invoke("autofill.runCommand", params),
@@ -133,35 +136,31 @@ export default {
listenAutotypeRequest: (
fn: (
windowTitle: string,
completeCallback: (
error: Error | null,
response: { username?: string; password?: string },
) => void,
completeCallback: (error: Error | null, response: AutotypeVaultData | null) => void,
) => void,
) => {
ipcRenderer.on(
"autofill.listenAutotypeRequest",
(
event,
_event,
data: {
windowTitle: string;
},
) => {
const { windowTitle } = data;
fn(windowTitle, (error, response) => {
fn(windowTitle, (error, vaultData) => {
if (error) {
ipcRenderer.send("autofill.completeError", {
const matchError: AutotypeMatchError = {
windowTitle,
error: error.message,
});
errorMessage: error.message,
};
ipcRenderer.send("autofill.completeAutotypeError", matchError);
return;
}
ipcRenderer.send("autofill.completeAutotypeRequest", {
windowTitle,
response,
});
if (vaultData !== null) {
ipcRenderer.send("autofill.completeAutotypeRequest", vaultData);
}
});
},
);

View File

@@ -13,6 +13,7 @@ import {
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { DeviceType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
@@ -24,6 +25,7 @@ import {
Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction,
} from "@bitwarden/common/platform/abstractions/fido2/fido2-authenticator.service.abstraction";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { parseCredentialId } from "@bitwarden/common/platform/services/fido2/credential-id-utils";
import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
@@ -53,9 +55,15 @@ export class DesktopAutofillService implements OnDestroy {
private configService: ConfigService,
private fido2AuthenticatorService: Fido2AuthenticatorServiceAbstraction<NativeWindowObject>,
private accountService: AccountService,
private platformUtilsService: PlatformUtilsService,
) {}
async init() {
// Currently only supported for MacOS
if (this.platformUtilsService.getDevice() !== DeviceType.MacOsDesktop) {
return;
}
this.configService
.getFeatureFlag$(FeatureFlag.MacOsNativeCredentialSync)
.pipe(

View File

@@ -0,0 +1,50 @@
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { getAutotypeVaultData } from "./desktop-autotype.service";
describe("getAutotypeVaultData", () => {
it("should return vault data when cipher has username and password", () => {
const cipherView = new CipherView();
cipherView.login.username = "foo";
cipherView.login.password = "bar";
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(error).toBeNull();
expect(vaultData?.username).toEqual("foo");
expect(vaultData?.password).toEqual("bar");
});
it("should return error when firstCipher is undefined", () => {
const cipherView = undefined;
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(vaultData).toBeNull();
expect(error).toBeDefined();
expect(error?.message).toEqual("No matching vault item.");
});
it("should return error when username is undefined", () => {
const cipherView = new CipherView();
cipherView.login.username = undefined;
cipherView.login.password = "bar";
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(vaultData).toBeNull();
expect(error).toBeDefined();
expect(error?.message).toEqual("Vault item is undefined.");
});
it("should return error when password is undefined", () => {
const cipherView = new CipherView();
cipherView.login.username = "foo";
cipherView.login.password = undefined;
const [error, vaultData] = getAutotypeVaultData(cipherView);
expect(vaultData).toBeNull();
expect(error).toBeDefined();
expect(error?.message).toEqual("Vault item is undefined.");
});
});

View File

@@ -17,6 +17,8 @@ import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.servi
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { UserId } from "@bitwarden/user-core";
import { AutotypeVaultData } from "../models/autotype-vault-data";
import { DesktopAutotypeDefaultSettingPolicy } from "./desktop-autotype-policy.service";
export const defaultWindowsAutotypeKeyboardShortcut: string[] = ["Control", "Shift", "B"];
@@ -27,6 +29,8 @@ export const AUTOTYPE_ENABLED = new KeyDefinition<boolean | null>(
{ deserializer: (b) => b },
);
export type Result<T, E = Error> = [E, null] | [null, T];
/*
Valid windows shortcut keys: Control, Alt, Super, Shift, letters A - Z
Valid macOS shortcut keys: Control, Alt, Command, Shift, letters A - Z
@@ -63,11 +67,8 @@ export class DesktopAutotypeService {
ipc.autofill.listenAutotypeRequest(async (windowTitle, callback) => {
const possibleCiphers = await this.matchCiphersToWindowTitle(windowTitle);
const firstCipher = possibleCiphers?.at(0);
return callback(null, {
username: firstCipher?.login?.username,
password: firstCipher?.login?.password,
});
const [error, vaultData] = getAutotypeVaultData(firstCipher);
callback(error, vaultData);
});
}
@@ -176,3 +177,23 @@ export class DesktopAutotypeService {
return possibleCiphers;
}
}
/**
* @return an `AutotypeVaultData` object or an `Error` if the
* cipher or vault data within are undefined.
*/
export function getAutotypeVaultData(
cipherView: CipherView | undefined,
): Result<AutotypeVaultData> {
if (!cipherView) {
return [Error("No matching vault item."), null];
} else if (cipherView.login.username === undefined || cipherView.login.password === undefined) {
return [Error("Vault item is undefined."), null];
} else {
const vaultData: AutotypeVaultData = {
username: cipherView.login.username,
password: cipherView.login.password,
};
return [null, vaultData];
}
}

View File

@@ -0,0 +1,48 @@
import { defer, from, map, Observable } from "rxjs";
import {
VaultTimeout,
VaultTimeoutOption,
VaultTimeoutStringType,
} from "@bitwarden/common/key-management/vault-timeout";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { SessionTimeoutSettingsComponentService } from "@bitwarden/key-management-ui";
export class DesktopSessionTimeoutSettingsComponentService
implements SessionTimeoutSettingsComponentService
{
availableTimeoutOptions$: Observable<VaultTimeoutOption[]> = defer(() =>
from(ipc.platform.powermonitor.isLockMonitorAvailable()).pipe(
map((isLockMonitorAvailable) => {
const options: VaultTimeoutOption[] = [
{ name: this.i18nService.t("oneMinute"), value: 1 },
{ name: this.i18nService.t("fiveMinutes"), value: 5 },
{ name: this.i18nService.t("fifteenMinutes"), value: 15 },
{ name: this.i18nService.t("thirtyMinutes"), value: 30 },
{ name: this.i18nService.t("oneHour"), value: 60 },
{ name: this.i18nService.t("fourHours"), value: 240 },
{ name: this.i18nService.t("onIdle"), value: VaultTimeoutStringType.OnIdle },
{ name: this.i18nService.t("onSleep"), value: VaultTimeoutStringType.OnSleep },
];
if (isLockMonitorAvailable) {
options.push({
name: this.i18nService.t("onLocked"),
value: VaultTimeoutStringType.OnLocked,
});
}
options.push(
{ name: this.i18nService.t("onRestart"), value: VaultTimeoutStringType.OnRestart },
{ name: this.i18nService.t("never"), value: VaultTimeoutStringType.Never },
);
return options;
}),
),
);
constructor(private readonly i18nService: I18nService) {}
onTimeoutSave(_: VaultTimeout): void {}
}

View File

@@ -69,6 +69,9 @@
}
}
},
"noEditPermissions": {
"message": "You don't have permission to edit this item"
},
"welcomeBack": {
"message": "Welcome back"
},
@@ -1035,6 +1038,9 @@
"selfHostedEnvFormInvalid": {
"message": "You must add either the base Server URL or at least one custom environment."
},
"selfHostedEnvMustUseHttps": {
"message": "URLs must use HTTPS."
},
"customEnvironment": {
"message": "Pasgemaakte omgewing"
},
@@ -2144,9 +2150,6 @@
"browserIntegrationErrorDesc": {
"message": "An error has occurred while enabling browser integration."
},
"browserIntegrationMasOnlyDesc": {
"message": "Ongelukkig word blaaierintegrasie tans slegs in die weergawe vir die Mac-toepwinkel ondersteun."
},
"browserIntegrationWindowsStoreDesc": {
"message": "Ongelukkig word blaaierintegrasie tans nie in die weergawe vir die Windows-winkel ondersteun nie."
},
@@ -4190,5 +4193,35 @@
},
"cardNumberLabel": {
"message": "Card number"
},
"upgradeNow": {
"message": "Upgrade now"
},
"builtInAuthenticator": {
"message": "Built-in authenticator"
},
"secureFileStorage": {
"message": "Secure file storage"
},
"emergencyAccess": {
"message": "Emergency access"
},
"breachMonitoring": {
"message": "Breach monitoring"
},
"andMoreFeatures": {
"message": "And more!"
},
"planDescPremium": {
"message": "Complete online security"
},
"upgradeToPremium": {
"message": "Upgrade to Premium"
},
"sessionTimeoutSettingsAction": {
"message": "Timeout action"
},
"sessionTimeoutHeader": {
"message": "Session timeout"
}
}

View File

@@ -69,6 +69,9 @@
}
}
},
"noEditPermissions": {
"message": "You don't have permission to edit this item"
},
"welcomeBack": {
"message": "مرحبًا بعودتك"
},
@@ -1035,6 +1038,9 @@
"selfHostedEnvFormInvalid": {
"message": "يجب عليك إضافة رابط الخادم الأساسي أو على الأقل بيئة مخصصة."
},
"selfHostedEnvMustUseHttps": {
"message": "URLs must use HTTPS."
},
"customEnvironment": {
"message": "بيئة مخصصة"
},
@@ -2144,9 +2150,6 @@
"browserIntegrationErrorDesc": {
"message": "حدث خطأ أثناء تمكين دمج المتصفح."
},
"browserIntegrationMasOnlyDesc": {
"message": "للأسف، لا يتم دعم تكامل المتصفح إلا في إصدار متجر تطبيقات ماك في الوقت الحالي."
},
"browserIntegrationWindowsStoreDesc": {
"message": "للأسف، لا يتم دعم تكامل المتصفح في إصدار متجر ويندوز في الوقت الحالي."
},
@@ -4190,5 +4193,35 @@
},
"cardNumberLabel": {
"message": "Card number"
},
"upgradeNow": {
"message": "Upgrade now"
},
"builtInAuthenticator": {
"message": "Built-in authenticator"
},
"secureFileStorage": {
"message": "Secure file storage"
},
"emergencyAccess": {
"message": "Emergency access"
},
"breachMonitoring": {
"message": "Breach monitoring"
},
"andMoreFeatures": {
"message": "And more!"
},
"planDescPremium": {
"message": "Complete online security"
},
"upgradeToPremium": {
"message": "Upgrade to Premium"
},
"sessionTimeoutSettingsAction": {
"message": "Timeout action"
},
"sessionTimeoutHeader": {
"message": "Session timeout"
}
}

View File

@@ -69,6 +69,9 @@
}
}
},
"noEditPermissions": {
"message": "Bu elementə düzəliş etmə icazəniz yoxdur"
},
"welcomeBack": {
"message": "Yenidən xoş gəlmisiniz"
},
@@ -508,7 +511,7 @@
"description": "This describes a value that is 'linked' (related) to another value."
},
"remove": {
"message": "Çıxart"
"message": "Xaric et"
},
"nameRequired": {
"message": "Ad lazımdır."
@@ -772,7 +775,7 @@
"message": "Vahid daxil olma üsulunu istifadə et"
},
"yourOrganizationRequiresSingleSignOn": {
"message": "Your organization requires single sign-on."
"message": "Təşkilatınız, vahid daxil olma tələb edir."
},
"submit": {
"message": "Göndər"
@@ -1035,6 +1038,9 @@
"selfHostedEnvFormInvalid": {
"message": "Təməl server URL-sini və ya ən azı bir özəl mühiti əlavə etməlisiniz."
},
"selfHostedEnvMustUseHttps": {
"message": "URL-lər, HTTPS istifadə etməlidir."
},
"customEnvironment": {
"message": "Özəl mühit"
},
@@ -1653,7 +1659,7 @@
}
},
"passwordSafe": {
"message": "Bu parol, veri pozuntularında qeydə alınmayıb. Rahatlıqla istifadə edə bilərsiniz."
"message": "Bu parol, veri pozuntularında qeydə alınmayıb. Əmniyyətlə istifadə edə bilərsiniz."
},
"baseDomain": {
"message": "Baza domeni",
@@ -2144,9 +2150,6 @@
"browserIntegrationErrorDesc": {
"message": "Brauzer inteqrasiyasını fəallaşdırarkən bir xəta baş verdi."
},
"browserIntegrationMasOnlyDesc": {
"message": "Təəssüf ki, brauzer inteqrasiyası indilik yalnız Mac App Store versiyasında dəstəklənir."
},
"browserIntegrationWindowsStoreDesc": {
"message": "Təəssüf ki, brauzer inteqrasiyası hal-hazırda Windows Store versiyasında dəstəklənmir."
},
@@ -3900,7 +3903,7 @@
"message": "Ana qovluğun adından sonra \"/\" əlavə edərək qovluğu ardıcıl yerləşdirin. Nümunə: Social/Forums"
},
"sendsTitleNoItems": {
"message": "Send, həssas məlumatlar təhlükəsizdir",
"message": "Send ilə həssas məlumatlar əmniyyətdədir",
"description": "'Send' is a noun and the name of a feature called 'Bitwarden Send'. It should not be translated."
},
"sendsBodyNoItems": {
@@ -3955,7 +3958,7 @@
"message": "Kimliklərinizlə, uzun qeydiyyat və ya əlaqə xanalarını daha tez avtomatik doldurun."
},
"newNoteNudgeTitle": {
"message": "Həssas verilərinizi güvənli şəkildə saxlayın"
"message": "Həssas verilərinizi əmniyyətdə saxlayın"
},
"newNoteNudgeBody": {
"message": "Notlarla, bankçılıq və ya sığorta təfsilatları kimi həssas veriləri təhlükəsiz saxlayın."
@@ -4190,5 +4193,35 @@
},
"cardNumberLabel": {
"message": "Kart nömrəsi"
},
"upgradeNow": {
"message": "İndi yüksəlt"
},
"builtInAuthenticator": {
"message": "Daxili kimlik doğrulayıcı"
},
"secureFileStorage": {
"message": "Güvənli fayl anbarı"
},
"emergencyAccess": {
"message": "Fövqəladə hal erişimi"
},
"breachMonitoring": {
"message": "Pozuntu monitorinqi"
},
"andMoreFeatures": {
"message": "Və daha çoxu!"
},
"planDescPremium": {
"message": "Tam onlayn təhlükəsizlik"
},
"upgradeToPremium": {
"message": "\"Premium\"a yüksəlt"
},
"sessionTimeoutSettingsAction": {
"message": "Vaxt bitmə əməliyyatı"
},
"sessionTimeoutHeader": {
"message": "Sessiya vaxt bitməsi"
}
}

View File

@@ -69,6 +69,9 @@
}
}
},
"noEditPermissions": {
"message": "You don't have permission to edit this item"
},
"welcomeBack": {
"message": "Welcome back"
},
@@ -1035,6 +1038,9 @@
"selfHostedEnvFormInvalid": {
"message": "You must add either the base Server URL or at least one custom environment."
},
"selfHostedEnvMustUseHttps": {
"message": "URLs must use HTTPS."
},
"customEnvironment": {
"message": "Карыстальніцкае асяроддзе"
},
@@ -2144,9 +2150,6 @@
"browserIntegrationErrorDesc": {
"message": "An error has occurred while enabling browser integration."
},
"browserIntegrationMasOnlyDesc": {
"message": "На жаль, інтэграцыя з браўзерам зараз падтрымліваецца толькі ў версіі для Mac App Store."
},
"browserIntegrationWindowsStoreDesc": {
"message": "На жаль, інтэграцыя з браўзерам у цяперашні час не падтрымліваецца ў версіі для Microsoft Store."
},
@@ -4190,5 +4193,35 @@
},
"cardNumberLabel": {
"message": "Card number"
},
"upgradeNow": {
"message": "Upgrade now"
},
"builtInAuthenticator": {
"message": "Built-in authenticator"
},
"secureFileStorage": {
"message": "Secure file storage"
},
"emergencyAccess": {
"message": "Emergency access"
},
"breachMonitoring": {
"message": "Breach monitoring"
},
"andMoreFeatures": {
"message": "And more!"
},
"planDescPremium": {
"message": "Complete online security"
},
"upgradeToPremium": {
"message": "Upgrade to Premium"
},
"sessionTimeoutSettingsAction": {
"message": "Timeout action"
},
"sessionTimeoutHeader": {
"message": "Session timeout"
}
}

Some files were not shown because too many files have changed in this diff Show More