import { execFile } from "node:child_process"; import { accessSync, constants, lstatSync } from "node:fs"; import { isAbsolute } from "node:path"; import { installDetail } from "./update-words.mjs"; // Installs a downloaded .deb behind one polkit prompt. Root copies the file to a root-only dir and checks the feed's SHA-512 before apt-get sees it, // so a file swapped in the user-writable cache after the download is refused. Only this fixed script runs as root, never renderer text. // pkexec is only trusted at its system location, not wherever PATH finds one. export const PKEXEC = "/usr/bin/pkexec"; const SHELL = "/bin/sh"; const INSTALL_TIMEOUT_MS = 15 * 60 * 1000; // $1: the staged .deb, $2: its expected SHA-512 as lowercase hex. export const ELEVATED_INSTALL_SCRIPT = `set -eu umask 077 export DEBIAN_FRONTEND=noninteractive dir=$(mktemp -d) trap 'rm -rf "$dir"' EXIT cp -- "$1" "$dir/update.deb" actual=$(sha512sum "$dir/update.deb" | cut -d ' ' -f 1) if [ "$actual" != "$2" ]; then echo "the downloaded package does not match its checksum" >&2 exit 65 fi chmod 0755 "$dir" chmod 0644 "$dir/update.deb" apt-get install -y -o Dpkg::Options::=--force-confold "$dir/update.deb" `; // pkexec exit codes: the dialog was dismissed, or authorization failed (also: no polkit agent in this session). const PKEXEC_DISMISSED = 126; const PKEXEC_NOT_AUTHORIZED = 127; const CHECKSUM_MISMATCH = 65; // `OMB_UPDATE_INSTALL=terminal` opts out. export function canInstallElevated({ platform = process.platform, env = process.env, pkexec = PKEXEC, access = accessSync, } = {}) { if (platform !== "linux") return false; if (env.OMB_UPDATE_INSTALL === "terminal") return false; if (!env.DISPLAY && !env.WAYLAND_DISPLAY) return false; try { access(pkexec, constants.X_OK); return true; } catch { return false; } } // electron-updater records the SHA-512 as base64 (a feed may carry hex); the script compares lowercase hex. export function sha512Hex(value) { if (typeof value !== "string" || value.length === 0) return null; if (/^[0-9a-f]{128}$/i.test(value)) return value.toLowerCase(); if (!/^[A-Za-z0-9+/]{86}==$/.test(value)) return null; return Buffer.from(value, "base64").toString("hex"); } function lastLine(text) { const lines = String(text ?? "").split("\n").map((line) => line.trim()).filter(Boolean); return lines.at(-1)?.slice(0, 200) ?? ""; } // Never throws. Resolves { ok: true } or { ok: false, reason, detail }, reason one of: // unverifiable (no file, not a .deb, no checksum), unavailable (no pkexec), declined (prompt closed), // refused (polkit said no), checksum (file changed since download), failed (apt-get or the script; detail is its last line). export function installPackageElevated({ file, sha512, pkexec = PKEXEC, run = execFile, stat = lstatSync, timeout = INSTALL_TIMEOUT_MS, } = {}) { const expected = sha512Hex(sha512); let regular = false; try { regular = typeof file === "string" && stat(file).isFile(); } catch { regular = false; } if (typeof file !== "string" || !isAbsolute(file) || !file.endsWith(".deb") || !regular) { return Promise.resolve({ ok: false, reason: "unverifiable", ...installDetail("native.update.packageMissing"), }); } if (!expected) { return Promise.resolve({ ok: false, reason: "unverifiable", ...installDetail("native.update.noChecksum"), }); } return new Promise((resolve) => { let child; try { child = run( pkexec, [SHELL, "-c", ELEVATED_INSTALL_SCRIPT, "sh", file, expected], { timeout, killSignal: "SIGTERM" }, (error, _stdout, stderr) => { if (!error) return resolve({ ok: true }); if (error.code === "ENOENT") { return resolve({ ok: false, reason: "unavailable", ...installDetail("native.update.noPkexec") }); } if (error.code === PKEXEC_DISMISSED) { return resolve({ ok: false, reason: "declined", ...installDetail("native.update.promptClosed") }); } if (error.code === PKEXEC_NOT_AUTHORIZED) { return resolve({ ok: false, reason: "refused", ...installDetail("native.update.noAdmin") }); } if (error.code === CHECKSUM_MISMATCH) { return resolve({ ok: false, reason: "checksum", ...installDetail("native.update.checksumMismatch"), }); } const reason = error.killed ? installDetail("native.update.installTimedOut").detail : lastLine(stderr) || error.message; const words = installDetail("native.update.installFailed", { reason, ...(error.killed ? { timedOut: "true" } : {}) }); resolve({ ok: false, reason: "failed", ...words, detail: words.detail.slice(0, 240) }); }, ); } catch (error) { resolve({ ok: false, reason: "unavailable", detail: String(error?.message ?? error) }); return; } child?.stdin?.end?.(); }); }