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:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
1
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -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>
|
||||
/**
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user