1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-06-20 10:05:22 -04:00
Files
DankMaterialShell/quickshell/Modules/Lock/Pam.qml
T
Rocho 097290f7da fix(lock): show the faillock lockout reason instead of "incorrect password" (#2669)
When pam_faillock locks the account (DMS authenticates through the system PAM
stack: /etc/pam.d/login -> system-auth), the lock screen kept showing the
generic "Incorrect password - try again" even though the real cause is a
lockout, so a correct password looks rejected and only a reboot (which clears
the tmpfs /run/faillock tally) appears to help. See #2647.

The previous onMessageChanged only matched the *English* faillock strings
("The account is locked ...") and then wiped that text again on the trailing
pam_unix "Password:" prompt. On a non-English system (e.g. German) the strings
never matched, so the lockout was never surfaced at all.

Detect the notice by position rather than by text: pam emits its informational
messages within an attempt before the password prompt. Collect every non-prompt
info message and, once the prompt arrives, surface the collected lines (minus
the prompt itself) as lockMessage. If the stack short-circuits without ever
prompting (e.g. pam_faillock preauth configured as requisite), the notice is
surfaced on completion instead. This is locale-independent. A per-attempt flag
keeps the message stable across repeated locked attempts and retires it when an
attempt completes without a lockout (faillock reset / unlock_time elapsed).

Fixes #2647
2026-06-18 09:54:02 -04:00

455 lines
12 KiB
QML

pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Pam
import qs.Common
Scope {
id: root
property bool lockSecured: false
property bool unlockInProgress: false
readonly property alias passwd: passwd
readonly property alias fprint: fprint
readonly property alias u2f: u2f
property string lockMessage
property string state
property string fprintState
property string u2fState
property bool u2fPending: false
property string u2fPendingMode
property string buffer
property var attemptInfoMessages: []
property bool lockoutAnnouncedThisAttempt: false
signal flashMsg
signal unlockRequested
function resetAuthFlows(): void {
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
root.unlockInProgress = false;
}
function recoverFromAuthStall(newState: string): void {
resetAuthFlows();
root.state = newState;
flashMsg();
stateReset.restart();
fprint.checkAvail();
u2f.checkAvail();
}
function completeUnlock(): void {
if (!root.unlockInProgress) {
root.unlockInProgress = true;
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
unlockRequestTimeout.restart();
unlockRequested();
}
}
function proceedAfterPrimaryAuth(): void {
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
u2f.startForSecondFactor();
} else {
completeUnlock();
}
}
function cancelU2fPending(): void {
if (!root.u2fPending)
return;
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
fprint.checkAvail();
}
FileView {
id: dankshellConfigWatcher
path: "/etc/pam.d/dankshell"
printErrors: false
}
FileView {
id: nixosMarker
path: "/etc/NIXOS"
printErrors: false
}
FileView {
id: u2fConfigWatcher
path: "/etc/pam.d/dankshell-u2f"
printErrors: false
}
// Detects Nix-installed DMS on non-NixOS systems
readonly property bool runningFromNixStore: Quickshell.shellDir.startsWith("/nix/store/")
PamContext {
id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
configDirectory: (dankshellConfigWatcher.loaded || nixosMarker.loaded || root.runningFromNixStore) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: {
// collected by position, not text, so it works in any locale
if (message.length > 0 && !responseRequired)
root.attemptInfoMessages = root.attemptInfoMessages.concat([message]);
}
onResponseRequiredChanged: {
if (!responseRequired)
return;
const notice = root.attemptInfoMessages.filter(m => m !== message);
if (notice.length > 0) {
root.lockMessage = notice.join("\n");
root.lockoutAnnouncedThisAttempt = true;
}
root.attemptInfoMessages = [];
respond(root.buffer);
}
onCompleted: res => {
// requisite preauth can lock without ever prompting; surface it here too
if (!root.lockoutAnnouncedThisAttempt) {
if (root.attemptInfoMessages.length > 0) {
root.lockMessage = root.attemptInfoMessages.join("\n");
root.lockoutAnnouncedThisAttempt = true;
} else {
root.lockMessage = "";
}
root.attemptInfoMessages = [];
}
if (res === PamResult.Success) {
if (!root.unlockInProgress) {
fprint.abort();
root.proceedAfterPrimaryAuth();
}
return;
}
unlockRequestTimeout.running = false;
root.unlockInProgress = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
u2fPendingTimeout.running = false;
u2f.abort();
if (res === PamResult.Error)
root.state = "error";
else if (res === PamResult.MaxTries)
root.state = "max";
else if (res === PamResult.Failed)
root.state = "fail";
root.flashMsg();
stateReset.restart();
}
}
Connections {
target: passwd
function onActiveChanged() {
if (passwd.active) {
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
passwdActiveTimeout.restart();
} else {
passwdActiveTimeout.running = false;
}
}
}
PamContext {
id: fprint
property bool available: SettingsData.lockFingerprintReady
property int tries
property int errorTries
function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured) {
abort();
return;
}
if (active)
return;
tries = 0;
errorTries = 0;
start();
}
config: "fprint"
configDirectory: Quickshell.shellDir + "/assets/pam"
onCompleted: res => {
if (!available)
return;
switch (res) {
case PamResult.Success:
if (!root.unlockInProgress) {
passwd.abort();
root.proceedAfterPrimaryAuth();
}
return;
case PamResult.Error:
errorTries++;
if (errorTries < 200) {
abort();
errorRetry.restart();
return;
}
abort();
return;
case PamResult.MaxTries:
tries++;
if (tries < SettingsData.maxFprintTries) {
root.fprintState = "fail";
start();
} else {
root.fprintState = "max";
abort();
}
break;
default:
return;
}
root.flashMsg();
fprintStateReset.start();
}
}
PamContext {
id: u2f
property bool available: SettingsData.lockU2fReady
function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
abort();
return;
}
if (SettingsData.u2fMode === "or")
abort();
}
function startForSecondFactor(): void {
if (!available || !SettingsData.enableU2f) {
root.completeUnlock();
return;
}
abort();
root.u2fPending = true;
root.u2fPendingMode = "and";
root.u2fState = "";
u2fPendingTimeout.restart();
start();
}
function startForAlternativeAuth(): void {
if (!available || !SettingsData.enableU2f || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
return;
abort();
root.u2fPending = true;
root.u2fPendingMode = "or";
root.u2fState = "";
u2fPendingTimeout.restart();
start();
}
config: u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: {
if (message.toLowerCase().includes("touch"))
root.u2fState = "waiting";
}
onCompleted: res => {
if (!available || root.unlockInProgress)
return;
if (res === PamResult.Success) {
root.completeUnlock();
return;
}
if (res === PamResult.Error || res === PamResult.MaxTries || res === PamResult.Failed) {
abort();
if (root.u2fPending) {
if (root.u2fPendingMode === "or") {
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = root.u2fState === "waiting" ? "" : "insert";
u2fPendingTimeout.running = false;
fprint.checkAvail();
return;
}
if (root.u2fState === "waiting") {
// AND mode: device was found but auth failed → back to password
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
fprint.checkAvail();
} else {
// AND mode: no device found → keep pending, show "Insert...", retry
root.u2fState = "insert";
u2fErrorRetry.restart();
}
} else {
root.u2fState = "insert";
}
}
}
}
Timer {
id: errorRetry
interval: 1500
onTriggered: fprint.start()
}
Timer {
id: u2fErrorRetry
interval: 800
onTriggered: u2f.start()
}
Timer {
id: u2fPendingTimeout
interval: 30000
onTriggered: root.cancelU2fPending()
}
Timer {
id: passwdActiveTimeout
interval: 15000
onTriggered: {
if (passwd.active)
root.recoverFromAuthStall("error");
}
}
Timer {
id: unlockRequestTimeout
interval: 8000
onTriggered: {
if (root.unlockInProgress)
root.recoverFromAuthStall("error");
}
}
Timer {
id: stateReset
interval: 4000
onTriggered: {
if (root.state !== "max")
root.state = "";
}
}
Timer {
id: fprintStateReset
interval: 4000
onTriggered: root.fprintState = ""
}
onLockSecuredChanged: {
if (!lockSecured) {
root.resetAuthFlows();
return;
}
root.state = "";
root.fprintState = "";
root.u2fState = "";
root.u2fPending = false;
root.u2fPendingMode = "";
root.lockMessage = "";
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
root.resetAuthFlows();
fprint.checkAvail();
u2f.checkAvail();
}
Connections {
target: SettingsData
function onEnableFprintChanged(): void {
fprint.checkAvail();
}
function onLockFingerprintReadyChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void {
u2f.checkAvail();
}
function onLockU2fReadyChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void {
if (root.lockSecured) {
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
unlockRequestTimeout.running = false;
root.u2fPending = false;
root.u2fPendingMode = "";
root.u2fState = "";
u2f.checkAvail();
}
}
}
}