Skip to content
This repository was archived by the owner on Oct 11, 2025. It is now read-only.

Commit 1c90c46

Browse files
author
Tuxx
committed
refactor: consolidate lockout logic between X11 and Wayland
Created a LockoutManager class to centralize lockout functionality that was previously duplicated in the X11 and Wayland implementations. This consolidation improves code maintainability and ensures consistent behavior across different display servers. The refactoring includes: - New lockout.go file containing the LockoutManager implementation - Updates to X11Locker and WaylandLocker to use the shared manager - Removal of redundant lockout-related fields from both implementations - Consistent lockout policy implementation and duration calculation The UI display code remains specific to each display server while the core security policy is now maintained in a single location.
1 parent 7649a44 commit 1c90c46

File tree

4 files changed

+207
-151
lines changed

4 files changed

+207
-151
lines changed

internal/lockout.go

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package internal
2+
3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
8+
// LockoutManager handles authentication failures and lockout periods
9+
type LockoutManager struct {
10+
failedAttempts int // Count of failed authentication attempts
11+
lockoutUntil time.Time // Time until which input is locked out
12+
lockoutActive bool // Whether a lockout is currently active
13+
lastFailureTime time.Time // Time of the last failed attempt
14+
config Configuration // Application configuration
15+
timerRunning bool // Track if the countdown timer is already running
16+
}
17+
18+
// NewLockoutManager creates a new lockout manager with the given configuration
19+
func NewLockoutManager(config Configuration) *LockoutManager {
20+
return &LockoutManager{
21+
failedAttempts: 0,
22+
lockoutActive: false,
23+
timerRunning: false,
24+
config: config,
25+
lastFailureTime: time.Now().Add(-24 * time.Hour), // Set to past to avoid initial penalty
26+
}
27+
}
28+
29+
// HandleFailedAttempt processes a failed authentication and returns lockout information
30+
// Returns: lockoutActive (bool), lockoutDuration (time.Duration), remainingAttempts (int)
31+
func (lm *LockoutManager) HandleFailedAttempt() (bool, time.Duration, int) {
32+
// Record the failure
33+
lm.failedAttempts++
34+
lm.lastFailureTime = time.Now()
35+
36+
Info("Authentication failed (%d/3 attempts)", lm.failedAttempts)
37+
38+
// If we've had 3+ failures, implement a lockout
39+
if lm.failedAttempts >= 3 {
40+
var lockoutDuration time.Duration
41+
42+
// Check if we're in debug mode
43+
if lm.config.DebugExit {
44+
// Use a shorter lockout in debug mode
45+
lockoutDuration = 5 * time.Second
46+
Info("Debug mode: Using shorter lockout duration of 5 seconds")
47+
} else {
48+
// Determine the lockout duration based on recent failures
49+
// First lockout is 30 seconds, increases for subsequent lockouts
50+
if !lm.lockoutActive {
51+
// First lockout
52+
lockoutDuration = 30 * time.Second
53+
} else {
54+
// Subsequent lockouts - increase duration but cap at 10 minutes
55+
// Increase by 30 seconds each time, based on failed attempts / 3
56+
lockoutDuration = 30 * time.Second * time.Duration(lm.failedAttempts/3)
57+
if lockoutDuration > 10*time.Minute {
58+
lockoutDuration = 10 * time.Minute
59+
}
60+
}
61+
}
62+
63+
// Set the lockout time
64+
lm.lockoutUntil = time.Now().Add(lockoutDuration)
65+
lm.lockoutActive = true
66+
67+
Info("Failed %d attempts, locking out for %v", lm.failedAttempts, lockoutDuration)
68+
69+
// Reset counter after implementing lockout
70+
remainingAttempts := 0
71+
lm.failedAttempts = 0
72+
73+
return true, lockoutDuration, remainingAttempts
74+
}
75+
76+
// Not locked out yet
77+
remainingAttempts := 3 - lm.failedAttempts
78+
return false, 0, remainingAttempts
79+
}
80+
81+
// IsLockedOut checks if authentication is currently locked out
82+
func (lm *LockoutManager) IsLockedOut() bool {
83+
if lm.lockoutActive && time.Now().Before(lm.lockoutUntil) {
84+
return true
85+
}
86+
87+
// If we were in a lockout but it's expired, clear the lockout state
88+
if lm.lockoutActive && time.Now().After(lm.lockoutUntil) {
89+
Info("Lockout period has expired, clearing lockout state")
90+
lm.lockoutActive = false
91+
}
92+
93+
return false
94+
}
95+
96+
// GetRemainingTime returns how much time is left in the lockout
97+
func (lm *LockoutManager) GetRemainingTime() time.Duration {
98+
if !lm.lockoutActive {
99+
return 0
100+
}
101+
102+
remaining := lm.lockoutUntil.Sub(time.Now())
103+
if remaining < 0 {
104+
remaining = 0
105+
lm.lockoutActive = false
106+
}
107+
108+
return remaining
109+
}
110+
111+
// FormatRemainingTime returns a nicely formatted string of the remaining lockout time
112+
func (lm *LockoutManager) FormatRemainingTime() string {
113+
remainingTime := lm.GetRemainingTime()
114+
minutes := int(remainingTime.Minutes())
115+
seconds := int(remainingTime.Seconds()) % 60
116+
return fmt.Sprintf("%02d:%02d", minutes, seconds)
117+
}
118+
119+
// ResetLockout resets the lockout state (e.g., after successful authentication)
120+
func (lm *LockoutManager) ResetLockout() {
121+
lm.failedAttempts = 0
122+
lm.lockoutActive = false
123+
lm.timerRunning = false
124+
}
125+
126+
// GetLockoutUntil returns the time when the lockout ends
127+
func (lm *LockoutManager) GetLockoutUntil() time.Time {
128+
return lm.lockoutUntil
129+
}
130+
131+
// IsTimerRunning returns whether the lockout timer is currently running
132+
func (lm *LockoutManager) IsTimerRunning() bool {
133+
return lm.timerRunning
134+
}
135+
136+
// SetTimerRunning sets the timer running state
137+
func (lm *LockoutManager) SetTimerRunning(running bool) {
138+
lm.timerRunning = running
139+
}

internal/types.go

Lines changed: 19 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -30,28 +30,24 @@ type IdleWatcher struct {
3030

3131
// X11Locker implements the ScreenLocker interface for X11
3232
type X11Locker struct {
33-
config Configuration
34-
conn *xgb.Conn
35-
screen *xproto.ScreenInfo
36-
window xproto.Window
37-
gc xproto.Gcontext
38-
width uint16
39-
height uint16
40-
helper *LockHelper
41-
mediaPlayer *MediaPlayer
42-
passwordBuf string
43-
isLocked bool
44-
passwordDots []bool // true for filled, false for empty
45-
maxDots int
46-
idleWatcher *IdleWatcher
47-
dotWindows []xproto.Window // Track password dot windows
48-
failedAttempts int // Count of failed authentication attempts
49-
lockoutUntil time.Time // Time until which input is locked out
50-
lockoutActive bool // Whether a lockout is currently active
51-
timerRunning bool // Track if the countdown timer is already running
52-
lastFailureTime time.Time // Time of the last failed attempt
53-
messageWindow xproto.Window // Window for displaying lockout messages
54-
textGC xproto.Gcontext // Graphics context for drawing text
33+
config Configuration
34+
conn *xgb.Conn
35+
screen *xproto.ScreenInfo
36+
window xproto.Window
37+
gc xproto.Gcontext
38+
width uint16
39+
height uint16
40+
helper *LockHelper
41+
mediaPlayer *MediaPlayer
42+
passwordBuf string
43+
isLocked bool
44+
passwordDots []bool // true for filled, false for empty
45+
maxDots int
46+
idleWatcher *IdleWatcher
47+
dotWindows []xproto.Window // Track password dot windows
48+
lockoutManager *LockoutManager // Use the shared lockout manager
49+
messageWindow xproto.Window // Window for displaying lockout messages
50+
textGC xproto.Gcontext // Graphics context for drawing text
5551
}
5652

5753
// MediaType defines the type of media file
@@ -262,9 +258,6 @@ type WaylandLocker struct {
262258
helper *LockHelper
263259
lockActive bool
264260
countdownActive bool
265-
failedAttempts int
266-
lockoutUntil time.Time
267-
lockoutActive bool
268-
lastFailureTime time.Time
261+
lockoutManager *LockoutManager // Use the shared lockout manager
269262
mu sync.Mutex
270263
}

internal/wayland.go

Lines changed: 13 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,9 @@ func NewWaylandLocker(config Configuration) *WaylandLocker {
108108
helper: NewLockHelper(config),
109109
lockActive: false,
110110
mediaPlayer: NewMediaPlayer(config),
111-
failedAttempts: 0,
112-
lockoutActive: false,
111+
lockoutManager: NewLockoutManager(config),
113112
countdownActive: false,
114-
securePassword: NewSecurePassword(), // Initialize the new securePassword field
113+
securePassword: NewSecurePassword(),
115114
}
116115
}
117116

@@ -756,21 +755,15 @@ func (l *WaylandLocker) Lock() error {
756755
}
757756

758757
func (l *WaylandLocker) authenticate() {
759-
// Check if we're in a lockout period
760-
if l.lockoutActive && time.Now().Before(l.lockoutUntil) {
758+
// Check if we're in a lockout period using the lockout manager
759+
if l.lockoutManager.IsLockedOut() {
761760
// Still in lockout period, don't even attempt authentication
762-
remainingTime := time.Until(l.lockoutUntil).Round(time.Second)
761+
remainingTime := l.lockoutManager.GetRemainingTime().Round(time.Second)
763762
Info("Authentication locked out for another %v", remainingTime)
764763
l.securePassword.Clear()
765764
return
766765
}
767766

768-
// If we were in a lockout but it's expired, clear the lockout state
769-
if l.lockoutActive && time.Now().After(l.lockoutUntil) {
770-
Info("Lockout period has expired, clearing lockout state")
771-
l.lockoutActive = false
772-
}
773-
774767
if l.helper == nil {
775768
l.helper = NewLockHelper(l.config)
776769
Debug("Created lock helper for PAM auth")
@@ -783,6 +776,9 @@ func (l *WaylandLocker) authenticate() {
783776
if result.Success {
784777
Debug("Auth OK, unlocking session")
785778

779+
// Reset lockout on successful authentication
780+
l.lockoutManager.ResetLockout()
781+
786782
go func() {
787783
if l.mediaPlayer != nil {
788784
Debug("Stopping media player")
@@ -825,46 +821,18 @@ func (l *WaylandLocker) authenticate() {
825821
} else {
826822
Debug("Auth failed: %s", result.Message)
827823

828-
// Authentication failed, increment counter
829-
l.failedAttempts++
830-
l.lastFailureTime = time.Now()
831-
Info("Authentication failed (%d/3 attempts): %s", l.failedAttempts, result.Message)
824+
// Authentication failed, use the lockout manager to handle the failed attempt
825+
lockoutActive, lockoutDuration, _ := l.lockoutManager.HandleFailedAttempt()
832826

833827
// First, do the password shake animation
834828
l.shakePasswordDots()
835829

836-
// Check if we should implement a lockout
837-
if l.failedAttempts >= 3 {
838-
// Determine lockout duration - start with 30 seconds
839-
lockoutDuration := 30 * time.Second
840-
841-
// If in debug mode, cap at 5 seconds
842-
if l.config.DebugExit {
843-
lockoutDuration = 5 * time.Second
844-
Info("Debug mode: Using shorter lockout duration of 5 seconds")
845-
} else {
846-
// If this isn't the first lockout, increase the duration
847-
if l.lockoutActive {
848-
// Increase by 30 seconds each time
849-
lockoutDuration = 30 * time.Second * time.Duration(l.failedAttempts/3)
850-
// Cap at 10 minutes
851-
if lockoutDuration > 10*time.Minute {
852-
lockoutDuration = 10 * time.Minute
853-
}
854-
}
855-
}
856-
857-
// Set the lockout time
858-
l.lockoutUntil = time.Now().Add(lockoutDuration)
859-
l.lockoutActive = true
860-
861-
Info("Failed %d attempts, locking out for %v", l.failedAttempts, lockoutDuration)
830+
// If lockout was activated, show the lockout message
831+
if lockoutActive {
832+
Info("Lockout activated until: %v", l.lockoutManager.GetLockoutUntil())
862833

863834
// Show lockout message on screen AFTER the shake animation is complete
864835
l.StartCountdown("Account locked", int(lockoutDuration.Seconds()))
865-
866-
// Reset counter after implementing lockout
867-
l.failedAttempts = 0
868836
}
869837
}
870838

0 commit comments

Comments
 (0)