"use strict"; const crypto = require("node:crypto"); const fs = require("node:fs"); const path = require("node:path"); const { execFileSync } = require("node:child_process"); // TN3179 / TN3178: Local Network privacy uses the executable's LC_UUID. // Electron's prebuilt launchers otherwise share it with unrelated applications. // Only change launcher UUIDs before signing; leave Electron Framework (and its // upstream debugging symbols), native runtimes and installed apps untouched. function uuidCommands(bytes) { const entries = []; const range = (offset, size, end = bytes.length) => { if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(size) || offset < 0 || size < 0 || offset + size > end) { throw new Error("Invalid Mach-O bounds."); } }; const thin = (start, size) => { range(start, size); range(start, 28, start + size); const magic = bytes.readUInt32BE(start); const little = magic === 0xcefaedfe || magic === 0xcffaedfe; if (![0xcefaedfe, 0xcffaedfe, 0xfeedface, 0xfeedfacf].includes(magic)) throw new Error("Unsupported Mach-O executable."); const read = (offset) => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); const headerSize = [0xcffaedfe, 0xfeedfacf].includes(magic) ? 32 : 28; const count = read(start + 16); const commandsSize = read(start + 20); range(start, headerSize + commandsSize, start + size); const end = start + headerSize + commandsSize; let cursor = start + headerSize; let found = false; for (let i = 0; i < count; i++) { range(cursor, 8, end); const command = read(cursor); const commandSize = read(cursor + 4); if (commandSize < 8 || commandSize % 4) throw new Error("Invalid Mach-O load command."); range(cursor, commandSize, end); if (command === 0x1b) { if (found || commandSize !== 24) throw new Error("Invalid or duplicate LC_UUID."); entries.push({ offset: cursor + 8, cpuType: read(start + 4), cpuSubtype: read(start + 8) }); found = true; } cursor += commandSize; } if (cursor !== end || !found) throw new Error("Mach-O executable must have exactly one LC_UUID per architecture."); }; range(0, 4); const magic = bytes.readUInt32BE(0); if ([0xcafebabe, 0xbebafeca, 0xcafebabf, 0xbfbafeca].includes(magic)) { range(0, 8); const little = [0xbebafeca, 0xbfbafeca].includes(magic); const fat64 = [0xcafebabf, 0xbfbafeca].includes(magic); const read = (offset) => little ? bytes.readUInt32LE(offset) : bytes.readUInt32BE(offset); const read64 = (offset) => Number(little ? bytes.readBigUInt64LE(offset) : bytes.readBigUInt64BE(offset)); const count = read(4); const stride = fat64 ? 32 : 20; range(8, count * stride); if (!count) throw new Error("Empty universal Mach-O executable."); let previousEnd = 8 + count * stride; for (let i = 0; i < count; i++) { const cursor = 8 + i * stride; const offset = fat64 ? read64(cursor + 8) : read(cursor + 8); const size = fat64 ? read64(cursor + 16) : read(cursor + 12); if (offset < previousEnd) throw new Error("Overlapping Mach-O architectures."); thin(offset, size); previousEnd = offset + size; } } else thin(0, bytes.length); return entries; } function launcherUuid(identity, entry) { const uuid = crypto.createHash("sha256") .update(JSON.stringify(["chj-local-network-v1", identity, entry.cpuType, entry.cpuSubtype])) .digest().subarray(0, 16); uuid[6] = (uuid[6] & 0x0f) | 0x80; // RFC 9562 custom UUID (version 8). uuid[8] = (uuid[8] & 0x3f) | 0x80; return uuid; } function readPlist(bundlePath) { return JSON.parse(execFileSync("/usr/bin/plutil", ["-convert", "json", "-o", "-", path.join(bundlePath, "Contents", "Info.plist")], { encoding: "utf8" })); } function processNetworkIdentity(appPath, expected, patch) { const info = readPlist(appPath); if (info.CFBundleIdentifier !== expected.appId) throw new Error("macOS bundle ID does not match the application identity."); if (info.CFBundleShortVersionString !== expected.version) throw new Error("macOS application version differs from package.json."); if (!String(info.NSLocalNetworkUsageDescription || "").trim()) throw new Error("macOS app lacks NSLocalNetworkUsageDescription."); const payload = crypto.createHash("sha256").update(fs.readFileSync(path.join(appPath, "Contents", "Resources", "app.asar"))).digest("hex"); const frameworks = path.join(appPath, "Contents", "Frameworks"); const bundles = [appPath, ...fs.readdirSync(frameworks, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && entry.name.endsWith(".app")) .map((entry) => path.join(frameworks, entry.name))]; const uuids = new Set(); const changes = []; const result = []; for (const bundle of bundles) { const plist = bundle === appPath ? info : readPlist(bundle); const name = plist.CFBundleExecutable; if (!name || path.basename(name) !== name) throw new Error("Invalid CFBundleExecutable."); const executable = path.join(bundle, "Contents", "MacOS", name); const relativePath = path.relative(appPath, executable).split(path.sep).join("/"); const bytes = fs.readFileSync(executable); const identity = [info.CFBundleIdentifier, plist.CFBundleIdentifier, relativePath, payload]; let changed = false; for (const entry of uuidCommands(bytes)) { const uuid = launcherUuid(identity, entry); const actual = bytes.subarray(entry.offset, entry.offset + 16); if (!actual.equals(uuid)) { if (!patch) throw new Error(`macOS launcher has a shared or unexpected LC_UUID: ${relativePath}`); uuid.copy(bytes, entry.offset); changed = true; } const hex = uuid.toString("hex"); if (uuids.has(hex)) throw new Error("Duplicate macOS launcher UUID."); uuids.add(hex); result.push({ executable: relativePath, uuid: hex }); } if (changed) changes.push({ executable, bytes }); } // Validate every architecture and bundle before changing any output file. for (const { executable, bytes } of changes) fs.writeFileSync(executable, bytes); return result; } module.exports = { uuidCommands, launcherUuid, prepareMacNetworkIdentity: (appPath, expected) => processNetworkIdentity(appPath, expected, true), verifyMacNetworkIdentity: (appPath, expected) => processNetworkIdentity(appPath, expected, false) };