From 3ff9564c9bd6a67631abb736aadbd02cfd7710b6 Mon Sep 17 00:00:00 2001 From: purian23 Date: Tue, 10 Mar 2026 15:02:26 -0400 Subject: [PATCH] (greeter): PAM auth improvements and defaults update --- core/cmd/dms/commands_greeter.go | 90 +++++++++++++++++--- core/internal/greeter/installer.go | 59 ++++++++++++- quickshell/Modules/Greetd/GreeterContent.qml | 81 ++++++++++++------ 3 files changed, 188 insertions(+), 42 deletions(-) diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index b0ee8146..269a13a4 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -1249,7 +1249,17 @@ func extractGreeterWrapperFromCommand(command string) string { if len(tokens) == 0 { return "" } - return strings.Trim(tokens[0], "\"") + wrapper := strings.Trim(tokens[0], "\"") + if wrapper == "" { + return "" + } + if len(tokens) > 1 { + next := strings.Trim(tokens[1], "\"") + if next != "" && (filepath.Base(wrapper) == "bash" || filepath.Base(wrapper) == "sh") && strings.Contains(filepath.Base(next), "dms-greeter") { + return fmt.Sprintf("%s (script: %s)", wrapper, next) + } + } + return wrapper } func extractGreeterPathOverrideFromCommand(command string) string { @@ -1335,6 +1345,26 @@ func packageInstallHint() string { } } +func systemPamManagerRemediationHint() string { + osInfo, err := distros.GetOSInfo() + if err != nil { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + config, exists := distros.Registry[osInfo.Distribution.ID] + if !exists { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + + switch config.Family { + case distros.FamilyFedora: + return "Disable it in authselect to force password-only greeter login." + case distros.FamilyDebian, distros.FamilyUbuntu: + return "Disable it in pam-auth-update to force password-only greeter login." + default: + return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } +} + func isPackageOnlyGreeterDistro() bool { osInfo, err := distros.GetOSInfo() if err != nil { @@ -1568,22 +1598,56 @@ func checkGreeterStatus() error { fmt.Println(" ⚠ Legacy unmanaged DMS PAM lines detected. Run 'dms greeter sync' to normalize.") allGood = false } + enableFprintToggle, enableU2fToggle := false, false + if enableFprint, enableU2f, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil { + enableFprintToggle = enableFprint + enableU2fToggle = enableU2f + } else { + fmt.Printf(" ℹ Could not read greeter auth toggles from settings: %v\n", settingsErr) + } + includedFprintFile := greeter.DetectIncludedPamModule(string(pamData), "pam_fprintd.so") - showIncludedFprintNotice := false - if includedFprintFile != "" { - if enableFprint, _, settingsErr := greeter.ReadGreeterAuthToggles(homeDir); settingsErr == nil && enableFprint { - showIncludedFprintNotice = greeter.FingerprintAuthAvailableForCurrentUser() + includedU2fFile := greeter.DetectIncludedPamModule(string(pamData), "pam_u2f.so") + fprintAvailableForCurrentUser := greeter.FingerprintAuthAvailableForCurrentUser() + + if managedFprint && includedFprintFile != "" { + fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) + fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.") + allGood = false + } + if managedU2f && includedU2fFile != "" { + fmt.Printf(" ⚠ pam_u2f found in both DMS managed block and %s.\n", includedU2fFile) + fmt.Println(" Double security-key auth detected — run 'dms greeter sync' to resolve.") + allGood = false + } + + if includedFprintFile != "" && !managedFprint { + if enableFprintToggle { + fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile) + if fprintAvailableForCurrentUser { + fmt.Println(" DMS toggle is enabled, and effective auth is coming from the included PAM stack.") + } else { + fmt.Println(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") + } + } else { + if fprintAvailableForCurrentUser { + fmt.Printf(" ℹ Fingerprint auth is active via included %s while DMS fingerprint toggle is off.\n", includedFprintFile) + fmt.Println(" Password login will work but may be delayed while the fingerprint module runs first.") + fmt.Printf(" To eliminate the delay, %s\n", systemPamManagerRemediationHint()) + } else { + fmt.Printf(" ℹ pam_fprintd is present via included %s, but no enrolled fingerprints were detected for user %s.\n", includedFprintFile, currentUser.Username) + fmt.Println(" Password auth remains the effective login path.") + } } } - if managedFprint { - if includedFprintFile != "" { - fmt.Printf(" ⚠ pam_fprintd found in both DMS managed block and %s.\n", includedFprintFile) - fmt.Println(" Double fingerprint auth detected — run 'dms greeter sync' to resolve.") - allGood = false + if includedU2fFile != "" && !managedU2f { + if enableU2fToggle { + fmt.Printf(" ℹ Security-key auth is enabled via included %s.\n", includedU2fFile) + fmt.Println(" DMS toggle is enabled, but effective auth is coming from the included PAM stack.") + } else { + fmt.Printf(" ⚠ Security-key auth is active via included %s while DMS security-key toggle is off.\n", includedU2fFile) + fmt.Printf(" %s\n", systemPamManagerRemediationHint()) } - } else if includedFprintFile != "" && showIncludedFprintNotice { - fmt.Printf(" ℹ Fingerprint auth is enabled via included %s.\n", includedFprintFile) - fmt.Println(" The DMS toggle only controls the managed block; disable fingerprint in authselect/pam-auth-update for password-only greeter login.") } } diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index cf4c7adb..2aaa1245 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -1424,9 +1424,30 @@ func FingerprintAuthAvailableForCurrentUser() bool { return FingerprintAuthAvailableForUser(username) } +func pamManagerHintForCurrentDistro() string { + osInfo, err := distros.GetOSInfo() + if err != nil { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + config, exists := distros.Registry[osInfo.Distribution.ID] + if !exists { + return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } + + switch config.Family { + case distros.FamilyFedora: + return "Disable it in authselect to force password-only greeter login." + case distros.FamilyDebian, distros.FamilyUbuntu: + return "Disable it in pam-auth-update to force password-only greeter login." + default: + return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." + } +} + func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword string, forceAuth bool) error { var wantFprint, wantU2f bool fprintToggleEnabled := forceAuth + u2fToggleEnabled := forceAuth if forceAuth { wantFprint = pamModuleExists("pam_fprintd.so") wantU2f = pamModuleExists("pam_u2f.so") @@ -1436,6 +1457,7 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str return err } fprintToggleEnabled = settings.GreeterEnableFprint + u2fToggleEnabled = settings.GreeterEnableU2f fprintModule := pamModuleExists("pam_fprintd.so") u2fModule := pamModuleExists("pam_u2f.so") wantFprint = settings.GreeterEnableFprint && fprintModule @@ -1464,14 +1486,43 @@ func syncGreeterPamConfig(homeDir string, logFunc func(string), sudoPassword str content, _ = stripLegacyGreeterPamLines(content) includedFprintFile := DetectIncludedPamModule(content, "pam_fprintd.so") + includedU2fFile := DetectIncludedPamModule(content, "pam_u2f.so") + fprintAvailableForCurrentUser := FingerprintAuthAvailableForCurrentUser() if wantFprint && includedFprintFile != "" { logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.") wantFprint = false } - showIncludedFprintNotice := fprintToggleEnabled && FingerprintAuthAvailableForCurrentUser() - if !wantFprint && includedFprintFile != "" && showIncludedFprintNotice { - logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".") - logFunc(" Disable fingerprint in your system PAM manager (authselect/pam-auth-update) to force password-only greeter login.") + if wantU2f && includedU2fFile != "" { + logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.") + wantU2f = false + } + if !wantFprint && includedFprintFile != "" { + if fprintToggleEnabled { + logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".") + if fprintAvailableForCurrentUser { + logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.") + } else { + logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") + } + } else { + if fprintAvailableForCurrentUser { + logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.") + logFunc(" Password login will work but may be delayed while the fingerprint module runs first.") + logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro()) + } else { + logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.") + logFunc(" Password auth remains the effective login path.") + } + } + } + if !wantU2f && includedU2fFile != "" { + if u2fToggleEnabled { + logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".") + logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.") + } else { + logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.") + logFunc(" " + pamManagerHintForCurrentDistro()) + } } if wantFprint || wantU2f { diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index 72976287..90d00b84 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -35,8 +35,8 @@ Item { property bool pendingPasswordResponse: false property bool passwordSubmitRequested: false property bool cancelingExternalAuthForPassword: false - property int defaultAuthTimeoutMs: 12000 - property int externalAuthTimeoutMs: 45000 + property int defaultAuthTimeoutMs: 10000 + property int externalAuthTimeoutMs: 36000 property int memoryFlushDelayMs: 120 property string pendingLaunchCommand: "" property var pendingLaunchEnv: [] @@ -50,9 +50,12 @@ Item { property string faillockConfigText: "" property bool greeterWallpaperOverrideExists: false property string externalAuthAutoStartedForUser: "" + property int passwordSessionTransitionRetryCount: 0 + property int maxPasswordSessionTransitionRetries: 2 readonly property bool greeterPamHasFprint: pamModuleEnabled(greetdPamText, "pam_fprintd") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_fprintd")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_fprintd")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_fprintd")) readonly property bool greeterPamHasU2f: pamModuleEnabled(greetdPamText, "pam_u2f") || (greetdPamText.includes("system-auth") && pamModuleEnabled(systemAuthPamText, "pam_u2f")) || (greetdPamText.includes("common-auth") && pamModuleEnabled(commonAuthPamText, "pam_u2f")) || (greetdPamText.includes("password-auth") && pamModuleEnabled(passwordAuthPamText, "pam_u2f")) readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f) + readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f function initWeatherService() { if (weatherInitialized) @@ -208,6 +211,13 @@ Item { authFeedbackMessage = ""; } + function resetPasswordSessionTransition(clearSubmitRequest) { + cancelingExternalAuthForPassword = false; + passwordSessionTransitionRetryCount = 0; + if (clearSubmitRequest) + passwordSubmitRequested = false; + } + Connections { target: GreetdSettings function onSettingsLoadedChanged() { @@ -348,8 +358,7 @@ Item { PortalService.getGreeterUserProfileImage(user); GreeterState.passwordBuffer = ""; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); maybeAutoStartExternalAuth(); } @@ -357,8 +366,7 @@ Item { if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0) return false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); awaitingExternalAuth = false; authTimeout.interval = defaultAuthTimeoutMs; authTimeout.restart(); @@ -369,9 +377,24 @@ Item { } function requestPasswordSessionTransition() { + if (!GreeterState.passwordBuffer || GreeterState.passwordBuffer.length === 0) + return; if (cancelingExternalAuthForPassword) return; + if (passwordSessionTransitionRetryCount >= maxPasswordSessionTransitionRetries) { + pendingPasswordResponse = false; + awaitingExternalAuth = false; + authTimeout.interval = defaultAuthTimeoutMs; + authTimeout.stop(); + resetPasswordSessionTransition(true); + GreeterState.pamState = "error"; + authFeedbackMessage = currentAuthMessage(); + placeholderDelay.restart(); + Greetd.cancelSession(); + return; + } cancelingExternalAuthForPassword = true; + passwordSessionTransitionRetryCount = passwordSessionTransitionRetryCount + 1; awaitingExternalAuth = false; pendingPasswordResponse = false; authTimeout.interval = defaultAuthTimeoutMs; @@ -388,9 +411,7 @@ Item { if (Greetd.state !== GreetdState.Inactive) { if (pendingPasswordResponse && hasPasswordBuffer) submitBufferedPassword(); - else if (awaitingExternalAuth && hasPasswordBuffer) { - passwordSubmitRequested = true; - } else if (hasPasswordBuffer) + else if (hasPasswordBuffer) passwordSubmitRequested = true; return; } @@ -404,7 +425,10 @@ Item { pendingPasswordResponse = false; passwordSubmitRequested = hasPasswordBuffer; awaitingExternalAuth = !hasPasswordBuffer && root.greeterExternalAuthAvailable; - authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs; + // Included PAM stacks (system-auth/common-auth/password-auth) may still run + // biometric/U2F modules before password even when DMS toggles are off. + const waitingOnPamExternalBeforePassword = hasPasswordBuffer && root.greeterPamHasExternalAuth; + authTimeout.interval = (awaitingExternalAuth || waitingOnPamExternalBeforePassword) ? externalAuthTimeoutMs : defaultAuthTimeoutMs; authTimeout.restart(); Greetd.createSession(GreeterState.username); } @@ -1559,18 +1583,30 @@ Item { function onAuthMessage(message, error, responseRequired, echoResponse) { if (responseRequired) { cancelingExternalAuthForPassword = false; + passwordSessionTransitionRetryCount = 0; awaitingExternalAuth = false; - authTimeout.interval = defaultAuthTimeoutMs; - authTimeout.restart(); pendingPasswordResponse = true; + const hasPasswordBuffer = GreeterState.passwordBuffer && GreeterState.passwordBuffer.length > 0; + if (!passwordSubmitRequested && hasPasswordBuffer) + passwordSubmitRequested = true; if (passwordSubmitRequested && !root.submitBufferedPassword()) passwordSubmitRequested = false; + if (passwordSubmitRequested || hasPasswordBuffer) { + authTimeout.interval = defaultAuthTimeoutMs; + authTimeout.restart(); + } else { + authTimeout.stop(); + } return; } pendingPasswordResponse = false; + const externalPrompt = root.isExternalAuthPrompt(message, responseRequired); if (!passwordSubmitRequested) - awaitingExternalAuth = root.isExternalAuthPrompt(message, responseRequired); - authTimeout.interval = awaitingExternalAuth ? externalAuthTimeoutMs : defaultAuthTimeoutMs; + awaitingExternalAuth = root.greeterExternalAuthAvailable && externalPrompt; + if (awaitingExternalAuth || (passwordSubmitRequested && externalPrompt && root.greeterPamHasExternalAuth)) + authTimeout.interval = externalAuthTimeoutMs; + else + authTimeout.interval = defaultAuthTimeoutMs; authTimeout.restart(); Greetd.respond(""); } @@ -1587,15 +1623,14 @@ Item { Qt.callLater(root.startAuthSession); return; } - passwordSubmitRequested = false; + resetPasswordSessionTransition(true); } } function onReadyToLaunch() { awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; authTimeout.stop(); passwordFailureCount = 0; @@ -1629,8 +1664,7 @@ Item { function onAuthFailure(message) { awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; authTimeout.stop(); launchTimeout.stop(); @@ -1651,8 +1685,7 @@ Item { function onError(error) { awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; authTimeout.stop(); launchTimeout.stop(); @@ -1688,8 +1721,7 @@ Item { return; awaitingExternalAuth = false; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); authTimeout.interval = defaultAuthTimeoutMs; GreeterState.pamState = "error"; authFeedbackMessage = currentAuthMessage(); @@ -1707,8 +1739,7 @@ Item { if (!GreeterState.unlocking) return; pendingPasswordResponse = false; - passwordSubmitRequested = false; - cancelingExternalAuthForPassword = false; + resetPasswordSessionTransition(true); GreeterState.unlocking = false; GreeterState.pamState = "error"; authFeedbackMessage = currentAuthMessage();