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
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user