lock: show the PAM prompt and track the conversation state (#3691)

Pam.qml holds the prompt text PAM asked for and separates "a response is
in flight" from "the conversation is open", so the UI stops treating an
open conversation as busy. The lock screen shows the prompt instead of a
hardcoded "Password", and clears the field when a new prompt arrives.
This commit is contained in:
hkoeck authored and GitHub committed 2026-10-12 11:16:18 -04:00
1 parent a8bb04a2b2
commit 10eb5f6ec3
2 files changed
+107 -20

No files matched your search

+83 -4
View File
@@ -24,6 +24,15 @@ Scope {
property string u2fPendingMode
property string buffer
// the prompt pam asked for, e.g. "PIN: "
property string authPromptText: ""
property bool responseSubmitted: false
property bool awaitingUserInput: false
property bool responseInFlight: false
// the conversation is held open while waiting, so passwd.active is not "busy"
readonly property bool authBusy: passwd.active && root.responseInFlight
// stops a conversation that never prompts from being reopened forever
property int autoRestartCount: 0
property var attemptInfoMessages: []
property bool lockoutAnnouncedThisAttempt: false
@@ -53,6 +62,37 @@ Scope {
u2f.checkAvail();
}
// open the conversation without answering it, so the prompt is known before typing
function beginAuthSession(): void {
if (passwd.active || root.unlockInProgress || !root.lockSecured)
return;
if (root.autoRestartCount >= 3)
return;
root.autoRestartCount = root.autoRestartCount + 1;
root.responseSubmitted = false;
root.awaitingUserInput = false;
root.responseInFlight = false;
eagerStart.restart();
}
function submitPassword(): void {
root.responseSubmitted = true;
if (!passwd.active) {
passwd.start();
return;
}
if (passwd.responseRequired) {
root.responseSubmitted = false;
root.awaitingUserInput = false;
root.responseInFlight = true;
passwdActiveTimeout.restart();
passwd.respond(root.buffer);
} else {
// kept and sent when the prompt arrives; a stack that never asks still times out
passwdActiveTimeout.restart();
}
}
function completeUnlock(): void {
if (!root.unlockInProgress) {
root.unlockInProgress = true;
@@ -187,10 +227,15 @@ Scope {
root.attemptInfoMessages = root.attemptInfoMessages.concat([message]);
}
onResponseRequiredChanged: {
// not responseRequiredChanged: that only fires on change, so two prompts in
// a row leave the second unhandled
onPamMessage: {
if (!responseRequired)
return;
root.authPromptText = (message || "").replace(/:\s*$/, "").trim();
root.autoRestartCount = 0;
const notice = root.attemptInfoMessages.filter(m => m !== message);
if (notice.length > 0) {
root.lockMessage = notice.join("\n");
@@ -198,7 +243,17 @@ Scope {
}
root.attemptInfoMessages = [];
respond(root.buffer);
// each prompt needs its own submission, or the second gets the first answer
if (root.responseSubmitted) {
root.responseSubmitted = false;
root.awaitingUserInput = false;
root.responseInFlight = true;
respond(root.buffer);
} else {
root.awaitingUserInput = true;
root.responseInFlight = false;
passwdActiveTimeout.running = false;
}
}
onCompleted: res => {
@@ -236,6 +291,12 @@ Scope {
else if (res === PamResult.Failed)
root.state = "fail";
root.responseSubmitted = false;
root.awaitingUserInput = false;
root.responseInFlight = false;
if (res !== PamResult.MaxTries)
root.beginAuthSession();
root.flashMsg();
stateReset.restart();
}
@@ -248,7 +309,11 @@ Scope {
if (passwd.active) {
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
passwdActiveTimeout.restart();
root.authPromptText = "";
if (root.responseSubmitted)
passwdActiveTimeout.restart();
else
passwdActiveTimeout.running = false;
} else {
passwdActiveTimeout.running = false;
}
@@ -431,7 +496,7 @@ Scope {
}
function startForAlternativeAuth(): void {
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByPrimaryPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByPrimaryPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || root.authBusy || active)
return;
abort();
root.u2fPending = true;
@@ -548,6 +613,16 @@ Scope {
onTriggered: root.cancelU2fPending()
}
Timer {
id: eagerStart
interval: 50
onTriggered: {
if (!passwd.active && root.lockSecured && !root.unlockInProgress)
passwd.start();
}
}
Timer {
id: passwdActiveTimeout
@@ -602,6 +677,10 @@ Scope {
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
root.resetAuthFlows();
root.responseSubmitted = false;
root.awaitingUserInput = false;
root.autoRestartCount = 0;
root.beginAuthSession();
fprint.tries = 0;
fprint.errorTries = 0;
if (!SettingsData.lockPamExternallyManaged && !dankshellConfigWatcher.loaded && !userPamWatcher.loaded)
@@ -30,7 +30,7 @@ Item {
readonly property bool ring: style === "ring"
readonly property bool morph: style === "expressive"
readonly property bool minimal: style === "minimal"
readonly property bool authenticating: (pam?.passwd.active ?? false) && !unlocking
readonly property bool authenticating: (pam?.authBusy ?? false) && !unlocking
readonly property bool failed: pamState !== ""
readonly property var morphShapes: ["cookie4", "clover4", "sunny", "cookie9", "softBurst", "pentagon", "oval", "cookie6"]
readonly property string morphShape: morphShapes[passwordBuffer.length % morphShapes.length]
@@ -113,7 +113,7 @@ Item {
}
function canStartSecurityKeyUnlock() {
return !demoMode && pam && pam.u2f && pam.u2f.available && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !pam.passwd.active && !pam.u2f.active && !pam.u2fPending && !unlocking;
return !demoMode && pam && pam.u2f && pam.u2f.available && SettingsData.enableU2f && SettingsData.u2fMode === "or" && !pam.authBusy && !pam.u2f.active && !pam.u2fPending && !unlocking;
}
function triggerSecurityKeyUnlock() {
@@ -161,6 +161,12 @@ Item {
if (keyboardController.isKeyboardActive)
keyboardController.hide();
}
// onStateChanged only fires when the conversation ends, too late between prompts
function onAwaitingUserInputChanged() {
if (root.pam.awaitingUserInput)
passwordField.clear();
}
}
ColumnLayout {
@@ -534,7 +540,7 @@ Item {
return root.accentColor;
return root.contained ? Theme.surfaceVariantText : Theme.withAlpha(root.plainColor, Theme.pendingOpacity);
}
opacity: root.pam?.passwd.active && !root.morph ? 0 : 1
opacity: root.pam?.authBusy && !root.morph ? 0 : 1
Behavior on opacity {
NumberAnimation {
@@ -652,8 +658,8 @@ Item {
activeFocusOnTab: !root.demoMode
onTextChanged: cursorPosition = text.length
onAccepted: {
if (!root.demoMode && !root.unlocking && !root.pam.passwd.active && !root.pam.u2fPending)
root.pam.passwd.start();
if (!root.demoMode && !root.unlocking && !root.pam.authBusy && !root.pam.u2fPending)
root.pam.submitPassword();
}
Keys.onPressed: event => handleKey(event)
@@ -686,7 +692,7 @@ Item {
return;
}
if (root.pam.passwd.active) {
if (root.pam.authBusy) {
root.log.debug("PAM is active, ignoring input");
event.accepted = true;
return;
@@ -809,7 +815,7 @@ Item {
return;
const committed = text;
text = "";
if (root.demoMode || root.unlocking || root.pam.passwd.active)
if (root.demoMode || root.unlocking || root.pam.authBusy)
return;
passwordField.insertText(committed);
}
@@ -848,14 +854,16 @@ Item {
return I18n.tr("Insert your security key...");
return I18n.tr("Touch your security key...");
}
if (root.pam.passwd.active)
if (root.pam.authBusy)
return I18n.tr("Authenticating...", "lock screen status text while the password is checked");
if (root.passwordVisibility !== "always")
return "";
if (root.pam.authPromptText !== "")
return root.pam.authPromptText;
return I18n.tr("Password", "lock screen password field placeholder") + "…";
}
color: {
if (root.unlocking || (root.pam?.passwd.active ?? false))
if (root.unlocking || (root.pam?.authBusy ?? false))
return root.accentColor;
return root.contained ? Theme.outline : Theme.withAlpha(root.plainColor, Theme.pendingOpacity);
}
@@ -969,7 +977,7 @@ Item {
x: passwordDisplay.x + passwordDisplay.cursorRectangle.x
y: passwordDisplay.y + passwordDisplay.cursorRectangle.y
height: passwordDisplay.cursorRectangle.height
shown: !root.demoMode && root.showPasswordField && !root.ring && passwordField.activeFocus && !(root.pam?.passwd.active ?? false) && !(root.pam?.u2fPending ?? false) && !root.unlocking
shown: !root.demoMode && root.showPasswordField && !root.ring && passwordField.activeFocus && !(root.pam?.authBusy ?? false) && !(root.pam?.u2fPending ?? false) && !root.unlocking
readonly property int fieldCursorPosition: passwordField.cursorPosition
readonly property string fieldText: passwordField.text
@@ -994,7 +1002,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: parent.showPassword ? "visibility_off" : "visibility"
buttonSize: Theme.buttonHeightXS
visible: !root.demoMode && !root.ring && !root.minimal && root.passwordBuffer.length > 0 && !(root.pam?.passwd.active ?? false) && !root.unlocking
visible: !root.demoMode && !root.ring && !root.minimal && root.passwordBuffer.length > 0 && !(root.pam?.authBusy ?? false) && !root.unlocking
enabled: visible
onClicked: parent.showPassword = !parent.showPassword
}
@@ -1031,7 +1039,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: Theme.buttonHeightXS
visible: !root.demoMode && !root.ring && !root.minimal && !(root.pam?.passwd.active ?? false) && !root.unlocking && !(root.pam?.u2fPending ?? false)
visible: !root.demoMode && !root.ring && !root.minimal && !(root.pam?.authBusy ?? false) && !root.unlocking && !(root.pam?.u2fPending ?? false)
enabled: visible && root.showPasswordField
onClicked: {
if (keyboardController.isKeyboardActive)
@@ -1049,7 +1057,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
width: Theme.iconSize
height: Theme.iconSize
visible: !root.demoMode && !root.ring && !root.morph && ((root.pam?.passwd.active ?? false) || root.unlocking)
visible: !root.demoMode && !root.ring && !root.morph && ((root.pam?.authBusy ?? false) || root.unlocking)
DIcon {
anchors.centerIn: parent
@@ -1073,7 +1081,7 @@ Item {
size: Theme.iconSize
contained: root.style === "expressive"
color: root.style === "expressive" ? Theme.onPrimaryContainer : root.accentColor
visible: (root.pam?.passwd.active ?? false) && !root.unlocking
visible: (root.pam?.authBusy ?? false) && !root.unlocking
running: visible
}
}
@@ -1089,11 +1097,11 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return"
buttonSize: Theme.buttonHeightXS
visible: !root.ring && !root.minimal && (root.demoMode || (!(root.pam?.passwd.active ?? false) && !root.unlocking && !(root.pam?.u2fPending ?? false)))
visible: !root.ring && !root.minimal && (root.demoMode || (!(root.pam?.authBusy ?? false) && !root.unlocking && !(root.pam?.u2fPending ?? false)))
enabled: !root.demoMode && root.showPasswordField
onClicked: {
if (!root.demoMode && !root.unlocking && !root.pam.u2fPending)
root.pam.passwd.start();
root.pam.submitPassword();
}
Behavior on opacity {