Security hardening based on triple audit (security, quality, performance)

- Remove SIGUSR1 unlock handler (unauthenticated unlock vector)
- Sanitize LD_PRELOAD (discard inherited environment)
- Refuse to run as root
- Validate D-Bus signal sender against fprintd proxy owner
- Pass bytearray (not str) to PAM conversation callback for wipeable password
- Resolve libc before returning CFUNCTYPE callback
- Bundle fingerprint success idle_add into single atomic callback
- Add running/device_proxy guards to VerifyStart retries with error handling
- Add fingerprint attempt counter (max 10 before disabling)
- Add power button confirmation dialog (inline yes/cancel)
- Move fingerprint D-Bus init before session lock to avoid mainloop blocking
- Resolve wallpaper path once, share across all monitor windows
- Document faillock as UI-only (pam_faillock handles real brute-force protection)
- Fix type hints (Callable), remove dead import (c_char), fix import order
This commit is contained in:
2026-03-26 22:11:00 +01:00
parent 5fda0dce0c
commit fb11c551bd
9 changed files with 164 additions and 64 deletions
+6
View File
@@ -56,6 +56,7 @@ class TestFingerprintListenerLifecycle:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
listener._signal_id = 42
listener.stop()
@@ -81,6 +82,7 @@ class TestFingerprintSignalHandling:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
on_success = MagicMock()
on_failure = MagicMock()
listener._on_success = on_success
@@ -94,6 +96,7 @@ class TestFingerprintSignalHandling:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
on_success = MagicMock()
on_failure = MagicMock()
listener._on_success = on_success
@@ -109,6 +112,7 @@ class TestFingerprintSignalHandling:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
on_success = MagicMock()
on_failure = MagicMock()
listener._on_success = on_success
@@ -124,6 +128,7 @@ class TestFingerprintSignalHandling:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
on_success = MagicMock()
on_failure = MagicMock()
listener._on_success = on_success
@@ -139,6 +144,7 @@ class TestFingerprintSignalHandling:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
on_success = MagicMock()
on_failure = MagicMock()
listener._on_success = on_success
+4
View File
@@ -66,6 +66,7 @@ class TestFingerprintAuthFlow:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
unlock_called = []
listener._on_success = lambda: unlock_called.append(True)
@@ -81,6 +82,7 @@ class TestFingerprintAuthFlow:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
listener._on_success = MagicMock()
listener._on_failure = MagicMock()
@@ -96,6 +98,7 @@ class TestFingerprintAuthFlow:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
listener._on_success = MagicMock()
listener._on_failure = MagicMock()
@@ -113,6 +116,7 @@ class TestFingerprintAuthFlow:
listener = FingerprintListener.__new__(FingerprintListener)
listener._device_proxy = MagicMock()
listener._running = True
listener._failed_attempts = 0
fp_unlock = []
listener._on_success = lambda: fp_unlock.append(True)
listener._on_failure = MagicMock()
+10 -18
View File
@@ -199,33 +199,19 @@ class TestDefensiveWindowCreation:
lock_instance.unlock.assert_not_called()
class TestSignalHandlers:
"""Tests for signal handlers and excepthook behavior."""
def test_sigusr1_triggers_unlock(self):
"""SIGUSR1 should schedule an unlock via GLib.idle_add."""
MoonlockApp, _ = _import_main()
from moonlock.main import _install_signal_handlers
import signal
app = MoonlockApp.__new__(MoonlockApp)
app._lock_instance = MagicMock()
_install_signal_handlers(app)
handler = signal.getsignal(signal.SIGUSR1)
assert handler is not signal.SIG_DFL
class TestExcepthook:
"""Tests for the global exception handler."""
def test_excepthook_does_not_unlock(self):
"""Unhandled exceptions must NOT unlock the session."""
MoonlockApp, _ = _import_main()
from moonlock.main import _install_signal_handlers
from moonlock.main import _install_excepthook
app = MoonlockApp.__new__(MoonlockApp)
app._lock_instance = MagicMock()
original_hook = sys.excepthook
_install_signal_handlers(app)
_install_excepthook()
try:
with patch("moonlock.main.logger"):
@@ -235,3 +221,9 @@ class TestSignalHandlers:
app._lock_instance.unlock.assert_not_called()
finally:
sys.excepthook = original_hook
def test_no_sigusr1_handler(self):
"""SIGUSR1 must NOT be handled — signal-based unlock is a security hole."""
import signal
handler = signal.getsignal(signal.SIGUSR1)
assert handler is signal.SIG_DFL