diff options
Diffstat (limited to 'raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go')
| -rw-r--r-- | raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go | 892 |
1 files changed, 0 insertions, 892 deletions
diff --git a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go b/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go deleted file mode 100644 index 16ca3f9..0000000 --- a/raveos-hyprland-theme/theme-data/DankMaterialShell/core/internal/pam/pam.go +++ /dev/null @@ -1,892 +0,0 @@ -package pam - -import ( - "context" - "encoding/json" - "fmt" - "os" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/AvengeMedia/DankMaterialShell/core/internal/distros" -) - -const ( - GreeterPamManagedBlockStart = "# BEGIN DMS GREETER AUTH (managed by dms greeter sync)" - GreeterPamManagedBlockEnd = "# END DMS GREETER AUTH" - - LockscreenPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN AUTH (managed by dms greeter sync)" - LockscreenPamManagedBlockEnd = "# END DMS LOCKSCREEN AUTH" - - LockscreenU2FPamManagedBlockStart = "# BEGIN DMS LOCKSCREEN U2F AUTH (managed by dms auth sync)" - LockscreenU2FPamManagedBlockEnd = "# END DMS LOCKSCREEN U2F AUTH" - - legacyGreeterPamFprintComment = "# DMS greeter fingerprint" - legacyGreeterPamU2FComment = "# DMS greeter U2F" - - GreetdPamPath = "/etc/pam.d/greetd" - DankshellPamPath = "/etc/pam.d/dankshell" - DankshellU2FPamPath = "/etc/pam.d/dankshell-u2f" -) - -var includedPamAuthFiles = []string{ - "system-auth", - "common-auth", - "password-auth", - "system-login", - "system-local-login", - "common-auth-pc", - "login", -} - -type AuthSettings struct { - EnableFprint bool `json:"enableFprint"` - EnableU2f bool `json:"enableU2f"` - GreeterEnableFprint bool `json:"greeterEnableFprint"` - GreeterEnableU2f bool `json:"greeterEnableU2f"` -} - -type SyncAuthOptions struct { - HomeDir string - ForceGreeterAuth bool -} - -type syncDeps struct { - pamDir string - greetdPath string - dankshellPath string - dankshellU2fPath string - isNixOS func() bool - readFile func(string) ([]byte, error) - stat func(string) (os.FileInfo, error) - createTemp func(string, string) (*os.File, error) - removeFile func(string) error - runSudoCmd func(string, string, ...string) error - pamModuleExists func(string) bool - fingerprintAvailableForCurrentUser func() bool -} - -type lockscreenPamIncludeDirective struct { - target string - filterType string -} - -type lockscreenPamResolver struct { - pamDir string - readFile func(string) ([]byte, error) -} - -func defaultSyncDeps() syncDeps { - return syncDeps{ - pamDir: "/etc/pam.d", - greetdPath: GreetdPamPath, - dankshellPath: DankshellPamPath, - dankshellU2fPath: DankshellU2FPamPath, - isNixOS: IsNixOS, - readFile: os.ReadFile, - stat: os.Stat, - createTemp: os.CreateTemp, - removeFile: os.Remove, - runSudoCmd: runSudoCmd, - pamModuleExists: pamModuleExists, - fingerprintAvailableForCurrentUser: FingerprintAuthAvailableForCurrentUser, - } -} - -func IsNixOS() bool { - _, err := os.Stat("/etc/NIXOS") - return err == nil -} - -func ReadAuthSettings(homeDir string) (AuthSettings, error) { - settingsPath := filepath.Join(homeDir, ".config", "DankMaterialShell", "settings.json") - data, err := os.ReadFile(settingsPath) - if err != nil { - if os.IsNotExist(err) { - return AuthSettings{}, nil - } - return AuthSettings{}, fmt.Errorf("failed to read settings at %s: %w", settingsPath, err) - } - if strings.TrimSpace(string(data)) == "" { - return AuthSettings{}, nil - } - - var settings AuthSettings - if err := json.Unmarshal(data, &settings); err != nil { - return AuthSettings{}, fmt.Errorf("failed to parse settings at %s: %w", settingsPath, err) - } - return settings, nil -} - -func ReadGreeterAuthToggles(homeDir string) (enableFprint bool, enableU2f bool, err error) { - settings, err := ReadAuthSettings(homeDir) - if err != nil { - return false, false, err - } - return settings.GreeterEnableFprint, settings.GreeterEnableU2f, nil -} - -func SyncAuthConfig(logFunc func(string), sudoPassword string, options SyncAuthOptions) error { - return syncAuthConfigWithDeps(logFunc, sudoPassword, options, defaultSyncDeps()) -} - -func RemoveManagedGreeterPamBlock(logFunc func(string), sudoPassword string) error { - return removeManagedGreeterPamBlockWithDeps(logFunc, sudoPassword, defaultSyncDeps()) -} - -func syncAuthConfigWithDeps(logFunc func(string), sudoPassword string, options SyncAuthOptions, deps syncDeps) error { - homeDir := strings.TrimSpace(options.HomeDir) - if homeDir == "" { - var err error - homeDir, err = os.UserHomeDir() - if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) - } - } - - settings, err := ReadAuthSettings(homeDir) - if err != nil { - return err - } - - if err := syncLockscreenPamConfigWithDeps(logFunc, sudoPassword, deps); err != nil { - return err - } - if err := syncLockscreenU2FPamConfigWithDeps(logFunc, sudoPassword, settings.EnableU2f, deps); err != nil { - return err - } - - if _, err := deps.stat(deps.greetdPath); err != nil { - if os.IsNotExist(err) { - logFunc("ℹ /etc/pam.d/greetd not found. Skipping greeter PAM sync.") - return nil - } - return fmt.Errorf("failed to inspect %s: %w", deps.greetdPath, err) - } - - if err := syncGreeterPamConfigWithDeps(logFunc, sudoPassword, settings, options.ForceGreeterAuth, deps); err != nil { - return err - } - - return nil -} - -func removeManagedGreeterPamBlockWithDeps(logFunc func(string), sudoPassword string, deps syncDeps) error { - if deps.isNixOS() { - return nil - } - - data, err := deps.readFile(deps.greetdPath) - if err != nil { - if os.IsNotExist(err) { - return nil - } - return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err) - } - - originalContent := string(data) - stripped, removed := stripManagedGreeterPamBlock(originalContent) - strippedAgain, removedLegacy := stripLegacyGreeterPamLines(stripped) - if !removed && !removedLegacy { - return nil - } - - if err := writeManagedPamFile(strippedAgain, deps.greetdPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to write %s: %w", deps.greetdPath, err) - } - - logFunc("✓ Removed DMS managed PAM block from " + deps.greetdPath) - return nil -} - -func ParseManagedGreeterPamAuth(pamText string) (managed bool, fingerprint bool, u2f bool, legacy bool) { - if pamText == "" { - return false, false, false, false - } - - lines := strings.Split(pamText, "\n") - inManaged := false - for _, line := range lines { - trimmed := strings.TrimSpace(line) - switch trimmed { - case GreeterPamManagedBlockStart: - managed = true - inManaged = true - continue - case GreeterPamManagedBlockEnd: - inManaged = false - continue - } - - if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) { - legacy = true - } - if !inManaged { - continue - } - if strings.Contains(trimmed, "pam_fprintd") { - fingerprint = true - } - if strings.Contains(trimmed, "pam_u2f") { - u2f = true - } - } - - return managed, fingerprint, u2f, legacy -} - -func StripManagedGreeterPamContent(pamText string) (string, bool) { - stripped, removed := stripManagedGreeterPamBlock(pamText) - stripped, removedLegacy := stripLegacyGreeterPamLines(stripped) - return stripped, removed || removedLegacy -} - -func PamTextIncludesFile(pamText, filename string) bool { - lines := strings.Split(pamText, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" || strings.HasPrefix(trimmed, "#") { - continue - } - if strings.Contains(trimmed, filename) && - (strings.Contains(trimmed, "include") || strings.Contains(trimmed, "substack") || strings.HasPrefix(trimmed, "@include")) { - return true - } - } - return false -} - -func PamFileHasModule(pamFilePath, module string) bool { - data, err := os.ReadFile(pamFilePath) - if err != nil { - return false - } - return pamContentHasModule(string(data), module) -} - -func DetectIncludedPamModule(pamText, module string) string { - return detectIncludedPamModule(pamText, module, defaultSyncDeps()) -} - -func detectIncludedPamModule(pamText, module string, deps syncDeps) string { - for _, includedFile := range includedPamAuthFiles { - if !PamTextIncludesFile(pamText, includedFile) { - continue - } - path := filepath.Join(deps.pamDir, includedFile) - data, err := deps.readFile(path) - if err != nil { - continue - } - if pamContentHasModule(string(data), module) { - return includedFile - } - } - return "" -} - -func pamContentHasModule(content, module string) bool { - lines := strings.Split(content, "\n") - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == "" || strings.HasPrefix(trimmed, "#") { - continue - } - if strings.Contains(trimmed, module) { - return true - } - } - return false -} - -func hasManagedLockscreenPamFile(content string) bool { - return strings.Contains(content, LockscreenPamManagedBlockStart) && - strings.Contains(content, LockscreenPamManagedBlockEnd) -} - -func hasManagedLockscreenU2FPamFile(content string) bool { - return strings.Contains(content, LockscreenU2FPamManagedBlockStart) && - strings.Contains(content, LockscreenU2FPamManagedBlockEnd) -} - -func pamDirectiveType(line string) string { - fields := strings.Fields(line) - if len(fields) == 0 { - return "" - } - - directiveType := strings.TrimPrefix(fields[0], "-") - switch directiveType { - case "auth", "account", "password", "session": - return directiveType - default: - return "" - } -} - -func isExcludedLockscreenPamLine(line string) bool { - for _, field := range strings.Fields(line) { - if strings.HasPrefix(field, "#") { - break - } - if strings.Contains(field, "pam_u2f") || strings.Contains(field, "pam_fprintd") { - return true - } - } - return false -} - -func parseLockscreenPamIncludeDirective(trimmed string, inheritedFilter string) (lockscreenPamIncludeDirective, bool) { - fields := strings.Fields(trimmed) - if len(fields) >= 2 && fields[0] == "@include" { - return lockscreenPamIncludeDirective{ - target: fields[1], - filterType: inheritedFilter, - }, true - } - - if len(fields) >= 3 && (fields[1] == "include" || fields[1] == "substack") { - lineType := pamDirectiveType(trimmed) - if lineType == "" { - return lockscreenPamIncludeDirective{}, false - } - return lockscreenPamIncludeDirective{ - target: fields[2], - filterType: lineType, - }, true - } - - if len(fields) >= 3 && fields[1] == "@include" { - lineType := pamDirectiveType(trimmed) - if lineType == "" { - return lockscreenPamIncludeDirective{}, false - } - return lockscreenPamIncludeDirective{ - target: fields[2], - filterType: lineType, - }, true - } - - return lockscreenPamIncludeDirective{}, false -} - -func resolveLockscreenPamIncludePath(pamDir, target string) (string, error) { - if strings.TrimSpace(target) == "" { - return "", fmt.Errorf("empty PAM include target") - } - - cleanPamDir := filepath.Clean(pamDir) - if filepath.IsAbs(target) { - cleanTarget := filepath.Clean(target) - if filepath.Dir(cleanTarget) != cleanPamDir { - return "", fmt.Errorf("unsupported PAM include outside %s: %s", cleanPamDir, target) - } - return cleanTarget, nil - } - - cleanTarget := filepath.Clean(target) - if cleanTarget == "." || cleanTarget == ".." || strings.HasPrefix(cleanTarget, ".."+string(os.PathSeparator)) { - return "", fmt.Errorf("invalid PAM include target: %s", target) - } - - return filepath.Join(cleanPamDir, cleanTarget), nil -} - -func (r lockscreenPamResolver) resolveService(serviceName string, filterType string, stack []string) ([]string, error) { - path, err := resolveLockscreenPamIncludePath(r.pamDir, serviceName) - if err != nil { - return nil, err - } - - for _, seen := range stack { - if seen == path { - chain := append(append([]string{}, stack...), path) - display := make([]string, 0, len(chain)) - for _, item := range chain { - display = append(display, filepath.Base(item)) - } - return nil, fmt.Errorf("cyclic PAM include detected: %s", strings.Join(display, " -> ")) - } - } - - data, err := r.readFile(path) - if err != nil { - return nil, fmt.Errorf("failed to read PAM file %s: %w", path, err) - } - - var resolved []string - for _, rawLine := range strings.Split(strings.ReplaceAll(string(data), "\r\n", "\n"), "\n") { - rawLine = strings.TrimRight(rawLine, "\r") - trimmed := strings.TrimSpace(rawLine) - if trimmed == "" || strings.HasPrefix(trimmed, "#") || trimmed == "#%PAM-1.0" { - continue - } - - if include, ok := parseLockscreenPamIncludeDirective(trimmed, filterType); ok { - lineType := pamDirectiveType(trimmed) - if filterType != "" && lineType != "" && lineType != filterType { - continue - } - - nested, err := r.resolveService(include.target, include.filterType, append(stack, path)) - if err != nil { - return nil, err - } - resolved = append(resolved, nested...) - continue - } - - lineType := pamDirectiveType(trimmed) - if lineType == "" { - return nil, fmt.Errorf("unsupported PAM directive in %s: %s", filepath.Base(path), trimmed) - } - if filterType != "" && lineType != filterType { - continue - } - if isExcludedLockscreenPamLine(trimmed) { - continue - } - - resolved = append(resolved, rawLine) - } - - return resolved, nil -} - -func buildManagedLockscreenPamContent(pamDir string, readFile func(string) ([]byte, error)) (string, error) { - resolver := lockscreenPamResolver{ - pamDir: pamDir, - readFile: readFile, - } - - resolvedLines, err := resolver.resolveService("login", "", nil) - if err != nil { - return "", err - } - if len(resolvedLines) == 0 { - return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login")) - } - - hasAuth := false - for _, line := range resolvedLines { - if pamDirectiveType(strings.TrimSpace(line)) == "auth" { - hasAuth = true - break - } - } - if !hasAuth { - return "", fmt.Errorf("no auth directives remained after filtering %s", filepath.Join(pamDir, "login")) - } - - var b strings.Builder - b.WriteString("#%PAM-1.0\n") - b.WriteString(LockscreenPamManagedBlockStart + "\n") - for _, line := range resolvedLines { - b.WriteString(line) - b.WriteByte('\n') - } - b.WriteString(LockscreenPamManagedBlockEnd + "\n") - return b.String(), nil -} - -func buildManagedLockscreenU2FPamContent() string { - var b strings.Builder - b.WriteString("#%PAM-1.0\n") - b.WriteString(LockscreenU2FPamManagedBlockStart + "\n") - b.WriteString("auth required pam_u2f.so cue nouserok timeout=10\n") - b.WriteString(LockscreenU2FPamManagedBlockEnd + "\n") - return b.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.") - return nil - } - - existingData, err := deps.readFile(deps.dankshellPath) - if err == nil { - if !hasManagedLockscreenPamFile(string(existingData)) { - logFunc("ℹ Custom /etc/pam.d/dankshell found (no DMS block). Skipping.") - return nil - } - } else if !os.IsNotExist(err) { - return fmt.Errorf("failed to read %s: %w", deps.dankshellPath, err) - } - - content, err := buildManagedLockscreenPamContent(deps.pamDir, deps.readFile) - if err != nil { - return fmt.Errorf("failed to build %s from %s: %w", deps.dankshellPath, filepath.Join(deps.pamDir, "login"), err) - } - - if err := writeManagedPamFile(content, deps.dankshellPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to write %s: %w", deps.dankshellPath, err) - } - - logFunc("✓ Created or updated /etc/pam.d/dankshell for lock screen authentication") - return nil -} - -func syncLockscreenU2FPamConfigWithDeps(logFunc func(string), sudoPassword string, enabled bool, deps syncDeps) error { - if deps.isNixOS() { - logFunc("ℹ NixOS detected. DMS does not manage /etc/pam.d/dankshell-u2f on NixOS. Keep using the bundled U2F helper or configure a custom PAM service yourself.") - return nil - } - - existingData, err := deps.readFile(deps.dankshellU2fPath) - if err != nil && !os.IsNotExist(err) { - return fmt.Errorf("failed to read %s: %w", deps.dankshellU2fPath, err) - } - - if enabled { - if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) { - logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Skipping.") - return nil - } - if err := writeManagedPamFile(buildManagedLockscreenU2FPamContent(), deps.dankshellU2fPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to write %s: %w", deps.dankshellU2fPath, err) - } - logFunc("✓ Created or updated /etc/pam.d/dankshell-u2f for lock screen security-key authentication") - return nil - } - - if os.IsNotExist(err) { - return nil - } - if err == nil && !hasManagedLockscreenU2FPamFile(string(existingData)) { - logFunc("ℹ Custom /etc/pam.d/dankshell-u2f found (no DMS block). Leaving it untouched.") - return nil - } - - if err := deps.runSudoCmd(sudoPassword, "rm", "-f", deps.dankshellU2fPath); err != nil { - return fmt.Errorf("failed to remove %s: %w", deps.dankshellU2fPath, err) - } - logFunc("✓ Removed DMS-managed /etc/pam.d/dankshell-u2f") - return nil -} - -func stripManagedGreeterPamBlock(content string) (string, bool) { - lines := strings.Split(content, "\n") - filtered := make([]string, 0, len(lines)) - inManagedBlock := false - removed := false - - for _, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed == GreeterPamManagedBlockStart { - inManagedBlock = true - removed = true - continue - } - if trimmed == GreeterPamManagedBlockEnd { - inManagedBlock = false - removed = true - continue - } - if inManagedBlock { - removed = true - continue - } - filtered = append(filtered, line) - } - - return strings.Join(filtered, "\n"), removed -} - -func stripLegacyGreeterPamLines(content string) (string, bool) { - lines := strings.Split(content, "\n") - filtered := make([]string, 0, len(lines)) - removed := false - - for i := 0; i < len(lines); i++ { - trimmed := strings.TrimSpace(lines[i]) - if strings.HasPrefix(trimmed, legacyGreeterPamFprintComment) || strings.HasPrefix(trimmed, legacyGreeterPamU2FComment) { - removed = true - if i+1 < len(lines) { - nextLine := strings.TrimSpace(lines[i+1]) - if strings.HasPrefix(nextLine, "auth") && - (strings.Contains(nextLine, "pam_fprintd") || strings.Contains(nextLine, "pam_u2f")) { - i++ - } - } - continue - } - filtered = append(filtered, lines[i]) - } - - return strings.Join(filtered, "\n"), removed -} - -func insertManagedGreeterPamBlock(content string, blockLines []string, greetdPamPath string) (string, error) { - lines := strings.Split(content, "\n") - for i, line := range lines { - trimmed := strings.TrimSpace(line) - if trimmed != "" && !strings.HasPrefix(trimmed, "#") && strings.HasPrefix(trimmed, "auth") { - block := strings.Join(blockLines, "\n") - prefix := strings.Join(lines[:i], "\n") - suffix := strings.Join(lines[i:], "\n") - switch { - case prefix == "": - return block + "\n" + suffix, nil - case suffix == "": - return prefix + "\n" + block, nil - default: - return prefix + "\n" + block + "\n" + suffix, nil - } - } - } - return "", fmt.Errorf("no auth directive found in %s", greetdPamPath) -} - -func syncGreeterPamConfigWithDeps(logFunc func(string), sudoPassword string, settings AuthSettings, forceAuth bool, deps syncDeps) error { - var wantFprint, wantU2f bool - fprintToggleEnabled := forceAuth - u2fToggleEnabled := forceAuth - if forceAuth { - wantFprint = deps.pamModuleExists("pam_fprintd.so") - wantU2f = deps.pamModuleExists("pam_u2f.so") - } else { - fprintToggleEnabled = settings.GreeterEnableFprint - u2fToggleEnabled = settings.GreeterEnableU2f - fprintModule := deps.pamModuleExists("pam_fprintd.so") - u2fModule := deps.pamModuleExists("pam_u2f.so") - wantFprint = settings.GreeterEnableFprint && fprintModule - wantU2f = settings.GreeterEnableU2f && u2fModule - if settings.GreeterEnableFprint && !fprintModule { - logFunc("⚠ Warning: greeter fingerprint toggle is enabled, but pam_fprintd.so was not found.") - } - if settings.GreeterEnableU2f && !u2fModule { - logFunc("⚠ Warning: greeter security key toggle is enabled, but pam_u2f.so was not found.") - } - } - - if deps.isNixOS() { - logFunc("ℹ NixOS detected: PAM config is managed by NixOS modules. Skipping DMS PAM block write.") - logFunc(" Configure fingerprint/U2F auth via your greetd NixOS module options (e.g. security.pam.services.greetd).") - return nil - } - - pamData, err := deps.readFile(deps.greetdPath) - if err != nil { - return fmt.Errorf("failed to read %s: %w", deps.greetdPath, err) - } - originalContent := string(pamData) - content, _ := stripManagedGreeterPamBlock(originalContent) - content, _ = stripLegacyGreeterPamLines(content) - - includedFprintFile := detectIncludedPamModule(content, "pam_fprintd.so", deps) - includedU2fFile := detectIncludedPamModule(content, "pam_u2f.so", deps) - fprintAvailableForCurrentUser := deps.fingerprintAvailableForCurrentUser() - if wantFprint && includedFprintFile != "" { - logFunc("⚠ pam_fprintd already present in included " + includedFprintFile + " (managed by authselect/pam-auth-update). Skipping DMS fprint block to avoid double-fingerprint auth.") - wantFprint = false - } - if wantU2f && includedU2fFile != "" { - logFunc("⚠ pam_u2f already present in included " + includedU2fFile + " (managed by authselect/pam-auth-update). Skipping DMS U2F block to avoid double security-key auth.") - wantU2f = false - } - if !wantFprint && includedFprintFile != "" { - if fprintToggleEnabled { - logFunc("ℹ Fingerprint auth is still enabled via included " + includedFprintFile + ".") - if fprintAvailableForCurrentUser { - logFunc(" DMS toggle is enabled, and effective auth is provided by the included PAM stack.") - } else { - logFunc(" No enrolled fingerprints detected for the current user; password auth remains the effective path.") - } - } else { - if fprintAvailableForCurrentUser { - logFunc("ℹ Fingerprint auth is active via included " + includedFprintFile + " while DMS fingerprint toggle is off.") - logFunc(" Password login will work but may be delayed while the fingerprint module runs first.") - logFunc(" To eliminate the delay, " + pamManagerHintForCurrentDistro()) - } else { - logFunc("ℹ pam_fprintd is present via included " + includedFprintFile + ", but no enrolled fingerprints were detected for the current user.") - logFunc(" Password auth remains the effective login path.") - } - } - } - if !wantU2f && includedU2fFile != "" { - if u2fToggleEnabled { - logFunc("ℹ Security-key auth is still enabled via included " + includedU2fFile + ".") - logFunc(" DMS toggle is enabled, but effective auth is provided by the included PAM stack.") - } else { - logFunc("⚠ Security-key auth is active via included " + includedU2fFile + " while DMS security-key toggle is off.") - logFunc(" " + pamManagerHintForCurrentDistro()) - } - } - - if wantFprint || wantU2f { - blockLines := []string{GreeterPamManagedBlockStart} - if wantFprint { - blockLines = append(blockLines, "auth sufficient pam_fprintd.so max-tries=1 timeout=5") - } - if wantU2f { - blockLines = append(blockLines, "auth sufficient pam_u2f.so cue nouserok timeout=10") - } - blockLines = append(blockLines, GreeterPamManagedBlockEnd) - - content, err = insertManagedGreeterPamBlock(content, blockLines, deps.greetdPath) - if err != nil { - return err - } - } - - if content == originalContent { - return nil - } - - if err := writeManagedPamFile(content, deps.greetdPath, sudoPassword, deps); err != nil { - return fmt.Errorf("failed to install updated PAM config at %s: %w", deps.greetdPath, err) - } - if wantFprint || wantU2f { - logFunc("✓ Configured greetd PAM for fingerprint/U2F") - } else { - logFunc("✓ Cleared DMS-managed greeter PAM auth block") - } - return nil -} - -func writeManagedPamFile(content string, destPath string, sudoPassword string, deps syncDeps) error { - tmpFile, err := deps.createTemp("", "dms-pam-*.conf") - if err != nil { - return err - } - tmpPath := tmpFile.Name() - defer func() { - _ = deps.removeFile(tmpPath) - }() - - if _, err := tmpFile.WriteString(content); err != nil { - tmpFile.Close() - return err - } - if err := tmpFile.Close(); err != nil { - return err - } - if err := deps.runSudoCmd(sudoPassword, "cp", tmpPath, destPath); err != nil { - return err - } - if err := deps.runSudoCmd(sudoPassword, "chmod", "644", destPath); err != nil { - return fmt.Errorf("failed to set permissions on %s: %w", destPath, err) - } - return nil -} - -func pamManagerHintForCurrentDistro() string { - osInfo, err := distros.GetOSInfo() - if err != nil { - return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } - config, exists := distros.Registry[osInfo.Distribution.ID] - if !exists { - return "Disable it in your PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } - - switch config.Family { - case distros.FamilyFedora: - return "Disable it in authselect to force password-only greeter login." - case distros.FamilyDebian, distros.FamilyUbuntu: - return "Disable it in pam-auth-update to force password-only greeter login." - default: - return "Disable it in your distro PAM manager (authselect/pam-auth-update) or in the included PAM stack to force password-only greeter login." - } -} - -func pamModuleExists(module string) bool { - for _, libDir := range []string{ - "/usr/lib64/security", - "/usr/lib/security", - "/lib64/security", - "/lib/security", - "/lib/x86_64-linux-gnu/security", - "/usr/lib/x86_64-linux-gnu/security", - "/lib/aarch64-linux-gnu/security", - "/usr/lib/aarch64-linux-gnu/security", - "/run/current-system/sw/lib64/security", - "/run/current-system/sw/lib/security", - } { - if _, err := os.Stat(filepath.Join(libDir, module)); err == nil { - return true - } - } - return false -} - -func hasEnrolledFingerprintOutput(output string) bool { - lower := strings.ToLower(output) - if strings.Contains(lower, "no fingers enrolled") || - strings.Contains(lower, "no fingerprints enrolled") || - strings.Contains(lower, "no prints enrolled") { - return false - } - if strings.Contains(lower, "has fingers enrolled") || - strings.Contains(lower, "has fingerprints enrolled") { - return true - } - for _, line := range strings.Split(lower, "\n") { - trimmed := strings.TrimSpace(line) - if strings.HasPrefix(trimmed, "finger:") { - return true - } - if strings.HasPrefix(trimmed, "- ") && strings.Contains(trimmed, "finger") { - return true - } - } - return false -} - -func FingerprintAuthAvailableForCurrentUser() bool { - username := strings.TrimSpace(os.Getenv("SUDO_USER")) - if username == "" { - username = strings.TrimSpace(os.Getenv("USER")) - } - if username == "" { - out, err := exec.Command("id", "-un").Output() - if err == nil { - username = strings.TrimSpace(string(out)) - } - } - return fingerprintAuthAvailableForUser(username) -} - -func fingerprintAuthAvailableForUser(username string) bool { - username = strings.TrimSpace(username) - if username == "" { - return false - } - if !pamModuleExists("pam_fprintd.so") { - return false - } - if _, err := exec.LookPath("fprintd-list"); err != nil { - return false - } - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - out, err := exec.CommandContext(ctx, "fprintd-list", username).CombinedOutput() - if err != nil { - return false - } - return hasEnrolledFingerprintOutput(string(out)) -} - -func runSudoCmd(sudoPassword string, command string, args ...string) error { - var cmd *exec.Cmd - - if sudoPassword != "" { - fullArgs := append([]string{command}, args...) - quotedArgs := make([]string, len(fullArgs)) - for i, arg := range fullArgs { - quotedArgs[i] = "'" + strings.ReplaceAll(arg, "'", "'\\''") + "'" - } - cmdStr := strings.Join(quotedArgs, " ") - - cmd = distros.ExecSudoCommand(context.Background(), sudoPassword, cmdStr) - } else { - cmd = exec.Command("sudo", append([]string{command}, args...)...) - } - - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - return cmd.Run() -} |