// The root script runs through a real shell here, with a recording apt-get standing in for the package manager. import assert from "node:assert/strict"; import { createHash } from "node:crypto"; import { execFileSync, spawnSync } from "node:child_process"; import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import test from "node:test"; import { ELEVATED_INSTALL_SCRIPT, PKEXEC, canInstallElevated, installPackageElevated, sha512Hex, } from "./elevated-install.mjs"; const hex = (text) => createHash("sha512").update(text).digest("hex"); const base64 = (text) => createHash("sha512").update(text).digest("base64"); test("a password prompt is only attempted where one can appear", () => { const ok = { platform: "linux", env: { DISPLAY: ":0" }, access: () => {} }; assert.equal(canInstallElevated(ok), true); assert.equal(canInstallElevated({ ...ok, env: { WAYLAND_DISPLAY: "wayland-0" } }), true); assert.equal(canInstallElevated({ ...ok, platform: "darwin" }), false); assert.equal(canInstallElevated({ ...ok, env: {} }), false, "no graphical session: ssh, a service"); assert.equal(canInstallElevated({ ...ok, env: { DISPLAY: ":0", OMB_UPDATE_INSTALL: "terminal" } }), false); assert.equal(canInstallElevated({ ...ok, access: () => { throw new Error("ENOENT"); } }), false); }); test("pkexec is trusted only at its system location", () => { assert.equal(PKEXEC, "/usr/bin/pkexec"); }); test("the feed's checksum becomes the hex the script compares", () => { assert.equal(sha512Hex(base64("deb bytes")), hex("deb bytes")); assert.equal(sha512Hex(hex("deb bytes").toUpperCase()), hex("deb bytes")); for (const bad of [undefined, null, "", "abc", "x".repeat(88), `${base64("a")}!`, 42]) { assert.equal(sha512Hex(bad), null, String(bad)); } }); function fixture() { const dir = mkdtempSync(join(tmpdir(), "omb-elevated-")); const cache = join(dir, "cache dir", "o'brien", "pending"); mkdirSync(cache, { recursive: true }); const file = join(cache, "OpenMausBot-2.0.0-amd64.deb"); writeFileSync(file, "deb bytes"); return { dir, file, cache, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; } test("the arguments are the system pkexec, a fixed script, and two values", async () => { const f = fixture(); try { const calls = []; const result = await installPackageElevated({ file: f.file, sha512: base64("deb bytes"), run: (program, args, options, callback) => { calls.push({ program, args, options }); callback(null, "", ""); return { stdin: { end() {} } }; }, }); assert.deepEqual(result, { ok: true }); assert.equal(calls.length, 1); assert.equal(calls[0].program, "/usr/bin/pkexec"); assert.deepEqual(calls[0].args, ["/bin/sh", "-c", ELEVATED_INSTALL_SCRIPT, "sh", f.file, hex("deb bytes")]); assert.ok(calls[0].options.timeout > 60_000); } finally { f.cleanup(); } }); test("each way the install can end maps to a reason the card can act on", async () => { const f = fixture(); try { const outcome = (error, stderr = "") => installPackageElevated({ file: f.file, sha512: hex("deb bytes"), run: (_p, _a, _o, callback) => { callback(error, "", stderr); return null; }, }); const coded = (code, extra = {}) => Object.assign(new Error(`exit ${code}`), { code, ...extra }); for (const [code, reason, key, detail] of [ [126, "declined", "native.update.promptClosed", "The password prompt was closed."], [127, "refused", "native.update.noAdmin", "This session could not ask for administrator rights."], [65, "checksum", "native.update.checksumMismatch", "The downloaded update did not match its checksum. Check again to download it fresh."], ["ENOENT", "unavailable", "native.update.noPkexec", "pkexec is not installed."], ]) assert.deepEqual(await outcome(coded(code)), { ok: false, reason, detail, detailKey: key }); const timedOut = await outcome(coded(null, { killed: true })); assert.deepEqual(timedOut, { ok: false, reason: "failed", detail: "The install did not finish: The install took too long.", detailKey: "native.update.installFailed", detailParams: { reason: "The install took too long.", timedOut: "true" } }); const failed = await outcome(coded(100), "W: noise\nE: Unable to acquire the dpkg frontend lock\n"); assert.equal(failed.reason, "failed"); assert.match(failed.detail, /Unable to acquire the dpkg frontend lock/); const slow = await outcome(coded(null, { killed: true })); assert.equal(slow.reason, "failed"); assert.match(slow.detail, /too long/); const thrown = await installPackageElevated({ file: f.file, sha512: hex("deb bytes"), run: () => { throw new Error("spawn failed"); }, }); assert.equal(thrown.reason, "unavailable"); } finally { f.cleanup(); } }); test("nothing is run for a file or checksum that cannot be trusted", async () => { const f = fixture(); try { const run = () => assert.fail("pkexec must not be started"); const link = join(f.dir, "link.deb"); symlinkSync(f.file, link); writeFileSync(join(f.dir, "not-a-deb.txt"), "x"); for (const file of [undefined, "", "relative.deb", join(f.dir, "gone.deb"), join(f.dir, "not-a-deb.txt"), f.cache, link]) { const result = await installPackageElevated({ file, sha512: hex("deb bytes"), run }); assert.equal(result.ok, false, String(file)); assert.equal(result.reason, "unverifiable", String(file)); } const noSum = await installPackageElevated({ file: f.file, sha512: undefined, run }); assert.equal(noSum.reason, "unverifiable"); assert.match(noSum.detail, /checksum/); } finally { f.cleanup(); } }); // ---- the script itself, in a real shell -------------------------------- // The root script only ever runs on Linux, and the stand-in apt-get uses GNU coreutils (stat -c, find, sha512sum). const linuxOnly = { skip: process.platform !== "linux" && "the root script runs only on Linux" }; function runScript(file, expected, { aptBody } = {}) { const dir = mkdtempSync(join(tmpdir(), "omb-script-")); const bin = join(dir, "bin"); const log = join(dir, "apt.log"); mkdirSync(bin); writeFileSync( join(bin, "apt-get"), aptBody ?? [ "#!/bin/sh", `printf '%s\\n' "$@" > '${log}'`, 'for last; do :; done', `printf 'dir=%s\\nmode=%s\\ndirmode=%s\\nbytes=%s\\nfront=%s\\n' "$(dirname "$last")" "$(stat -c %a "$last")" "$(stat -c %a "$(dirname "$last")")" "$(cat "$last")" "$DEBIAN_FRONTEND" >> '${log}'`, "", ].join("\n"), ); chmodSync(join(bin, "apt-get"), 0o755); const result = spawnSync("/bin/sh", ["-c", ELEVATED_INSTALL_SCRIPT, "sh", file, expected], { encoding: "utf8", env: { PATH: `${bin}:${process.env.PATH}`, TMPDIR: dir }, }); return { result, log: existsSync(log) ? readFileSync(log, "utf8") : null, leftovers: execFileSync("find", [dir, "-maxdepth", "1", "-name", "tmp.*"], { encoding: "utf8" }).trim(), cleanup: () => rmSync(dir, { recursive: true, force: true }), }; } test("root installs its own verified copy, not the file in the user's cache", linuxOnly, () => { const f = fixture(); const run = runScript(f.file, hex("deb bytes")); try { assert.equal(run.result.status, 0, run.result.stderr); const lines = run.log.trim().split("\n"); assert.deepEqual(lines.slice(0, 4), ["install", "-y", "-o", "Dpkg::Options::=--force-confold"]); const installed = lines[4]; assert.match(installed, /\/tmp\.[^/]+\/update\.deb$/); assert.ok(!installed.startsWith(f.cache), "must not install straight from the user-writable cache"); assert.ok(run.log.includes("mode=644") && run.log.includes("dirmode=755")); assert.ok(run.log.includes("bytes=deb bytes") && run.log.includes("front=noninteractive")); assert.equal(run.leftovers, ""); } finally { run.cleanup(); f.cleanup(); } }); test("a file swapped after the download is refused before apt-get runs", linuxOnly, () => { const f = fixture(); writeFileSync(f.file, "swapped for something else"); const run = runScript(f.file, hex("deb bytes")); try { assert.equal(run.result.status, 65); assert.match(run.result.stderr, /checksum/); assert.equal(run.log, null, "apt-get must never see an unverified package"); assert.equal(run.leftovers, ""); } finally { run.cleanup(); f.cleanup(); } }); test("a failing apt-get fails the script and still cleans up", linuxOnly, () => { const f = fixture(); const run = runScript(f.file, hex("deb bytes"), { aptBody: "#!/bin/sh\necho 'E: broken dependencies' >&2\nexit 100\n" }); try { assert.equal(run.result.status, 100); assert.match(run.result.stderr, /broken dependencies/); assert.equal(run.leftovers, ""); } finally { run.cleanup(); f.cleanup(); } });