mirror of
https://github.com/bitwarden/browser
synced 2025-12-22 03:03:43 +00:00
438 lines
14 KiB
TypeScript
438 lines
14 KiB
TypeScript
import { existsSync, promises as fs } from "fs";
|
|
import { homedir, userInfo } from "os";
|
|
import * as path from "path";
|
|
|
|
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 { 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: await this.loadChromeIds(),
|
|
};
|
|
|
|
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 [name, [key, subkey]] of Object.entries(nmhs)) {
|
|
let manifestPath = path.join(destination, "chrome.json");
|
|
if (name === "Firefox") {
|
|
manifestPath = path.join(destination, "firefox.json");
|
|
}
|
|
await windows_registry.createKey(key, subkey, 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": {
|
|
for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
|
|
if (existsSync(value)) {
|
|
if (key === "Firefox") {
|
|
await this.writeManifest(
|
|
path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
|
|
firefoxJson,
|
|
);
|
|
} else {
|
|
await this.writeManifest(
|
|
path.join(value, "NativeMessagingHosts", "com.8bit.bitwarden.json"),
|
|
chromeJson,
|
|
);
|
|
}
|
|
} else {
|
|
this.logService.warning(`${key} not found, skipping.`);
|
|
}
|
|
}
|
|
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 [, [key, subkey]] of Object.entries(nmhs)) {
|
|
await windows_registry.deleteKey(key, subkey);
|
|
}
|
|
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": {
|
|
for (const [key, value] of Object.entries(this.getLinuxNMHS())) {
|
|
if (key === "Firefox") {
|
|
await this.removeIfExists(
|
|
path.join(value, "native-messaging-hosts", "com.8bit.bitwarden.json"),
|
|
);
|
|
} else {
|
|
await this.removeIfExists(
|
|
path.join(value, "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 getLinuxNMHS() {
|
|
return {
|
|
Firefox: `${this.homedir()}/.mozilla/`,
|
|
Chrome: `${this.homedir()}/.config/google-chrome/`,
|
|
Chromium: `${this.homedir()}/.config/chromium/`,
|
|
"Microsoft Edge": `${this.homedir()}/.config/microsoft-edge/`,
|
|
};
|
|
}
|
|
|
|
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 async loadChromeIds(): Promise<string[]> {
|
|
const ids: Set<string> = new Set([
|
|
// Chrome extension
|
|
"chrome-extension://nngceckbapebfimnlniiiahkandclblb/",
|
|
// Chrome beta extension
|
|
"chrome-extension://hccnnhgbibccigepcmlgppchkpfdophk/",
|
|
// Edge extension
|
|
"chrome-extension://jbkfoedolllekgbhcbcoahefnbanhhlh/",
|
|
// Opera extension
|
|
"chrome-extension://ccnckbpmaceehanjmeomladnmlffdjgn/",
|
|
]);
|
|
|
|
if (!isDev()) {
|
|
return Array.from(ids);
|
|
}
|
|
|
|
// The dev builds of the extension have a different random ID per user, so to make development easier
|
|
// we try to find the extension IDs from the user's Chrome profiles when we're running in dev mode.
|
|
let chromePaths: string[];
|
|
switch (process.platform) {
|
|
case "darwin": {
|
|
chromePaths = Object.entries(this.getDarwinNMHS())
|
|
.filter(([key]) => key !== "Firefox")
|
|
.map(([, value]) => value);
|
|
break;
|
|
}
|
|
case "linux": {
|
|
chromePaths = Object.entries(this.getLinuxNMHS())
|
|
.filter(([key]) => key !== "Firefox")
|
|
.map(([, value]) => value);
|
|
break;
|
|
}
|
|
case "win32": {
|
|
// TODO: Add more supported browsers for Windows?
|
|
chromePaths = [
|
|
path.join(process.env.LOCALAPPDATA, "Microsoft", "Edge", "User Data"),
|
|
path.join(process.env.LOCALAPPDATA, "Google", "Chrome", "User Data"),
|
|
];
|
|
break;
|
|
}
|
|
}
|
|
|
|
for (const chromePath of chromePaths) {
|
|
try {
|
|
// The chrome profile directories are named "Default", "Profile 1", "Profile 2", etc.
|
|
const profiles = (await fs.readdir(chromePath)).filter((f) => {
|
|
const lower = f.toLowerCase();
|
|
return lower == "default" || lower.startsWith("profile ");
|
|
});
|
|
|
|
for (const profile of profiles) {
|
|
try {
|
|
// Read the profile Preferences file and find the extension commands section
|
|
const prefs = JSON.parse(
|
|
await fs.readFile(path.join(chromePath, profile, "Preferences"), "utf8"),
|
|
);
|
|
const commands: Map<string, any> = prefs.extensions.commands;
|
|
|
|
// If one of the commands is autofill_login or generate_password, we know it's probably the Bitwarden extension
|
|
for (const { command_name, extension } of Object.values(commands)) {
|
|
if (command_name === "autofill_login" || command_name === "generate_password") {
|
|
ids.add(`chrome-extension://${extension}/`);
|
|
this.logService.info(`Found extension from ${chromePath}: ${extension}`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
this.logService.info(`Error reading preferences: ${e}`);
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Browser is not installed, we can just skip it
|
|
}
|
|
}
|
|
|
|
return Array.from(ids);
|
|
}
|
|
|
|
private binaryPath() {
|
|
const ext = process.platform === "win32" ? ".exe" : "";
|
|
|
|
if (isDev()) {
|
|
const devPath = path.join(
|
|
this.appPath,
|
|
"..",
|
|
"desktop_native",
|
|
"target",
|
|
"debug",
|
|
`desktop_proxy${ext}`,
|
|
);
|
|
|
|
// isDev() returns true when using a production build with ELECTRON_IS_DEV=1,
|
|
// so we need to fall back to the prod binary if the dev binary doesn't exist.
|
|
if (existsSync(devPath)) {
|
|
return devPath;
|
|
}
|
|
}
|
|
|
|
return path.join(path.dirname(this.exePath), `desktop_proxy${ext}`);
|
|
}
|
|
|
|
private homedir() {
|
|
if (process.platform === "darwin") {
|
|
return userInfo().homedir;
|
|
} else {
|
|
return homedir();
|
|
}
|
|
}
|
|
|
|
private async removeIfExists(path: string) {
|
|
if (existsSync(path)) {
|
|
await fs.unlink(path);
|
|
}
|
|
}
|
|
}
|