diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 9020e08362e..4fe1dc8cd8d 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -754,6 +754,22 @@ dependencies = [ "syn", ] +[[package]] +name = "ctor" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67773048316103656a637612c4a62477603b777d91d9c62ff2290f9cde178fdb" +dependencies = [ + "ctor-proc-macro", + "dtor", +] + +[[package]] +name = "ctor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2931af7e13dc045d8e9d26afccc6fa115d64e115c9c84b1166288b46f6782c2" + [[package]] name = "ctr" version = "0.9.2" @@ -1049,6 +1065,21 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" +[[package]] +name = "dtor" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58a0764cddb55ab28955347b45be00ade43d4d6f3ba4bf3dc354e4ec9432934" +dependencies = [ + "dtor-proc-macro", +] + +[[package]] +name = "dtor-proc-macro" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f678cf4a922c215c63e0de95eb1ff08a958a81d47e485cf9da1e27bf6305cfa5" + [[package]] name = "ecdsa" version = "0.16.9" @@ -1886,7 +1917,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55740c4ae1d8696773c78fdafd5d0e5fe9bc9f1b071c7ba493ba5c413a9184f3" dependencies = [ "bitflags", - "ctor", + "ctor 0.2.9", "napi-derive", "napi-sys", "once_cell", @@ -2559,6 +2590,15 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "process_isolation" +version = "0.0.0" +dependencies = [ + "ctor 0.5.0", + "desktop_core", + "libc", +] + [[package]] name = "quick-xml" version = "0.37.5" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 0a637b12de9..39c77f53254 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -6,6 +6,7 @@ members = [ "core", "macos_provider", "napi", + "process_isolation", "proxy", "windows_plugin_authenticator" ] @@ -27,6 +28,7 @@ byteorder = "=1.5.0" bytes = "=1.10.1" cbc = "=0.1.2" core-foundation = "=0.10.1" +ctor = "=0.5.0" dirs = "=6.0.0" ed25519 = "=2.2.3" embed_plist = "=1.2.2" diff --git a/apps/desktop/desktop_native/build.js b/apps/desktop/desktop_native/build.js index 125cb1bb567..8b13fcc6eb3 100644 --- a/apps/desktop/desktop_native/build.js +++ b/apps/desktop/desktop_native/build.js @@ -45,6 +45,20 @@ function buildProxyBin(target, release = true) { } } +function buildProcessIsolation() { + if (process.platform !== "linux") { + return; + } + + child_process.execSync(`cargo build --release`, { + stdio: 'inherit', + cwd: path.join(__dirname, "process_isolation") + }); + + console.log("Copying process isolation library to dist folder"); + fs.copyFileSync(path.join(__dirname, "target", "release", "libprocess_isolation.so"), path.join(__dirname, "dist", `libprocess_isolation.so`)); +} + function installTarget(target) { child_process.execSync(`rustup target add ${target}`, { stdio: 'inherit', cwd: __dirname }); } @@ -53,6 +67,7 @@ if (!crossPlatform && !target) { console.log(`Building native modules in ${mode} mode for the native architecture`); buildNapiModule(false, mode === "release"); buildProxyBin(false, mode === "release"); + buildProcessIsolation(); return; } @@ -61,6 +76,7 @@ if (target) { installTarget(target); buildNapiModule(target, mode === "release"); buildProxyBin(target, mode === "release"); + buildProcessIsolation(); return; } @@ -78,4 +94,5 @@ platformTargets.forEach(([target, _]) => { installTarget(target); buildNapiModule(target); buildProxyBin(target); + buildProcessIsolation(); }); diff --git a/apps/desktop/desktop_native/process_isolation/Cargo.toml b/apps/desktop/desktop_native/process_isolation/Cargo.toml new file mode 100644 index 00000000000..8e4072f1a90 --- /dev/null +++ b/apps/desktop/desktop_native/process_isolation/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "process_isolation" +edition = { workspace = true } +license = { workspace = true } +version = { workspace = true } +publish = { workspace = true } + +[lib] +crate-type = ["cdylib"] + +[dependencies] +ctor = { workspace = true } +desktop_core = { path = "../core" } +libc = { workspace = true } diff --git a/apps/desktop/desktop_native/process_isolation/src/lib.rs b/apps/desktop/desktop_native/process_isolation/src/lib.rs new file mode 100644 index 00000000000..57275817b9f --- /dev/null +++ b/apps/desktop/desktop_native/process_isolation/src/lib.rs @@ -0,0 +1,46 @@ +#![cfg(target_os = "linux")] + +//! This library compiles to a pre-loadable shared object. When preloaded, it +//! immediately isolates the process using the methods available on the platform. +//! 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}; + +static ORIGINAL_UNSETENV: LazyLock i32> = + LazyLock::new(|| unsafe { + std::mem::transmute(libc::dlsym(libc::RTLD_NEXT, c"unsetenv".as_ptr())) + }); + +/// Hooks unsetenv to fix a bug in zypak-wrapper. +/// Zypak unsets the env in Flatpak as a side-effect, which means that only the top level +/// processes would be hooked. With this work-around all processes in the tree are hooked +#[unsafe(no_mangle)] +unsafe extern "C" fn unsetenv(name: *const c_char) -> i32 { + unsafe { + let Ok(name_str) = std::ffi::CStr::from_ptr(name).to_str() else { + return ORIGINAL_UNSETENV(name); + }; + + if name_str == "LD_PRELOAD" { + // This env variable is provided by the flatpak configuration + let ld_preload = std::env::var("PROCESS_ISOLATION_LD_PRELOAD").unwrap_or_default(); + std::env::set_var("LD_PRELOAD", ld_preload); + return 0; + } + + ORIGINAL_UNSETENV(name) + } +} + +// Hooks the shared object being loaded into the process +#[ctor::ctor] +fn preload_init() { + let pid = unsafe { libc::getpid() }; + unsafe { + println!("[Process Isolation] Enabling memory security for process {pid}"); + process_isolation::isolate_process(); + process_isolation::disable_coredumps(); + } +} diff --git a/apps/desktop/desktop_native/process_isolation/test_isolation.sh b/apps/desktop/desktop_native/process_isolation/test_isolation.sh new file mode 100644 index 00000000000..91f3b7933df --- /dev/null +++ b/apps/desktop/desktop_native/process_isolation/test_isolation.sh @@ -0,0 +1,40 @@ +#!/bin/bash + +# This script tests the memory isolation status of bitwarden-desktop processes. The script will print "isolated" +# if the memory is not accessible by other processes. + +CURRENT_USER=$(whoami) + +# Find processes with "bitwarden" in the command +pids=$(pgrep -f bitwarden) + +if [[ -z "$pids" ]]; then + echo "No bitwarden processes found." + exit 0 +fi + +for pid in $pids; do + # Get process info: command, PPID, RSS memory + read cmd ppid rss <<<$(ps -o comm=,ppid=,rss= -p "$pid") + + # Explicitly skip if the command line does not contain "bitwarden" + if ! grep -q "bitwarden" <<<"$cmd"; then + continue + fi + + # Check ownership of /proc/$pid/environ + owner=$(stat -c "%U" /proc/$pid/environ 2>/dev/null) + + if [[ "$owner" == "root" ]]; then + status="isolated" + elif [[ "$owner" == "$CURRENT_USER" ]]; then + status="insecure" + else + status="unknown-owner:$owner" + fi + + # Convert memory to MB + mem_mb=$((rss / 1024)) + + echo "PID: $pid | CMD: $cmd | Mem: ${mem_mb}MB | Owner: $owner | Status: $status" +done diff --git a/apps/desktop/electron-builder.json b/apps/desktop/electron-builder.json index 2e780bf6b1d..f7dcfb65044 100644 --- a/apps/desktop/electron-builder.json +++ b/apps/desktop/electron-builder.json @@ -106,6 +106,10 @@ { "from": "desktop_native/dist/desktop_proxy.${platform}-${arch}", "to": "desktop_proxy" + }, + { + "from": "desktop_native/dist/libprocess_isolation.so", + "to": "libprocess_isolation.so" } ], "target": ["deb", "freebsd", "rpm", "AppImage", "snap"], diff --git a/apps/desktop/package.json b/apps/desktop/package.json index ee987905980..26d48742b7c 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -35,9 +35,10 @@ "build:renderer:watch": "cross-env NODE_ENV=development webpack --config webpack.config.js --config-name renderer --watch", "electron": "node ./scripts/start.js", "electron:ignore": "node ./scripts/start.js --ignore-certificate-errors", + "flatpak:dev": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --force-clean --install --user ../../.flatpak/ ./resources/com.bitwarden.desktop.devel.yaml && flatpak run com.bitwarden.desktop", "clean:dist": "rimraf ./dist", "pack:dir": "npm run clean:dist && electron-builder --dir -p never", - "pack:lin:flatpak": "npm run clean:dist && electron-builder --dir -p never && flatpak-builder --repo=build/.repo build/.flatpak ./resources/com.bitwarden.desktop.devel.yaml --install-deps-from=flathub --force-clean && flatpak build-bundle ./build/.repo/ ./dist/com.bitwarden.desktop.flatpak com.bitwarden.desktop", + "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:mac": "npm run clean:dist && electron-builder --mac --universal -p never", diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml index 858fb6e1af2..e72df98e22b 100644 --- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -46,4 +46,6 @@ modules: commands: - ulimit -c 0 - export TMPDIR="$XDG_RUNTIME_DIR/app/$FLATPAK_ID" + - export ZYPAK_LD_PRELOAD="/app/bin/libprocess_isolation.so" + - export PROCESS_ISOLATION_LD_PRELOAD="/app/bin/libprocess_isolation.so" - exec zypak-wrapper /app/bin/bitwarden-app "$@" diff --git a/apps/desktop/resources/linux-wrapper.sh b/apps/desktop/resources/linux-wrapper.sh index dd53eb9811c..50a323e1c18 100644 --- a/apps/desktop/resources/linux-wrapper.sh +++ b/apps/desktop/resources/linux-wrapper.sh @@ -7,12 +7,19 @@ ulimit -c 0 RAW_PATH=$(readlink -f "$0") APP_PATH=$(dirname $RAW_PATH) -# force use of base image libdus in snap -if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ] -then +# force use of base image libdbus in snap +if [ -e "/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" ]; then export LD_PRELOAD="/usr/lib/x86_64-linux-gnu/libdbus-1.so.3" fi +# If running in non-snap, add libprocess_isolation.so from app path to LD_PRELOAD +# This prevents debugger / memory dumping on all desktop processes +if [ -z "$SNAP" ] && [ -f "$APP_PATH/libprocess_isolation.so" ]; then + LIBPROCESS_ISOLATION_SO="$APP_PATH/libprocess_isolation.so" + LD_PRELOAD="$LIBPROCESS_ISOLATION_SO${LD_PRELOAD:+:$LD_PRELOAD}" + export LD_PRELOAD +fi + PARAMS="--enable-features=UseOzonePlatform,WaylandWindowDecorations --ozone-platform-hint=auto" if [ "$USE_X11" = "true" ]; then PARAMS=""