Mouse Unlock!—no password, just a secret click pattern
Implementing Pattern-Based Screen Unlocking via Raw Input Events on Linux
Current Situation Analysis
Legacy hardware deployments frequently lack modern biometric sensors, forcing users to rely on manual credential entry for session management. In environments where screen locking is enforced by policy or habit, the cumulative friction of typing complex passwords dozens of times daily creates significant workflow disruption. While biometric authentication is the industry standard for friction reduction, retrofitting older devices with fingerprint readers or IR cameras is often cost-prohibitive or physically impossible due to chassis constraints.
This problem is frequently misunderstood as a hardware limitation that requires hardware solutions. However, the Linux input subsystem provides a software-defined path to low-friction authentication that operates independently of the display server. Many developers assume that Wayland's input isolation prevents third-party applications from sniffing input events. This is a misconception regarding the kernel-level input stack. The evdev interface exposes raw input events directly from the kernel, bypassing the compositor entirely. This means a daemon can monitor mouse events even on a Wayland lock screen, provided it has the necessary permissions.
Data from user experience studies indicates that high-frequency unlock cycles (40+ times per day) contribute to measurable productivity loss and increased error rates in password entry. Leveraging existing peripherals, such as a standard three-button mouse, to encode unlock gestures offers a zero-cost alternative that reduces unlock time to sub-second intervals without compromising the underlying session security model.
WOW Moment: Key Findings
The critical insight is that raw input access via evdev enables gesture-based authentication on Wayland, a platform often cited as restrictive for input monitoring. By reading events at the kernel level, the solution remains agnostic to the display server and works across X11 and Wayland sessions. Furthermore, this approach decouples authentication from the PAM stack, reducing the attack surface associated with credential handling.
| Approach | Hardware Dependency | Unlock Friction | Security Model | Wayland Compatibility |
|---|---|---|---|---|
| Standard Password | None | High (Typing) | Cryptographic | Native |
| Biometric Sensor | Fingerprint/IR | Low (Touch/Glance) | Cryptographic | Native |
| Mouse Pattern | Standard Mouse | Low (Gesture) | Pattern Entropy | Yes (via evdev) |
| X11 Keylogger | None | Low | Compromised | No (X11 Only) |
This finding matters because it democratizes friction reduction for legacy fleets. Organizations can deploy gesture-based unlock scripts on decade-old hardware without purchasing peripherals or modifying the PAM configuration, achieving a user experience comparable to biometric systems at zero marginal cost.
Core Solution
The implementation relies on a Python daemon that interfaces with the Linux input subsystem using the python-evdev library. The architecture consists of four components: device discovery, event buffering, pattern matching, and session control.
Architecture Decisions
- Raw Input Access: We use
evdevto read from/dev/input/eventX. This ensures compatibility with Wayland and avoids the overhead of X11 extensions. - Session State Verification: The daemon queries
loginctlto verify the session is locked before attempting to unlock. This prevents race conditions where a pattern might be triggered during an active session. - Dynamic Device Resolution: Hardcoding device paths is fragile due to hotplugging. The solution dynamically identifies the mouse device by inspecting capabilities.
- Debouncing and State Reset: Input events can be noisy. The implementation includes debouncing logic and a timeout mechanism to reset the pattern buffer, preventing accidental matches.
Implementation
The following TypeScript-style logic is implemented in Python. The code defines a class-based daemon that manages the event loop and pattern state.
import evdev
import subprocess
import time
import logging
from evdev import ecodes
from typing import List, Optional
class GestureUnlockDaemon:
"""
Monitors raw mouse events for a specific click pattern to unlock the session.
Operates via evdev for Wayland/X11 compatibility.
"""
def __init__(self, pattern: List[str], debounce_ms: int = 200, timeout_s: float = 2.0):
self.pattern = pattern
self.buffer: List[str] = []
self.debounce_ms = debounce_ms
self.timeout_s = timeout_s
self.last_event_time = 0.0
self.device: Optional[evdev.InputDevice] = None
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(message)s')
def discover_mouse_device(self) -> Optional[evdev.InputDevice]:
"""Dynamically locate the primary mouse device based on capabilities."""
try:
devices = [evdev.InputDevice(path) for path in evdev.list_devices()]
# Filter for devices that report key events (BTN_LEFT, BTN_RIGHT, etc.)
mouse = next(
(d for d in devices if ecodes.EV_KEY in d.capabilities(absinfo=False)),
None
)
return mouse
except PermissionError:
logging.error("Permission denied accessing /dev/input/. Check group membership.")
return None
def is_session_locked(self) -> bool:
"""Check session lock state via loginctl."""
try:
result = subprocess.run(
['loginctl', 'show-session', '-p', 'LockedHint'],
capture_output=True, text=True, check=True
)
return 'LockedHint=yes' in result.stdout
except subprocess.CalledProcessError:
logging.warning("Failed to query loginctl session state.")
return False
def unlock_session(self) -> None:
"""Trigger session unlock via loginctl."""
if self.is_session_locked():
try:
subprocess.run(['loginctl', 'unlock-session'], check=True)
logging.info("Session unlocked via gesture pattern.")
except subprocess.CalledProcessError:
logging.error("Failed to unlock session via loginctl.")
def handle_event(self, event: evdev.InputEvent) -> None:
"""Process individual input events."""
# Filter for button press events only
if event.type == ecodes.EV_KEY and event.value == 1:
current_time = time.time()
# Debounce check
if (current_time - self.last_event_time) * 1000 < self.debounce_ms:
return
# Timeout check: reset buffer if too much time passes between clicks
if (current_time - self.last_event_time) > self.timeout_s:
self.buffer.clear()
self.last_event_time = current_time
# Map button code to pattern symbol
symbol = None
if event.code == ecodes.BTN_LEFT:
symbol = 'L'
elif event.code == ecodes.BTN_RIGHT:
symbol = 'R'
elif event.code == ecodes.BTN_MIDDLE:
symbol = 'M'
if symbol:
self.buffer.append(symbol)
self._check_pattern()
def _check_pattern(self) -> None:
"""Compare buffer against target pattern."""
if len(self.buffer) == len(self.pattern):
if self.buffer == self.pattern:
self.unlock_session()
# Reset buffer after full length check regardless of match
self.buffer.clear()
def run(self) -> None:
"""Main event loop."""
self.device = self.discover_mouse_device()
if not self.device:
logging.critical("Mouse device not found. Exiting.")
return
logging.info(f"Monitoring device: {self.device.name} ({self.device.path})")
logging.info(f"Target pattern: {'-'.join(self.pattern)}")
try:
for event in self.device.read_loop():
self.handle_event(event)
except KeyboardInterrupt:
logging.info("Daemon stopped by user.")
except Exception as e:
logging.error(f"Daemon crashed: {e}")
Rationale
- Class Structure: Encapsulates state and logic, making the daemon testable and maintainable.
- Dynamic Discovery: The
discover_mouse_devicemethod inspects capabilities rather than relying on static paths, ensuring resilience across reboots and device changes. - Pattern Symbols: Using
L,R,Mallows for patterns that utilize all mouse buttons, increasing entropy. - Timeout Reset: The
timeout_sparameter ensures that a slow sequence of clicks does not accidentally match a pattern intended for rapid execution. - Immediate Unlock Check: The
unlock_sessionmethod re-verifies the lock state immediately before callingloginctl, preventing unintended unlocks if the session state changes between event processing and action execution.
Pitfall Guide
Wayland Input Isolation Misconception
- Explanation: Developers often assume Wayland blocks all input sniffing. While Wayland prevents clients from reading events via the compositor,
evdevreads directly from the kernel input device nodes. - Fix: Ensure the daemon runs with read access to
/dev/input/eventX. This is a kernel-level operation, not a compositor-level one.
- Explanation: Developers often assume Wayland blocks all input sniffing. While Wayland prevents clients from reading events via the compositor,
Device Path Volatility
- Explanation:
/dev/input/event3may become/dev/input/event4after a reboot or USB reconnection. Hardcoding paths causes the daemon to fail silently. - Fix: Implement dynamic device discovery based on device capabilities (e.g., presence of
EV_KEYand specific button codes) as shown in the solution.
- Explanation:
Insufficient Pattern Entropy
- Explanation: Patterns like
L-L-LorR-R-Rare trivial to guess or trigger accidentally. Low entropy undermines the security model. - Fix: Enforce a minimum pattern length (e.g., 5+ events) and require mixed button usage. Avoid patterns that mimic common double-click behaviors.
- Explanation: Patterns like
Permission Denied Errors
- Explanation: The daemon requires read access to input devices, which is restricted to the
inputgroup or root. Running as a regular user without group membership will fail. - Fix: Add the service user to the
inputgroup, or configure systemdDeviceAllowdirectives to grant specific access without broad group membership.
- Explanation: The daemon requires read access to input devices, which is restricted to the
Event Storm and CPU Usage
- Explanation: Mouse movement generates a high volume of
EV_RELevents. Processing every event can spike CPU usage. - Fix: Filter events strictly for
EV_KEYtypes. Theevdevlibrary'sread_loop()is efficient, but the handler must discard non-key events immediately.
- Explanation: Mouse movement generates a high volume of
Race Conditions with Session State
- Explanation: If the user unlocks via password while the pattern buffer is partially filled, the daemon might unlock a session that is already unlocked or interfere with active input.
- Fix: Always verify
LockedHintimmediately before invokingloginctl unlock-session. Clear the buffer on any non-click event if strict gesture isolation is required.
Systemd Service Crashes
- Explanation: Hardware removal or permission changes can cause the daemon to crash. Without restart policies, the unlock feature becomes unavailable.
- Fix: Configure
Restart=alwaysandRestartSec=5in the systemd unit file to ensure automatic recovery.
Production Bundle
Action Checklist
- Install
python-evdevvia package manager or pip. - Create the daemon script with the desired pattern configuration.
- Define a systemd service unit with appropriate user and group settings.
- Configure
DeviceAllowin the service unit for/dev/input/access. - Enable and start the service; verify logs for device detection.
- Test pattern matching on a locked session; verify unlock behavior.
- Review pattern entropy; adjust length or button mix if necessary.- [ ] Monitor service stability over multiple reboot cycles.
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|---|---|---|
| Legacy Hardware, No Biometrics | Mouse Pattern Daemon | Zero hardware cost; leverages existing mouse; works on Wayland. | $0 |
| High Security Requirement | Biometric + Password | Pattern entropy is insufficient for high-value targets. | Hardware cost |
| Wayland Environment | evdev-based Daemon |
Compositor-agnostic; kernel-level access ensures reliability. | $0 |
| Multi-User Workstation | Per-User Pattern Config | Patterns should be user-specific to prevent cross-user unlocks. | Config complexity |
Configuration Template
Systemd Service Unit (/etc/systemd/system/mouse-gesture-unlock.service)
[Unit]
Description=Mouse Gesture Screen Unlock Daemon
After=systemd-logind.service
Wants=systemd-logind.service
[Service]
Type=simple
User=youruser
Group=youruser
ExecStart=/usr/bin/python3 /opt/gesture-unlock/daemon.py
Restart=always
RestartSec=5
# Security hardening
DeviceAllow=/dev/input/event* rw
DeviceAllow=/dev/uinput rw
NoNewPrivileges=true
ProtectSystem=strict
ReadWritePaths=/var/log/gesture-unlock
[Install]
WantedBy=graphical-session.target
Pattern Configuration Snippet
# daemon.py configuration
# Pattern: Left, Left, Right, Middle, Left
# High entropy: Uses all buttons, length 5, no repetition of single button > 2
TARGET_PATTERN = ['L', 'L', 'R', 'M', 'L']
if __name__ == "__main__":
daemon = GestureUnlockDaemon(
pattern=TARGET_PATTERN,
debounce_ms=250,
timeout_s=1.5
)
daemon.run()
Quick Start Guide
- Install Dependencies: Run
sudo dnf install python3-evdev(Fedora) orsudo apt install python3-evdev(Debian/Ubuntu). - Deploy Script: Save the daemon code to
/opt/gesture-unlock/daemon.pyand make it executable. - Enable Service: Copy the systemd unit file to
/etc/systemd/system/, then runsudo systemctl daemon-reload && sudo systemctl enable --now mouse-gesture-unlock.service. - Verify: Lock your screen and execute the configured click pattern. The session should unlock immediately. Check
journalctl -u mouse-gesture-unlock.servicefor diagnostics.
Mid-Year Sale — Unlock Full Article
Base plan from just $4.99/mo or $49/yr
Sign in to read the full article and unlock all tutorials.
Sign In / Register — Start Free Trial7-day free trial · Cancel anytime · 30-day money-back
