mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-01 19:18:28 -04:00
3938e60ce4
- Gate greeter external-auth status fprint availability only on confirmed setups - Reload dankshell-U2F/U2F-Key watchers live Related: #2874 Port 1.5
558 lines
16 KiB
QML
558 lines
16 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 (!root.u2fSuppressedByPrimaryPam && 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 fprintSuppressedByPrimaryPam: SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineFprint)
|
|
readonly property bool u2fSuppressedByPrimaryPam: SettingsData.lockPamExternallyManaged || (customPamActive && SettingsData.lockPamInlineU2f)
|
|
readonly property bool customU2fPamActive: SettingsData.lockU2fPamPath !== "" && customU2fPamWatcher.loaded
|
|
|
|
FileView {
|
|
id: customPamWatcher
|
|
|
|
path: SettingsData.lockPamPath !== "" ? SettingsData.lockPamPath : ""
|
|
printErrors: false
|
|
}
|
|
|
|
FileView {
|
|
id: dankshellConfigWatcher
|
|
|
|
path: "/etc/pam.d/dankshell"
|
|
printErrors: false
|
|
}
|
|
|
|
FileView {
|
|
id: u2fConfigWatcher
|
|
|
|
path: "/etc/pam.d/dankshell-u2f"
|
|
watchChanges: true
|
|
printErrors: false
|
|
}
|
|
|
|
FileView {
|
|
id: customU2fPamWatcher
|
|
|
|
path: SettingsData.lockU2fPamPath !== "" ? SettingsData.lockU2fPamPath : ""
|
|
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 (SettingsData.lockPamExternallyManaged || resolveUserPam.running)
|
|
return;
|
|
resolveUserPam.running = true;
|
|
}
|
|
|
|
Component.onCompleted: ensureUserPamConfig()
|
|
|
|
PamContext {
|
|
id: passwd
|
|
|
|
config: {
|
|
if (root.customPamActive)
|
|
return SettingsData.lockPamPath.slice(SettingsData.lockPamPath.lastIndexOf("/") + 1);
|
|
if (SettingsData.lockPamExternallyManaged)
|
|
return "login";
|
|
if (dankshellConfigWatcher.loaded)
|
|
return "dankshell";
|
|
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 (SettingsData.lockPamExternallyManaged)
|
|
return "/etc/pam.d";
|
|
if (dankshellConfigWatcher.loaded)
|
|
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.fprintSuppressedByPrimaryPam) {
|
|
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.u2fSuppressedByPrimaryPam) {
|
|
abort();
|
|
return;
|
|
}
|
|
|
|
if (SettingsData.u2fMode === "or")
|
|
abort();
|
|
}
|
|
|
|
function startForSecondFactor(): void {
|
|
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByPrimaryPam) {
|
|
root.completeUnlock();
|
|
return;
|
|
}
|
|
abort();
|
|
root.u2fPending = true;
|
|
root.u2fPendingMode = "and";
|
|
root.u2fState = "";
|
|
u2fPendingTimeout.restart();
|
|
start();
|
|
}
|
|
|
|
function startForAlternativeAuth(): void {
|
|
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByPrimaryPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
|
|
return;
|
|
abort();
|
|
root.u2fPending = true;
|
|
root.u2fPendingMode = "or";
|
|
root.u2fState = "";
|
|
u2fPendingTimeout.restart();
|
|
start();
|
|
}
|
|
|
|
config: {
|
|
if (root.customU2fPamActive)
|
|
return SettingsData.lockU2fPamPath.slice(SettingsData.lockU2fPamPath.lastIndexOf("/") + 1);
|
|
return u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f";
|
|
}
|
|
configDirectory: {
|
|
if (root.customU2fPamActive) {
|
|
const idx = SettingsData.lockU2fPamPath.lastIndexOf("/");
|
|
return idx > 0 ? SettingsData.lockU2fPamPath.slice(0, idx) : "/";
|
|
}
|
|
return 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 (!SettingsData.lockPamExternallyManaged && !dankshellConfigWatcher.loaded && !userPamWatcher.loaded)
|
|
ensureUserPamConfig();
|
|
// FileView cannot watch a path that does not exist yet; re-read so a
|
|
// dedicated service created after startup is used on the next lock.
|
|
u2fConfigWatcher.reload();
|
|
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 onLockPamExternallyManagedChanged(): void {
|
|
root.resetAuthFlows();
|
|
if (!SettingsData.lockPamExternallyManaged)
|
|
root.ensureUserPamConfig();
|
|
fprint.checkAvail();
|
|
u2f.checkAvail();
|
|
}
|
|
|
|
function onLockU2fPamPathChanged(): void {
|
|
u2f.abort();
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
}
|