mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-07-04 13:23:13 +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 json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
@ -118,6 +120,25 @@ class ScreenLocker(
|
|||||||
self._start_phone_check()
|
self._start_phone_check()
|
||||||
self._grab_input()
|
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:
|
def _setup_window(self) -> None:
|
||||||
"""Configure the window for fullscreen lock."""
|
"""Configure the window for fullscreen lock."""
|
||||||
screen_w = self.root.winfo_screenwidth()
|
screen_w = self.root.winfo_screenwidth()
|
||||||
@ -127,6 +148,8 @@ class ScreenLocker(
|
|||||||
self.root.attributes(fullscreen=True)
|
self.root.attributes(fullscreen=True)
|
||||||
self.root.attributes(topmost=True)
|
self.root.attributes(topmost=True)
|
||||||
self.root.configure(bg="#1a1a1a", cursor="arrow")
|
self.root.configure(bg="#1a1a1a", cursor="arrow")
|
||||||
|
if not self.demo_mode:
|
||||||
|
self._disable_vt_switching()
|
||||||
|
|
||||||
def _setup_verify_window(self) -> None:
|
def _setup_verify_window(self) -> None:
|
||||||
"""Configure window for post-sick-day workout verification."""
|
"""Configure window for post-sick-day workout verification."""
|
||||||
@ -483,6 +506,8 @@ class ScreenLocker(
|
|||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Close the application and exit."""
|
"""Close the application and exit."""
|
||||||
|
if not self.demo_mode:
|
||||||
|
self._restore_vt_switching()
|
||||||
self.root.destroy()
|
self.root.destroy()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
|||||||
@ -58,6 +58,26 @@ def _block_real_tk_and_exit() -> Iterator[None]:
|
|||||||
yield
|
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)
|
@pytest.fixture(autouse=True)
|
||||||
def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
|
def _isolate_sick_history(tmp_path: Path) -> Iterator[None]:
|
||||||
"""Redirect SICK_HISTORY_FILE to tmp_path so tests cannot touch real state."""
|
"""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