1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-03 20:32:07 -04:00

fix(lock): Restore system PAM fallback, faillock support, and auth feedback

- Re-add loginConfigWatcher so installs can still fall through to
  /etc/pam.d instead of the bundled PAM assets
- Add login-faillock bundled PAM asset at runtime. Use it as the bundled fallback when dankshell config is absent
- Fix invalid bare property writes (u2fPending, u2fState, unlockInProgress,
  state) in Pam.qml
- Improve lockscreen auth feedback
This commit is contained in:
purian23
2026-03-25 16:39:37 -04:00
parent faa5e7e02d
commit d5ceea8a56
6 changed files with 90 additions and 45 deletions

View File

@@ -545,6 +545,7 @@ Singleton {
property bool enableFprint: false property bool enableFprint: false
property int maxFprintTries: 15 property int maxFprintTries: 15
property bool lockFaillockSupported: false
property bool fprintdAvailable: false property bool fprintdAvailable: false
property bool lockFingerprintCanEnable: false property bool lockFingerprintCanEnable: false
property bool lockFingerprintReady: false property bool lockFingerprintReady: false

View File

@@ -33,6 +33,7 @@ Singleton {
property int pamSupportProbeExitCode: 0 property int pamSupportProbeExitCode: 0
property bool pamFprintSupportDetected: false property bool pamFprintSupportDetected: false
property bool pamU2fSupportDetected: false property bool pamU2fSupportDetected: false
property bool pamFaillockSupportDetected: false
readonly property string homeDir: Quickshell.env("HOME") || "" readonly property string homeDir: Quickshell.env("HOME") || ""
readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : "" readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : ""
@@ -70,14 +71,13 @@ Singleton {
fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed"; fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed";
} }
if (forcedFprintAvailable === null || forcedU2fAvailable === null) { pamFprintSupportDetected = false;
pamFprintSupportDetected = false; pamU2fSupportDetected = false;
pamU2fSupportDetected = false; pamFaillockSupportDetected = false;
pamSupportProbeOutput = ""; pamSupportProbeOutput = "";
pamSupportProbeStreamFinished = false; pamSupportProbeStreamFinished = false;
pamSupportProbeExited = false; pamSupportProbeExited = false;
pamSupportDetectionProcess.running = true; pamSupportDetectionProcess.running = true;
}
recomputeAuthCapabilities(); recomputeAuthCapabilities();
} }
@@ -321,6 +321,7 @@ Singleton {
return; return;
recomputeFingerprintCapabilities(); recomputeFingerprintCapabilities();
recomputeU2fCapabilities(); recomputeU2fCapabilities();
settingsRoot.lockFaillockSupported = pamFaillockSupportDetected;
settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady; settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady;
settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady; settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady;
} }
@@ -338,6 +339,7 @@ Singleton {
pamFprintSupportDetected = false; pamFprintSupportDetected = false;
pamU2fSupportDetected = false; pamU2fSupportDetected = false;
pamFaillockSupportDetected = false;
const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/); const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/);
for (let i = 0; i < lines.length; i++) { for (let i = 0; i < lines.length; i++) {
@@ -348,6 +350,8 @@ Singleton {
pamFprintSupportDetected = parts[1] === "true"; pamFprintSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_u2f.so") else if (parts[0] === "pam_u2f.so")
pamU2fSupportDetected = parts[1] === "true"; pamU2fSupportDetected = parts[1] === "true";
else if (parts[0] === "pam_faillock.so")
pamFaillockSupportDetected = parts[1] === "true";
} }
if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support") if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support")
@@ -401,7 +405,7 @@ Singleton {
} }
property var pamSupportDetectionProcess: Process { property var pamSupportDetectionProcess: Process {
command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"] command: ["sh", "-c", "for module in pam_fprintd.so pam_u2f.so pam_faillock.so; do found=false; for dir in /usr/lib64/security /usr/lib/security /lib/security /lib/x86_64-linux-gnu/security /usr/lib/x86_64-linux-gnu/security /usr/lib/aarch64-linux-gnu/security /run/current-system/sw/lib/security; do if [ -f \"$dir/$module\" ]; then found=true; break; fi; done; printf '%s:%s\\n' \"$module\" \"$found\"; done"]
running: false running: false
stdout: StdioCollector { stdout: StdioCollector {

View File

@@ -355,6 +355,7 @@ var SPEC = {
lockAtStartup: { def: false }, lockAtStartup: { def: false },
enableFprint: { def: false }, enableFprint: { def: false },
maxFprintTries: { def: 15 }, maxFprintTries: { def: 15 },
lockFaillockSupported: { def: false, persist: false },
fprintdAvailable: { def: false, persist: false }, fprintdAvailable: { def: false, persist: false },
lockFingerprintCanEnable: { def: false, persist: false }, lockFingerprintCanEnable: { def: false, persist: false },
lockFingerprintReady: { def: false, persist: false }, lockFingerprintReady: { def: false, persist: false },

View File

@@ -39,6 +39,38 @@ Item {
lockerReadyArmed = true; lockerReadyArmed = true;
unlocking = false; unlocking = false;
pamState = ""; pamState = "";
if (pam)
pam.lockMessage = "";
}
function currentAuthFeedbackText() {
if (!pam)
return "";
if (pam.u2fState === "insert" && !pam.u2fPending)
return I18n.tr("Insert your security key...");
if (pam.u2fState === "waiting" && !pam.u2fPending)
return I18n.tr("Touch your security key...");
if (pam.lockMessage && pam.lockMessage.length > 0)
return pam.lockMessage;
if (pam.fprintState === "error") {
const detail = (pam.fprint.message || "").trim();
return detail.length > 0 ? I18n.tr("Fingerprint error: %1").arg(detail) : I18n.tr("Fingerprint error");
}
if (pam.fprintState === "max")
return I18n.tr("Maximum fingerprint attempts reached. Please use password.");
if (pam.fprintState === "fail")
return I18n.tr("Fingerprint not recognized (%1/%2). Please try again or use password.").arg(pam.fprint.tries).arg(SettingsData.maxFprintTries);
if (root.pamState === "error")
return I18n.tr("Authentication error - try again");
if (root.pamState === "max")
return I18n.tr("Too many attempts - locked out");
if (root.pamState === "fail")
return I18n.tr("Incorrect password - try again");
return "";
}
function authFeedbackIsHint() {
return pam && (pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending;
} }
Component.onCompleted: { Component.onCompleted: {
@@ -1045,30 +1077,18 @@ Item {
} }
StyledText { StyledText {
id: authFeedbackText
Layout.fillWidth: true Layout.fillWidth: true
Layout.preferredHeight: 20 Layout.preferredHeight: text.length > 0 ? Math.min(implicitHeight, Math.ceil(Theme.fontSizeSmall * 4.5)) : 0
text: { text: root.currentAuthFeedbackText()
if (pam.u2fState === "insert" && !pam.u2fPending) { color: root.authFeedbackIsHint() ? Theme.outline : Theme.error
return "Insert your security key...";
}
if (pam.u2fState === "waiting" && !pam.u2fPending) {
return "Touch your security key...";
}
if (root.pamState === "error") {
return "Authentication error - try again";
}
if (root.pamState === "max") {
return "Too many attempts - locked out";
}
if (root.pamState === "fail") {
return "Incorrect password - try again";
}
return "";
}
color: (pam.u2fState === "waiting" || pam.u2fState === "insert") ? Theme.outline : Theme.error
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
opacity: (root.pamState !== "" || ((pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending)) ? 1 : 0 wrapMode: Text.WordWrap
maximumLineCount: 3
elide: Text.ElideRight
opacity: text.length > 0 ? 1 : 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {

View File

@@ -34,14 +34,14 @@ Scope {
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
passwdActiveTimeout.running = false; passwdActiveTimeout.running = false;
unlockRequestTimeout.running = false; unlockRequestTimeout.running = false;
u2fPending = false; root.u2fPending = false;
u2fState = ""; root.u2fState = "";
unlockInProgress = false; root.unlockInProgress = false;
} }
function recoverFromAuthStall(newState: string): void { function recoverFromAuthStall(newState: string): void {
resetAuthFlows(); resetAuthFlows();
state = newState; root.state = newState;
flashMsg(); flashMsg();
stateReset.restart(); stateReset.restart();
fprint.checkAvail(); fprint.checkAvail();
@@ -49,16 +49,16 @@ Scope {
} }
function completeUnlock(): void { function completeUnlock(): void {
if (!unlockInProgress) { if (!root.unlockInProgress) {
unlockInProgress = true; root.unlockInProgress = true;
passwd.abort(); passwd.abort();
fprint.abort(); fprint.abort();
u2f.abort(); u2f.abort();
errorRetry.running = false; errorRetry.running = false;
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; root.u2fPending = false;
u2fState = ""; root.u2fState = "";
unlockRequestTimeout.restart(); unlockRequestTimeout.restart();
unlockRequested(); unlockRequested();
} }
@@ -73,13 +73,13 @@ Scope {
} }
function cancelU2fPending(): void { function cancelU2fPending(): void {
if (!u2fPending) if (!root.u2fPending)
return; return;
u2f.abort(); u2f.abort();
u2fErrorRetry.running = false; u2fErrorRetry.running = false;
u2fPendingTimeout.running = false; u2fPendingTimeout.running = false;
u2fPending = false; root.u2fPending = false;
u2fState = ""; root.u2fState = "";
fprint.checkAvail(); fprint.checkAvail();
} }
@@ -90,6 +90,13 @@ Scope {
printErrors: false printErrors: false
} }
FileView {
id: loginConfigWatcher
path: "/etc/pam.d/login"
printErrors: false
}
FileView { FileView {
id: u2fConfigWatcher id: u2fConfigWatcher
@@ -97,17 +104,22 @@ Scope {
printErrors: false printErrors: false
} }
readonly property string bundledPasswdConfig: SettingsData.lockFaillockSupported ? "login-faillock" : "login"
PamContext { PamContext {
id: passwd id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login" config: dankshellConfigWatcher.loaded ? "dankshell" : root.bundledPasswdConfig
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam" configDirectory: (dankshellConfigWatcher.loaded || loginConfigWatcher.loaded) ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: { onMessageChanged: {
if (message.startsWith("The account is locked")) if (message.startsWith("The account is locked")) {
root.lockMessage = message; root.lockMessage = message;
else if (root.lockMessage && message.endsWith(" left to unlock)")) } else if (root.lockMessage && message.endsWith(" left to unlock)")) {
root.lockMessage += "\n" + message; root.lockMessage += "\n" + message;
} else if (root.lockMessage && message && message.length > 0) {
root.lockMessage = "";
}
} }
onResponseRequiredChanged: { onResponseRequiredChanged: {

View File

@@ -0,0 +1,7 @@
#%PAM-1.0
auth required pam_env.so
auth required pam_faillock.so preauth
auth [success=1 default=bad] pam_unix.so try_first_pass nullok
auth [default=die] pam_faillock.so authfail
auth required pam_faillock.so authsucc
account required pam_unix.so