mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-04-03 20:32:07 -04:00
- 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
577 lines
20 KiB
QML
577 lines
20 KiB
QML
pragma Singleton
|
|
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
|
|
Singleton {
|
|
id: root
|
|
|
|
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;
|
|
}
|
|
|
|
function detectFprintd() {
|
|
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
|
|
|
|
stdout: StdioCollector {
|
|
onStreamFinished: {
|
|
if (!settingsRoot)
|
|
return;
|
|
if (text && text.trim()) {
|
|
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";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
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();
|
|
}
|
|
}
|
|
|
|
property var pluginSettingsCheckProcess: Process {
|
|
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
|
running: false
|
|
|
|
onExited: function (exitCode) {
|
|
if (!settingsRoot)
|
|
return;
|
|
settingsRoot.pluginSettingsFileExists = (exitCode === 0);
|
|
}
|
|
}
|
|
}
|