1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

feat(auth): expand shared PAM support for custom policies

- Add support for DMS and System managed factor policies
- Add validated dedicated security-key PAM / U2F Key sources
- Restore U2F OR/AND flows

Related: #2874
Port 1.5

(cherry picked from commit 48e36ae146)
This commit is contained in:
purian23
2026-07-16 09:24:12 -04:00
committed by dms-ci[bot]
parent b17ef80c91
commit c2a6a7aebd
10 changed files with 544 additions and 105 deletions
+18 -4
View File
@@ -91,21 +91,30 @@ var authListServicesCmd = &cobra.Command{
var authValidateCmd = &cobra.Command{
Use: "validate",
Short: "Validate a PAM service file for use as the DMS lock-screen password stack",
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password stack. Exits 1 when the file is not usable.",
Short: "Validate a PAM service file for use by the DMS lock screen",
Long: "Validate one PAM service (by --service NAME or --path /abs/file) for use as the DMS lock-screen password or dedicated U2F stack. Exits 1 when the file is not usable.",
Run: func(cmd *cobra.Command, args []string) {
path, _ := cmd.Flags().GetString("path")
service, _ := cmd.Flags().GetString("service")
purpose, _ := cmd.Flags().GetString("purpose")
asJSON, _ := cmd.Flags().GetBool("json")
if (path == "") == (service == "") {
log.Fatalf("Error: exactly one of --path or --service is required")
}
if purpose != "password" && purpose != "u2f" {
log.Fatalf("Error: --purpose must be password or u2f")
}
var result sharedpam.LockscreenPamValidation
switch {
case service != "":
result = sharedpam.ValidateLockscreenPamService(service)
if purpose == "u2f" {
result = sharedpam.ValidateLockscreenU2fPamService(service)
} else {
result = sharedpam.ValidateLockscreenPamService(service)
}
case !filepath.IsAbs(path):
result = sharedpam.LockscreenPamValidation{
Path: path,
@@ -114,7 +123,11 @@ var authValidateCmd = &cobra.Command{
Errors: []string{"--path must be an absolute file path"},
}
default:
result = sharedpam.ValidateLockscreenPamPath(path)
if purpose == "u2f" {
result = sharedpam.ValidateLockscreenU2fPamPath(path)
} else {
result = sharedpam.ValidateLockscreenPamPath(path)
}
}
if asJSON {
@@ -159,6 +172,7 @@ func init() {
authValidateCmd.Flags().String("path", "", "Absolute path to a PAM service file to validate")
authValidateCmd.Flags().String("service", "", "Name of a PAM service to resolve across the system PAM dirs")
authValidateCmd.Flags().String("purpose", "password", "Validation purpose: password or u2f")
authValidateCmd.Flags().Bool("json", false, "Output as JSON")
}
+71 -1
View File
@@ -646,6 +646,7 @@ type lockscreenPamAnalysis struct {
inlineFingerprint bool
inlineU2f bool
modules []string
authModules []string
unknownDirectives []string
err error
}
@@ -730,6 +731,9 @@ func (r lockscreenPamResolver) analyzeInto(path string, filterType string, stack
}
if !foundModule && strings.HasSuffix(field, ".so") {
acc.modules = append(acc.modules, field)
if lineType == "auth" {
acc.authModules = append(acc.authModules, field)
}
foundModule = true
}
}
@@ -774,6 +778,14 @@ func ValidateLockscreenPamPath(path string) LockscreenPamValidation {
return validateLockscreenPam("", path, defaultValidateDeps())
}
func ValidateLockscreenU2fPamService(name string) LockscreenPamValidation {
return validateLockscreenU2fPam(name, "", defaultValidateDeps())
}
func ValidateLockscreenU2fPamPath(path string) LockscreenPamValidation {
return validateLockscreenU2fPam("", path, defaultValidateDeps())
}
func validateLockscreenPam(serviceName string, path string, deps lockscreenPamValidateDeps) LockscreenPamValidation {
result := LockscreenPamValidation{
MissingModules: []string{},
@@ -838,6 +850,64 @@ func validateLockscreenPam(serviceName string, path string, deps lockscreenPamVa
return result
}
func validateLockscreenU2fPam(serviceName string, path string, deps lockscreenPamValidateDeps) LockscreenPamValidation {
result := validateLockscreenPam(serviceName, path, deps)
if result.Path == "" {
return result
}
resolver := lockscreenPamResolver{baseDirs: deps.baseDirs, readFile: deps.readFile}
analysis := resolver.analyzePath(result.Path)
if analysis.err != nil {
return result
}
filteredWarnings := result.Warnings[:0]
for _, warning := range result.Warnings {
if strings.Contains(warning, "pam_u2f is present") && strings.Contains(warning, "double-prompt") {
continue
}
filteredWarnings = append(filteredWarnings, warning)
}
result.Warnings = filteredWarnings
hasU2fAuth := false
unsafeModules := []string{}
unsafeSeen := map[string]bool{}
for _, ref := range analysis.authModules {
name := filepath.Base(ref)
if name == "pam_u2f.so" {
hasU2fAuth = true
continue
}
switch name {
case "pam_env.so", "pam_faildelay.so", "pam_nologin.so":
continue
default:
if !unsafeSeen[name] {
unsafeSeen[name] = true
unsafeModules = append(unsafeModules, name)
}
}
}
if !hasU2fAuth {
result.Errors = append(result.Errors, "no pam_u2f auth directive found; select a dedicated security-key PAM service")
}
for _, name := range unsafeModules {
result.Errors = append(result.Errors, fmt.Sprintf("additional auth module %s is not allowed in a dedicated security-key PAM service", name))
}
for _, name := range result.MissingModules {
if strings.Contains(name, "pam_u2f") {
result.Errors = append(result.Errors, fmt.Sprintf("%s is not installed or its configured path is unavailable", name))
break
}
}
result.Valid = len(result.Errors) == 0
return result
}
func moduleReferenceExists(ref string, deps lockscreenPamValidateDeps) bool {
if filepath.IsAbs(ref) {
_, err := deps.stat(ref)
@@ -895,7 +965,7 @@ func buildManagedLockscreenU2FPamContent() string {
func syncLockscreenPamConfigWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error {
if deps.isNixOS() {
logFunc(" NixOS detected. DMS continues to use /etc/pam.d/login for lock screen password auth on NixOS unless you declare security.pam.services.dankshell yourself. U2F and fingerprint are handled separately and should not be included in dankshell.")
logFunc(" NixOS detected. DMS does not write /etc/pam.d/dankshell; the lock screen uses a sanitized password-only service in the user state directory unless you select a custom PAM source.")
return nil
}
+80 -2
View File
@@ -611,8 +611,8 @@ func TestSyncLockscreenPamConfigWithDeps(t *testing.T) {
if err != nil {
t.Fatalf("syncLockscreenPamConfigWithDeps returned error on NixOS path: %v", err)
}
if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "/etc/pam.d/login") {
t.Fatalf("expected NixOS informational log mentioning /etc/pam.d/login, got %v", logs)
if len(logs) == 0 || !strings.Contains(logs[0], "NixOS detected") || !strings.Contains(logs[0], "sanitized password-only service") {
t.Fatalf("expected NixOS informational log describing the user-state fallback, got %v", logs)
}
if _, err := os.Stat(env.dankshellPath); !os.IsNotExist(err) {
t.Fatalf("expected no dankshell file to be written on NixOS path, stat err = %v", err)
@@ -994,6 +994,84 @@ func TestValidateLockscreenPam(t *testing.T) {
})
}
func TestValidateLockscreenU2fPam(t *testing.T) {
t.Parallel()
t.Run("accepts a dedicated U2F stack with custom options", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\nauth required pam_u2f.so cue authfile=/etc/u2f-mappings\naccount required pam_permit.so\n")
result := validateLockscreenU2fPam("dankshell-u2f", "", env.validateDeps())
if !result.Valid {
t.Fatalf("expected valid dedicated U2F stack, got %+v", result)
}
if !result.InlineU2f {
t.Fatalf("expected inline U2F detection, got %+v", result)
}
if containsSubstr(result.Warnings, "double-prompt") {
t.Fatalf("dedicated U2F validation should not warn about its expected U2F module: %v", result.Warnings)
}
})
t.Run("rejects a primary login stack that also prompts for a password", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.availableModules["pam_u2f.so"] = true
env.writePamFile(t, "login", "#%PAM-1.0\nauth required pam_unix.so\nauth required pam_u2f.so cue\naccount required pam_unix.so\n")
result := validateLockscreenU2fPam("login", "", env.validateDeps())
if result.Valid {
t.Fatalf("expected mixed password/U2F stack to be rejected, got %+v", result)
}
if !containsSubstr(result.Errors, "pam_unix.so") || !containsSubstr(result.Errors, "dedicated security-key") {
t.Fatalf("expected actionable mixed-stack error, got %v", result.Errors)
}
})
t.Run("rejects a stack without pam_u2f", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_unix.so"] = true
env.writePamFile(t, "password-only", "#%PAM-1.0\nauth required pam_unix.so\n")
result := validateLockscreenU2fPam("password-only", "", env.validateDeps())
if result.Valid || !containsSubstr(result.Errors, "pam_u2f") {
t.Fatalf("expected missing-U2F error, got %+v", result)
}
})
t.Run("does not accept a similarly named module as pam_u2f", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.availableModules["pam_u2f_helper.so"] = true
env.writePamFile(t, "not-u2f", "#%PAM-1.0\nauth required pam_u2f_helper.so\n")
result := validateLockscreenU2fPam("not-u2f", "", env.validateDeps())
if result.Valid || !containsSubstr(result.Errors, "no pam_u2f auth directive") {
t.Fatalf("expected exact pam_u2f module validation, got %+v", result)
}
})
t.Run("rejects a missing pam_u2f module", func(t *testing.T) {
t.Parallel()
env := newPamTestEnv(t)
env.writePamFile(t, "dankshell-u2f", "#%PAM-1.0\nauth required pam_u2f.so cue\n")
result := validateLockscreenU2fPam("dankshell-u2f", "", env.validateDeps())
if result.Valid || !containsSubstr(result.Errors, "pam_u2f.so is not installed") {
t.Fatalf("expected missing-module error, got %+v", result)
}
})
}
func containsSubstr(items []string, substr string) bool {
for _, item := range items {
if strings.Contains(item, substr) {
@@ -23,3 +23,51 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
t.Fatalf("passwordField should handle physical key text manually instead of relying on a text input control")
}
}
func TestLockScreenAuthenticationCardOwnsFactorControls(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Settings/LockScreenTab.qml")
if err != nil {
t.Fatalf("read lock screen settings QML: %v", err)
}
content := string(data)
authCard := strings.Index(content, `title: I18n.tr("Lock Screen Authentication")`)
behaviorCard := strings.Index(content, `title: I18n.tr("Lock Screen behaviour")`)
fingerprintToggle := strings.Index(content, `settingKey: "enableFprint"`)
u2fToggle := strings.Index(content, `settingKey: "enableU2f"`)
u2fSource := strings.Index(content, `settingKey: "lockU2fPamPath"`)
if authCard < 0 || behaviorCard < 0 || fingerprintToggle < 0 || u2fToggle < 0 || u2fSource < 0 {
t.Fatalf("expected authentication card, factor toggles, and U2F source setting")
}
for name, position := range map[string]int{
"fingerprint toggle": fingerprintToggle,
"U2F toggle": u2fToggle,
"U2F source": u2fSource,
} {
if position < authCard || position > behaviorCard {
t.Fatalf("%s must remain in the authentication card", name)
}
}
}
func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
data, err := os.ReadFile("../../../quickshell/Modules/Lock/Pam.qml")
if err != nil {
t.Fatalf("read lock screen PAM QML: %v", err)
}
content := string(data)
for _, required := range []string{
"SettingsData.lockPamExternallyManaged",
"SettingsData.lockU2fPamPath",
"customU2fPamActive",
"u2fSuppressedByPrimaryPam",
} {
if !strings.Contains(content, required) {
t.Fatalf("lock screen PAM must contain %q", required)
}
}
if strings.Contains(content, "runningFromNixStore || resolveUserPam.running") {
t.Fatalf("DMS-managed policy must generate the sanitized user PAM stack on Nix-store installs")
}
}
+2
View File
@@ -898,6 +898,8 @@ Singleton {
property string lockPamPath: ""
property bool lockPamInlineFprint: false
property bool lockPamInlineU2f: false
property bool lockPamExternallyManaged: false
property string lockU2fPamPath: ""
property bool greeterPamExternallyManaged: false
property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0
+8 -1
View File
@@ -63,6 +63,7 @@ Singleton {
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 lockU2fCustomSourceDetected: (settingsRoot?.lockU2fPamPath || "") !== "" && customU2fPamWatcher.loaded
readonly property bool greeterPamHasFprint: greeterPamStackHasModule("pam_fprintd")
readonly property bool greeterPamHasU2f: greeterPamStackHasModule("pam_u2f")
@@ -176,7 +177,7 @@ Singleton {
readonly property bool lockU2fReady: {
if (forcedU2fAvailable !== null)
return forcedU2fAvailable;
return lockU2fCustomConfigDetected || homeU2fKeysDetected;
return lockU2fCustomSourceDetected || lockU2fCustomConfigDetected || homeU2fKeysDetected;
}
readonly property bool lockU2fCanEnable: {
@@ -727,6 +728,12 @@ Singleton {
onLoadFailed: root.dankshellU2fPamText = ""
}
FileView {
id: customU2fPamWatcher
path: root.settingsRoot?.lockU2fPamPath || ""
printErrors: false
}
FileView {
id: u2fKeysWatcher
path: root.u2fKeysPath
@@ -459,6 +459,8 @@ var SPEC = {
lockPamPath: { def: "" },
lockPamInlineFprint: { def: false },
lockPamInlineU2f: { def: false },
lockPamExternallyManaged: { def: false },
lockU2fPamPath: { def: "" },
lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 },
lockScreenVideoEnabled: { def: false },
+43 -22
View File
@@ -71,7 +71,7 @@ Scope {
}
function proceedAfterPrimaryAuth(): void {
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
if (!root.u2fSuppressedByPrimaryPam && SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
u2f.startForSecondFactor();
} else {
completeUnlock();
@@ -91,8 +91,9 @@ Scope {
}
readonly property bool customPamActive: SettingsData.lockPamPath !== "" && customPamWatcher.loaded
readonly property bool fprintSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineFprint
readonly property bool u2fSuppressedByCustomPam: customPamActive && SettingsData.lockPamInlineU2f
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
@@ -109,16 +110,16 @@ Scope {
}
FileView {
id: nixosMarker
id: u2fConfigWatcher
path: "/etc/NIXOS"
path: "/etc/pam.d/dankshell-u2f"
printErrors: false
}
FileView {
id: u2fConfigWatcher
id: customU2fPamWatcher
path: "/etc/pam.d/dankshell-u2f"
path: SettingsData.lockU2fPamPath !== "" ? SettingsData.lockU2fPamPath : ""
printErrors: false
}
@@ -145,26 +146,23 @@ Scope {
}
function ensureUserPamConfig(): void {
if (root.runningFromNixStore || resolveUserPam.running)
if (SettingsData.lockPamExternallyManaged || 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 (SettingsData.lockPamExternallyManaged)
return "login";
if (dankshellConfigWatcher.loaded)
return "dankshell";
if (nixosMarker.loaded || root.runningFromNixStore)
return "login";
if (userPamWatcher.loaded)
return "dankshell";
return "login";
@@ -174,9 +172,9 @@ Scope {
const idx = SettingsData.lockPamPath.lastIndexOf("/");
return idx > 0 ? SettingsData.lockPamPath.slice(0, idx) : "/";
}
if (dankshellConfigWatcher.loaded)
if (SettingsData.lockPamExternallyManaged)
return "/etc/pam.d";
if (nixosMarker.loaded || root.runningFromNixStore)
if (dankshellConfigWatcher.loaded)
return "/etc/pam.d";
if (userPamWatcher.loaded)
return root.userPamDir;
@@ -265,7 +263,7 @@ Scope {
property int errorTries
function checkAvail(): void {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByCustomPam) {
if (!available || !SettingsData.enableFprint || !root.lockSecured || root.fprintSuppressedByPrimaryPam) {
abort();
return;
}
@@ -325,7 +323,7 @@ Scope {
property bool available: SettingsData.lockU2fReady
function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByCustomPam) {
if (!available || !SettingsData.enableU2f || !root.lockSecured || root.u2fSuppressedByPrimaryPam) {
abort();
return;
}
@@ -335,7 +333,7 @@ Scope {
}
function startForSecondFactor(): void {
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam) {
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByPrimaryPam) {
root.completeUnlock();
return;
}
@@ -348,7 +346,7 @@ Scope {
}
function startForAlternativeAuth(): void {
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByCustomPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
if (!available || !SettingsData.enableU2f || root.u2fSuppressedByPrimaryPam || SettingsData.u2fMode !== "or" || root.unlockInProgress || passwd.active || active)
return;
abort();
root.u2fPending = true;
@@ -358,8 +356,18 @@ Scope {
start();
}
config: u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
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"))
@@ -478,7 +486,7 @@ Scope {
root.attemptInfoMessages = [];
root.lockoutAnnouncedThisAttempt = false;
root.resetAuthFlows();
if (!dankshellConfigWatcher.loaded && !nixosMarker.loaded && !userPamWatcher.loaded)
if (!SettingsData.lockPamExternallyManaged && !dankshellConfigWatcher.loaded && !userPamWatcher.loaded)
ensureUserPamConfig();
fprint.checkAvail();
u2f.checkAvail();
@@ -516,6 +524,19 @@ Scope {
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();
+190 -53
View File
@@ -11,6 +11,10 @@ Item {
readonly property bool lockFprintToggleAvailable: SettingsData.lockFingerprintCanEnable || SettingsData.enableFprint
readonly property bool lockU2fToggleAvailable: SettingsData.lockU2fCanEnable || SettingsData.enableU2f
readonly property bool primaryPamHasFprint: SettingsData.lockPamPath !== "" && SettingsData.lockPamInlineFprint
readonly property bool primaryPamHasU2f: SettingsData.lockPamPath !== "" && SettingsData.lockPamInlineU2f
readonly property bool lockFprintControlledByPrimary: SettingsData.lockPamExternallyManaged || primaryPamHasFprint
readonly property bool lockU2fControlledByPrimary: SettingsData.lockPamExternallyManaged || primaryPamHasU2f
property var authServices: []
property bool authValidateRunning: false
@@ -19,6 +23,12 @@ Item {
property string authValidateMessage: ""
property string authPendingApplyPath: ""
property bool authShowCustom: false
property bool u2fValidateRunning: false
property bool u2fValidateOk: false
property bool u2fValidateWarn: false
property string u2fValidateMessage: ""
property string u2fPendingApplyPath: ""
property bool u2fShowCustom: false
readonly property string authAutoLabel: I18n.tr("Auto", "automatic PAM authentication source option")
readonly property string authCustomLabel: I18n.tr("Custom...", "custom PAM authentication source option")
@@ -37,6 +47,8 @@ Item {
return svc ? authServiceLabel(svc) : authCustomLabel;
}
readonly property string u2fAuthCurrentValue: SettingsData.lockU2fPamPath === "" ? authAutoLabel : authCustomLabel
function refreshAuthServices() {
authListServicesProcess.running = true;
}
@@ -62,6 +74,25 @@ Item {
authValidateProcess.running = true;
}
function applyAutoU2fSource() {
SettingsData.set("lockU2fPamPath", "");
root.u2fValidateOk = false;
root.u2fValidateWarn = false;
root.u2fValidateMessage = "";
}
function validateAndApplyU2fSource(path) {
if (!path)
return;
root.u2fPendingApplyPath = path;
root.u2fValidateMessage = "";
root.u2fValidateOk = false;
root.u2fValidateWarn = false;
root.u2fValidateRunning = true;
u2fValidateProcess.command = ["dms", "auth", "validate", "--purpose", "u2f", "--path", path, "--json"];
u2fValidateProcess.running = true;
}
function lockFingerprintDescription() {
switch (SettingsData.lockFingerprintReason) {
case "ready":
@@ -161,12 +192,12 @@ Item {
} catch (e) {}
if (!data) {
root.authValidateMessage = "validation failed — is dms in PATH?";
root.authValidateMessage = "Validation failed — is DMS in PATH?";
return;
}
if (!data.valid) {
const errs = Array.isArray(data.errors) ? data.errors : [];
root.authValidateMessage = ["not applied:", ...errs].join("\n");
root.authValidateMessage = ["Not applied.", ...errs].join("\n");
return;
}
@@ -176,7 +207,46 @@ Item {
const warns = Array.isArray(data.warnings) ? data.warnings : [];
root.authValidateOk = true;
root.authValidateWarn = warns.length > 0;
root.authValidateMessage = warns.length > 0 ? ["applied with warnings:", ...warns].join("\n") : "applied";
root.authValidateMessage = warns.length > 0 ? ["Applied with warnings.", ...warns].join("\n") : "Applied.";
}
}
Process {
id: u2fValidateProcess
running: false
property string collected: ""
stdout: StdioCollector {
onStreamFinished: u2fValidateProcess.collected = text || ""
}
onExited: exitCode => {
root.u2fValidateRunning = false;
root.u2fValidateOk = false;
root.u2fValidateWarn = false;
let data = null;
try {
data = JSON.parse(u2fValidateProcess.collected);
} catch (e) {}
if (!data) {
root.u2fValidateMessage = "Validation failed — is DMS in PATH?";
return;
}
if (!data.valid) {
const errs = Array.isArray(data.errors) ? data.errors : [];
root.u2fValidateMessage = ["Not applied.", ...errs].join("\n");
return;
}
SettingsData.set("lockU2fPamPath", root.u2fPendingApplyPath);
const warns = Array.isArray(data.warnings) ? data.warnings : [];
root.u2fValidateOk = true;
root.u2fValidateWarn = warns.length > 0;
root.u2fValidateMessage = warns.length > 0 ? ["Applied with warnings.", ...warns].join("\n") : "Applied.";
root.refreshAuthDetection();
}
}
@@ -326,9 +396,17 @@ Item {
SettingsCard {
width: parent.width
iconName: "key"
title: "Authentication Source"
title: I18n.tr("Lock Screen Authentication")
settingKey: "lockAuthSource"
StyledText {
text: I18n.tr("Changes apply automatically")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
}
SettingsDropdownRow {
settingKey: "lockPamPath"
tags: ["lock", "screen", "pam", "authentication", "source", "service"]
@@ -395,14 +473,120 @@ Item {
}
StyledText {
visible: (SettingsData.lockPamInlineFprint && SettingsData.enableFprint) || (SettingsData.lockPamInlineU2f && SettingsData.enableU2f)
text: "The pinned PAM stack already prompts for fingerprint or security key, so DMS skips its own prompts"
visible: !SettingsData.lockPamExternallyManaged && (root.primaryPamHasFprint || root.primaryPamHasU2f)
text: I18n.tr("Selected PAM source already manages the detected factors.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
SettingsToggleRow {
settingKey: "lockPamExternallyManaged"
tags: ["lock", "screen", "pam", "managed", "external", "authentication", "policy"]
text: I18n.tr("Use system PAM authentication")
description: SettingsData.lockPamExternallyManaged ? I18n.tr("System PAM sets the authentication policy.") : I18n.tr("DMS manages the factors below.")
checked: SettingsData.lockPamExternallyManaged
onToggled: checked => SettingsData.set("lockPamExternallyManaged", checked)
}
SettingsToggleRow {
settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
text: I18n.tr("Enable fingerprint authentication")
description: root.lockFprintControlledByPrimary ? I18n.tr("Managed by the primary PAM source.") : root.lockFingerprintDescription()
descriptionColor: root.lockFprintControlledByPrimary || SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableFprint || root.primaryPamHasFprint
enabled: root.lockFprintToggleAvailable && !root.lockFprintControlledByPrimary
onToggled: checked => SettingsData.set("enableFprint", checked)
}
SettingsToggleRow {
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: root.lockU2fControlledByPrimary ? I18n.tr("Managed by the primary PAM source.") : root.lockU2fDescription()
descriptionColor: root.lockU2fControlledByPrimary || SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableU2f || root.primaryPamHasU2f
enabled: root.lockU2fToggleAvailable && !root.lockU2fControlledByPrimary
onToggled: checked => SettingsData.set("enableU2f", checked)
}
SettingsDropdownRow {
settingKey: "u2fMode"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
description: I18n.tr("Alternative uses the passkey button. Second factor follows password or fingerprint.", "lock screen U2F security key mode setting")
visible: SettingsData.enableU2f && !root.lockU2fControlledByPrimary
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
onValueChanged: value => {
if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint"))
SettingsData.set("u2fMode", "and");
else
SettingsData.set("u2fMode", "or");
}
}
SettingsDropdownRow {
settingKey: "lockU2fPamPath"
tags: ["lock", "screen", "pam", "u2f", "security", "key", "source", "service"]
text: I18n.tr("Security Key PAM Source")
description: SettingsData.lockU2fPamPath !== "" ? SettingsData.lockU2fPamPath : I18n.tr("Auto uses an installed or bundled key-only service.")
visible: !root.lockU2fControlledByPrimary
options: [root.authAutoLabel, root.authCustomLabel]
currentValue: root.u2fAuthCurrentValue
onValueChanged: value => {
if (value === root.authAutoLabel) {
root.u2fShowCustom = false;
root.applyAutoU2fSource();
return;
}
root.u2fShowCustom = true;
}
}
Row {
width: parent.width
spacing: Theme.spacingS
visible: !root.lockU2fControlledByPrimary && (root.u2fShowCustom || root.u2fAuthCurrentValue === root.authCustomLabel)
DankTextField {
id: customU2fPamField
width: parent.width - validateU2fPamButton.width - Theme.spacingS
placeholderText: "/etc/pam.d/dankshell-u2f"
text: SettingsData.lockU2fPamPath
backgroundColor: Theme.surfaceContainerHighest
}
DankButton {
id: validateU2fPamButton
text: I18n.tr("Apply Changes", "validate and apply custom U2F PAM authentication source")
enabled: !root.u2fValidateRunning && customU2fPamField.text.trim() !== ""
onClicked: root.validateAndApplyU2fSource(customU2fPamField.text.trim())
}
}
Rectangle {
width: parent.width
height: Math.min(160, u2fStatusText.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.surfaceContainerHighest
visible: !root.lockU2fControlledByPrimary && root.u2fValidateMessage !== ""
StyledText {
id: u2fStatusText
anchors.fill: parent
anchors.margins: Theme.spacingM
text: root.u2fValidateMessage
font.pixelSize: Theme.fontSizeSmall
font.family: "monospace"
color: !root.u2fValidateOk ? Theme.error : (root.u2fValidateWarn ? Theme.warning : Theme.surfaceVariantText)
wrapMode: Text.Wrap
verticalAlignment: Text.AlignTop
}
}
}
SettingsCard {
@@ -461,53 +645,6 @@ Item {
checked: SettingsData.lockAtStartup
onToggled: checked => SettingsData.set("lockAtStartup", checked)
}
StyledText {
text: I18n.tr("Lock screen authentication changes apply automatically and may open a terminal when sudo authentication is required.")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
wrapMode: Text.Wrap
topPadding: Theme.spacingS
}
SettingsToggleRow {
settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
text: I18n.tr("Enable fingerprint authentication")
description: root.lockFingerprintDescription()
descriptionColor: SettingsData.lockFingerprintReason === "ready" ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableFprint
enabled: root.lockFprintToggleAvailable
onToggled: checked => SettingsData.set("enableFprint", checked)
}
SettingsToggleRow {
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: root.lockU2fDescription()
descriptionColor: SettingsData.lockU2fReason === "ready" ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableU2f
enabled: root.lockU2fToggleAvailable
onToggled: checked => SettingsData.set("enableU2f", checked)
}
SettingsDropdownRow {
settingKey: "u2fMode"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
visible: SettingsData.enableU2f
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
onValueChanged: value => {
if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint"))
SettingsData.set("u2fMode", "and");
else
SettingsData.set("u2fMode", "or");
}
}
}
SettingsCard {
@@ -4634,24 +4634,6 @@
"zenbrowser"
]
},
{
"section": "lockAuthSource",
"label": "Authentication Source",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"lock",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source"
],
"icon": "key"
},
{
"section": "lockPamPath",
"label": "Authentication Source",
@@ -4728,10 +4710,14 @@
"fprint",
"lock",
"login",
"managed",
"password",
"primary",
"screen",
"security"
]
"security",
"source"
],
"description": "Managed by the primary PAM source."
},
{
"section": "loginctlLockIntegration",
@@ -4769,12 +4755,16 @@
"key",
"lock",
"login",
"managed",
"password",
"primary",
"screen",
"security",
"source",
"u2f",
"yubikey"
]
],
"description": "Managed by the primary PAM source."
},
{
"section": "_tab_11",
@@ -4814,6 +4804,25 @@
"icon": "palette",
"description": "Font used for the clock and date on the lock screen"
},
{
"section": "lockAuthSource",
"label": "Lock Screen Authentication",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"lock",
"lockscreen",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source"
],
"icon": "key"
},
{
"section": "lockDisplay",
"label": "Lock Screen Display",
@@ -5005,24 +5014,54 @@
],
"description": "Turn off all displays immediately when the lock screen activates"
},
{
"section": "lockU2fPamPath",
"label": "Security Key PAM Source",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"auto",
"bundled",
"installed",
"key",
"lock",
"login",
"pam",
"password",
"screen",
"security",
"service",
"source",
"u2f",
"uses"
],
"description": "Auto uses an installed or bundled key-only service."
},
{
"section": "u2fMode",
"label": "Security key mode",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"alternative",
"button",
"factor",
"fingerprint",
"follows",
"key",
"lock",
"login",
"mode",
"passkey",
"password",
"screen",
"second",
"security",
"u2f",
"uses",
"yubikey"
]
],
"description": "Alternative uses the passkey button. Second factor follows password or fingerprint."
},
{
"section": "lockScreenShowMediaPlayer",
@@ -5159,6 +5198,27 @@
"time"
]
},
{
"section": "lockPamExternallyManaged",
"label": "Use system PAM authentication",
"tabIndex": 11,
"category": "Lock Screen",
"keywords": [
"authentication",
"external",
"lock",
"login",
"managed",
"pam",
"password",
"policy",
"screen",
"security",
"sets",
"system"
],
"description": "System PAM sets the authentication policy."
},
{
"section": "videoScreensaver",
"label": "Video Screensaver",