1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-12 14:34:02 +00:00

Implement polkit support for flatpak

This commit is contained in:
Bernd Schoolmann
2024-11-15 16:17:28 +01:00
parent b0c5b5e9e6
commit 49e1b0cf29
10 changed files with 135 additions and 30 deletions

View File

@@ -14,6 +14,10 @@ impl super::BiometricTrait for Biometric {
bail!("platform not supported");
}
async fn needs_setup() -> Result<bool> {
bail!("platform not supported");
}
fn derive_key_material(_iv_str: Option<&str>) -> Result<OsDerivedKey> {
bail!("platform not supported");
}

View File

@@ -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<u8>, message: String) -> Result<bool>;
#[allow(async_fn_in_trait)]
async fn available() -> Result<bool>;
fn derive_key_material(secret: Option<&str>) -> Result<OsDerivedKey>;
fn set_biometric_secret(
@@ -40,6 +39,7 @@ pub trait BiometricTrait {
account: &str,
key_material: Option<KeyMaterial>,
) -> Result<String>;
async fn needs_setup() -> Result<bool>;
}
fn encrypt(secret: &str, key_material: &KeyMaterial, iv_b64: &str) -> Result<String> {

View File

@@ -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<bool> {
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<u8>, _message: String) -> Result<bool> {
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<bool> {
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<bool> {
if action_available(BITWARDEN_ACTION.to_string()).await? {
return Ok(false);
}
return Ok(true);
}
fn derive_key_material(challenge_str: Option<&str>) -> Result<OsDerivedKey> {
let challenge: [u8; 16] = match challenge_str {
Some(challenge_str) => base64_engine

View File

@@ -67,6 +67,10 @@ impl super::BiometricTrait for Biometric {
}
}
async fn needs_setup() -> Result<bool> {
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

View File

@@ -17,6 +17,7 @@ export declare namespace passwords {
export declare namespace biometrics {
export function prompt(hwnd: Buffer, message: string): Promise<boolean>
export function available(): Promise<boolean>
export function needsSetup(): Promise<boolean>
export function setBiometricSecret(service: string, account: string, secret: string, keyMaterial: KeyMaterial | undefined | null, ivB64: string): Promise<string>
export function getBiometricSecret(service: string, account: string, keyMaterial?: KeyMaterial | undefined | null): Promise<string>
/**

View File

@@ -66,6 +66,13 @@ pub mod biometrics {
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
async fn needs_setup() -> napi::Result<bool> {
Biometric::needs_setup()
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn set_biometric_secret(
service: String,

View File

@@ -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

View File

@@ -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" },

View File

@@ -101,23 +101,32 @@ export default class BiometricUnixMain implements OsBiometricService {
async osBiometricsNeedsSetup(): Promise<boolean> {
// check whether the polkit policy is loaded via dbus call to polkit
return !(await biometrics.available());
return await biometrics.needsSetup();
}
async osBiometricsCanAutoSetup(): Promise<boolean> {
// 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<void> {
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) => {

View File

@@ -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<string[]> {
const ids: Set<string> = new Set([
// Chrome extension