A Windows idle-guard / lock overlay built with PySide6. When the machine has been idle past a configurable timeout, SentinelGuard fades a fullscreen, always-on-top, frameless overlay across every attached monitor and requires re-authentication with a TOTP 2FA code, a master password, or (optionally) facial recognition before it disappears.
Alongside the auth panel it renders two informational "islands": a live weather widget, served by an embedded FastAPI server and displayed in a QWebEngineView, and a storage dashboard with animated usage gauges and a recycle-bin cleaner.
Part of the ZashironSentinel project.
- First-run registration — name, email (regex-validated), date of birth, master password, and idle timeout.
- Live password strength checklist — 8+ characters, 1 uppercase, 1 digit, 1 symbol, updated as you type.
- TOTP 2FA via
pyotp, with a QR code for Google Authenticator / Authy plus the raw base32 secret. - Recovery code — 25 characters from an unambiguous alphabet (no
0/O,1/I/L), ~129 bits of entropy, grouped for transcription. Accepted with or without dashes and in any case. - Master-password sign-in as a fallback when the authenticator is unavailable.
- Settings panel, gated behind the master password, for the idle timeout, password rotation, and face enrolment.
- Brute-force throttling — 4 free attempts, then an exponential lockout (15s, 30s, 60s … capped at 15 minutes) with a live countdown. The counter is persisted, so restarting the app does not clear it.
- Reveal toggles on every password field, and inline error messages rather than modal dialogs.
This is the part most worth knowing about, and it changed substantially:
| Secret | At rest | Why |
|---|---|---|
| Master password | PBKDF2-HMAC-SHA256, 600 000 iterations, 16-byte random salt | Never needs reading back; compared with hmac.compare_digest |
| Recovery code | Same, over the normalised code | As above |
| TOTP secret | Sealed with the Windows Data Protection API (DPAPI), bound to the logged-in user | Must be reversible to generate codes, so hashing is not an option |
DPAPI ciphertext is scoped to the Windows account and to this application via an entropy value, so another program running as the same user cannot unseal it, and a copied registry hive is useless on another machine. None of this requires a third-party crypto dependency.
Installs created by earlier versions are migrated automatically on first launch: plaintext values are hashed or sealed, then the plaintext keys are removed. Migration is idempotent and only deletes the old keys once the replacements are safely written. The work factor is also re-checked at each successful sign-in, so raising _ITERATIONS later transparently upgrades existing hashes.
- Idle time from the Win32
GetLastInputInfo/GetTickCountAPIs, with correct handling of the 32-bit ~49.7-day counter rollover. - Audio-aware — if any process has an active audio render session (via
pycaw), the machine counts as in use and the guard will not trigger. Watching a video does not lock the screen. - Spans all monitors using the virtual desktop geometry, rather than only the primary screen.
- Click-through toggling —
WS_EX_TRANSPARENTis set while hidden so input reaches the desktop, and cleared when the guard appears. - Always returns to the sign-in prompt when raised; it never resumes on a settings page.
closeEventand Esc are swallowed — the guard is dismissed by authenticating.- Single-instance enforcement via a named mutex, so two copies cannot fight over the fullscreen window.
- Local FastAPI + uvicorn server on a background daemon thread, bound to
127.0.0.1on an automatically chosen free port. GET /api/weather— geolocates by IP (ipinfo.io, with a fallback location), then queries OpenWeatherMap's current and 5-day forecast endpoints and returns a normalised payload.- Two-layer caching — weather for 10 minutes, geolocation for 6 hours, both behind a lock. A separate last-known-good payload is replayed with
stale: trueif upstream fails. GET /weather— a self-contained animated card with day/night gradients, emoji conditions, and a live clock. Served same-origin, so no CORS middleware is needed.GET /healthz— liveness and configuration check.
- Exact drive totals from
psutil, plus a measured recycle-bin size. - Two scan modes. A quick scan gives instant totals with an estimated User/Apps/System split; pressing Analyze runs a real background walk of
Users,Program Files*,ProgramDataandWindowsto measure the split, deriving "Other" by subtraction. The UI states which is on screen, and a completed measurement is never overwritten by a later estimate. - The analyze pass skips junctions and symlinks, so the WinSxS hardlink farm and
C:\Users\All Usersare not counted repeatedly. It is cancellable and pauses the auto-refresh while running. - Drive metadata (label, model, media type) via
wmi; the import is optional and degrades to generic labels. - Empty recycle bin via
SHEmptyRecycleBinW, scoped to the configured drive, behind a confirmation dialog that names the size being deleted.
SentinelGuard/
├── SentinelGuard.py # Entry point: SentinelWall main window and auth panels
├── requirements.txt # Core runtime dependencies
├── requirements-face.txt # Optional facial-recognition extra
├── .env.example # Template for OWM_API_KEY
└── Guard/
├── Head.py # Shared imports, resource/idle/audio helpers, face workers
├── SentinelCrypto.py # PBKDF2 hashing + Windows DPAPI sealing
├── SentinelVault.py # Persisted state, schema migration, lockout tracking
├── SentinelWeather.py # FastAPI app, OpenWeatherMap client, weather HTML
├── SentinelStorage.py # StoragePage widget, gauges, scan/clean workers
├── SentinelFacialRec.py # ArcFace ONNX + MediaPipe face enrol/auth (optional)
├── port_handler.py # Module-level PORT constant
├── find_free_port.py # Free-port helper
├── Style_Qss/
│ ├── qss.py # Qt stylesheet for the storage page
│ └── AnimatedDot.py # Animated indicator widget
├── Build/
│ └── SentinelGuard_Build.json # auto-py-to-exe / PyInstaller configuration
└── background/ # Icons and assets
- Windows only. Depends on
ctypes.windll(user32,kernel32,crypt32,shell32),pycaw, and optionallywmi.main()exits early with a clear message on other platforms. - Python 3.10+ (3.11 recommended; verified on 3.11.0 with PySide6 6.11).
python -m pip install -r requirements.txtFrom the repository root, so the Guard package resolves:
python SentinelGuard.pyAdd --verbose for debug logging. Logs go to stderr and to %LOCALAPPDATA%\SentinelGuard\sentinelguard.log.
On first launch you are taken through profile creation → authenticator linking → the 2FA prompt. Save the recovery code — it is shown once, and without it (or the master password) a reset means clearing the registry keys by hand.
| Setting | How to set it |
|---|---|
OWM_API_KEY |
Environment variable, or copy .env.example to .env. Without it the weather card shows a configuration message and everything else works normally. |
| Idle timeout | Set at registration; changeable in More options → Settings (1–1440 minutes). |
| Master password | Set at registration; rotatable in the settings panel. |
State is persisted through QSettings("ZashironSentinel", "SentinelGuard") — on Windows, HKEY_CURRENT_USER\Software\ZashironSentinel\SentinelGuard.
Getting a key: OpenWeatherMap's free tier is sufficient. Sign up at https://openweathermap.org/api. Note that a new key can take a couple of hours to activate.
Guard/Build/SentinelGuard_Build.json is an auto-py-to-exe configuration: one-file, windowed, custom icon, with Guard/background bundled as data. Paths are now repository-relative, so load it and build from the repository root.
The equivalent PyInstaller invocation:
pyinstaller --noconfirm --onefile --windowed --name SentinelGuard --icon "Guard/background/Icon/Icon-100.ico" --add-data "Guard/background;Guard/background/" SentinelGuard.pyResource lookup understands sys._MEIPASS, so bundled icons resolve correctly from a one-file build.
Guard/SentinelFacialRec.py implements headless face enrolment and authentication using MediaPipe Face Landmarker for detection and an ArcFace ONNX model for 512-d embeddings, compared by cosine similarity. It also has a CLI:
python Guard/SentinelFacialRec.py enrollThe feature is wired into the UI and detects its own availability at runtime: facial_recognition_status() checks for the optional packages and both model files, and the "Sign in with Face" option is hidden when they are absent. Enrolment and authentication run on a QThread so the GUI never blocks.
To enable it:
python -m pip install -r requirements-face.txtthen supply the two models, which are not distributed here:
| File | Source |
|---|---|
Guard/Face_Model/face_landmarker.task |
MediaPipe Face Landmarker model bundle |
Guard/model/arcface.onnx |
Any ArcFace ONNX export with a 112×112 input, e.g. from insightface |
Enable it in Settings, capture a face, and the option appears under More options. The embedding is written to %LOCALAPPDATA%\SentinelGuard\ rather than next to the script, so it survives a one-file build.
Consider face sign-in a convenience, not a security boundary. The default similarity threshold is low (FACE_SIM_THRESHOLD = 0.09) and single-frame 2-D recognition is spoofable with a photograph. TOTP remains the strong factor.
Fixes to real defects found in the previous version:
- TOTP sign-in was broken. The shared success path called
self.mp_login_input.clear()unconditionally, but that widget isNoneunless you signed in by master password, so every successful 2FA login raisedAttributeErrorinside the slot. - Emptying the recycle bin never asked.
_confirm_and_cleanwent straight to deletion despite its name, and passedNoneas the drive root — which empties the recycle bin on every drive, not justC:. - Four methods were defined twice (
update_last_input,check_idle,animate_hide,animate_show); the earlier wall-clock implementations were silently dead. - NVMe SSDs were reported as HDDs.
MSFT_PhysicalDiskreturns numericMediaType/BusTypecodes, so substring-matching them against"ssd"/"nvme"could never match, and the query did not filter to the disk backing the volume. - The password checklist shipped mojibake (
✗) from a mismatched encoding. Now written as\uXXXXescapes so the source is pure ASCII and cannot regress. - The face module could never import —
Head.pyreferencedGuard.SentinelFacialRecwhile the file lived inGuard/Style_Qss/. - The idle calculation could go negative across the 32-bit tick rollover.
- The face embedding was written next to the script, which is a read-only temp directory under a one-file build.
Security and robustness:
- Credentials are hashed/sealed as described above, with automatic migration.
- The OpenWeatherMap key is no longer committed; it comes from the environment.
- Added brute-force lockout, single-instance enforcement, multi-monitor coverage, and Esc suppression.
- The storage breakdown is measured rather than fabricated, and the UI says which mode it is showing.
- Weather requests are cached, pooled, and lock-guarded; CORS
allow_origins=["*"]was dropped in favour of same-origin fetches.
- Face sign-in is a convenience factor, not a strong one — see the note above.
QSettingson Windows means the registry; DPAPI protects the TOTP secret's confidentiality, but a process running as you can still call the same API. This defends against offline/registry-copy attacks, not against malware already running in your session.- The storage page analyses one drive at a time. The drive is persisted (
storage_drive) but there is no UI to change it yet. DeepCleanWorker(junk/duplicate detection) is implemented and cancellable but still not surfaced in the UI.- There are no automated tests. The modules were verified by hand this revision: DPAPI round-trip, migration from a simulated v1 install, directory measurement against an independent
os.walk, and a full panel-navigation and auth-flow sweep.