Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
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
96 changes: 96 additions & 0 deletions packages/core/src/darwin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { spawn } from "node:child_process";
import Logger from "./util/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");

async function invokeCodesign(commandLineOptions: string[]) {
logger.debug("Invoking codesign with args:", commandLineOptions);
const codesignChild = spawn("/usr/bin/codesign", commandLineOptions, { stdio: "pipe" });

codesignChild.on("spawn", () => {
logger.debug(`Spawned codesign (pid: ${codesignChild.pid})`);
});
codesignChild.stdout.on("data", (data) => {
logger.debug("codesign stdout:", data.toString());
});
codesignChild.stderr.on("data", (data) => {
logger.debug("codesign stderr:", data.toString());
});

await new Promise<void>((resolve, reject) => {
codesignChild.on("exit", (code, signal) => {
if (signal == null && code === 0) {
logger.debug("codesign peacefully exited");
resolve();
} else {
const reason = code != null ? `code ${code}` : `signal ${signal}`;
reject(`codesign exited with ${reason}`);
}
});
});
}

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

export function sign(bundlePath: string, options: SigningOptions): Promise<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;
}

return invokeCodesign(Array.from(cliOptions()));
}

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

return invokeCodesign(Array.from(cliOptions())).then(
() => true,

// we're conflating codesign(1) itself erroring and codesign(1)
// successfully returning that the bundle is invalid, because it'd exit in
// an error in either case, but that's probably OK
() => false
);
}
45 changes: 40 additions & 5 deletions packages/core/src/persist.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,55 @@
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 "./darwin";

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

export default function persist(asarPath: string) {
export default async function persist(asarPath: string) {
try {
if (process.platform === "win32") {
persistWin32(asarPath);
persistAsar(asarPath);

if (process.platform === "darwin") {
// the asar is at Discord.app/Contents/Resources/app.asar
const inferredBundlePath = resolve(join(dirname(asarPath), "..", ".."));
await postPersistSign(inferredBundlePath);
}
} catch (e) {
logger.error(`Failed to persist moonlight: ${e}`);
}
}

function persistWin32(asarPath: string) {
async 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 (await darwin.verify(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)");
}

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

if (await darwin.verify(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 persistAsar(asarPath: string) {
const updaterModule = require(join(asarPath, "common", "updater"));
const updater = updaterModule.Updater;

Expand Down
2 changes: 1 addition & 1 deletion packages/injector/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,7 @@ export async function inject(asarPath: string) {
if (isMoonlightDesktop) return;

if (!hasOpenAsar && !isMoonlightDesktop) {
persist(asarPath);
await persist(asarPath);
}

// Need to do this instead of require() or it breaks require.main
Expand Down