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:
279
apps/desktop/desktop_native/Cargo.lock
generated
279
apps/desktop/desktop_native/Cargo.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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::*;
|
||||
@@ -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"];
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
mod config;
|
||||
mod crypto;
|
||||
mod impersonate;
|
||||
mod log;
|
||||
mod main;
|
||||
|
||||
pub(crate) use main::main;
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
browser’s 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 @@ browser’s 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.
|
||||
|
||||
15
apps/desktop/desktop_native/chromium_importer/build.rs
Normal file
15
apps/desktop/desktop_native/chromium_importer/build.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>(
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
pub mod config {
|
||||
include!("../config_constants.rs");
|
||||
}
|
||||
|
||||
pub mod chromium;
|
||||
pub mod metadata;
|
||||
mod util;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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>;
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
23
apps/desktop/desktop_native/napi/index.d.ts
vendored
23
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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. */
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
14
apps/desktop/desktop_native/napi/scripts/build.js
Normal file
14
apps/desktop/desktop_native/napi/scripts/build.js
Normal 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 });
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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> =
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
7
apps/desktop/desktop_native/rustfmt.toml
Normal file
7
apps/desktop/desktop_native/rustfmt.toml
Normal 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"
|
||||
@@ -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;
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"yargs": "18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.18.11",
|
||||
"@types/node": "22.19.1",
|
||||
"typescript": "5.4.2"
|
||||
},
|
||||
"_moduleAliases": {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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++) {
|
||||
|
||||
8
apps/desktop/src/autofill/models/autotype-errors.ts
Normal file
8
apps/desktop/src/autofill/models/autotype-errors.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* This error is surfaced when there is no matching
|
||||
* vault item found.
|
||||
*/
|
||||
export interface AutotypeMatchError {
|
||||
windowTitle: string;
|
||||
errorMessage: string;
|
||||
}
|
||||
8
apps/desktop/src/autofill/models/autotype-vault-data.ts
Normal file
8
apps/desktop/src/autofill/models/autotype-vault-data.ts
Normal 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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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.");
|
||||
});
|
||||
});
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user