1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00
Files
browser/apps/desktop/src/main/native-messaging.main.ts
Daniel García 55874b72bf [PM-7846] Implement a rust based native messaging proxy and IPC system (#9894)
* [PM-7846] Implement a rust based native messaging proxy and IPC system

* Only build desktop_proxy

* Bundle the desktop_proxy file

* Make sys deps optional for the proxy

* Restore accidentally deleted after-sign

* Update native cache to contain dist folder

* Add some test logging

* Native module cache seems very aggressive

* Fix invalid directory

* Fix debug print

* Remove cache force

* Remove cache debug code

* Only log to file in debug builds

* Place the binary in the correct place for mac and make sure it's signed

* Fix platform paths

* Test unsigned appx

* Revert "Test unsigned appx"

This reverts commit e47535440a.

* Fix comment

* Remove logs

* Use debug builds in native code, and test private path on MacOS

* Add connected message

* Update IPC API comments

* Update linux to also use XDG_ dir

* Update main.rs comment

* Improve docs and split some tasks spawned into separate functions

* Update send docs and return number of elements sent

* Mark `listen` as async to ensure it runs in a tokio context, handle errors better

* Add log on client channel closed

* Move binary to MacOS folder, and sign it manually so it gets the correct entitlements

* Fix some review comments

* Run prettier

* Added missing zbus_polkit dep

* Extract magic number and increase it to match spec

* Comment fix

* Use Napi object, combine nativeBinding export, always log to file

* Missed one comment

* Remove unnecessary generics

* Correct comment

* Select only codesigning identities

* Filter certificates

* Also add local dev cert

* Remove log

* Fix package ID

* debug_assert won't run the pop() in release mode

* Better error messages

* Fix review comments

* Remove unnecessary comment

* Update napi generated TS file

* Temporary fix for DDG
2024-09-05 12:54:24 +02:00

400 lines
12 KiB
TypeScript

import { existsSync, promises as fs } from "fs";
import { homedir, userInfo } from "os";
import * as path from "path";
import * as util from "util";
import { ipcMain } from "electron";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { ipc } from "@bitwarden/desktop-napi";
import { isDev } from "../utils";
import { WindowMain } from "./window.main";
export class NativeMessagingMain {
private ipcServer: ipc.IpcServer | null;
private connected: number[] = [];
constructor(
private logService: LogService,
private windowMain: WindowMain,
private userPath: string,
private exePath: string,
private appPath: string,
) {
ipcMain.handle(
"nativeMessaging.manifests",
async (_event: any, options: { create: boolean }) => {
if (options.create) {
try {
await this.listen();
await this.generateManifests();
} catch (e) {
this.logService.error("Error generating manifests: " + e);
return e;
}
} else {
this.stop();
try {
await this.removeManifests();
} catch (e) {
this.logService.error("Error removing manifests: " + e);
return e;
}
}
return null;
},
);
ipcMain.handle(
"nativeMessaging.ddgManifests",
async (_event: any, options: { create: boolean }) => {
if (options.create) {
try {
await this.listen();
await this.generateDdgManifests();
} catch (e) {
this.logService.error("Error generating duckduckgo manifests: " + e);
return e;
}
} else {
this.stop();
try {
await this.removeDdgManifests();
} catch (e) {
this.logService.error("Error removing duckduckgo manifests: " + e);
return e;
}
}
return null;
},
);
}
async listen() {
if (this.ipcServer) {
this.ipcServer.stop();
}
this.ipcServer = await ipc.IpcServer.listen("bitwarden", (error, msg) => {
switch (msg.kind) {
case ipc.IpcMessageType.Connected: {
this.connected.push(msg.clientId);
this.logService.info("Native messaging client " + msg.clientId + " has connected");
break;
}
case ipc.IpcMessageType.Disconnected: {
const index = this.connected.indexOf(msg.clientId);
if (index > -1) {
this.connected.splice(index, 1);
}
this.logService.info("Native messaging client " + msg.clientId + " has disconnected");
break;
}
case ipc.IpcMessageType.Message:
this.windowMain.win.webContents.send("nativeMessaging", JSON.parse(msg.message));
break;
}
});
ipcMain.on("nativeMessagingReply", (event, msg) => {
if (msg != null) {
this.send(msg);
}
});
}
stop() {
this.ipcServer?.stop();
}
send(message: object) {
this.ipcServer?.send(JSON.stringify(message));
}
async generateManifests() {
const baseJson = {
name: "com.8bit.bitwarden",
description: "Bitwarden desktop <-> browser bridge",
path: this.binaryPath(),
type: "stdio",
};
if (!existsSync(baseJson.path)) {
throw new Error(`Unable to find binary: ${baseJson.path}`);
}
const firefoxJson = {
...baseJson,
...{ allowed_extensions: ["{446900e4-71c2-419f-a6a7-df9c091e268b}"] },
};
const chromeJson = {
...baseJson,
...{
allowed_origins: [
// Chrome extension
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
// Chrome beta extension
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
// Edge extension
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
// Opera extension
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
],
},
};
switch (process.platform) {
case "win32": {
const destination = path.join(this.userPath, "browsers");
await this.writeManifest(path.join(destination, "firefox.json"), firefoxJson);
await this.writeManifest(path.join(destination, "chrome.json"), chromeJson);
const nmhs = this.getWindowsNMHS();
for (const [key, value] of Object.entries(nmhs)) {
let manifestPath = path.join(destination, "chrome.json");
if (key === "Firefox") {
manifestPath = path.join(destination, "firefox.json");
}
await this.createWindowsRegistry(value, manifestPath);
}
break;
}
case "darwin": {
const nmhs = this.getDarwinNMHS();
for (const [key, value] of Object.entries(nmhs)) {
if (existsSync(value)) {
const p = path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json");
let manifest: any = chromeJson;
if (key === "Firefox") {
manifest = firefoxJson;
}
await this.writeManifest(p, manifest);
} else {
this.logService.warning(`${key} not found, skipping.`);
}
}
break;
}
case "linux":
if (existsSync(`${this.homedir()}/.mozilla/`)) {
await this.writeManifest(
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`,
firefoxJson,
);
}
if (existsSync(`${this.homedir()}/.config/google-chrome/`)) {
await this.writeManifest(
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
}
if (existsSync(`${this.homedir()}/.config/microsoft-edge/`)) {
await this.writeManifest(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
chromeJson,
);
}
break;
default:
break;
}
}
async generateDdgManifests() {
const manifest = {
name: "com.8bit.bitwarden",
description: "Bitwarden desktop <-> DuckDuckGo bridge",
path: this.binaryPath(),
type: "stdio",
};
if (!existsSync(manifest.path)) {
throw new Error(`Unable to find binary: ${manifest.path}`);
}
switch (process.platform) {
case "darwin": {
/* eslint-disable-next-line no-useless-escape */
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
await this.writeManifest(path, manifest);
break;
}
default:
break;
}
}
async removeManifests() {
switch (process.platform) {
case "win32": {
await this.removeIfExists(path.join(this.userPath, "browsers", "firefox.json"));
await this.removeIfExists(path.join(this.userPath, "browsers", "chrome.json"));
const nmhs = this.getWindowsNMHS();
for (const [, value] of Object.entries(nmhs)) {
await this.deleteWindowsRegistry(value);
}
break;
}
case "darwin": {
const nmhs = this.getDarwinNMHS();
for (const [, value] of Object.entries(nmhs)) {
await this.removeIfExists(
path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"),
);
}
break;
}
case "linux": {
await this.removeIfExists(
`${this.homedir()}/.mozilla/native-messaging-hosts/com.8bit.bitwarden.json`,
);
await this.removeIfExists(
`${this.homedir()}/.config/google-chrome/NativeMessagingHosts/com.8bit.bitwarden.json`,
);
await this.removeIfExists(
`${this.homedir()}/.config/microsoft-edge/NativeMessagingHosts/com.8bit.bitwarden.json`,
);
break;
}
default:
break;
}
}
async removeDdgManifests() {
switch (process.platform) {
case "darwin": {
/* eslint-disable-next-line no-useless-escape */
const path = `${this.homedir()}/Library/Containers/com.duckduckgo.macos.browser/Data/Library/Application\ Support/NativeMessagingHosts/com.8bit.bitwarden.json`;
await this.removeIfExists(path);
break;
}
default:
break;
}
}
private getWindowsNMHS() {
return {
Firefox: "HKCU\\SOFTWARE\\Mozilla\\NativeMessagingHosts\\com.8bit.bitwarden",
Chrome: "HKCU\\SOFTWARE\\Google\\Chrome\\NativeMessagingHosts\\com.8bit.bitwarden",
Chromium: "HKCU\\SOFTWARE\\Chromium\\NativeMessagingHosts\\com.8bit.bitwarden",
// Edge uses the same registry key as Chrome as a fallback, but it's has its own separate key as well.
"Microsoft Edge": "HKCU\\SOFTWARE\\Microsoft\\Edge\\NativeMessagingHosts\\com.8bit.bitwarden",
};
}
private getDarwinNMHS() {
/* eslint-disable no-useless-escape */
return {
Firefox: `${this.homedir()}/Library/Application\ Support/Mozilla/`,
Chrome: `${this.homedir()}/Library/Application\ Support/Google/Chrome/`,
"Chrome Beta": `${this.homedir()}/Library/Application\ Support/Google/Chrome\ Beta/`,
"Chrome Dev": `${this.homedir()}/Library/Application\ Support/Google/Chrome\ Dev/`,
"Chrome Canary": `${this.homedir()}/Library/Application\ Support/Google/Chrome\ Canary/`,
Chromium: `${this.homedir()}/Library/Application\ Support/Chromium/`,
"Microsoft Edge": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge/`,
"Microsoft Edge Beta": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Beta/`,
"Microsoft Edge Dev": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Dev/`,
"Microsoft Edge Canary": `${this.homedir()}/Library/Application\ Support/Microsoft\ Edge\ Canary/`,
Vivaldi: `${this.homedir()}/Library/Application\ Support/Vivaldi/`,
};
/* eslint-enable no-useless-escape */
}
private async writeManifest(destination: string, manifest: object) {
this.logService.debug(`Writing manifest: ${destination}`);
if (!existsSync(path.dirname(destination))) {
await fs.mkdir(path.dirname(destination));
}
await fs.writeFile(destination, JSON.stringify(manifest, null, 2));
}
private binaryPath() {
const ext = process.platform === "win32" ? ".exe" : "";
if (isDev()) {
return path.join(
this.appPath,
"..",
"desktop_native",
"target",
"debug",
`desktop_proxy${ext}`,
);
}
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
}
private getRegeditInstance() {
// eslint-disable-next-line
const regedit = require("regedit");
regedit.setExternalVBSLocation(path.join(path.dirname(this.exePath), "resources/regedit/vbs"));
return regedit;
}
private async createWindowsRegistry(location: string, jsonFile: string) {
const regedit = this.getRegeditInstance();
const createKey = util.promisify(regedit.createKey);
const putValue = util.promisify(regedit.putValue);
this.logService.debug(`Adding registry: ${location}`);
await createKey(location);
// Insert path to manifest
const obj: any = {};
obj[location] = {
default: {
value: jsonFile,
type: "REG_DEFAULT",
},
};
return putValue(obj);
}
private async deleteWindowsRegistry(key: string) {
const regedit = this.getRegeditInstance();
const list = util.promisify(regedit.list);
const deleteKey = util.promisify(regedit.deleteKey);
this.logService.debug(`Removing registry: ${key}`);
try {
await list(key);
await deleteKey(key);
} catch {
this.logService.error(`Unable to delete registry key: ${key}`);
}
}
private homedir() {
if (process.platform === "darwin") {
return userInfo().homedir;
} else {
return homedir();
}
}
private async removeIfExists(path: string) {
if (existsSync(path)) {
await fs.unlink(path);
}
}
}