1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-04-12 16:52:10 -04:00

refactor(greeter): Update auth flows and add configurable opts

- Finally fix debug info logs before dms greeter loads
- prevent greeter/lockscreen auth stalls with timeout recovery and unlock-state sync
This commit is contained in:
purian23
2026-03-04 14:17:56 -05:00
committed by bbedward
parent 2ff42eba41
commit 73c75fcc2c
13 changed files with 1032 additions and 281 deletions

View File

@@ -31,6 +31,17 @@ Item {
signal launchRequested
property bool weatherInitialized: false
property bool awaitingExternalAuth: false
property int defaultAuthTimeoutMs: 12000
property int externalAuthTimeoutMs: 45000
property int passwordFailureCount: 0
property int passwordAttemptLimitHint: 0
property string authFeedbackMessage: ""
property string greetdPamText: ""
property string systemAuthPamText: ""
property string commonAuthPamText: ""
property string passwordAuthPamText: ""
property string faillockConfigText: ""
function initWeatherService() {
if (weatherInitialized)
@@ -44,16 +55,238 @@ Item {
WeatherService.forceRefresh();
}
function stripPamComment(line) {
if (!line)
return "";
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#"))
return "";
const hashIdx = trimmed.indexOf("#");
if (hashIdx >= 0)
return trimmed.substring(0, hashIdx).trim();
return trimmed;
}
function usesPamLockoutPolicy(pamText) {
if (!pamText)
return false;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (line.includes("pam_faillock.so") || line.includes("pam_tally2.so") || line.includes("pam_tally.so"))
return true;
}
return false;
}
function parsePamLineDenyValue(pamText) {
if (!pamText)
return -1;
const lines = pamText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
if (!line.includes("pam_faillock.so") && !line.includes("pam_tally2.so") && !line.includes("pam_tally.so"))
continue;
const denyMatch = line.match(/\bdeny\s*=\s*(\d+)\b/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function parseFaillockDenyValue(configText) {
if (!configText)
return -1;
const lines = configText.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
const line = stripPamComment(lines[i]);
if (!line)
continue;
const denyMatch = line.match(/^deny\s*=\s*(\d+)\s*$/i);
if (!denyMatch)
continue;
const parsed = parseInt(denyMatch[1], 10);
if (!isNaN(parsed))
return parsed;
}
return -1;
}
function refreshPasswordAttemptPolicyHint() {
const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText];
let lockoutConfigured = false;
let denyFromPam = -1;
for (let i = 0; i < pamSources.length; i++) {
const source = pamSources[i];
if (!source)
continue;
if (usesPamLockoutPolicy(source))
lockoutConfigured = true;
const denyValue = parsePamLineDenyValue(source);
if (denyValue >= 0 && (denyFromPam < 0 || denyValue < denyFromPam))
denyFromPam = denyValue;
}
if (!lockoutConfigured) {
passwordAttemptLimitHint = 0;
return;
}
const denyFromConfig = parseFaillockDenyValue(faillockConfigText);
if (denyFromConfig >= 0) {
passwordAttemptLimitHint = denyFromConfig;
return;
}
if (denyFromPam >= 0) {
passwordAttemptLimitHint = denyFromPam;
return;
}
// pam_faillock default deny value when no explicit config is set.
passwordAttemptLimitHint = 3;
}
function isLikelyLockoutMessage(message) {
const lower = (message || "").toLowerCase();
return lower.includes("account is locked") || lower.includes("too many") || lower.includes("maximum number of") || lower.includes("auth_err");
}
function currentAuthMessage() {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "max")
return "Too many failed attempts - account may be locked";
if (GreeterState.pamState === "fail") {
if (passwordAttemptLimitHint > 0) {
const attempt = Math.max(1, Math.min(passwordFailureCount, passwordAttemptLimitHint));
const remaining = Math.max(passwordAttemptLimitHint - attempt, 0);
if (remaining > 0) {
return "Incorrect password - attempt " + attempt + " of " + passwordAttemptLimitHint + " (lockout may follow)";
}
return "Incorrect password - next failures may trigger account lockout";
}
return "Incorrect password";
}
return "";
}
function clearAuthFeedback() {
GreeterState.pamState = "";
authFeedbackMessage = "";
}
Connections {
target: GreetdSettings
function onSettingsLoadedChanged() {
if (GreetdSettings.settingsLoaded)
if (GreetdSettings.settingsLoaded) {
initWeatherService();
if (isPrimaryScreen) {
applyLastSuccessfulUser();
finalizeSessionSelection();
}
}
}
function onRememberLastUserChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastUser && GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
applyLastSuccessfulUser();
}
function onRememberLastSessionChanged() {
if (!isPrimaryScreen)
return;
if (!GreetdSettings.rememberLastSession && GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
finalizeSessionSelection();
}
}
FileView {
id: greetdPamWatcher
path: "/etc/pam.d/greetd"
printErrors: false
onLoaded: {
root.greetdPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.greetdPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: systemAuthPamWatcher
path: "/etc/pam.d/system-auth"
printErrors: false
onLoaded: {
root.systemAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.systemAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: commonAuthPamWatcher
path: "/etc/pam.d/common-auth"
printErrors: false
onLoaded: {
root.commonAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.commonAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: passwordAuthPamWatcher
path: "/etc/pam.d/password-auth"
printErrors: false
onLoaded: {
root.passwordAuthPamText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.passwordAuthPamText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
FileView {
id: faillockConfigWatcher
path: "/etc/security/faillock.conf"
printErrors: false
onLoaded: {
root.faillockConfigText = text();
root.refreshPasswordAttemptPolicyHint();
}
onLoadFailed: {
root.faillockConfigText = "";
root.refreshPasswordAttemptPolicyHint();
}
}
Component.onCompleted: {
initWeatherService();
refreshPasswordAttemptPolicyHint();
if (isPrimaryScreen)
applyLastSuccessfulUser();
@@ -63,6 +296,8 @@ Item {
}
function applyLastSuccessfulUser() {
if (!GreetdSettings.settingsLoaded || !GreetdSettings.rememberLastUser)
return;
const lastUser = GreetdMemory.lastSuccessfulUser;
if (lastUser && !GreeterState.showPasswordInput && !GreeterState.username) {
GreeterState.username = lastUser;
@@ -72,6 +307,38 @@ Item {
}
}
function submitUsername(rawValue) {
const user = (rawValue || "").trim();
if (!user)
return;
if (GreeterState.username !== user) {
passwordFailureCount = 0;
clearAuthFeedback();
}
GreeterState.username = user;
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(user);
GreeterState.passwordBuffer = "";
}
function startAuthSession() {
if (!GreeterState.showPasswordInput || !GreeterState.username)
return;
if (GreeterState.unlocking || Greetd.state !== GreetdState.Inactive)
return;
if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0)
return;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.restart();
Greetd.createSession(GreeterState.username);
}
function isExternalAuthPrompt(message, responseRequired) {
// Non-response PAM messages commonly represent waiting states (fprint/U2F/token touch).
return !responseRequired;
}
Component.onDestruction: {
if (weatherInitialized)
WeatherService.removeRef();
@@ -421,15 +688,10 @@ Item {
}
onAccepted: {
if (GreeterState.showPasswordInput) {
if (Greetd.state === GreetdState.Inactive && GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
root.startAuthSession();
} else {
if (text.trim()) {
GreeterState.username = text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
root.submitUsername(text);
syncingFromState = true;
text = "";
syncingFromState = false;
@@ -568,15 +830,10 @@ Item {
enabled: true
onClicked: {
if (GreeterState.showPasswordInput) {
if (GreeterState.username) {
Greetd.createSession(GreeterState.username);
}
root.startAuthSession();
} else {
if (inputField.text.trim()) {
GreeterState.username = inputField.text.trim();
GreeterState.showPasswordInput = true;
PortalService.getGreeterUserProfileImage(GreeterState.username);
GreeterState.passwordBuffer = "";
root.submitUsername(inputField.text);
inputField.text = "";
}
}
@@ -601,20 +858,16 @@ Item {
StyledText {
Layout.fillWidth: true
Layout.preferredHeight: 20
Layout.preferredHeight: 38
Layout.topMargin: -Theme.spacingS
Layout.bottomMargin: -Theme.spacingS
text: {
if (GreeterState.pamState === "error")
return "Authentication error - try again";
if (GreeterState.pamState === "fail")
return "Incorrect password";
return "";
}
text: root.authFeedbackMessage
color: Theme.error
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
opacity: GreeterState.pamState !== "" ? 1 : 0
wrapMode: Text.WordWrap
maximumLineCount: 2
opacity: root.authFeedbackMessage !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation {
@@ -1029,9 +1282,11 @@ Item {
return;
if (!GreetdMemory.memoryReady)
return;
if (!GreetdSettings.settingsLoaded)
return;
const savedSession = GreetdMemory.lastSessionId;
if (savedSession) {
const savedSession = GreetdSettings.rememberLastSession ? GreetdMemory.lastSessionId : "";
if (savedSession && GreetdSettings.rememberLastSession) {
for (var i = 0; i < GreeterState.sessionPaths.length; i++) {
if (GreeterState.sessionPaths[i] === savedSession) {
GreeterState.currentSessionIndex = i;
@@ -1163,45 +1418,100 @@ Item {
enabled: isPrimaryScreen
function onAuthMessage(message, error, responseRequired, echoResponse) {
awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired);
authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs;
authTimeout.restart();
if (responseRequired) {
Greetd.respond(GreeterState.passwordBuffer);
GreeterState.passwordBuffer = "";
inputField.text = "";
return;
}
if (!error)
Greetd.respond("");
Greetd.respond("");
}
function onStateChanged() {
if (Greetd.state === GreetdState.Inactive) {
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
}
}
function onReadyToLaunch() {
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
passwordFailureCount = 0;
clearAuthFeedback();
const sessionCmd = GreeterState.selectedSession || GreeterState.sessionExecs[GreeterState.currentSessionIndex];
const sessionPath = GreeterState.selectedSessionPath || GreeterState.sessionPaths[GreeterState.currentSessionIndex];
if (!sessionCmd) {
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
return;
}
GreeterState.unlocking = true;
launchTimeout.restart();
GreetdMemory.setLastSessionId(sessionPath);
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
if (GreetdSettings.rememberLastSession) {
GreetdMemory.setLastSessionId(sessionPath);
} else if (GreetdMemory.lastSessionId) {
GreetdMemory.setLastSessionId("");
}
if (GreetdSettings.rememberLastUser) {
GreetdMemory.setLastSuccessfulUser(GreeterState.username);
} else if (GreetdMemory.lastSuccessfulUser) {
GreetdMemory.setLastSuccessfulUser("");
}
Greetd.launch(sessionCmd.split(" "), ["XDG_SESSION_TYPE=wayland"]);
}
function onAuthFailure(message) {
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop();
GreeterState.unlocking = false;
GreeterState.pamState = "fail";
if (isLikelyLockoutMessage(message)) {
GreeterState.pamState = "max";
} else {
GreeterState.pamState = "fail";
passwordFailureCount = passwordFailureCount + 1;
}
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
function onError(error) {
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
authTimeout.stop();
launchTimeout.stop();
GreeterState.unlocking = false;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
Greetd.cancelSession();
}
}
Timer {
id: authTimeout
interval: defaultAuthTimeoutMs
onTriggered: {
if (GreeterState.unlocking || Greetd.state === GreetdState.Inactive)
return;
awaitingExternalAuth = false;
authTimeout.interval = defaultAuthTimeoutMs;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
GreeterState.passwordBuffer = "";
inputField.text = "";
placeholderDelay.restart();
@@ -1217,6 +1527,7 @@ Item {
return;
GreeterState.unlocking = false;
GreeterState.pamState = "error";
authFeedbackMessage = currentAuthMessage();
placeholderDelay.restart();
Greetd.cancelSession();
}
@@ -1225,7 +1536,7 @@ Item {
Timer {
id: placeholderDelay
interval: 4000
onTriggered: GreeterState.pamState = ""
onTriggered: clearAuthFeedback()
}
LockPowerMenu {