Skip to content
Draft
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
47 changes: 42 additions & 5 deletions packages/core/src/persist.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,49 @@
import { join, dirname } from "node:path";
import { resolve, join, dirname } from "node:path";
import { mkdirSync, renameSync, existsSync, copyFileSync, readdirSync } from "node:fs";
import Logger from "./util/logger";
import * as darwin from "./util/darwin";

const logger = new Logger("core/persist");

export default function persist(asarPath: string) {
try {
if (process.platform === "win32") {
persistWin32(asarPath);
}
hookUpdaterForPersistence(asarPath);
} catch (e) {
logger.error(`Failed to persist moonlight: ${e}`);
}
}

function persistWin32(asarPath: string) {
function postPersistSign(bundlePath: string) {
if (process.platform !== "darwin") {
logger.error("Ignoring call to postPersistSign because we're not on Darwin");
return;
}

logger.debug("Inferred bundle path:", bundlePath);

if (darwin.verifySync(bundlePath, { verbosityLevel: 3 })) {
logger.warn("Bundle is currently passing code signing, no need to sign");
return;
} else {
logger.debug("Bundle no longer passes code signing (this is expected)");
}

darwin.signSync(bundlePath, {
deep: true,
force: true,
// TODO: let this be configurable
identity: "moonlight",
verbosityLevel: 3
});

if (darwin.verifySync(bundlePath, { verbosityLevel: 3 })) {
logger.info("Bundle signed succesfully!");
} else {
logger.error("Bundle didn't pass code signing even after signing, the app might be broken now :(");
}
}

function hookUpdaterForPersistence(asarPath: string) {
const updaterModule = require(join(asarPath, "common", "updater"));
const updater = updaterModule.Updater;

Expand Down Expand Up @@ -42,6 +71,14 @@ function persistWin32(asarPath: string) {
for (const file of readdirSync(currentAppDir)) {
copyFileSync(join(currentAppDir, file), join(newAppDir, file));
}

// on darwin, making changes disrupted the code signature on the app
// bundle, so we need to re-sign
if (process.platform === "darwin") {
// the asar is at Discord.app/Contents/Resources/app.asar
const inferredBundlePath = resolve(join(dirname(asarPath), "..", ".."));
postPersistSign(inferredBundlePath);
}
}

return realEmit.call(this, event, ...args);
Expand Down
86 changes: 86 additions & 0 deletions packages/core/src/util/darwin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Helper functions that wrap shelling out to codesign(1). This is only
// relevant for Darwin (macOS). These are synchronous because they need to
// block the updater.

import { spawnSync } from "node:child_process";
import Logger from "./logger";

/** Flags that may be passed to `codesign(1)` regardless of action. */
export interface SharedCodesignOptions {
verbosityLevel?: number;
}

/** Flags that may be passed to `codesign(1)` when signing a bundle. */
export interface SigningOptions extends SharedCodesignOptions {
/**
* Sign nested code items (such as Helper (Renderer).app, Helper (GPU).app, etc.)
*
* `--deep` is technically Bad (https://forums.developer.apple.com/forums/thread/129980),
* but it works for now.
*/
deep?: boolean;

/** Steamrolls any existing code signature present in the bundle. */
force?: boolean;

/**
* The name of the signing identity to use. Signing identities are automatically queried from the user's keychains.
*
* `-` specifies ad-hoc signing, which does not involve an identity at all
* and is used to sign exactly one instance of code.
*/
identity: "-" | (string & {}); // "& {}" prevents TS from unifying the literal.
}

const logger = new Logger("core/darwin");

function codesignSync(commandLineOptions: string[]) {
logger.debug("Invoking codesign with args:", commandLineOptions);
const result = spawnSync("/usr/bin/codesign", commandLineOptions, { stdio: "pipe" });

if (result.stdout) logger.debug("codesign stdout:", result.stdout);
if (result.stderr) logger.debug("codesign stderr:", result.stderr);

if (result.signal == null && result.status === 0) {
logger.debug("codesign peacefully exited");
} else {
const reason = result.status != null ? `code ${result.status}` : `signal ${result.signal}`;
throw new Error(`codesign exited with ${reason}`);
}
}

function* generateSharedCommandLineOptions(options: SharedCodesignOptions): IterableIterator<string> {
if (options.verbosityLevel) yield "-" + "v".repeat(options.verbosityLevel);
}

export function signSync(bundlePath: string, options: SigningOptions): void {
// codesign -s <IDENTITY> [-v] [--deep] [--force] <PATH>
function* cliOptions(): IterableIterator<string> {
yield "-s";
yield options.identity;

yield* generateSharedCommandLineOptions(options);
if (options.deep) yield "--deep";
if (options.force) yield "--force";

yield bundlePath;
}

codesignSync(Array.from(cliOptions()));
}

export function verifySync(bundlePath: string, options: SharedCodesignOptions = {}): boolean {
// codesign --verify [-v] <PATH>
function* cliOptions(): IterableIterator<string> {
yield "--verify";
yield* generateSharedCommandLineOptions(options);
yield bundlePath;
}

try {
codesignSync(Array.from(cliOptions()));
return true;
} catch {
return false;
}
}