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 { const ids: Set = 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 = 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); } } }