#!/usr/bin/env bash # The authentication prompt asks for the password to everything, so the rules # here are not style, they are the reason it was safe to write at all. # # 1. The agent process must never handle a password. It talks to polkitd and # hands the shell a request; the shell talks to the setuid helper. # 2. The password reaches the helper on STDIN and nowhere else. argv is # world-readable through /proc, so an argument is a broadcast. # 3. The prompt takes EXCLUSIVE keyboard focus. A password field that lets # keystrokes through to the window behind it is a keylogger with extra # steps. # 4. The request file carries a one-time capability and is created 0600. # 5. The stock agent stays installed. Only one agent may register per session, # so a broken replacement must have something to fall back to. # # Static, plus a live check of the runtime directory. It never authenticates. set -uo pipefail repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" agent="$repo_dir/config/dot/quickshell/scripts/panama-polkit-agent" service="$repo_dir/config/dot/quickshell/services/Polkit.qml" prompt="$repo_dir/config/dot/quickshell/modules/polkit/PolkitPrompt.qml" autostart="$repo_dir/config/dot/hypr/autostart.lua" fail() { printf 'polkit agent contract: %s\n' "$1" >&2 exit 1 } for path in "$agent" "$service" "$prompt" "$autostart"; do [[ -r "$path" ]] || fail "missing $path" done [[ -x "$agent" ]] || fail 'the agent is not executable' # ── 1. The agent never touches a password ─────────────────────────────────── # Comments and docstrings are stripped first: this file EXPLAINS at length why # it must not touch a password, and an earlier version of this check failed on # the explanation rather than on any behaviour. agent_code="$(python3 - "$agent" <<'STRIP' import ast, sys source = open(sys.argv[1], encoding="utf-8").read() tree = ast.parse(source) # Drop every docstring, then print what is left as code. for node in ast.walk(tree): if isinstance(node, (ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)): if (node.body and isinstance(node.body[0], ast.Expr) and isinstance(node.body[0].value, ast.Constant) and isinstance(node.body[0].value.value, str)): node.body.pop(0) print(ast.unparse(tree)) STRIP )" [[ -n "$agent_code" ]] || fail 'could not read the agent source' grep -qE 'polkit-agent-helper|PAM_PROMPT|password' <<<"$agent_code" \ && fail 'the agent process handles passwords; that belongs on the shell side only' # ── 2. The password goes to the helper on stdin ───────────────────────────── grep -q 'stdinEnabled: true' "$service" \ || fail 'the helper is spawned without a writable stdin, so the password has nowhere to go' grep -q 'helper.write(root.pendingSecret' "$service" \ || fail 'the password is not written to the helper stdin' # It must never appear in the argument list. helper_command="$(grep -n 'helper.command' "$service" || true)" grep -qE 'pendingSecret|password' <<<"$helper_command" \ && fail 'the password appears in the helper command line' grep -q 'root.pendingSecret = "";' "$service" \ || fail 'the password is never cleared after being handed over' # ── 3. The prompt owns the keyboard ───────────────────────────────────────── grep -q 'WlrKeyboardFocus.Exclusive' "$prompt" \ || fail 'the prompt does not take exclusive keyboard focus' grep -q 'field.text = ""' "$prompt" \ || fail 'the password field is not cleared when the prompt closes' # ── 4. The request file is private ────────────────────────────────────────── grep -q '0o600' "$agent" || fail 'the request file is not created 0600' grep -q '0o700' "$agent" || fail 'the runtime directory is not private' grep -qE 'O_CREAT \| os\.O_EXCL' "$agent" \ || fail 'the request file is not created exclusively, so it could be pre-created by someone else' # ── 5. A fallback exists ──────────────────────────────────────────────────── grep -q 'hyprpolkitagent' "$autostart" \ || fail 'nothing records how to get the stock agent back if this one fails' command -v rpm >/dev/null 2>&1 && { rpm -q hyprpolkitagent >/dev/null 2>&1 \ || fail 'the stock agent is no longer installed, so there is nothing to fall back to' } # Only one agent may be started by the session. Lua comments are excluded: the # file documents how to restore the stock agent, and that sentence is not a # command the session runs. started="$(grep -vE '^\s*--' "$autostart" | grep -E 'systemctl --user start .*polkit' | head -1)" [[ -n "$started" ]] || fail 'the session starts no polkit agent at all' grep -q 'panama-polkit-agent' <<<"$started" \ || fail 'the session does not start the Panama agent' grep -q 'hyprpolkitagent.service' <<<"$started" \ && fail 'the session starts both agents; the second to register will fail' # ── Live: the runtime directory is private, if it exists ──────────────────── runtime="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/panama-polkit" if [[ -d "$runtime" ]]; then mode="$(stat -c '%a' "$runtime")" [[ "$mode" == "700" ]] || fail "the runtime directory is mode $mode, not 700" leaked="$(find "$runtime" -type f ! -perm 600 2>/dev/null | head -1)" [[ -z "$leaked" ]] || fail "a request file is readable beyond its owner: $leaked" fi printf 'polkit agent contract: PASS (password never leaves the shell, prompt owns the keyboard, fallback intact)\n'