From 49e1b0cf29caca3b07bf9fb36f79523bdcf27eee Mon Sep 17 00:00:00 2001 From: Bernd Schoolmann Date: Fri, 15 Nov 2024 16:17:28 +0100 Subject: [PATCH] Implement polkit support for flatpak --- .../core/src/biometric/macos.rs | 4 ++ .../desktop_native/core/src/biometric/mod.rs | 4 +- .../desktop_native/core/src/biometric/unix.rs | 68 +++++++++++++++---- .../core/src/biometric/windows.rs | 4 ++ apps/desktop/desktop_native/napi/index.d.ts | 1 + apps/desktop/desktop_native/napi/src/lib.rs | 7 ++ .../com.bitwarden.desktop.devel.yaml | 6 +- .../src/app/accounts/settings.component.ts | 2 +- .../biometrics/biometric.unix.main.ts | 31 ++++++--- .../desktop/src/main/native-messaging.main.ts | 38 ++++++++++- 10 files changed, 135 insertions(+), 30 deletions(-) diff --git a/apps/desktop/desktop_native/core/src/biometric/macos.rs b/apps/desktop/desktop_native/core/src/biometric/macos.rs index 01ee4519ce6..db21c5e5b74 100644 --- a/apps/desktop/desktop_native/core/src/biometric/macos.rs +++ b/apps/desktop/desktop_native/core/src/biometric/macos.rs @@ -14,6 +14,10 @@ impl super::BiometricTrait for Biometric { bail!("platform not supported"); } + async fn needs_setup() -> Result { + bail!("platform not supported"); + } + fn derive_key_material(_iv_str: Option<&str>) -> Result { bail!("platform not supported"); } diff --git a/apps/desktop/desktop_native/core/src/biometric/mod.rs b/apps/desktop/desktop_native/core/src/biometric/mod.rs index 72352cf2288..1a4df48785d 100644 --- a/apps/desktop/desktop_native/core/src/biometric/mod.rs +++ b/apps/desktop/desktop_native/core/src/biometric/mod.rs @@ -22,10 +22,9 @@ pub struct OsDerivedKey { pub iv_b64: String, } +#[allow(async_fn_in_trait)] pub trait BiometricTrait { - #[allow(async_fn_in_trait)] async fn prompt(hwnd: Vec, message: String) -> Result; - #[allow(async_fn_in_trait)] async fn available() -> Result; fn derive_key_material(secret: Option<&str>) -> Result; fn set_biometric_secret( @@ -40,6 +39,7 @@ pub trait BiometricTrait { account: &str, key_material: Option, ) -> Result; + async fn needs_setup() -> Result; } fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result { diff --git a/apps/desktop/desktop_native/core/src/biometric/unix.rs b/apps/desktop/desktop_native/core/src/biometric/unix.rs index 563bd1dfe52..a214cc10c03 100644 --- a/apps/desktop/desktop_native/core/src/biometric/unix.rs +++ b/apps/desktop/desktop_native/core/src/biometric/unix.rs @@ -1,4 +1,4 @@ -use std::str::FromStr; +use std::{collections::HashMap, hash::Hash, str::FromStr, string}; use anyhow::Result; use base64::Engine; @@ -6,31 +6,71 @@ use rand::RngCore; use sha2::{Digest, Sha256}; use crate::biometric::{base64_engine, KeyMaterial, OsDerivedKey}; -use zbus::Connection; +use zbus::{names::OwnedUniqueName, zvariant::OwnedValue, Connection}; use zbus_polkit::policykit1::*; use super::{decrypt, encrypt}; use crate::crypto::CipherString; use anyhow::anyhow; +const BITWARDEN_ACTION: &str = "com.bitwarden.Bitwarden.unlock"; +const SYSTEM_ACTION: &str = "org.freedesktop.policykit.exec"; + /// The Unix implementation of the biometric trait. pub struct Biometric {} +async fn action_available(action_id: String) -> Result { + let connection = Connection::system().await?; + let proxy = AuthorityProxy::new(&connection).await?; + let res = proxy.enumerate_actions("en").await?; + for action in res { + if action.action_id == action_id { + return Ok(true); + } + } + return Ok(false); +} + impl super::BiometricTrait for Biometric { async fn prompt(_hwnd: Vec, _message: String) -> Result { let connection = Connection::system().await?; let proxy = AuthorityProxy::new(&connection).await?; - let subject = Subject::new_for_owner(std::process::id(), None, None)?; + let mut subject_details = HashMap::new(); + let bus_name = if let Some(name) = connection.unique_name() { + name + } else { + println!("polkit: could not get bus name"); + return Ok(false); + }; + + subject_details.insert("name".to_string(), OwnedUniqueName::from(bus_name.clone()).try_into()?); + let subject = Subject{ + subject_kind: "system-bus-name".to_string(), + subject_details, + }; let details = std::collections::HashMap::new(); - let result = proxy + + let result = if action_available(BITWARDEN_ACTION.to_string()).await? { + proxy + .check_authorization( + &subject, + BITWARDEN_ACTION, + &details, + CheckAuthorizationFlags::AllowUserInteraction.into(), + "", + ) + .await + } else { + proxy .check_authorization( &subject, - "com.bitwarden.Bitwarden.unlock", + SYSTEM_ACTION, &details, CheckAuthorizationFlags::AllowUserInteraction.into(), "", ) - .await; + .await + }; match result { Ok(result) => { @@ -44,17 +84,19 @@ impl super::BiometricTrait for Biometric { } async fn available() -> Result { - let connection = Connection::system().await?; - let proxy = AuthorityProxy::new(&connection).await?; - let res = proxy.enumerate_actions("en").await?; - for action in res { - if action.action_id == "com.bitwarden.Bitwarden.unlock" { - return Ok(true); - } + if action_available(BITWARDEN_ACTION.to_string()).await? || action_available(SYSTEM_ACTION.to_string()).await? { + return Ok(true); } return Ok(false); } + async fn needs_setup() -> Result { + if action_available(BITWARDEN_ACTION.to_string()).await? { + return Ok(false); + } + return Ok(true); + } + fn derive_key_material(challenge_str: Option<&str>) -> Result { let challenge: [u8; 16] = match challenge_str { Some(challenge_str) => base64_engine diff --git a/apps/desktop/desktop_native/core/src/biometric/windows.rs b/apps/desktop/desktop_native/core/src/biometric/windows.rs index d5e8b6dc915..aa5ddd6ab8b 100644 --- a/apps/desktop/desktop_native/core/src/biometric/windows.rs +++ b/apps/desktop/desktop_native/core/src/biometric/windows.rs @@ -67,6 +67,10 @@ impl super::BiometricTrait for Biometric { } } + async fn needs_setup() -> Result { + Ok(false) + } + /// Derive the symmetric encryption key from the Windows Hello signature. /// /// This works by signing a static challenge string with Windows Hello protected key store. The diff --git a/apps/desktop/desktop_native/napi/index.d.ts b/apps/desktop/desktop_native/napi/index.d.ts index 08f9f45b1be..d1e6a0078a8 100644 --- a/apps/desktop/desktop_native/napi/index.d.ts +++ b/apps/desktop/desktop_native/napi/index.d.ts @@ -17,6 +17,7 @@ export declare namespace passwords { export declare namespace biometrics { export function prompt(hwnd: Buffer, message: string): Promise export function available(): Promise + export function needsSetup(): Promise export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise /** diff --git a/apps/desktop/desktop_native/napi/src/lib.rs b/apps/desktop/desktop_native/napi/src/lib.rs index 841d2155f10..a42d8f71384 100644 --- a/apps/desktop/desktop_native/napi/src/lib.rs +++ b/apps/desktop/desktop_native/napi/src/lib.rs @@ -66,6 +66,13 @@ pub mod biometrics { .map_err(|e| napi::Error::from_reason(e.to_string())) } + #[napi] + async fn needs_setup() -> napi::Result { + Biometric::needs_setup() + .await + .map_err(|e| napi::Error::from_reason(e.to_string())) + } + #[napi] pub async fn set_biometric_secret( service: String, diff --git a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml index 234d37905cc..620ef13088d 100644 --- a/apps/desktop/resources/com.bitwarden.desktop.devel.yaml +++ b/apps/desktop/resources/com.bitwarden.desktop.devel.yaml @@ -18,11 +18,15 @@ finish-args: - --talk-name=org.freedesktop.secrets - --talk-name=com.canonical.AppMenu.Registrar - --system-talk-name=org.freedesktop.PolicyKit1 - # Lock on lockscreen + # Lock on lockscreen - --talk-name=org.gnome.ScreenSaver - --talk-name=org.freedesktop.ScreenSaver - --system-talk-name=org.freedesktop.login1 - --filesystem=xdg-download + # setup biometrics + # note: this allows bitwarden to spawn processes outside of the sandbox + - --talk-name=org.freedesktop.Flatpak + - --filesystem=home:rw modules: - name: bitwarden-desktop buildsystem: simple diff --git a/apps/desktop/src/app/accounts/settings.component.ts b/apps/desktop/src/app/accounts/settings.component.ts index a8ce45f53c7..4410cb7a4ef 100644 --- a/apps/desktop/src/app/accounts/settings.component.ts +++ b/apps/desktop/src/app/accounts/settings.component.ts @@ -658,7 +658,7 @@ export class SettingsComponent implements OnInit, OnDestroy { this.form.controls.enableBrowserIntegration.setValue(false); return; - } else if (ipc.platform.isSnapStore || ipc.platform.isFlatpak) { + } else if (ipc.platform.isSnapStore) { await this.dialogService.openSimpleDialog({ title: { key: "browserIntegrationUnsupportedTitle" }, content: { key: "browserIntegrationLinuxDesc" }, diff --git a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts index 8962e7f3ecf..3af2417db2e 100644 --- a/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts +++ b/apps/desktop/src/key-management/biometrics/biometric.unix.main.ts @@ -101,23 +101,32 @@ export default class BiometricUnixMain implements OsBiometricService { async osBiometricsNeedsSetup(): Promise { // check whether the polkit policy is loaded via dbus call to polkit - return !(await biometrics.available()); + return await biometrics.needsSetup(); } async osBiometricsCanAutoSetup(): Promise { - // We cannot auto setup on snap or flatpak since the filesystem is sandboxed. - // The user needs to manually set up the polkit policy outside of the sandbox - // since we allow access to polkit via dbus for the sandboxed clients, the authentication works from - // the sandbox, once the policy is set up outside of the sandbox. - return isLinux() && !isSnapStore() && !isFlatpak(); + // We cannot auto setup on snap since the filesystem is sandboxed. + return isLinux() && !isSnapStore(); } async osBiometricsSetup(): Promise { - const process = spawn("pkexec", [ - "bash", - "-c", - `echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`, - ]); + let process = null; + if (isFlatpak()) { + // To set up on flatpak, we escape the sandbox via the flatpak portal + process = spawn("flatpak-spawn", [ + "--host", + "pkexec", + "bash", + "-c", + `echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`, + ]); + } else { + process = spawn("pkexec", [ + "bash", + "-c", + `echo '${polkitPolicy}' > ${policyPath + policyFileName} && chown root:root ${policyPath + policyFileName} && chcon system_u:object_r:usr_t:s0 ${policyPath + policyFileName}`, + ]); + } await new Promise((resolve, reject) => { process.on("close", (code) => { diff --git a/apps/desktop/src/main/native-messaging.main.ts b/apps/desktop/src/main/native-messaging.main.ts index 42be83a303f..8c541bd7354 100644 --- a/apps/desktop/src/main/native-messaging.main.ts +++ b/apps/desktop/src/main/native-messaging.main.ts @@ -7,7 +7,7 @@ import { ipcMain } from "electron"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { ipc, windows_registry } from "@bitwarden/desktop-napi"; -import { isDev } from "../utils"; +import { isDev, isFlatpak } from "../utils"; import { WindowMain } from "./window.main"; @@ -134,7 +134,7 @@ export class NativeMessagingMain { type: "stdio", }; - if (!existsSync(baseJson.path)) { + if (!existsSync(baseJson.path) && !isFlatpak()) { throw new Error(`Unable to find binary: ${baseJson.path}`); } @@ -185,11 +185,29 @@ export class NativeMessagingMain { for (const [key, value] of Object.entries(this.getLinuxNMHS())) { if (existsSync(value)) { if (key === "Firefox") { + if (isFlatpak()) { + const proxyScriptLocation = path.join( + value, + "native-messaging-hosts", + "com.8bit.bitwarden.sh", + ); + await this.writeFlatpakProxyScript(proxyScriptLocation); + firefoxJson.path = proxyScriptLocation; + } await this.writeManifest( path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"), firefoxJson, ); } else { + if (isFlatpak()) { + const proxyScriptLocation = path.join( + value, + "native-messaging-hosts", + "com.8bit.bitwarden.sh", + ); + await this.writeFlatpakProxyScript(proxyScriptLocation); + chromeJson.path = proxyScriptLocation; + } await this.writeManifest( path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"), chromeJson, @@ -257,10 +275,16 @@ export class NativeMessagingMain { await this.removeIfExists( path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"), ); + await this.removeIfExists( + path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.sh"), + ); } else { await this.removeIfExists( path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"), ); + await this.removeIfExists( + path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.sh"), + ); } } @@ -334,6 +358,16 @@ export class NativeMessagingMain { await fs.writeFile(destination, JSON.stringify(manifest, null, 2)); } + private async writeFlatpakProxyScript(destination: string) { + const content = + "#!/bin/bash\n/usr/bin/flatpak run --command=desktop_proxy com.bitwarden.desktop $@"; + if (!existsSync(path.dirname(destination))) { + await fs.mkdir(path.dirname(destination)); + } + await fs.writeFile(destination, content); + await fs.chmod(destination, 0o755); + } + private async loadChromeIds(): Promise { const ids: Set = new Set([ // Chrome extension