Files
Panama/config/dot/quickshell/modules/polkit/PolkitPrompt.qml
T
Gabriel Brown 116510caa8 Draw the authentication prompt ourselves
hyprpolkitagent's dialog is compiled into its binary -- no config, no
stylesheet, nothing to theme -- and it was the one window on this
desktop that looked like it belonged to something else.

The split between the two halves is the security design, not an
implementation detail. A small agent process owns the D-Bus side: it
registers with polkitd, receives the request, and hands the shell the
action, the message, who may answer, and a one-time cookie. It never
sees a password. The shell draws the prompt and, on submit, spawns the
setuid polkit-agent-helper-1 itself and writes the password to that
helper's stdin; the helper runs the PAM conversation and reports to
polkitd directly. The password exists in the shell and in the helper's
stdin and nowhere else -- never on a command line, never over D-Bus,
never through IPC arguments.

The prompt takes exclusive keyboard focus, because a password field that
lets keystrokes reach the window behind it is a keylogger with extra
steps. The request travels as a file created 0600 with O_EXCL inside a
0700 runtime directory: a cookie is not a password, but it is a
capability, and capabilities do not belong in a process listing either.

Three things cost real time. polkitd calls back on the same connection
that registered, so exporting the object on the session bus while
registering from the system bus failed every request as "Not authorized"
with no error anywhere. XDG_SESSION_ID is absent in a systemd user unit,
which runs under [email protected] and belongs to no login session, so the
session comes from logind's Display property instead. And PyGObject does
not accept the @ placeholder in variant format strings.

hyprpolkitagent stays installed as the fallback, only one agent is
started, and the comment beside the autostart says how to get the stock
prompt back. Verified end to end, including a real password accepted and
three cancellations refused.

Claude-Session: https://claude.ai/code/session_01BRvzt4H8XXLPVH5MyYdk9L
2026-08-19 18:08:52 -04:00

158 lines
4.7 KiB
QML

// The authentication prompt.
//
// Deliberately a Panama surface rather than the compositor's stock agent: this
// is the window that asks for the password to everything, and it was the one
// window on the desktop that looked like it belonged to something else.
//
// Two things here are security, not styling. It takes EXCLUSIVE keyboard focus,
// so keystrokes cannot reach the window underneath while a password is being
// typed. And the field is cleared on every exit path, including the ones nobody
// plans for.
import Quickshell
import Quickshell.Wayland
import QtQuick
import qs.config
import qs.services
import qs.modules.settings
PanelWindow {
id: root
visible: Polkit.active
color: "transparent"
anchors { top: true; bottom: true; left: true; right: true }
exclusiveZone: 0
WlrLayershell.namespace: "qs-polkit"
WlrLayershell.layer: WlrLayer.Overlay
// Exclusive, not OnDemand: a password prompt that lets keystrokes through
// to whatever is behind it is a keylogger with extra steps.
WlrLayershell.keyboardFocus: Polkit.active
? WlrKeyboardFocus.Exclusive
: WlrKeyboardFocus.None
onVisibleChanged: {
if (root.visible) {
field.text = "";
field.forceActiveFocus();
} else {
field.text = "";
}
}
// Dims what is behind, and swallows clicks so nothing outside the dialog
// can be operated while it is waiting.
Rectangle {
anchors.fill: parent
color: Theme.alpha(Theme.bgDark, Theme.overlayAlpha)
MouseArea {
anchors.fill: parent
// Clicking outside does nothing on purpose. Dismissing an
// authentication request by misclick, and having the thing that
// asked report a mysterious failure, is worse than an explicit
// Cancel.
hoverEnabled: true
}
}
Rectangle {
id: dialog
anchors.centerIn: parent
width: 420
implicitHeight: layout.implicitHeight + 44
radius: Theme.popoverRadius
color: Theme.alpha(Theme.bgPopover, Theme.popoverAlpha)
border.width: 1
border.color: Theme.alpha(Theme.fg, 0.1)
Column {
id: layout
anchors.centerIn: parent
width: parent.width - 44
spacing: 14
Text {
width: parent.width
text: "Authentication required"
color: Theme.fg
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.DemiBold
wrapMode: Text.WordWrap
}
Text {
width: parent.width
text: Polkit.message
color: Theme.fgDim
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSize
wrapMode: Text.WordWrap
}
Text {
width: parent.width
visible: Polkit.users.length > 1
text: "Authenticating as " + Polkit.chosenUser
color: Theme.fgMuted
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
PasswordField {
id: field
width: parent.width
placeholder: "Password"
enabled: !Polkit.authenticating
onAccepted: {
if (field.text !== "")
Polkit.submit(field.text);
field.text = "";
}
}
Text {
width: parent.width
visible: Polkit.failureText !== ""
text: Polkit.failureText
color: Theme.danger
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.WordWrap
}
Row {
anchors.right: parent.right
spacing: 8
SettingsButton {
text: "Cancel"
onClicked: Polkit.cancel()
}
SettingsButton {
text: Polkit.authenticating ? "Checking…" : "Authenticate"
tone: "accent"
enabled: !Polkit.authenticating
onClicked: {
if (field.text !== "")
Polkit.submit(field.text);
field.text = "";
}
}
}
}
}
// Escape cancels, which is what every other dialog on this desktop does.
Item {
anchors.fill: parent
focus: true
Keys.onEscapePressed: Polkit.cancel()
}
}