mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:43:37 +02:00
security: harden digital-wellbeing bypass vectors
- Screen locker: disable VT switching (Ctrl+Alt+Fn) via setxkbmap srvrkeys:none on startup; restore on close (production mode only). Gracefully skips if setxkbmap is not installed (shutil.which). Tests: 7 new tests, 100% branch coverage maintained. - Midnight shutdown: restore real schedule values (Mon-Wed 21:00, Thu-Sun 22:00, morning end 05:00); re-enable the three commented-out leniency checks in check_schedule_protection(); self-lock script with chattr +i at end of enable_midnight_shutdown(). - Hosts install: add UNBLOCK_STATE_FILE tracking for whitelisted domains; check_unblock_entries_protection() blocks installation if the unblock list grows; save state after install; self-lock install.sh and generate_hosts_file.sh with chattr +i.
This commit is contained in:
parent
7a14ea9b74
commit
eafe933440
@ -11,6 +11,8 @@ from datetime import datetime, timezone
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
@ -118,6 +120,25 @@ class ScreenLocker(
|
||||
self._start_phone_check()
|
||||
self._grab_input()
|
||||
|
||||
def _disable_vt_switching(self) -> None:
|
||||
"""Disable VT switching in X11 while the lock is active.
|
||||
|
||||
Prevents bypassing the lock by switching to a TTY with Ctrl+Alt+Fn.
|
||||
Best-effort: silently ignored if setxkbmap is unavailable.
|
||||
"""
|
||||
setxkbmap = shutil.which("setxkbmap")
|
||||
if setxkbmap is None:
|
||||
_logger.warning("setxkbmap not found; VT switching will not be disabled")
|
||||
return
|
||||
subprocess.run([setxkbmap, "-option", "srvrkeys:none"], check=False)
|
||||
|
||||
def _restore_vt_switching(self) -> None:
|
||||
"""Restore VT switching after the lock is dismissed."""
|
||||
setxkbmap = shutil.which("setxkbmap")
|
||||
if setxkbmap is None:
|
||||
return
|
||||
subprocess.run([setxkbmap, "-option", ""], check=False)
|
||||
|
||||
def _setup_window(self) -> None:
|
||||
"""Configure the window for fullscreen lock."""
|
||||
screen_w = self.root.winfo_screenwidth()
|
||||
@ -127,6 +148,8 @@ class ScreenLocker(
|
||||
self.root.attributes(fullscreen=True)
|
||||
self.root.attributes(topmost=True)
|
||||
self.root.configure(bg="#1a1a1a", cursor="arrow")
|
||||
if not self.demo_mode:
|
||||
self._disable_vt_switching()
|
||||
|
||||
def _setup_verify_window(self) -> None:
|
||||
"""Configure window for post-sick-day workout verification."""
|
||||
@ -483,6 +506,8 @@ class ScreenLocker(
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the application and exit."""
|
||||
if not self.demo_mode:
|
||||
self._restore_vt_switching()
|
||||
self.root.destroy()
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
@ -58,6 +58,26 @@ def _block_real_tk_and_exit() -> Iterator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_subprocess_run() -> Generator[MagicMock]:
|
||||
"""Block real subprocess calls (e.g. setxkbmap) for every test.
|
||||
|
||||
Also exposed as a named fixture so individual tests can assert
|
||||
on the calls made (e.g. VT switching tests).
|
||||
|
||||
``shutil.which`` is mocked to return a stable fake path so tests work
|
||||
regardless of whether setxkbmap is installed on the host machine.
|
||||
"""
|
||||
with (
|
||||
patch(
|
||||
"python_pkg.screen_locker.screen_lock.shutil.which",
|
||||
return_value="/usr/bin/setxkbmap",
|
||||
),
|
||||
patch("python_pkg.screen_locker.screen_lock.subprocess.run") as mock,
|
||||
):
|
||||
yield mock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
|
||||
"""Redirect SICK_HISTORY_FILE to tmp_path so tests cannot touch real state."""
|
||||
|
||||
136
screen_locker/tests/test_vt_switching.py
Normal file
136
screen_locker/tests/test_vt_switching.py
Normal file
@ -0,0 +1,136 @@
|
||||
"""Tests for VT switching disable/restore during screen lock."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import MagicMock, call, patch
|
||||
|
||||
from python_pkg.screen_locker.tests.conftest import create_locker
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
_SETXKBMAP = "/usr/bin/setxkbmap"
|
||||
|
||||
|
||||
class TestVTSwitching:
|
||||
"""Tests for VT switching disable/restore behaviour."""
|
||||
|
||||
def test_vt_switching_disabled_in_production_mode(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""setxkbmap srvrkeys:none is called when locker starts in production."""
|
||||
create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
[_SETXKBMAP, "-option", "srvrkeys:none"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_vt_switching_not_disabled_in_demo_mode(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""setxkbmap is NOT called in demo mode."""
|
||||
create_locker(mock_tk, tmp_path, demo_mode=True)
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
|
||||
def test_vt_switching_restored_on_close_in_production(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""setxkbmap -option '' is called when close() runs in production."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
locker.close()
|
||||
|
||||
mock_subprocess_run.assert_called_once_with(
|
||||
[_SETXKBMAP, "-option", ""],
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_vt_switching_not_restored_in_demo_mode(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""close() does NOT call setxkbmap in demo mode."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=True)
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
locker.close()
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
|
||||
def test_disable_then_restore_are_complementary(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Full lifecycle: disable on init, restore on close in production."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
|
||||
assert mock_subprocess_run.call_count == 1
|
||||
assert mock_subprocess_run.call_args_list[0] == call(
|
||||
[_SETXKBMAP, "-option", "srvrkeys:none"],
|
||||
check=False,
|
||||
)
|
||||
|
||||
locker.close()
|
||||
|
||||
assert mock_subprocess_run.call_count == 2
|
||||
assert mock_subprocess_run.call_args_list[1] == call(
|
||||
[_SETXKBMAP, "-option", ""],
|
||||
check=False,
|
||||
)
|
||||
|
||||
def test_disable_graceful_when_setxkbmap_missing(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No crash and no subprocess call when setxkbmap is not installed."""
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
|
||||
def test_restore_graceful_when_setxkbmap_missing(
|
||||
self,
|
||||
mock_tk: MagicMock,
|
||||
mock_subprocess_run: MagicMock,
|
||||
mock_sys_exit: MagicMock,
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""No crash and no subprocess call on close when setxkbmap is not installed."""
|
||||
locker = create_locker(mock_tk, tmp_path, demo_mode=False)
|
||||
mock_subprocess_run.reset_mock()
|
||||
|
||||
with patch(
|
||||
"python_pkg.screen_locker.screen_lock.shutil.which",
|
||||
return_value=None,
|
||||
):
|
||||
locker.close()
|
||||
|
||||
mock_subprocess_run.assert_not_called()
|
||||
Loading…
Reference in New Issue
Block a user