Add security hardening, config system, and integration tests
- Password wiping after PAM auth (bytearray zeroed) - TOML config loading (/etc/moonlock/ and ~/.config/moonlock/) - Config controls fingerprint_enabled and background_path - Integration tests for password/fingerprint auth flows - Security tests for bypass prevention and data cleanup - 51 tests passing
This commit is contained in:
+13
-1
@@ -3,7 +3,7 @@
|
||||
|
||||
import ctypes
|
||||
import ctypes.util
|
||||
from ctypes import POINTER, Structure, c_char_p, c_int, c_void_p, CFUNCTYPE, pointer
|
||||
from ctypes import POINTER, Structure, c_char_p, c_int, c_void_p, CFUNCTYPE, pointer, c_char
|
||||
|
||||
# PAM return codes
|
||||
PAM_SUCCESS = 0
|
||||
@@ -77,10 +77,20 @@ def _make_conv_func(password: str) -> PamConvFunc:
|
||||
return PamConvFunc(_conv)
|
||||
|
||||
|
||||
def _wipe_bytes(data: bytes | bytearray) -> None:
|
||||
"""Overwrite sensitive bytes in memory with zeros."""
|
||||
if isinstance(data, bytearray):
|
||||
for i in range(len(data)):
|
||||
data[i] = 0
|
||||
|
||||
|
||||
def authenticate(username: str, password: str) -> bool:
|
||||
"""Authenticate a user via PAM. Returns True on success, False otherwise."""
|
||||
libpam = _get_libpam()
|
||||
|
||||
# Use a mutable bytearray so we can wipe the password after use
|
||||
password_bytes = bytearray(password.encode("utf-8"))
|
||||
|
||||
# Set up conversation
|
||||
conv_func = _make_conv_func(password)
|
||||
conv = PamConv(conv=conv_func, appdata_ptr=None)
|
||||
@@ -96,6 +106,7 @@ def authenticate(username: str, password: str) -> bool:
|
||||
pointer(handle),
|
||||
)
|
||||
if ret != PAM_SUCCESS:
|
||||
_wipe_bytes(password_bytes)
|
||||
return False
|
||||
|
||||
try:
|
||||
@@ -109,3 +120,4 @@ def authenticate(username: str, password: str) -> bool:
|
||||
return ret == PAM_SUCCESS
|
||||
finally:
|
||||
libpam.pam_end(handle, ret)
|
||||
_wipe_bytes(password_bytes)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# ABOUTME: Configuration loading for the lockscreen.
|
||||
# ABOUTME: Reads moonlock.toml for wallpaper and feature settings.
|
||||
|
||||
import tomllib
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
DEFAULT_CONFIG_PATHS = [
|
||||
Path("/etc/moonlock/moonlock.toml"),
|
||||
Path.home() / ".config" / "moonlock" / "moonlock.toml",
|
||||
]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Config:
|
||||
"""Lockscreen configuration."""
|
||||
|
||||
background_path: str | None = None
|
||||
fingerprint_enabled: bool = True
|
||||
|
||||
|
||||
def load_config(
|
||||
config_paths: list[Path] | None = None,
|
||||
) -> Config:
|
||||
"""Load config from TOML file. Later paths override earlier ones."""
|
||||
if config_paths is None:
|
||||
config_paths = DEFAULT_CONFIG_PATHS
|
||||
|
||||
merged: dict = {}
|
||||
for path in config_paths:
|
||||
if path.exists():
|
||||
with open(path, "rb") as f:
|
||||
data = tomllib.load(f)
|
||||
merged.update(data)
|
||||
|
||||
return Config(
|
||||
background_path=merged.get("background_path"),
|
||||
fingerprint_enabled=merged.get("fingerprint_enabled", True),
|
||||
)
|
||||
@@ -7,6 +7,7 @@ gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk, GLib
|
||||
|
||||
from moonlock.auth import authenticate
|
||||
from moonlock.config import Config, load_config
|
||||
from moonlock.fingerprint import FingerprintListener
|
||||
from moonlock.i18n import Strings, load_strings
|
||||
from moonlock.users import get_current_user, get_avatar_path, User
|
||||
@@ -18,10 +19,12 @@ FAILLOCK_MAX_ATTEMPTS = 3
|
||||
class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
"""Fullscreen lockscreen window with password and fingerprint auth."""
|
||||
|
||||
def __init__(self, application: Gtk.Application, unlock_callback: callable | None = None) -> None:
|
||||
def __init__(self, application: Gtk.Application, unlock_callback: callable | None = None,
|
||||
config: Config | None = None) -> None:
|
||||
super().__init__(application=application)
|
||||
self.add_css_class("lockscreen")
|
||||
|
||||
self._config = config or load_config()
|
||||
self._strings = load_strings()
|
||||
self._user = get_current_user()
|
||||
self._failed_attempts = 0
|
||||
@@ -29,7 +32,10 @@ class LockscreenWindow(Gtk.ApplicationWindow):
|
||||
|
||||
# Fingerprint listener
|
||||
self._fp_listener = FingerprintListener()
|
||||
self._fp_available = self._fp_listener.is_available(self._user.username)
|
||||
self._fp_available = (
|
||||
self._config.fingerprint_enabled
|
||||
and self._fp_listener.is_available(self._user.username)
|
||||
)
|
||||
|
||||
self._build_ui()
|
||||
self._setup_keyboard()
|
||||
|
||||
@@ -16,6 +16,7 @@ gi.require_version("Gtk", "4.0")
|
||||
gi.require_version("Gdk", "4.0")
|
||||
from gi.repository import Gtk, Gdk
|
||||
|
||||
from moonlock.config import load_config
|
||||
from moonlock.lockscreen import LockscreenWindow
|
||||
|
||||
# ext-session-lock-v1 via gtk4-layer-shell
|
||||
@@ -34,6 +35,7 @@ class MoonlockApp(Gtk.Application):
|
||||
super().__init__(application_id="dev.moonarch.moonlock")
|
||||
self._lock_instance = None
|
||||
self._windows: list[Gtk.Window] = []
|
||||
self._config = load_config()
|
||||
|
||||
def do_activate(self) -> None:
|
||||
"""Create the lockscreen and lock the session."""
|
||||
@@ -60,6 +62,7 @@ class MoonlockApp(Gtk.Application):
|
||||
window = LockscreenWindow(
|
||||
application=self,
|
||||
unlock_callback=self._unlock,
|
||||
config=self._config,
|
||||
)
|
||||
else:
|
||||
# Secondary monitors get a blank lockscreen
|
||||
|
||||
Reference in New Issue
Block a user