From 1e6a73fd602aefc6c9b7185d343072f4e5f2fbf3 Mon Sep 17 00:00:00 2001 From: purian23 Date: Thu, 12 Mar 2026 15:06:07 -0400 Subject: [PATCH] greeter(auth): Enhance fingerprint/U2F auth support w/Quickshell PAM - Split auth capability state by lock screen and greeter - Share detection between settings UI and lock runtime - Broaden greeter PAM include detection across supported distros --- core/cmd/dms/commands_greeter.go | 16 +- core/internal/greeter/installer.go | 46 +- quickshell/Common/SettingsData.qml | 20 +- quickshell/Common/settings/Processes.qml | 534 +++++++++++++++++- quickshell/Common/settings/SettingsSpec.js | 17 + quickshell/Modules/Greetd/GreeterContent.qml | 109 +++- quickshell/Modules/Lock/Pam.qml | 84 ++- quickshell/Modules/Settings/GreeterTab.qml | 81 ++- quickshell/Modules/Settings/LockScreenTab.qml | 56 +- 9 files changed, 890 insertions(+), 73 deletions(-) diff --git a/core/cmd/dms/commands_greeter.go b/core/cmd/dms/commands_greeter.go index 269a13a4..a2ac360c 100644 --- a/core/cmd/dms/commands_greeter.go +++ b/core/cmd/dms/commands_greeter.go @@ -524,6 +524,16 @@ func syncInTerminal(nonInteractive bool, forceAuth bool, local bool) error { return runCommandInTerminal(shellCmd) } +func resolveLocalWrapperShell() (string, error) { + for _, shellName := range []string{"bash", "sh"} { + shellPath, err := exec.LookPath(shellName) + if err == nil { + return shellPath, nil + } + } + return "", fmt.Errorf("could not find bash or sh in PATH for local greeter wrapper") +} + func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error { if !nonInteractive { fmt.Println("=== DMS Greeter Theme Sync ===") @@ -660,8 +670,12 @@ func syncGreeter(nonInteractive bool, forceAuth bool, local bool) error { localWrapperScript := filepath.Join(dmsPath, "Modules", "Greetd", "assets", "dms-greeter") restoreWrapperOverride := func() {} if info, statErr := os.Stat(localWrapperScript); statErr == nil && !info.IsDir() { + wrapperShell, shellErr := resolveLocalWrapperShell() + if shellErr != nil { + return shellErr + } previousWrapperOverride, hadWrapperOverride := os.LookupEnv("DMS_GREETER_WRAPPER_CMD") - wrapperCmdOverride := "/usr/bin/bash " + localWrapperScript + wrapperCmdOverride := wrapperShell + " " + localWrapperScript _ = os.Setenv("DMS_GREETER_WRAPPER_CMD", wrapperCmdOverride) restoreWrapperOverride = func() { if hadWrapperOverride { diff --git a/core/internal/greeter/installer.go b/core/internal/greeter/installer.go index 2aaa1245..eab30ab5 100644 --- a/core/internal/greeter/installer.go +++ b/core/internal/greeter/installer.go @@ -33,14 +33,23 @@ const ( legacyGreeterPamU2FComment = "# DMS greeter U2F" ) -var includedPamAuthFiles = []string{"system-auth", "common-auth", "password-auth"} +// Common PAM auth stack names referenced by greetd across supported distros. +var includedPamAuthFiles = []string{ + "system-auth", + "common-auth", + "password-auth", + "system-login", + "system-local-login", + "common-auth-pc", + "login", +} func DetectDMSPath() (string, error) { return config.LocateDMSConfig() } // IsNixOS returns true when running on NixOS, which manages PAM configs through -// its module system. The DMS PAM managed block must not be written on NixOS. +// its module system. The DMS PAM managed block won't be written on NixOS. func IsNixOS() bool { _, err := os.Stat("/etc/NIXOS") return err == nil @@ -440,8 +449,21 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { obsSlug := getDebianOBSSlug(osInfo) keyURL := fmt.Sprintf("https://download.opensuse.org/repositories/home:AvengeMedia:danklinux/%s/Release.key", obsSlug) repoLine := fmt.Sprintf("deb [signed-by=/etc/apt/keyrings/danklinux.gpg] https://download.opensuse.org/repositories/home:/AvengeMedia:/danklinux/%s/ /", obsSlug) - failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\ncurl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg\necho '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list\nsudo apt update && sudo apt install dms-greeter", keyURL, repoLine) + failHint = fmt.Sprintf("⚠ dms-greeter install failed. Add OBS repo manually:\nsudo apt-get install -y gnupg\nsudo mkdir -p /etc/apt/keyrings\ncurl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg\necho '%s' | sudo tee /etc/apt/sources.list.d/danklinux.list\nsudo apt update && sudo apt-get install -y dms-greeter", keyURL, repoLine) logFunc(fmt.Sprintf("Adding DankLinux OBS repository (%s)...", obsSlug)) + if _, err := exec.LookPath("gpg"); err != nil { + logFunc("Installing gnupg for OBS repository key import...") + installGPGCmd := exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "gnupg") + installGPGCmd.Stdout = os.Stdout + installGPGCmd.Stderr = os.Stderr + if err := installGPGCmd.Run(); err != nil { + logFunc(fmt.Sprintf("⚠ Failed to install gnupg: %v", err)) + } + } + mkdirCmd := exec.CommandContext(ctx, "sudo", "mkdir", "-p", "/etc/apt/keyrings") + mkdirCmd.Stdout = os.Stdout + mkdirCmd.Stderr = os.Stderr + mkdirCmd.Run() addKeyCmd := exec.CommandContext(ctx, "bash", "-c", fmt.Sprintf(`curl -fsSL %s | sudo gpg --dearmor -o /etc/apt/keyrings/danklinux.gpg`, keyURL)) addKeyCmd.Stdout = os.Stdout @@ -465,7 +487,7 @@ func TryInstallGreeterPackage(logFunc func(string), sudoPassword string) bool { exec.CommandContext(ctx, "sudo", "zypper", "refresh").Run() installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "dms-greeter") case distros.FamilyUbuntu: - failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install dms-greeter" + failHint = "⚠ dms-greeter install failed. Add PPA manually: sudo add-apt-repository ppa:avengemedia/danklinux && sudo apt-get update && sudo apt-get install -y dms-greeter" logFunc("Enabling PPA ppa:avengemedia/danklinux...") ppacmd := exec.CommandContext(ctx, "sudo", "add-apt-repository", "-y", "ppa:avengemedia/danklinux") ppacmd.Stdout = os.Stdout @@ -834,7 +856,14 @@ func EnsureACLInstalled(logFunc func(string), sudoPassword string) error { installCmd = exec.CommandContext(ctx, "sudo", "zypper", "install", "-y", "acl") } - case distros.FamilyUbuntu, distros.FamilyDebian: + case distros.FamilyUbuntu: + if sudoPassword != "" { + installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl") + } else { + installCmd = exec.CommandContext(ctx, "sudo", "apt-get", "install", "-y", "acl") + } + + case distros.FamilyDebian: if sudoPassword != "" { installCmd = distros.ExecSudoCommand(ctx, sudoPassword, "apt-get install -y acl") } else { @@ -1209,9 +1238,14 @@ func pamModuleExists(module string) bool { for _, libDir := range []string{ "/usr/lib64/security", "/usr/lib/security", + "/lib64/security", + "/lib/security", "/lib/x86_64-linux-gnu/security", "/usr/lib/x86_64-linux-gnu/security", + "/lib/aarch64-linux-gnu/security", "/usr/lib/aarch64-linux-gnu/security", + "/run/current-system/sw/lib64/security", + "/run/current-system/sw/lib/security", } { if _, err := os.Stat(filepath.Join(libDir, module)); err == nil { return true @@ -2149,7 +2183,7 @@ func DisableConflictingDisplayManagers(sudoPassword string, logFunc func(string) switch state { case "enabled", "enabled-runtime", "static", "indirect", "alias": logFunc(fmt.Sprintf("Disabling conflicting display manager: %s", dm)) - if err := runSudoCmd(sudoPassword, "systemctl", "disable", "--now", dm); err != nil { + if err := runSudoCmd(sudoPassword, "systemctl", "disable", dm); err != nil { logFunc(fmt.Sprintf("⚠ Warning: Failed to disable %s: %v", dm, err)) } else { logFunc(fmt.Sprintf("✓ Disabled %s", dm)) diff --git a/quickshell/Common/SettingsData.qml b/quickshell/Common/SettingsData.qml index 07fe53e6..bf4ff1bc 100644 --- a/quickshell/Common/SettingsData.qml +++ b/quickshell/Common/SettingsData.qml @@ -506,6 +506,23 @@ Singleton { property bool enableFprint: false property int maxFprintTries: 15 property bool fprintdAvailable: false + property bool lockFingerprintCanEnable: false + property bool lockFingerprintReady: false + property string lockFingerprintReason: "probe_failed" + property bool greeterFingerprintCanEnable: false + property bool greeterFingerprintReady: false + property string greeterFingerprintReason: "probe_failed" + property string greeterFingerprintSource: "none" + property bool enableU2f: false + property string u2fMode: "or" + property bool u2fAvailable: false + property bool lockU2fCanEnable: false + property bool lockU2fReady: false + property string lockU2fReason: "probe_failed" + property bool greeterU2fCanEnable: false + property bool greeterU2fReady: false + property string greeterU2fReason: "probe_failed" + property string greeterU2fSource: "none" property string lockScreenActiveMonitor: "all" property string lockScreenInactiveColor: "#000000" property int lockScreenNotificationMode: 0 @@ -992,8 +1009,7 @@ Singleton { if (isGreeterMode) return; Processes.settingsRoot = root; - Processes.detectFprintd(); - Processes.detectU2f(); + Processes.detectAuthCapabilities(); } Component.onCompleted: { diff --git a/quickshell/Common/settings/Processes.qml b/quickshell/Common/settings/Processes.qml index 50cc2e38..a69d8e11 100644 --- a/quickshell/Common/settings/Processes.qml +++ b/quickshell/Common/settings/Processes.qml @@ -10,6 +10,49 @@ Singleton { property var settingsRoot: null + property string greetdPamText: "" + property string systemAuthPamText: "" + property string commonAuthPamText: "" + property string passwordAuthPamText: "" + property string systemLoginPamText: "" + property string systemLocalLoginPamText: "" + property string commonAuthPcPamText: "" + property string loginPamText: "" + property string dankshellU2fPamText: "" + property string u2fKeysText: "" + + property string fingerprintProbeOutput: "" + property int fingerprintProbeExitCode: 0 + property bool fingerprintProbeStreamFinished: false + property bool fingerprintProbeExited: false + property string fingerprintProbeState: "probe_failed" + + property string pamSupportProbeOutput: "" + property bool pamSupportProbeStreamFinished: false + property bool pamSupportProbeExited: false + property int pamSupportProbeExitCode: 0 + property bool pamFprintSupportDetected: false + property bool pamU2fSupportDetected: false + + readonly property string homeDir: Quickshell.env("HOME") || "" + readonly property string u2fKeysPath: homeDir ? homeDir + "/.config/Yubico/u2f_keys" : "" + readonly property bool homeU2fKeysDetected: u2fKeysPath !== "" && u2fKeysWatcher.loaded && u2fKeysText.trim() !== "" + readonly property bool lockU2fCustomConfigDetected: pamModuleEnabled(dankshellU2fPamText, "pam_u2f") + readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") + readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f") + + function envFlag(name) { + const value = (Quickshell.env(name) || "").trim().toLowerCase(); + if (value === "1" || value === "true" || value === "yes" || value === "on") + return true; + if (value === "0" || value === "false" || value === "no" || value === "off") + return false; + return null; + } + + readonly property var forcedFprintAvailable: envFlag("DMS_FORCE_FPRINT_AVAILABLE") + readonly property var forcedU2fAvailable: envFlag("DMS_FORCE_U2F_AVAILABLE") + function detectQtTools() { qtToolsDetectionProcess.running = true; } @@ -18,10 +61,305 @@ Singleton { fprintdDetectionProcess.running = true; } + function detectAuthCapabilities() { + if (!settingsRoot) + return; + + if (forcedFprintAvailable === null) { + fingerprintProbeOutput = ""; + fingerprintProbeStreamFinished = false; + fingerprintProbeExited = false; + fingerprintProbeProcess.running = true; + } else { + fingerprintProbeState = forcedFprintAvailable ? "ready" : "probe_failed"; + } + + if (forcedFprintAvailable === null || forcedU2fAvailable === null) { + pamFprintSupportDetected = false; + pamU2fSupportDetected = false; + pamSupportProbeOutput = ""; + pamSupportProbeStreamFinished = false; + pamSupportProbeExited = false; + pamSupportDetectionProcess.running = true; + } + + recomputeAuthCapabilities(); + } + + function detectFprintd() { + detectAuthCapabilities(); + } + + function detectU2f() { + detectAuthCapabilities(); + } + function checkPluginSettings() { pluginSettingsCheckProcess.running = true; } + 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 pamModuleEnabled(pamText, moduleName) { + if (!pamText || !moduleName) + 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(moduleName)) + return true; + } + return false; + } + + function pamTextIncludesFile(pamText, filename) { + if (!pamText || !filename) + 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(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include"))) + return true; + } + return false; + } + + function greeterPamStackHasModule(moduleName) { + if (pamModuleEnabled(greetdPamText, moduleName)) + return true; + const includedPamStacks = [ + ["system-auth", systemAuthPamText], + ["common-auth", commonAuthPamText], + ["password-auth", passwordAuthPamText], + ["system-login", systemLoginPamText], + ["system-local-login", systemLocalLoginPamText], + ["common-auth-pc", commonAuthPcPamText], + ["login", loginPamText] + ]; + for (let i = 0; i < includedPamStacks.length; i++) { + const stack = includedPamStacks[i]; + if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName)) + return true; + } + return false; + } + + function hasEnrolledFingerprintOutput(output) { + const lower = (output || "").toLowerCase(); + if (lower.includes("has fingers enrolled") || lower.includes("has fingerprints enrolled")) + return true; + const lines = lower.split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const trimmed = lines[i].trim(); + if (trimmed.startsWith("finger:")) + return true; + if (trimmed.startsWith("- ") && trimmed.includes("finger")) + return true; + } + return false; + } + + function hasMissingFingerprintEnrollmentOutput(output) { + const lower = (output || "").toLowerCase(); + return lower.includes("no fingers enrolled") + || lower.includes("no fingerprints enrolled") + || lower.includes("no prints enrolled"); + } + + function hasMissingFingerprintReaderOutput(output) { + const lower = (output || "").toLowerCase(); + return lower.includes("no devices available") + || lower.includes("no device available") + || lower.includes("no devices found") + || lower.includes("list_devices failed") + || lower.includes("no device"); + } + + function parseFingerprintProbe(exitCode, output) { + if (hasEnrolledFingerprintOutput(output)) + return "ready"; + if (hasMissingFingerprintEnrollmentOutput(output)) + return "missing_enrollment"; + if (hasMissingFingerprintReaderOutput(output)) + return "missing_reader"; + if (exitCode === 0) + return "missing_enrollment"; + if (exitCode === 127 || (output || "").includes("__missing_command__")) + return "probe_failed"; + return pamFprintSupportDetected ? "probe_failed" : "missing_pam_support"; + } + + function setLockFingerprintCapability(canEnable, ready, reason) { + settingsRoot.lockFingerprintCanEnable = canEnable; + settingsRoot.lockFingerprintReady = ready; + settingsRoot.lockFingerprintReason = reason; + } + + function setLockU2fCapability(canEnable, ready, reason) { + settingsRoot.lockU2fCanEnable = canEnable; + settingsRoot.lockU2fReady = ready; + settingsRoot.lockU2fReason = reason; + } + + function setGreeterFingerprintCapability(canEnable, ready, reason, source) { + settingsRoot.greeterFingerprintCanEnable = canEnable; + settingsRoot.greeterFingerprintReady = ready; + settingsRoot.greeterFingerprintReason = reason; + settingsRoot.greeterFingerprintSource = source; + } + + function setGreeterU2fCapability(canEnable, ready, reason, source) { + settingsRoot.greeterU2fCanEnable = canEnable; + settingsRoot.greeterU2fReady = ready; + settingsRoot.greeterU2fReason = reason; + settingsRoot.greeterU2fSource = source; + } + + function recomputeFingerprintCapabilities() { + if (forcedFprintAvailable !== null) { + const reason = forcedFprintAvailable ? "ready" : "probe_failed"; + const source = forcedFprintAvailable ? "dms" : "none"; + setLockFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason); + setGreeterFingerprintCapability(forcedFprintAvailable, forcedFprintAvailable, reason, source); + return; + } + + const state = fingerprintProbeState; + + switch (state) { + case "ready": + setLockFingerprintCapability(true, true, "ready"); + break; + case "missing_enrollment": + setLockFingerprintCapability(true, false, "missing_enrollment"); + break; + case "missing_reader": + setLockFingerprintCapability(false, false, "missing_reader"); + break; + case "missing_pam_support": + setLockFingerprintCapability(false, false, "missing_pam_support"); + break; + default: + setLockFingerprintCapability(false, false, "probe_failed"); + break; + } + + if (greeterPamHasFprint) { + switch (state) { + case "ready": + setGreeterFingerprintCapability(true, true, "configured_externally", "pam"); + break; + case "missing_enrollment": + setGreeterFingerprintCapability(true, false, "missing_enrollment", "pam"); + break; + case "missing_reader": + setGreeterFingerprintCapability(false, false, "missing_reader", "pam"); + break; + default: + setGreeterFingerprintCapability(true, false, "probe_failed", "pam"); + break; + } + return; + } + + switch (state) { + case "ready": + setGreeterFingerprintCapability(true, true, "ready", "dms"); + break; + case "missing_enrollment": + setGreeterFingerprintCapability(true, false, "missing_enrollment", "dms"); + break; + case "missing_reader": + setGreeterFingerprintCapability(false, false, "missing_reader", "none"); + break; + case "missing_pam_support": + setGreeterFingerprintCapability(false, false, "missing_pam_support", "none"); + break; + default: + setGreeterFingerprintCapability(false, false, "probe_failed", "none"); + break; + } + } + + function recomputeU2fCapabilities() { + if (forcedU2fAvailable !== null) { + const reason = forcedU2fAvailable ? "ready" : "probe_failed"; + const source = forcedU2fAvailable ? "dms" : "none"; + setLockU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason); + setGreeterU2fCapability(forcedU2fAvailable, forcedU2fAvailable, reason, source); + return; + } + + const lockReady = lockU2fCustomConfigDetected || homeU2fKeysDetected; + const lockCanEnable = lockReady || pamU2fSupportDetected; + const lockReason = lockReady ? "ready" : (lockCanEnable ? "missing_key_registration" : "missing_pam_support"); + setLockU2fCapability(lockCanEnable, lockReady, lockReason); + + if (greeterPamHasU2f) { + setGreeterU2fCapability(true, true, "configured_externally", "pam"); + return; + } + + const greeterReady = homeU2fKeysDetected; + const greeterCanEnable = greeterReady || pamU2fSupportDetected; + const greeterReason = greeterReady ? "ready" : (greeterCanEnable ? "missing_key_registration" : "missing_pam_support"); + setGreeterU2fCapability(greeterCanEnable, greeterReady, greeterReason, greeterCanEnable ? "dms" : "none"); + } + + function recomputeAuthCapabilities() { + if (!settingsRoot) + return; + recomputeFingerprintCapabilities(); + recomputeU2fCapabilities(); + settingsRoot.fprintdAvailable = settingsRoot.lockFingerprintReady || settingsRoot.greeterFingerprintReady; + settingsRoot.u2fAvailable = settingsRoot.lockU2fReady || settingsRoot.greeterU2fReady; + } + + function finalizeFingerprintProbe() { + if (!fingerprintProbeStreamFinished || !fingerprintProbeExited) + return; + fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput); + recomputeAuthCapabilities(); + } + + function finalizePamSupportProbe() { + if (!pamSupportProbeStreamFinished || !pamSupportProbeExited) + return; + + pamFprintSupportDetected = false; + pamU2fSupportDetected = false; + + const lines = (pamSupportProbeOutput || "").trim().split(/\r?\n/); + for (let i = 0; i < lines.length; i++) { + const parts = lines[i].split(":"); + if (parts.length !== 2) + continue; + if (parts[0] === "pam_fprintd.so") + pamFprintSupportDetected = parts[1] === "true"; + else if (parts[0] === "pam_u2f.so") + pamU2fSupportDetected = parts[1] === "true"; + } + + if (forcedFprintAvailable === null && fingerprintProbeState === "missing_pam_support") + fingerprintProbeState = parseFingerprintProbe(fingerprintProbeExitCode, fingerprintProbeOutput); + + recomputeAuthCapabilities(); + } + property var qtToolsDetectionProcess: Process { command: ["sh", "-c", "echo -n 'qt5ct:'; command -v qt5ct >/dev/null && echo 'true' || echo 'false'; echo -n 'qt6ct:'; command -v qt6ct >/dev/null && echo 'true' || echo 'false'; echo -n 'gtk:'; (command -v gsettings >/dev/null || command -v dconf >/dev/null) && echo 'true' || echo 'false'"] running: false @@ -31,15 +369,15 @@ Singleton { if (!settingsRoot) return; if (text && text.trim()) { - var lines = text.trim().split('\n'); - for (var i = 0; i < lines.length; i++) { - var line = lines[i]; - if (line.startsWith('qt5ct:')) { - settingsRoot.qt5ctAvailable = line.split(':')[1] === 'true'; - } else if (line.startsWith('qt6ct:')) { - settingsRoot.qt6ctAvailable = line.split(':')[1] === 'true'; - } else if (line.startsWith('gtk:')) { - settingsRoot.gtkAvailable = line.split(':')[1] === 'true'; + const lines = text.trim().split("\n"); + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith("qt5ct:")) { + settingsRoot.qt5ctAvailable = line.split(":")[1] === "true"; + } else if (line.startsWith("qt6ct:")) { + settingsRoot.qt6ctAvailable = line.split(":")[1] === "true"; + } else if (line.startsWith("gtk:")) { + settingsRoot.gtkAvailable = line.split(":")[1] === "true"; } } } @@ -47,13 +385,181 @@ Singleton { } } - property var fprintdDetectionProcess: Process { - command: ["sh", "-c", "command -v fprintd-list >/dev/null 2>&1 && fprintd-list \"${USER:-$(id -un)}\" >/dev/null 2>&1"] + property var fingerprintProbeProcess: Process { + command: ["sh", "-c", "if command -v fprintd-list >/dev/null 2>&1; then fprintd-list \"${USER:-$(id -un)}\" 2>&1; else printf '__missing_command__\\n'; exit 127; fi"] running: false + + stdout: StdioCollector { + onStreamFinished: { + root.fingerprintProbeOutput = text || ""; + root.fingerprintProbeStreamFinished = true; + root.finalizeFingerprintProbe(); + } + } + onExited: function (exitCode) { - if (!settingsRoot) - return; - settingsRoot.fprintdAvailable = (exitCode === 0); + root.fingerprintProbeExitCode = exitCode; + root.fingerprintProbeExited = true; + root.finalizeFingerprintProbe(); + } + } + + 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"] + running: false + + stdout: StdioCollector { + onStreamFinished: { + root.pamSupportProbeOutput = text || ""; + root.pamSupportProbeStreamFinished = true; + root.finalizePamSupportProbe(); + } + } + + onExited: function (exitCode) { + root.pamSupportProbeExitCode = exitCode; + root.pamSupportProbeExited = true; + root.finalizePamSupportProbe(); + } + } + + FileView { + id: greetdPamWatcher + path: "/etc/pam.d/greetd" + printErrors: false + onLoaded: { + root.greetdPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.greetdPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: systemAuthPamWatcher + path: "/etc/pam.d/system-auth" + printErrors: false + onLoaded: { + root.systemAuthPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.systemAuthPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: commonAuthPamWatcher + path: "/etc/pam.d/common-auth" + printErrors: false + onLoaded: { + root.commonAuthPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.commonAuthPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: passwordAuthPamWatcher + path: "/etc/pam.d/password-auth" + printErrors: false + onLoaded: { + root.passwordAuthPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.passwordAuthPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: systemLoginPamWatcher + path: "/etc/pam.d/system-login" + printErrors: false + onLoaded: { + root.systemLoginPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.systemLoginPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: systemLocalLoginPamWatcher + path: "/etc/pam.d/system-local-login" + printErrors: false + onLoaded: { + root.systemLocalLoginPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.systemLocalLoginPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: commonAuthPcPamWatcher + path: "/etc/pam.d/common-auth-pc" + printErrors: false + onLoaded: { + root.commonAuthPcPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.commonAuthPcPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: loginPamWatcher + path: "/etc/pam.d/login" + printErrors: false + onLoaded: { + root.loginPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.loginPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: dankshellU2fPamWatcher + path: "/etc/pam.d/dankshell-u2f" + printErrors: false + onLoaded: { + root.dankshellU2fPamText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.dankshellU2fPamText = ""; + root.recomputeAuthCapabilities(); + } + } + + FileView { + id: u2fKeysWatcher + path: root.u2fKeysPath + printErrors: false + onLoaded: { + root.u2fKeysText = text(); + root.recomputeAuthCapabilities(); + } + onLoadFailed: { + root.u2fKeysText = ""; + root.recomputeAuthCapabilities(); } } diff --git a/quickshell/Common/settings/SettingsSpec.js b/quickshell/Common/settings/SettingsSpec.js index 0e29dbe2..026d252c 100644 --- a/quickshell/Common/settings/SettingsSpec.js +++ b/quickshell/Common/settings/SettingsSpec.js @@ -329,6 +329,23 @@ var SPEC = { enableFprint: { def: false }, maxFprintTries: { def: 15 }, fprintdAvailable: { def: false, persist: false }, + lockFingerprintCanEnable: { def: false, persist: false }, + lockFingerprintReady: { def: false, persist: false }, + lockFingerprintReason: { def: "probe_failed", persist: false }, + greeterFingerprintCanEnable: { def: false, persist: false }, + greeterFingerprintReady: { def: false, persist: false }, + greeterFingerprintReason: { def: "probe_failed", persist: false }, + greeterFingerprintSource: { def: "none", persist: false }, + enableU2f: { def: false }, + u2fMode: { def: "or" }, + u2fAvailable: { def: false, persist: false }, + lockU2fCanEnable: { def: false, persist: false }, + lockU2fReady: { def: false, persist: false }, + lockU2fReason: { def: "probe_failed", persist: false }, + greeterU2fCanEnable: { def: false, persist: false }, + greeterU2fReady: { def: false, persist: false }, + greeterU2fReason: { def: "probe_failed", persist: false }, + greeterU2fSource: { def: "none", persist: false }, lockScreenActiveMonitor: { def: "all" }, lockScreenInactiveColor: { def: "#000000" }, lockScreenNotificationMode: { def: 0 }, diff --git a/quickshell/Modules/Greetd/GreeterContent.qml b/quickshell/Modules/Greetd/GreeterContent.qml index aa431f6d..e19d174d 100644 --- a/quickshell/Modules/Greetd/GreeterContent.qml +++ b/quickshell/Modules/Greetd/GreeterContent.qml @@ -36,7 +36,7 @@ Item { property bool passwordSubmitRequested: false property bool cancelingExternalAuthForPassword: false property int defaultAuthTimeoutMs: 10000 - property int externalAuthTimeoutMs: 36000 + property int externalAuthTimeoutMs: 30000 property int memoryFlushDelayMs: 120 property string pendingLaunchCommand: "" property var pendingLaunchEnv: [] @@ -47,13 +47,17 @@ Item { property string systemAuthPamText: "" property string commonAuthPamText: "" property string passwordAuthPamText: "" + property string systemLoginPamText: "" + property string systemLocalLoginPamText: "" + property string commonAuthPcPamText: "" + property string loginPamText: "" 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 greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd") + readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f") readonly property bool greeterExternalAuthAvailable: (greeterPamHasFprint && GreetdSettings.greeterEnableFprint) || (greeterPamHasU2f && GreetdSettings.greeterEnableU2f) readonly property bool greeterPamHasExternalAuth: greeterPamHasFprint || greeterPamHasU2f @@ -95,6 +99,40 @@ Item { return false; } + function pamTextIncludesFile(pamText, filename) { + if (!pamText || !filename) + 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(filename) && (line.includes("include") || line.includes("substack") || line.startsWith("@include"))) + return true; + } + return false; + } + + function greeterPamStackHasModule(moduleName) { + if (pamModuleEnabled(greetdPamText, moduleName)) + return true; + const includedPamStacks = [ + ["system-auth", systemAuthPamText], + ["common-auth", commonAuthPamText], + ["password-auth", passwordAuthPamText], + ["system-login", systemLoginPamText], + ["system-local-login", systemLocalLoginPamText], + ["common-auth-pc", commonAuthPcPamText], + ["login", loginPamText] + ]; + for (let i = 0; i < includedPamStacks.length; i++) { + const stack = includedPamStacks[i]; + if (pamTextIncludesFile(greetdPamText, stack[0]) && pamModuleEnabled(stack[1], moduleName)) + return true; + } + return false; + } + function usesPamLockoutPolicy(pamText) { if (!pamText) return false; @@ -148,7 +186,7 @@ Item { } function refreshPasswordAttemptPolicyHint() { - const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText]; + const pamSources = [greetdPamText, systemAuthPamText, commonAuthPamText, passwordAuthPamText, systemLoginPamText, systemLocalLoginPamText, commonAuthPcPamText, loginPamText]; let lockoutConfigured = false; let denyFromPam = -1; for (let i = 0; i < pamSources.length; i++) { @@ -271,6 +309,7 @@ Item { onLoaded: { root.systemAuthPamText = text(); root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); } onLoadFailed: { root.systemAuthPamText = ""; @@ -285,6 +324,7 @@ Item { onLoaded: { root.commonAuthPamText = text(); root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); } onLoadFailed: { root.commonAuthPamText = ""; @@ -299,6 +339,7 @@ Item { onLoaded: { root.passwordAuthPamText = text(); root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); } onLoadFailed: { root.passwordAuthPamText = ""; @@ -306,6 +347,66 @@ Item { } } + FileView { + id: systemLoginPamWatcher + path: "/etc/pam.d/system-login" + printErrors: false + onLoaded: { + root.systemLoginPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); + } + onLoadFailed: { + root.systemLoginPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + + FileView { + id: systemLocalLoginPamWatcher + path: "/etc/pam.d/system-local-login" + printErrors: false + onLoaded: { + root.systemLocalLoginPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); + } + onLoadFailed: { + root.systemLocalLoginPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + + FileView { + id: commonAuthPcPamWatcher + path: "/etc/pam.d/common-auth-pc" + printErrors: false + onLoaded: { + root.commonAuthPcPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); + } + onLoadFailed: { + root.commonAuthPcPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + + FileView { + id: loginPamWatcher + path: "/etc/pam.d/login" + printErrors: false + onLoaded: { + root.loginPamText = text(); + root.refreshPasswordAttemptPolicyHint(); + root.maybeAutoStartExternalAuth(); + } + onLoadFailed: { + root.loginPamText = ""; + root.refreshPasswordAttemptPolicyHint(); + } + } + FileView { id: faillockConfigWatcher path: "/etc/security/faillock.conf" diff --git a/quickshell/Modules/Lock/Pam.qml b/quickshell/Modules/Lock/Pam.qml index 2ef89d69..b5df43bb 100644 --- a/quickshell/Modules/Lock/Pam.qml +++ b/quickshell/Modules/Lock/Pam.qml @@ -158,7 +158,7 @@ Scope { PamContext { id: fprint - property bool available + property bool available: SettingsData.lockFingerprintReady property int tries property int errorTries @@ -212,13 +212,71 @@ Scope { } } - Process { - id: availProc + PamContext { + id: u2f - command: ["sh", "-c", "fprintd-list \"${USER:-$(id -un)}\""] - onExited: code => { - fprint.available = code === 0; - fprint.checkAvail(); + property bool available: SettingsData.lockU2fReady + + function checkAvail(): void { + if (!available || !SettingsData.enableU2f || !root.lockSecured) { + abort(); + return; + } + + if (SettingsData.u2fMode === "or") { + start(); + } + } + + function startForSecondFactor(): void { + if (!available || !SettingsData.enableU2f) { + root.completeUnlock(); + return; + } + abort(); + root.u2fPending = true; + 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.u2fState === "waiting") { + // AND mode: device was found but auth failed → back to password + root.u2fPending = false; + root.u2fState = ""; + fprint.checkAvail(); + } else { + // AND mode: no device found → keep pending, show "Insert...", retry + root.u2fState = "insert"; + u2fErrorRetry.restart(); + } + } else { + // OR mode: prompt to insert key, silently retry + root.u2fState = "insert"; + u2fErrorRetry.restart(); + } + } } } @@ -285,11 +343,13 @@ Scope { onLockSecuredChanged: { if (lockSecured) { - availProc.running = true; + SettingsData.refreshAuthAvailability(); root.state = ""; root.fprintState = ""; root.lockMessage = ""; root.resetAuthFlows(); + fprint.checkAvail(); + u2f.checkAvail(); } else { root.resetAuthFlows(); } @@ -302,10 +362,18 @@ Scope { 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(); diff --git a/quickshell/Modules/Settings/GreeterTab.qml b/quickshell/Modules/Settings/GreeterTab.qml index 73a79376..c4cc84c0 100644 --- a/quickshell/Modules/Settings/GreeterTab.qml +++ b/quickshell/Modules/Settings/GreeterTab.qml @@ -14,8 +14,63 @@ import qs.Modules.Settings.Widgets Item { id: root - readonly property bool greeterFprintToggleAvailable: SettingsData.fprintdAvailable || SettingsData.greeterEnableFprint - readonly property bool greeterU2fToggleAvailable: SettingsData.u2fAvailable || SettingsData.greeterEnableU2f + readonly property bool greeterFprintToggleAvailable: SettingsData.greeterFingerprintCanEnable || SettingsData.greeterEnableFprint + readonly property bool greeterU2fToggleAvailable: SettingsData.greeterU2fCanEnable || SettingsData.greeterEnableU2f + + function greeterFingerprintDescription() { + const source = SettingsData.greeterFingerprintSource; + const reason = SettingsData.greeterFingerprintReason; + + if (source === "pam") { + switch (reason) { + case "configured_externally": + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM already provides fingerprint auth.") : I18n.tr("PAM already provides fingerprint auth. Enable this to show it at login."); + case "missing_enrollment": + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled. PAM provides fingerprint auth, but no prints are enrolled yet.") : I18n.tr("PAM provides fingerprint auth, but no prints are enrolled yet."); + case "missing_reader": + return I18n.tr("PAM provides fingerprint auth, but no reader was detected."); + default: + return I18n.tr("PAM provides fingerprint auth, but availability could not be confirmed."); + } + } + + switch (reason) { + case "ready": + return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only affects DMS-managed PAM. If greetd already includes pam_fprintd, fingerprint stays enabled."); + case "missing_enrollment": + if (SettingsData.greeterEnableFprint) + return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints and run Sync."); + return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and run Sync later."); + case "missing_reader": + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); + case "missing_pam_support": + return I18n.tr("Not available — install fprintd and pam_fprintd, or configure greetd PAM."); + default: + return SettingsData.greeterEnableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed."); + } + } + + function greeterU2fDescription() { + const source = SettingsData.greeterU2fSource; + const reason = SettingsData.greeterU2fReason; + + if (source === "pam") { + return SettingsData.greeterEnableU2f ? I18n.tr("Enabled. PAM already provides security-key auth.") : I18n.tr("PAM already provides security-key auth. Enable this to show it at login."); + } + + switch (reason) { + case "ready": + return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Available."); + case "missing_key_registration": + if (SettingsData.greeterEnableU2f) + return I18n.tr("Enabled, but no registered security key was found yet. Register a key and run Sync."); + return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later."); + case "missing_pam_support": + return I18n.tr("Not available — install or configure pam_u2f, or configure greetd PAM."); + default: + return SettingsData.greeterEnableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed."); + } + } function refreshAuthDetection() { SettingsData.refreshAuthAvailability(); @@ -481,15 +536,8 @@ Item { settingKey: "greeterEnableFprint" tags: ["greeter", "fingerprint", "fprintd", "login", "auth"] text: I18n.tr("Enable fingerprint at login") - description: { - if (!SettingsData.fprintdAvailable) { - if (SettingsData.greeterEnableFprint) - return I18n.tr("Enabled in settings, but fingerprint availability could not yet be confirmed. Re-open after enrolling fingerprints or reconnecting the reader."); - return I18n.tr("Not available — install fprintd and enroll fingerprints."); - } - return SettingsData.greeterEnableFprint ? I18n.tr("Run Sync to apply. Fingerprint-only login may not unlock GNOME Keyring.") : I18n.tr("Only off for DMS-managed PAM lines. If greetd includes system-auth/common-auth/password-auth with pam_fprintd, fingerprint still stays enabled."); - } - descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning + description: root.greeterFingerprintDescription() + descriptionColor: (SettingsData.greeterFingerprintReason === "ready" || SettingsData.greeterFingerprintReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning checked: SettingsData.greeterEnableFprint enabled: root.greeterFprintToggleAvailable onToggled: checked => SettingsData.set("greeterEnableFprint", checked) @@ -499,15 +547,8 @@ Item { settingKey: "greeterEnableU2f" tags: ["greeter", "u2f", "security", "key", "login", "auth"] text: I18n.tr("Enable security key at login") - description: { - if (!SettingsData.u2fAvailable) { - if (SettingsData.greeterEnableU2f) - return I18n.tr("Enabled in settings, but security key availability could not yet be confirmed. Re-open after enrolling keys or updating pam_u2f."); - return I18n.tr("Not available — install pam_u2f and enroll keys."); - } - return SettingsData.greeterEnableU2f ? I18n.tr("Run Sync to apply.") : I18n.tr("Disabled."); - } - descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning + description: root.greeterU2fDescription() + descriptionColor: (SettingsData.greeterU2fReason === "ready" || SettingsData.greeterU2fReason === "configured_externally") ? Theme.surfaceVariantText : Theme.warning checked: SettingsData.greeterEnableU2f enabled: root.greeterU2fToggleAvailable onToggled: checked => SettingsData.set("greeterEnableU2f", checked) diff --git a/quickshell/Modules/Settings/LockScreenTab.qml b/quickshell/Modules/Settings/LockScreenTab.qml index 69464d5d..6e16a969 100644 --- a/quickshell/Modules/Settings/LockScreenTab.qml +++ b/quickshell/Modules/Settings/LockScreenTab.qml @@ -8,8 +8,40 @@ import qs.Modules.Settings.Widgets Item { id: root - readonly property bool lockFprintToggleAvailable: SettingsData.fprintdAvailable || SettingsData.enableFprint - readonly property bool lockU2fToggleAvailable: SettingsData.u2fAvailable || SettingsData.enableU2f + readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint + readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f + + function lockFingerprintDescription() { + switch (SettingsData.lockFingerprintReason) { + case "ready": + return I18n.tr("Use fingerprint authentication for the lock screen."); + case "missing_enrollment": + if (SettingsData.enableFprint) + return I18n.tr("Enabled, but no prints are enrolled yet. Enroll fingerprints to use it."); + return I18n.tr("Fingerprint reader detected, but no prints are enrolled yet. You can enable this now and enroll later."); + case "missing_reader": + return SettingsData.enableFprint ? I18n.tr("Enabled, but no fingerprint reader was detected.") : I18n.tr("No fingerprint reader detected."); + case "missing_pam_support": + return I18n.tr("Not available — install fprintd and pam_fprintd."); + default: + return SettingsData.enableFprint ? I18n.tr("Enabled, but fingerprint availability could not be confirmed.") : I18n.tr("Fingerprint availability could not be confirmed."); + } + } + + function lockU2fDescription() { + switch (SettingsData.lockU2fReason) { + case "ready": + return I18n.tr("Use a security key for lock screen authentication.", "lock screen U2F security key setting"); + case "missing_key_registration": + if (SettingsData.enableU2f) + return I18n.tr("Enabled, but no registered security key was found yet. Register a key or update your U2F config."); + return I18n.tr("Security-key support was detected, but no registered key was found yet. You can enable this now and register one later."); + case "missing_pam_support": + return I18n.tr("Not available — install or configure pam_u2f."); + default: + return SettingsData.enableU2f ? I18n.tr("Enabled, but security-key availability could not be confirmed.") : I18n.tr("Security-key availability could not be confirmed."); + } + } function refreshAuthDetection() { SettingsData.refreshAuthAvailability(); @@ -184,14 +216,8 @@ Item { settingKey: "enableFprint" tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"] text: I18n.tr("Enable fingerprint authentication") - description: { - if (SettingsData.fprintdAvailable) - return I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)"); - if (SettingsData.enableFprint) - return I18n.tr("Enabled in settings, but fingerprint availability could not yet be confirmed. Re-open after enrolling fingerprints or reconnecting the reader."); - return I18n.tr("Not available — install fprintd and enroll fingerprints."); - } - descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning + description: root.lockFingerprintDescription() + descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning checked: SettingsData.enableFprint enabled: root.lockFprintToggleAvailable onToggled: checked => SettingsData.set("enableFprint", checked) @@ -201,14 +227,8 @@ Item { settingKey: "enableU2f" tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"] text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen") - description: { - if (SettingsData.u2fAvailable) - return I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting"); - if (SettingsData.enableU2f) - return I18n.tr("Enabled in settings, but security key availability could not yet be confirmed. Re-open after enrolling keys or updating pam_u2f."); - return I18n.tr("Not available — install pam_u2f and enroll keys."); - } - descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning + description: root.lockU2fDescription() + descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning checked: SettingsData.enableU2f enabled: root.lockU2fToggleAvailable onToggled: checked => SettingsData.set("enableU2f", checked)