Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions apps/desktop/src/main/native-messaging.main.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { existsSync, promises as fs } from "fs";
import { homedir, tmpdir } from "os";
import * as path from "path";

import { mock } from "jest-mock-extended";

import { LogService } from "@bitwarden/common/platform/abstractions/log.service";

import { NativeMessagingMain } from "./native-messaging.main";
import { WindowMain } from "./window.main";

// native-messaging.main.ts registers ipcMain handlers at construction, and its
// WindowMain import registers a privileged scheme at module load time. Mock the
// surface both touch so the module can be loaded in Jest.
jest.mock("electron", () => ({
app: {},
BrowserWindow: jest.fn(),
ipcMain: { on: jest.fn(), handle: jest.fn() },
nativeTheme: {},
screen: {},
session: {},
protocol: { registerSchemesAsPrivileged: jest.fn() },
net: {},
}));

// Both modules load native .node modules at import time.
jest.mock("@bitwarden/desktop-napi", () => ({
ipc: { NativeIpcServer: { listen: jest.fn() } },
windows_registry: { createKey: jest.fn(), deleteKey: jest.fn() },
processisolations: {
isolateProcess: jest.fn(),
isCoreDumpingDisabled: jest.fn(),
disableCoredumps: jest.fn(),
},
}));

jest.mock("os", () => ({
...jest.requireActual("os"),
homedir: jest.fn(),
}));

describe("NativeMessagingMain", () => {
const originalPlatform = process.platform;

let home: string;
let logService: LogService;
let nativeMessagingMain: NativeMessagingMain;

const firefoxManifest = () =>
path.join(home, ".mozilla", "native-messaging-hosts", "com.8bit.bitwarden.json");
const chromeManifest = () =>
path.join(home, ".config", "google-chrome", "NativeMessagingHosts", "com.8bit.bitwarden.json");
const edgeManifest = () =>
path.join(home, ".config", "microsoft-edge", "NativeMessagingHosts", "com.8bit.bitwarden.json");

beforeEach(async () => {
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
globalThis.BIT_ENVIRONMENT = "production";

home = await fs.mkdtemp(path.join(tmpdir(), "bw-nmh-"));
jest.mocked(homedir).mockReturnValue(home);

// The proxy binary must exist for generateManifests to proceed.
const appDir = path.join(home, "app");
await fs.mkdir(appDir, { recursive: true });
await fs.writeFile(path.join(appDir, "desktop_proxy"), "");

logService = mock<LogService>();
nativeMessagingMain = new NativeMessagingMain(
logService,
mock<WindowMain>(),
path.join(home, "userData"),
path.join(appDir, "bitwarden"),
appDir,
);
});

afterEach(async () => {
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true });
await fs.rm(home, { recursive: true, force: true });
jest.clearAllMocks();
});

describe("generateManifests on linux", () => {
it("creates the native messaging hosts directory for chromium browsers", async () => {
// Browser config directory present, but no NativeMessagingHosts subdirectory in it.
await fs.mkdir(path.join(home, ".config", "google-chrome"), { recursive: true });

await nativeMessagingMain.generateManifests();

expect(existsSync(chromeManifest())).toBe(true);
expect(
existsSync(
path.join(
home,
".config",
"google-chrome",
"NativeMessagingHosts",
".bitwarden_desktop_proxy",
),
),
).toBe(true);
});

it("continues with the remaining browsers when one fails", async () => {
await fs.mkdir(path.join(home, ".mozilla"), { recursive: true });
await fs.mkdir(path.join(home, ".config", "chromium"), { recursive: true });
await fs.mkdir(path.join(home, ".config", "microsoft-edge"), { recursive: true });

// A file where the directory belongs makes the Chromium step fail. Chromium sits
// between Firefox and Microsoft Edge in the iteration order.
await fs.writeFile(path.join(home, ".config", "chromium", "NativeMessagingHosts"), "");

await expect(nativeMessagingMain.generateManifests()).resolves.not.toThrow();

expect(existsSync(firefoxManifest())).toBe(true);
expect(existsSync(edgeManifest())).toBe(true);
expect(logService.error).toHaveBeenCalledWith(
expect.stringContaining("Failed to set up Chromium"),
expect.anything(),
);
});
});
});
91 changes: 46 additions & 45 deletions apps/desktop/src/main/native-messaging.main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,24 @@ export class NativeMessagingMain {
case "darwin": {
const nmhs = this.getDarwinNMHS();
for (const [key, browserDirectory] of Object.entries(nmhs)) {
if (existsSync(browserDirectory)) {
const nmhsPath = path.join(browserDirectory, "NativeMessagingHosts");
const manifestPath = path.join(nmhsPath, "com.8bit.bitwarden.json");

let manifest: any = await this.generateChromeJson(binaryPath);
if (key === "Firefox" || key === "Zen") {
// Only generate the NMHS dir if the browser directory exists
await fs.mkdir(nmhsPath, { recursive: true });
manifest = await this.generateFirefoxJson(binaryPath);
}

await this.writeManifest(manifestPath, manifest);
} else {
if (!existsSync(browserDirectory)) {
this.logService.warning(`${key} not found, skipping.`);
continue;
}

const nmhsPath = path.join(browserDirectory, "NativeMessagingHosts");
const manifestPath = path.join(nmhsPath, "com.8bit.bitwarden.json");

try {
await fs.mkdir(nmhsPath, { recursive: true });
await this.writeManifest(
manifestPath,
key === "Firefox" || key === "Zen"
? await this.generateFirefoxJson(binaryPath)
: await this.generateChromeJson(binaryPath),
);
} catch (e) {
this.logService.error(`[Native messaging] Failed to set up ${key}, skipping:`, e);
}
}
break;
Expand All @@ -217,46 +221,40 @@ export class NativeMessagingMain {

// Unsandboxed browser
for (const [key, browserDirectory] of Object.entries(this.getLinuxNMHS())) {
if (existsSync(browserDirectory)) {
let nhmsPath = path.join(browserDirectory, "NativeMessagingHosts");
if (key === "Firefox") {
nhmsPath = path.join(browserDirectory, "native-messaging-hosts");
}
const browserBinaryPath = path.join(nhmsPath, ".bitwarden_desktop_proxy");
if (!existsSync(browserDirectory)) {
this.logService.warning(`${key} not found, skipping.`);
continue;
}

if (key === "Firefox") {
// Only generate the NMHS dir if the browser directory exists
await fs.mkdir(nhmsPath, { recursive: true });
}
const nmhsPath = path.join(
browserDirectory,
key === "Firefox" ? "native-messaging-hosts" : "NativeMessagingHosts",
);
const browserBinaryPath = path.join(nmhsPath, ".bitwarden_desktop_proxy");

try {
await fs.mkdir(nmhsPath, { recursive: true });
await this.linkOrCopy(binaryPath, browserBinaryPath);
this.logService.info(
`[Native messaging] Hard-linked ${binaryPath} to ${browserBinaryPath}`,
await this.writeManifest(
path.join(nmhsPath, "com.8bit.bitwarden.json"),
key === "Firefox"
? await this.generateFirefoxJson(browserBinaryPath)
: await this.generateChromeJson(browserBinaryPath),
);

if (key === "Firefox") {
await this.writeManifest(
path.join(nhmsPath, "com.8bit.bitwarden.json"),
await this.generateFirefoxJson(browserBinaryPath),
);
} else {
await this.writeManifest(
path.join(nhmsPath, "com.8bit.bitwarden.json"),
await this.generateChromeJson(browserBinaryPath),
);
}
} else {
this.logService.warning(`${key} not found, skipping.`);
} catch (e) {
this.logService.error(`[Native messaging] Failed to set up ${key}, skipping:`, e);
}
}

for (const [key, value] of Object.entries(this.getFlatpakNMHS())) {
if (existsSync(value)) {
if (!existsSync(value)) {
this.logService.warning(`${key} not found, skipping.`);
continue;
}

try {
const sandboxedProxyBinaryPath = path.join(value, ".bitwarden_desktop_proxy");
await this.linkOrCopy(binaryPath, sandboxedProxyBinaryPath);
this.logService.info(
`[Native messaging] Hard-linked ${binaryPath} to ${sandboxedProxyBinaryPath}`,
);

if (key === "Firefox") {
await this.writeManifest(
Expand All @@ -271,8 +269,11 @@ export class NativeMessagingMain {
} else {
this.logService.warning(`Flatpak ${key} not supported, skipping.`);
}
} else {
this.logService.warning(`${key} not found, skipping.`);
} catch (e) {
this.logService.error(
`[Native messaging] Failed to set up Flatpak ${key}, skipping:`,
e,
);
}
}

Expand Down
Loading