mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 11:38:30 -04:00
9967db0c5a
- Introduce external management of greetd PAM
- New functionality to validate and apply custom PAM service paths in lockscreen
Port 1.5
(cherry picked from commit b169fe0d77)
533 lines
15 KiB
QML
533 lines
15 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Services.Pam
|
|
import qs.Common
|
|
|
|
Scope {
|
|
id: root
|
|
|
|
property bool lockSecured: false
|
|
property bool unlockInProgress: false
|
|
|
|
readonly property alias passwd: passwd
|
|
readonly property alias fprint: fprint
|
|
readonly property alias u2f: u2f
|
|
property string lockMessage
|
|
property string state
|
|
property string fprintState
|
|
property string u2fState
|
|
property bool u2fPending: false
|
|
property string u2fPendingMode
|
|
property string buffer
|
|
|
|
property var attemptInfoMessages: []
|
|
property bool lockoutAnnouncedThisAttempt: false
|
|
|
|
signal flashMsg
|
|
signal unlockRequested
|
|
|
|
function resetAuthFlows(): void {
|
|
passwd.abort();
|
|
fprint.abort();
|
|
u2f.abort();
|
|
errorRetry.running = false;
|
|
u2fErrorRetry.running = false;
|
|
u2fPendingTimeout.running = false;
|
|
passwdActiveTimeout.running = false;
|
|
unlockRequestTimeout.running = false;
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = "";
|
|
root.unlockInProgress = false;
|
|
}
|
|
|
|
function recoverFromAuthStall(newState: string): void {
|
|
resetAuthFlows();
|
|
root.state = newState;
|
|
flashMsg();
|
|
stateReset.restart();
|
|
fprint.checkAvail();
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
function completeUnlock(): void {
|
|
if (!root.unlockInProgress) {
|
|
root.unlockInProgress = true;
|
|
passwd.abort();
|
|
fprint.abort();
|
|
u2f.abort();
|
|
errorRetry.running = false;
|
|
u2fErrorRetry.running = false;
|
|
u2fPendingTimeout.running = false;
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = "";
|
|
unlockRequestTimeout.restart();
|
|
unlockRequested();
|
|
}
|
|
}
|
|
|
|
function proceedAfterPrimaryAuth(): void {
|
|
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
|
|
u2f.startForSecondFactor();
|
|
} else {
|
|
completeUnlock();
|
|
}
|
|
}
|
|
|
|
function cancelU2fPending(): void {
|
|
if (!root.u2fPending)
|
|
return;
|
|
u2f.abort();
|
|
u2fErrorRetry.running = false;
|
|
u2fPendingTimeout.running = false;
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = "";
|
|
fprint.checkAvail();
|
|
}
|
|
|
|
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
|
|
readonly property bool fprintSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineFprint
|
|
readonly property bool u2fSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineU2f
|
|
|
|
FileView {
|
|
id: customPamWatcher
|
|
|
|
path: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : ""
|
|
printErrors: false
|
|
}
|
|
|
|
FileView {
|
|
id: dankshellConfigWatcher
|
|
|
|
path: "/etc/pam.d/dankshell"
|
|
printErrors: false
|
|
}
|
|
|
|
FileView {
|
|
id: nixosMarker
|
|
|
|
path: "/etc/NIXOS"
|
|
printErrors: false
|
|
}
|
|
|
|
FileView {
|
|
id: u2fConfigWatcher
|
|
|
|
path: "/etc/pam.d/dankshell-u2f"
|
|
printErrors: false
|
|
}
|
|
|
|
// Fallback stack written by `dms auth resolve-lock` when no managed
|
|
// /etc/pam.d/dankshell exists. See #2789.
|
|
readonly property string userPamDir: Paths.strip(Paths.state) + "/pam"
|
|
|
|
FileView {
|
|
id: userPamWatcher
|
|
|
|
path: root.userPamDir + "/dankshell"
|
|
printErrors: false
|
|
}
|
|
|
|
Process {
|
|
id: resolveUserPam
|
|
|
|
command: ["dms", "auth", "resolve-lock", "--quiet"]
|
|
running: false
|
|
onExited: exitCode => {
|
|
if (exitCode === 0)
|
|
userPamWatcher.reload();
|
|
}
|
|
}
|
|
|
|
function ensureUserPamConfig(): void {
|
|
if (root.runningFromNixStore || resolveUserPam.running)
|
|
return;
|
|
resolveUserPam.running = true;
|
|
}
|
|
|
|
Component.onCompleted: ensureUserPamConfig()
|
|
|
|
// Detects Nix-installed DMS on non-NixOS systems
|
|
readonly property bool runningFromNixStore: Quickshell.shellDir.startsWith("/nix/store/")
|
|
|
|
PamContext {
|
|
id: passwd
|
|
|
|
config: {
|
|
if (root.customPamActive)
|
|
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
|
|
if (dankshellConfigWatcher.loaded)
|
|
return "dankshell";
|
|
if (nixosMarker.loaded || root.runningFromNixStore)
|
|
return "login";
|
|
if (userPamWatcher.loaded)
|
|
return "dankshell";
|
|
return "login";
|
|
}
|
|
configDirectory: {
|
|
if (root.customPamActive) {
|
|
const idx = SettingsData.lockPamPath.lastIndexOf("/");
|
|
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
|
|
}
|
|
if (dankshellConfigWatcher.loaded)
|
|
return "/etc/pam.d";
|
|
if (nixosMarker.loaded || root.runningFromNixStore)
|
|
return "/etc/pam.d";
|
|
if (userPamWatcher.loaded)
|
|
return root.userPamDir;
|
|
return Quickshell.shellDir + "/assets/pam";
|
|
}
|
|
|
|
onMessageChanged: {
|
|
// collected by position, not text, so it works in any locale
|
|
if (message.length > 0 && !responseRequired)
|
|
root.attemptInfoMessages = root.attemptInfoMessages.concat([message]);
|
|
}
|
|
|
|
onResponseRequiredChanged: {
|
|
if (!responseRequired)
|
|
return;
|
|
|
|
const notice = root.attemptInfoMessages.filter(m => m !== message);
|
|
if (notice.length > 0) {
|
|
root.lockMessage = notice.join("\n");
|
|
root.lockoutAnnouncedThisAttempt = true;
|
|
}
|
|
root.attemptInfoMessages = [];
|
|
|
|
respond(root.buffer);
|
|
}
|
|
|
|
onCompleted: res => {
|
|
// requisite preauth can lock without ever prompting; surface it here too
|
|
if (!root.lockoutAnnouncedThisAttempt) {
|
|
if (root.attemptInfoMessages.length > 0) {
|
|
root.lockMessage = root.attemptInfoMessages.join("\n");
|
|
root.lockoutAnnouncedThisAttempt = true;
|
|
} else {
|
|
root.lockMessage = "";
|
|
}
|
|
root.attemptInfoMessages = [];
|
|
}
|
|
|
|
if (res === PamResult.Success) {
|
|
if (!root.unlockInProgress) {
|
|
fprint.abort();
|
|
root.proceedAfterPrimaryAuth();
|
|
}
|
|
return;
|
|
}
|
|
|
|
unlockRequestTimeout.running = false;
|
|
root.unlockInProgress = false;
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = "";
|
|
u2fPendingTimeout.running = false;
|
|
u2f.abort();
|
|
|
|
if (res === PamResult.Error)
|
|
root.state = "error";
|
|
else if (res === PamResult.MaxTries)
|
|
root.state = "max";
|
|
else if (res === PamResult.Failed)
|
|
root.state = "fail";
|
|
|
|
root.flashMsg();
|
|
stateReset.restart();
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: passwd
|
|
|
|
function onActiveChanged() {
|
|
if (passwd.active) {
|
|
root.attemptInfoMessages = [];
|
|
root.lockoutAnnouncedThisAttempt = false;
|
|
passwdActiveTimeout.restart();
|
|
} else {
|
|
passwdActiveTimeout.running = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
PamContext {
|
|
id: fprint
|
|
|
|
property bool available: SettingsData.lockFingerprintReady
|
|
property int tries
|
|
property int errorTries
|
|
|
|
function checkAvail(): void {
|
|
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByCustomPam) {
|
|
abort();
|
|
return;
|
|
}
|
|
if (active)
|
|
return;
|
|
|
|
tries = 0;
|
|
errorTries = 0;
|
|
start();
|
|
}
|
|
|
|
config: "fprint"
|
|
configDirectory: Quickshell.shellDir + "/assets/pam"
|
|
|
|
onCompleted: res => {
|
|
if (!available)
|
|
return;
|
|
|
|
switch (res) {
|
|
case PamResult.Success:
|
|
if (!root.unlockInProgress) {
|
|
passwd.abort();
|
|
root.proceedAfterPrimaryAuth();
|
|
}
|
|
return;
|
|
case PamResult.Error:
|
|
errorTries++;
|
|
if (errorTries < 200) {
|
|
abort();
|
|
errorRetry.restart();
|
|
return;
|
|
}
|
|
abort();
|
|
return;
|
|
case PamResult.MaxTries:
|
|
tries++;
|
|
if (tries < SettingsData.maxFprintTries) {
|
|
root.fprintState = "fail";
|
|
start();
|
|
} else {
|
|
root.fprintState = "max";
|
|
abort();
|
|
}
|
|
break;
|
|
default:
|
|
return;
|
|
}
|
|
|
|
root.flashMsg();
|
|
fprintStateReset.start();
|
|
}
|
|
}
|
|
|
|
PamContext {
|
|
id: u2f
|
|
|
|
property bool available: SettingsData.lockU2fReady
|
|
|
|
function checkAvail(): void {
|
|
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByCustomPam) {
|
|
abort();
|
|
return;
|
|
}
|
|
|
|
if (SettingsData.u2fMode === "or")
|
|
abort();
|
|
}
|
|
|
|
function startForSecondFactor(): void {
|
|
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam) {
|
|
root.completeUnlock();
|
|
return;
|
|
}
|
|
abort();
|
|
root.u2fPending = true;
|
|
root.u2fPendingMode = "and";
|
|
root.u2fState = "";
|
|
u2fPendingTimeout.restart();
|
|
start();
|
|
}
|
|
|
|
function startForAlternativeAuth(): void {
|
|
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
|
|
return;
|
|
abort();
|
|
root.u2fPending = true;
|
|
root.u2fPendingMode = "or";
|
|
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.u2fPendingMode === "or") {
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = root.u2fState === "waiting" ? "" : "insert";
|
|
u2fPendingTimeout.running = false;
|
|
fprint.checkAvail();
|
|
return;
|
|
}
|
|
|
|
if (root.u2fState === "waiting") {
|
|
// AND mode: device was found but auth failed → back to password
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = "";
|
|
fprint.checkAvail();
|
|
} else {
|
|
// AND mode: no device found → keep pending, show "Insert...", retry
|
|
root.u2fState = "insert";
|
|
u2fErrorRetry.restart();
|
|
}
|
|
} else {
|
|
root.u2fState = "insert";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: errorRetry
|
|
|
|
interval: 1500
|
|
onTriggered: fprint.start()
|
|
}
|
|
|
|
Timer {
|
|
id: u2fErrorRetry
|
|
|
|
interval: 800
|
|
onTriggered: u2f.start()
|
|
}
|
|
|
|
Timer {
|
|
id: u2fPendingTimeout
|
|
|
|
interval: 30000
|
|
onTriggered: root.cancelU2fPending()
|
|
}
|
|
|
|
Timer {
|
|
id: passwdActiveTimeout
|
|
|
|
interval: 15000
|
|
onTriggered: {
|
|
if (passwd.active)
|
|
root.recoverFromAuthStall("error");
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: unlockRequestTimeout
|
|
|
|
interval: 8000
|
|
onTriggered: {
|
|
if (root.unlockInProgress)
|
|
root.recoverFromAuthStall("error");
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: stateReset
|
|
|
|
interval: 4000
|
|
onTriggered: {
|
|
if (root.state !== "max")
|
|
root.state = "";
|
|
}
|
|
}
|
|
|
|
Timer {
|
|
id: fprintStateReset
|
|
|
|
interval: 4000
|
|
onTriggered: root.fprintState = ""
|
|
}
|
|
|
|
onLockSecuredChanged: {
|
|
if (!lockSecured) {
|
|
root.resetAuthFlows();
|
|
return;
|
|
}
|
|
root.state = "";
|
|
root.fprintState = "";
|
|
root.u2fState = "";
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.lockMessage = "";
|
|
root.attemptInfoMessages = [];
|
|
root.lockoutAnnouncedThisAttempt = false;
|
|
root.resetAuthFlows();
|
|
if (!dankshellConfigWatcher.loaded && !nixosMarker.loaded && !userPamWatcher.loaded)
|
|
ensureUserPamConfig();
|
|
fprint.checkAvail();
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
Connections {
|
|
target: SettingsData
|
|
|
|
function onEnableFprintChanged(): void {
|
|
fprint.checkAvail();
|
|
}
|
|
|
|
function onLockFingerprintReadyChanged(): void {
|
|
fprint.checkAvail();
|
|
}
|
|
|
|
function onEnableU2fChanged(): void {
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
function onLockU2fReadyChanged(): void {
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
function onLockPamPathChanged(): void {
|
|
fprint.checkAvail();
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
function onLockPamInlineFprintChanged(): void {
|
|
fprint.checkAvail();
|
|
}
|
|
|
|
function onLockPamInlineU2fChanged(): void {
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
function onU2fModeChanged(): void {
|
|
if (root.lockSecured) {
|
|
u2f.abort();
|
|
u2fErrorRetry.running = false;
|
|
u2fPendingTimeout.running = false;
|
|
unlockRequestTimeout.running = false;
|
|
root.u2fPending = false;
|
|
root.u2fPendingMode = "";
|
|
root.u2fState = "";
|
|
u2f.checkAvail();
|
|
}
|
|
}
|
|
}
|
|
}
|