Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 39d9cbb624 | |||
| 3adc5e980d | |||
| 3f4448c641 | |||
| b621b4e9fe | |||
| b89435b810 | |||
| 06dadc5cbf |
@@ -1,7 +1,5 @@
|
|||||||
# Moonlock
|
# Moonlock
|
||||||
|
|
||||||
**Name**: Nyx (Göttin der Nacht — passend zum Lockscreen, der den Bildschirm verdunkelt)
|
|
||||||
|
|
||||||
## Projekt
|
## Projekt
|
||||||
|
|
||||||
Moonlock ist ein sicherer Wayland-Lockscreen, gebaut mit Rust + gtk4-rs + ext-session-lock-v1.
|
Moonlock ist ein sicherer Wayland-Lockscreen, gebaut mit Rust + gtk4-rs + ext-session-lock-v1.
|
||||||
@@ -44,7 +42,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
|||||||
- `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
|
- `i18n.rs` — Locale-Erkennung (OnceLock-cached) und String-Tabellen (DE/EN), faillock_warning mit konfigurierbarem max_attempts
|
||||||
- `config.rs` — TOML-Config (background_path, background_blur clamped [0,100], fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection via symlink_metadata + Parse-Error-Logging
|
- `config.rs` — TOML-Config (background_path, background_blur clamped [0,100], fingerprint_enabled als Option<bool>) + Wallpaper-Fallback + Symlink-Rejection via symlink_metadata + Parse-Error-Logging
|
||||||
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking mit 30s Timeout und Generation Counter, FP-Label/Start separat verdrahtet mit pam_acct_mgmt-Check und auto-resume, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode (Downscale auf max 1920px), Blur/Avatar-Cache für Multi-Monitor
|
- `lockscreen.rs` — GTK4 UI via LockscreenHandles, PAM-Auth via gio::spawn_blocking mit 30s Timeout und Generation Counter, FP-Label/Start separat verdrahtet mit pam_acct_mgmt-Check und auto-resume, Zeroizing<String> für Passwort, Power-Confirm, GPU-Blur via GskBlurNode (Downscale auf max 1920px), Blur/Avatar-Cache für Multi-Monitor
|
||||||
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Multi-Monitor mit shared Blur/Avatar-Caches, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present(), Wallpaper-Laden nach lock()
|
- `main.rs` — Entry Point, Panic-Hook (vor Logging), Root-Check, ext-session-lock-v1 (Pflicht in Release), Monitor-Hotplug via `connect_monitor`-Signal (v1_2), shared Blur/Avatar-Caches in Rc, systemd-Journal-Logging, Debug-Level per `MOONLOCK_DEBUG` Env-Var, async fprintd-Init nach window.present()
|
||||||
|
|
||||||
## Sicherheit
|
## Sicherheit
|
||||||
|
|
||||||
@@ -55,7 +53,7 @@ LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moonlock
|
|||||||
- fprintd: D-Bus Signal-Sender wird gegen fprintd's unique bus name validiert (Anti-Spoofing)
|
- fprintd: D-Bus Signal-Sender wird gegen fprintd's unique bus name validiert (Anti-Spoofing)
|
||||||
- Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<CString> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und strdup-Kopie in PAM werden nicht gezeroized — inhärente GTK/libc-Limitierung)
|
- Passwort: Zeroizing<String> ab GTK-Entry-Extraktion, Zeroizing<CString> im PAM-FFI-Layer (bekannte Einschränkung: GLib-GString und strdup-Kopie in PAM werden nicht gezeroized — inhärente GTK/libc-Limitierung)
|
||||||
- Fingerprint-Unlock: pam_acct_mgmt-Check nach verify-match erzwingt Account-Policies (Lockout, Ablauf), resume_async() startet FP bei transientem Fehler neu (mit failed_attempts-Reset und Signal-Handler-Cleanup)
|
- Fingerprint-Unlock: pam_acct_mgmt-Check nach verify-match erzwingt Account-Policies (Lockout, Ablauf), resume_async() startet FP bei transientem Fehler neu (mit failed_attempts-Reset und Signal-Handler-Cleanup)
|
||||||
- Wallpaper wird nach lock() geladen — Disk-I/O verzögert nicht die Lock-Akquisition
|
- Wallpaper wird vor lock() geladen — connect_monitor feuert während lock() und braucht die Textur; lokales JPEG-Laden ist schnell genug
|
||||||
- PAM-Timeout: 30s Timeout verhindert permanentes Aussperren bei hängenden PAM-Modulen, Generation Counter verhindert Interferenz paralleler Auth-Versuche
|
- PAM-Timeout: 30s Timeout verhindert permanentes Aussperren bei hängenden PAM-Modulen, Generation Counter verhindert Interferenz paralleler Auth-Versuche
|
||||||
- Root-Check: Exit mit Fehler wenn als root gestartet
|
- Root-Check: Exit mit Fehler wenn als root gestartet
|
||||||
- Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv)
|
- Faillock: UI-Warnung nach 3 Fehlversuchen, aber PAM entscheidet über Lockout (Entry bleibt aktiv)
|
||||||
|
|||||||
Generated
+1
-1
@@ -575,7 +575,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.6.8"
|
version = "0.6.9"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"gdk-pixbuf",
|
"gdk-pixbuf",
|
||||||
"gdk4",
|
"gdk4",
|
||||||
|
|||||||
+2
-2
@@ -1,13 +1,13 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "moonlock"
|
name = "moonlock"
|
||||||
version = "0.6.8"
|
version = "0.6.10"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
description = "A secure Wayland lockscreen with GTK4, PAM and fingerprint support"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
gtk4 = { version = "0.11", features = ["v4_10"] }
|
gtk4 = { version = "0.11", features = ["v4_10"] }
|
||||||
gtk4-session-lock = { version = "0.4", features = ["v1_1"] }
|
gtk4-session-lock = { version = "0.4", features = ["v1_2"] }
|
||||||
glib = "0.22"
|
glib = "0.22"
|
||||||
gdk4 = "0.11"
|
gdk4 = "0.11"
|
||||||
gdk-pixbuf = "0.22"
|
gdk-pixbuf = "0.22"
|
||||||
|
|||||||
+22
-8
@@ -2,58 +2,72 @@
|
|||||||
|
|
||||||
Architectural and design decisions for Moonlock, in reverse chronological order.
|
Architectural and design decisions for Moonlock, in reverse chronological order.
|
||||||
|
|
||||||
|
## 2026-04-24 – Audit fixes: RefCell borrow across await, async avatar decode
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: Triple audit found two HIGH issues. (1) `init_fingerprint_async` held a `RefCell` immutable borrow across `is_available_async().await` — a concurrent `connect_monitor` signal (hotplug / suspend-resume) invoking `borrow_mut()` during the await would panic. (2) `set_avatar_from_file` decoded avatars synchronously via `Pixbuf::from_file_at_scale`, blocking the GTK main thread inside the `connect_monitor` handler. With `MAX_AVATAR_FILE_SIZE` at 10 MB the worst-case stall was 200–500 ms on monitor hotplug.
|
||||||
|
- **Tradeoffs**: Avatar is shown as the symbolic default icon for a brief window while decoding completes. Wallpaper stays synchronous because `connect_monitor` fires during `lock()` and needs the texture already present (see 2026-04-09).
|
||||||
|
- **How**: (1) Extract `username` into a local `String` in `init_fingerprint_async`, drop the borrow before the await, re-borrow in a new scope after — no awaits inside the second borrow, so hotplug during signal setup is safe. (2) `set_avatar_from_file` now uses `gio::File::read_future` + `Pixbuf::from_stream_at_scale_future` for async I/O and decode. The default icon is shown immediately; the decoded texture replaces it when ready. `Pixbuf` itself is `!Send`, so `gio::spawn_blocking` does not apply — the GIO async stream loader keeps the `Pixbuf` on the main thread while the kernel does the I/O asynchronously.
|
||||||
|
|
||||||
|
## 2026-04-09 – Monitor hotplug via connect_monitor signal
|
||||||
|
|
||||||
|
- **Who**: ClaudeCode, Dom
|
||||||
|
- **Why**: moonlock crashed with segfault in libgtk-4.so after suspend/resume — HDMI monitor disconnect/reconnect invalidated GDK monitor objects, and the statically created windows referenced destroyed surfaces. Crash at consistent GTK4 offset (0x278 NULL dereference), 3x reproduced.
|
||||||
|
- **Tradeoffs**: Wallpaper texture now loaded before `lock()` instead of after (connect_monitor fires during lock() and needs the texture). Local JPEG loading is fast enough that the delay is negligible. Shared state moved to Rc's for the signal closure — slightly more indirection but necessary for dynamic window creation.
|
||||||
|
- **How**: (1) Bump gtk4-session-lock feature from `v1_1` to `v1_2` to enable `Instance::connect_monitor`. (2) Replace manual monitor iteration with `lock.connect_monitor()` signal handler that creates windows on demand. (3) Signal fires once per existing monitor at `lock()` and again on hotplug. (4) Windows auto-unmap when their monitor disappears (ext-session-lock-v1 guarantee). (5) Fingerprint listener published to shared Rc so hotplugged monitors get FP labels.
|
||||||
|
|
||||||
## 2026-03-31 – Fourth audit: peek icon, blur limit, GResource compression, sync markers
|
## 2026-03-31 – Fourth audit: peek icon, blur limit, GResource compression, sync markers
|
||||||
|
|
||||||
- **Who**: Ragnar, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Fourth triple audit found blur limit inconsistency (moonlock 0–100 vs moongreet/moonset 0–200), missing GResource compression, peek icon inconsistency, and duplicated code without sync markers.
|
- **Why**: Fourth triple audit found blur limit inconsistency (moonlock 0–100 vs moongreet/moonset 0–200), missing GResource compression, peek icon inconsistency, and duplicated code without sync markers.
|
||||||
- **Tradeoffs**: Peek icon enabled in lockscreen — user decision favoring UX consistency over shoulder-surfing protection. Acceptable for single-user desktop. Blur limit raised to 200 for ecosystem consistency.
|
- **Tradeoffs**: Peek icon enabled in lockscreen — user decision favoring UX consistency over shoulder-surfing protection. Acceptable for single-user desktop. Blur limit raised to 200 for ecosystem consistency.
|
||||||
- **How**: (1) `show_peek_icon(true)` in lockscreen password entry. (2) `clamp(0.0, 200.0)` for blur in config.rs. (3) `compressed="true"` on CSS/SVG GResource entries. (4) SYNC comments on duplicated blur/background functions pointing to moongreet and moonset.
|
- **How**: (1) `show_peek_icon(true)` in lockscreen password entry. (2) `clamp(0.0, 200.0)` for blur in config.rs. (3) `compressed="true"` on CSS/SVG GResource entries. (4) SYNC comments on duplicated blur/background functions pointing to moongreet and moonset.
|
||||||
|
|
||||||
## 2026-03-30 – Third audit: blur offset, lock-before-IO, FP signal lifecycle, TOCTOU
|
## 2026-03-30 – Third audit: blur offset, lock-before-IO, FP signal lifecycle, TOCTOU
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Third triple audit (quality, performance, security) found: blur padding offset rendering texture at (0,0) instead of (-pad,-pad) causing edge darkening on left/top (BUG), wallpaper disk I/O blocking before lock() extending the unsecured window (PERF/SEC), signal handler duplication on resume_async (SEC), failed_attempts not reset on FP resume (SEC), unknown VerifyStatus with done=false hanging FP listener (SEC), TOCTOU in is_file+is_symlink checks (SEC), dead code in faillock_warning (QUALITY), unbounded blur sigma (SEC).
|
- **Why**: Third triple audit (quality, performance, security) found: blur padding offset rendering texture at (0,0) instead of (-pad,-pad) causing edge darkening on left/top (BUG), wallpaper disk I/O blocking before lock() extending the unsecured window (PERF/SEC), signal handler duplication on resume_async (SEC), failed_attempts not reset on FP resume (SEC), unknown VerifyStatus with done=false hanging FP listener (SEC), TOCTOU in is_file+is_symlink checks (SEC), dead code in faillock_warning (QUALITY), unbounded blur sigma (SEC).
|
||||||
- **Tradeoffs**: Wallpaper loads after lock() — screen briefly shows without wallpaper until texture is ready. Acceptable: security > aesthetics. Blur sigma clamped to [0.0, 100.0] — arbitrary upper bound but prevents GPU memory exhaustion.
|
- **Tradeoffs**: Wallpaper loads after lock() — screen briefly shows without wallpaper until texture is ready. Acceptable: security > aesthetics. Blur sigma clamped to [0.0, 100.0] — arbitrary upper bound but prevents GPU memory exhaustion.
|
||||||
- **How**: (1) Texture offset to (-pad, -pad) in render_blurred_texture. (2) lock.lock() before resolve_background_path. (3) begin_verification disconnects old signal_id before registering new. (4) resume_async resets failed_attempts. (5) Unknown VerifyStatus with done=true triggers restart. (6) symlink_metadata() for atomic file+symlink check. (7) faillock_warning dead code removed, saturating_sub. (8) background_blur clamped. (9) Redundant Zeroizing<Vec<u8>> removed. (10) Default impl for FingerprintListener. (11) on_verify_status restricted to pub(crate). (12) Warn logging for non-UTF-8 GECOS and avatar paths.
|
- **How**: (1) Texture offset to (-pad, -pad) in render_blurred_texture. (2) lock.lock() before resolve_background_path. (3) begin_verification disconnects old signal_id before registering new. (4) resume_async resets failed_attempts. (5) Unknown VerifyStatus with done=true triggers restart. (6) symlink_metadata() for atomic file+symlink check. (7) faillock_warning dead code removed, saturating_sub. (8) background_blur clamped. (9) Redundant Zeroizing<Vec<u8>> removed. (10) Default impl for FingerprintListener. (11) on_verify_status restricted to pub(crate). (12) Warn logging for non-UTF-8 GECOS and avatar paths.
|
||||||
|
|
||||||
## 2026-03-30 – Second audit: zeroize CString, FP account check, PAM timeout, blur downscale
|
## 2026-03-30 – Second audit: zeroize CString, FP account check, PAM timeout, blur downscale
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Second triple audit (quality, performance, security) found: CString password copy not zeroized (HIGH), fingerprint unlock bypassing pam_acct_mgmt (MEDIUM), no PAM timeout leaving user locked out on hanging modules (MEDIUM), GPU blur on full wallpaper resolution (MEDIUM), no-monitor edge case doing `return` instead of `exit(1)` (MEDIUM).
|
- **Why**: Second triple audit (quality, performance, security) found: CString password copy not zeroized (HIGH), fingerprint unlock bypassing pam_acct_mgmt (MEDIUM), no PAM timeout leaving user locked out on hanging modules (MEDIUM), GPU blur on full wallpaper resolution (MEDIUM), no-monitor edge case doing `return` instead of `exit(1)` (MEDIUM).
|
||||||
- **Tradeoffs**: PAM timeout (30s) uses a generation counter to avoid stale result interference — adds complexity but prevents parallel PAM sessions. FP restart after failed account check re-claims the device, adding a D-Bus round-trip, but prevents permanent FP death on transient failures. Blur downscale to 1920px cap trades negligible quality for ~4x less GPU work on 4K wallpapers.
|
- **Tradeoffs**: PAM timeout (30s) uses a generation counter to avoid stale result interference — adds complexity but prevents parallel PAM sessions. FP restart after failed account check re-claims the device, adding a D-Bus round-trip, but prevents permanent FP death on transient failures. Blur downscale to 1920px cap trades negligible quality for ~4x less GPU work on 4K wallpapers.
|
||||||
- **How**: (1) `Zeroizing<CString>` wraps password in auth.rs, `zeroize/std` feature enabled. (2) `check_account()` calls pam_acct_mgmt after FP match; `resume_async()` restarts FP on transient failure. (3) `auth_generation` counter invalidates stale PAM results; 30s timeout re-enables UI. (4) `MAX_BLUR_DIMENSION` caps blur input at 1920px, sigma scaled proportionally. (5) `exit(1)` on no-monitor after `lock.lock()`.
|
- **How**: (1) `Zeroizing<CString>` wraps password in auth.rs, `zeroize/std` feature enabled. (2) `check_account()` calls pam_acct_mgmt after FP match; `resume_async()` restarts FP on transient failure. (3) `auth_generation` counter invalidates stale PAM results; 30s timeout re-enables UI. (4) `MAX_BLUR_DIMENSION` caps blur input at 1920px, sigma scaled proportionally. (5) `exit(1)` on no-monitor after `lock.lock()`.
|
||||||
|
|
||||||
## 2026-03-28 – Remove embedded wallpaper from binary
|
## 2026-03-28 – Remove embedded wallpaper from binary
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
|
- **Why**: Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg. Embedding a 374K JPEG in the binary is redundant. GTK background color (Catppuccin Mocha base) is a clean fallback.
|
||||||
- **Tradeoffs**: Without moonarch installed AND without config, lockscreen shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
|
- **Tradeoffs**: Without moonarch installed AND without config, lockscreen shows plain dark background instead of wallpaper. Acceptable — that's the expected minimal state.
|
||||||
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip background picture creation when no texture available.
|
- **How**: Remove wallpaper.jpg from GResources, return None from resolve_background_path when no file found, skip background picture creation when no texture available.
|
||||||
|
|
||||||
## 2026-03-28 – Audit-driven security and lifecycle fixes (v0.6.0)
|
## 2026-03-28 – Audit-driven security and lifecycle fixes (v0.6.0)
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Triple audit (quality, performance, security) revealed a critical D-Bus signal spoofing vector, fingerprint lifecycle bugs, and multi-monitor performance issues.
|
- **Why**: Triple audit (quality, performance, security) revealed a critical D-Bus signal spoofing vector, fingerprint lifecycle bugs, and multi-monitor performance issues.
|
||||||
- **Tradeoffs**: `cleanup_dbus()` extraction adds a method but clarifies the stop/match ownership; `running_flag: Rc<Cell<bool>>` adds a field but prevents race between async restart and stop; sender validation adds a check per signal but closes the only known auth bypass.
|
- **Tradeoffs**: `cleanup_dbus()` extraction adds a method but clarifies the stop/match ownership; `running_flag: Rc<Cell<bool>>` adds a field but prevents race between async restart and stop; sender validation adds a check per signal but closes the only known auth bypass.
|
||||||
- **How**: (1) Validate D-Bus VerifyStatus sender against fprintd's unique bus name. (2) Extract `cleanup_dbus()` from `stop()`, call it on verify-match. (3) `Rc<Cell<bool>>` running flag checked after await in `restart_verify_async`. (4) Consistent 3s D-Bus timeouts. (5) Panic hook before logging. (6) Blur and avatar caches shared across monitors. (7) Peek icon disabled. (8) Symlink rejection for background_path. (9) TOML parse errors logged.
|
- **How**: (1) Validate D-Bus VerifyStatus sender against fprintd's unique bus name. (2) Extract `cleanup_dbus()` from `stop()`, call it on verify-match. (3) `Rc<Cell<bool>>` running flag checked after await in `restart_verify_async`. (4) Consistent 3s D-Bus timeouts. (5) Panic hook before logging. (6) Blur and avatar caches shared across monitors. (7) Peek icon disabled. (8) Symlink rejection for background_path. (9) TOML parse errors logged.
|
||||||
|
|
||||||
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
## 2026-03-28 – GPU blur via GskBlurNode replaces CPU blur
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache mitigated repeat starts but added ~100 lines of complexity.
|
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms–2s on 4K wallpapers at cold cache. Disk cache mitigated repeat starts but added ~100 lines of complexity.
|
||||||
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper. Removes `image` and `dirs` dependencies entirely. No disk cache needed.
|
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper. Removes `image` and `dirs` dependencies entirely. No disk cache needed.
|
||||||
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency.
|
- **How**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` on `connect_realize`. Blur happens once on the GPU when the widget gets its renderer, producing a concrete `gdk::Texture`. Zero startup latency.
|
||||||
|
|
||||||
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
## 2026-03-28 – Optional background blur via `image` crate (superseded)
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern
|
- **Why**: Consistent with moonset/moongreet — blurred wallpaper as lockscreen background is a common UX pattern
|
||||||
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
|
- **Tradeoffs**: Adds `image` crate dependency (~15 transitive crates); CPU-side Gaussian blur at load time adds startup latency proportional to image size and sigma. Acceptable because blur runs once and the texture is shared across monitors.
|
||||||
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns `gdk::Texture`. Config option `background_blur: Option<f32>` in TOML.
|
- **How**: `load_background_texture(bg_path, blur_radius)` loads texture, optionally applies `imageops::blur()`, returns `gdk::Texture`. Config option `background_blur: Option<f32>` in TOML.
|
||||||
|
|
||||||
## 2026-03-28 – Shared wallpaper texture pattern (aligned with moonset/moongreet)
|
## 2026-03-28 – Shared wallpaper texture pattern (aligned with moonset/moongreet)
|
||||||
|
|
||||||
- **Who**: Nyx, Dom
|
- **Who**: ClaudeCode, Dom
|
||||||
- **Why**: Previously loaded wallpaper per-window via `Picture::for_filename()`. Multi-monitor setups decoded the JPEG redundantly. Blur feature requires texture pixel access anyway.
|
- **Why**: Previously loaded wallpaper per-window via `Picture::for_filename()`. Multi-monitor setups decoded the JPEG redundantly. Blur feature requires texture pixel access anyway.
|
||||||
- **Tradeoffs**: Slightly more code in main.rs (texture loaded before window creation), but avoids redundant decoding and enables the blur feature.
|
- **Tradeoffs**: Slightly more code in main.rs (texture loaded before window creation), but avoids redundant decoding and enables the blur feature.
|
||||||
- **How**: `load_background_texture()` in lockscreen.rs decodes once, `create_background_picture()` wraps shared `gdk::Texture` in `gtk::Picture`. Same pattern as moonset/moongreet.
|
- **How**: `load_background_texture()` in lockscreen.rs decodes once, `create_background_picture()` wraps shared `gdk::Texture` in `gtk::Picture`. Same pattern as moonset/moongreet.
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ Part of the Moonarch ecosystem.
|
|||||||
- **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash, `exit(1)` in release if unsupported)
|
- **ext-session-lock-v1** — Protocol-guaranteed screen locking (compositor keeps screen locked on crash, `exit(1)` in release if unsupported)
|
||||||
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter
|
- **PAM authentication** — Uses system PAM stack (`/etc/pam.d/moonlock`) with 30s timeout and generation counter
|
||||||
- **Fingerprint unlock** — fprintd D-Bus integration with sender validation, async init (window appears instantly), `pam_acct_mgmt` check after verify, auto-resume on transient errors
|
- **Fingerprint unlock** — fprintd D-Bus integration with sender validation, async init (window appears instantly), `pam_acct_mgmt` check after verify, auto-resume on transient errors
|
||||||
- **Multi-monitor** — Lockscreen on every monitor with shared blur and avatar caches
|
- **Multi-monitor + hotplug** — Lockscreen on every monitor with shared blur and avatar caches; monitors added after suspend/resume get windows automatically via `connect_monitor` signal
|
||||||
- **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0–100)
|
- **GPU blur** — Background blur via GskBlurNode (downscale to max 1920px, configurable 0–100)
|
||||||
- **i18n** — German and English (auto-detected)
|
- **i18n** — German and English (auto-detected)
|
||||||
- **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
|
- **Faillock warning** — Progressive UI warning after failed attempts, PAM decides lockout
|
||||||
- **Panic safety** — Panic hook logs but never unlocks (installed before logging)
|
- **Panic safety** — Panic hook logs but never unlocks (installed before logging)
|
||||||
- **Password wiping** — `Zeroize` on drop from GTK entry through PAM FFI layer
|
- **Password wiping** — `Zeroize` on drop from GTK entry through PAM FFI layer
|
||||||
- **Journal logging** — `journalctl -t moonlock`, debug level via `MOONLOCK_DEBUG` env var
|
- **Journal logging** — `journalctl -t moonlock`, debug level via `MOONLOCK_DEBUG` env var
|
||||||
- **Lock-first architecture** — Wallpaper loaded after `lock()` so disk I/O never delays lock acquisition
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
|
|||||||
+12
-12
@@ -1,9 +1,9 @@
|
|||||||
/* ABOUTME: GTK4 CSS stylesheet for the Moonlock lockscreen. */
|
/* ABOUTME: GTK4 CSS stylesheet for the Moonlock lockscreen. */
|
||||||
/* ABOUTME: Dark theme styling matching the Moonarch ecosystem. */
|
/* ABOUTME: Uses GTK theme colors for consistency with the active desktop theme. */
|
||||||
|
|
||||||
/* Main window background */
|
/* Main window background */
|
||||||
window.lockscreen {
|
window.lockscreen {
|
||||||
background-color: #1a1a2e;
|
background-color: @theme_bg_color;
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
background-position: center;
|
background-position: center;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -27,14 +27,14 @@ window.lockscreen.visible {
|
|||||||
min-width: 128px;
|
min-width: 128px;
|
||||||
min-height: 128px;
|
min-height: 128px;
|
||||||
background-color: @theme_selected_bg_color;
|
background-color: @theme_selected_bg_color;
|
||||||
border: 3px solid alpha(white, 0.3);
|
border: 3px solid alpha(@theme_fg_color, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Username label */
|
/* Username label */
|
||||||
.username-label {
|
.username-label {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: white;
|
color: @theme_fg_color;
|
||||||
margin-top: 12px;
|
margin-top: 12px;
|
||||||
margin-bottom: 40px;
|
margin-bottom: 40px;
|
||||||
}
|
}
|
||||||
@@ -46,29 +46,29 @@ window.lockscreen.visible {
|
|||||||
|
|
||||||
/* Error message label */
|
/* Error message label */
|
||||||
.error-label {
|
.error-label {
|
||||||
color: #ff6b6b;
|
color: @error_color;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fingerprint status indicator */
|
/* Fingerprint status indicator */
|
||||||
.fingerprint-label {
|
.fingerprint-label {
|
||||||
color: alpha(white, 0.6);
|
color: alpha(@theme_fg_color, 0.6);
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fingerprint-label.success {
|
.fingerprint-label.success {
|
||||||
color: #51cf66;
|
color: @success_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fingerprint-label.failed {
|
.fingerprint-label.failed {
|
||||||
color: #ff6b6b;
|
color: @error_color;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Confirmation prompt */
|
/* Confirmation prompt */
|
||||||
.confirm-label {
|
.confirm-label {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: white;
|
color: @theme_fg_color;
|
||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,12 +103,12 @@ window.lockscreen.visible {
|
|||||||
min-height: 48px;
|
min-height: 48px;
|
||||||
padding: 0px;
|
padding: 0px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background-color: alpha(white, 0.1);
|
background-color: alpha(@theme_fg_color, 0.1);
|
||||||
color: white;
|
color: @theme_fg_color;
|
||||||
border: none;
|
border: none;
|
||||||
margin: 4px;
|
margin: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.power-button:hover {
|
.power-button:hover {
|
||||||
background-color: alpha(white, 0.25);
|
background-color: alpha(@theme_fg_color, 0.25);
|
||||||
}
|
}
|
||||||
|
|||||||
+20
-9
@@ -623,30 +623,41 @@ fn render_blurred_texture(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load an image file and set it as the avatar. Stores the texture in the cache.
|
/// Load an image file and set it as the avatar. Stores the texture in the cache.
|
||||||
|
/// Decoding runs via GIO async I/O + async pixbuf stream loader so the GTK main
|
||||||
|
/// loop stays responsive — avatars may be loaded inside the `connect_monitor`
|
||||||
|
/// signal handler at hotplug time, which must not block. The fallback icon is
|
||||||
|
/// shown immediately; the decoded texture replaces it when ready.
|
||||||
fn set_avatar_from_file(
|
fn set_avatar_from_file(
|
||||||
image: >k::Image,
|
image: >k::Image,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
cache: &Rc<RefCell<Option<gdk::Texture>>>,
|
||||||
) {
|
) {
|
||||||
let path_str = match path.to_str() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => {
|
|
||||||
log::warn!("Avatar path is not valid UTF-8: {:?}", path);
|
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
image.set_icon_name(Some("avatar-default-symbolic"));
|
||||||
|
|
||||||
|
let display_path = path.to_path_buf();
|
||||||
|
let file = gio::File::for_path(path);
|
||||||
|
let image_clone = image.clone();
|
||||||
|
let cache_clone = cache.clone();
|
||||||
|
|
||||||
|
glib::spawn_future_local(async move {
|
||||||
|
let stream = match file.read_future(glib::Priority::default()).await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("Failed to open avatar {}: {e}", display_path.display());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match Pixbuf::from_file_at_scale(path_str, AVATAR_SIZE, AVATAR_SIZE, true) {
|
match Pixbuf::from_stream_at_scale_future(&stream, AVATAR_SIZE, AVATAR_SIZE, true).await {
|
||||||
Ok(pixbuf) => {
|
Ok(pixbuf) => {
|
||||||
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
let texture = gdk::Texture::for_pixbuf(&pixbuf);
|
||||||
image.set_paintable(Some(&texture));
|
image_clone.set_paintable(Some(&texture));
|
||||||
*cache.borrow_mut() = Some(texture);
|
*cache_clone.borrow_mut() = Some(texture);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::warn!("Failed to load avatar from {:?}: {e}", path);
|
log::warn!("Failed to decode avatar from {}: {e}", display_path.display());
|
||||||
image.set_icon_name(Some("avatar-default-symbolic"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
/// Load the default avatar SVG from GResources, tinted with the foreground color.
|
||||||
|
|||||||
+89
-38
@@ -59,17 +59,18 @@ fn activate(app: >k::Application) {
|
|||||||
|
|
||||||
fn activate_with_session_lock(
|
fn activate_with_session_lock(
|
||||||
app: >k::Application,
|
app: >k::Application,
|
||||||
display: &gdk::Display,
|
_display: &gdk::Display,
|
||||||
config: &config::Config,
|
config: &config::Config,
|
||||||
) {
|
) {
|
||||||
let lock = gtk4_session_lock::Instance::new();
|
let lock = gtk4_session_lock::Instance::new();
|
||||||
lock.lock();
|
|
||||||
|
|
||||||
// Load wallpaper AFTER lock — disk I/O must not delay the lock acquisition
|
// Load wallpaper before lock — connect_monitor fires during lock() and needs the
|
||||||
let bg_texture = config::resolve_background_path(config)
|
// texture. This means disk I/O happens before locking, but loading a local JPEG
|
||||||
.and_then(|path| lockscreen::load_background_texture(&path));
|
// is fast enough that the delay is negligible.
|
||||||
|
let bg_texture: Rc<Option<gdk::Texture>> = Rc::new(
|
||||||
let monitors = display.monitors();
|
config::resolve_background_path(config)
|
||||||
|
.and_then(|path| lockscreen::load_background_texture(&path)),
|
||||||
|
);
|
||||||
|
|
||||||
// Shared unlock callback — unlocks session and quits.
|
// Shared unlock callback — unlocks session and quits.
|
||||||
// Guard prevents double-unlock if PAM and fingerprint succeed simultaneously.
|
// Guard prevents double-unlock if PAM and fingerprint succeed simultaneously.
|
||||||
@@ -91,68 +92,116 @@ fn activate_with_session_lock(
|
|||||||
let blur_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
let blur_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
||||||
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
let avatar_cache: Rc<RefCell<Option<gdk::Texture>>> = Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
// Create all monitor windows immediately — no D-Bus calls here
|
// Shared config for use in the monitor signal handler
|
||||||
let mut all_handles = Vec::new();
|
let config = Rc::new(config.clone());
|
||||||
let mut created_any = false;
|
|
||||||
for i in 0..monitors.n_items() {
|
// Shared handles list — populated by connect_monitor, read by fingerprint init
|
||||||
if let Some(monitor) = monitors
|
let all_handles: Rc<RefCell<Vec<lockscreen::LockscreenHandles>>> =
|
||||||
.item(i)
|
Rc::new(RefCell::new(Vec::new()));
|
||||||
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
|
|
||||||
{
|
// Shared fingerprint listener — None until async init completes.
|
||||||
let handles = lockscreen::create_lockscreen_window(
|
// The monitor handler checks this to wire up FP labels on hotplugged monitors.
|
||||||
bg_texture.as_ref(),
|
let shared_fp: Rc<RefCell<Option<Rc<RefCell<FingerprintListener>>>>> =
|
||||||
config,
|
Rc::new(RefCell::new(None));
|
||||||
|
|
||||||
|
// The ::monitor signal fires once per existing monitor at lock(), and again
|
||||||
|
// whenever a monitor is hotplugged (e.g. after suspend/resume). This replaces
|
||||||
|
// the old manual monitor iteration and handles hotplug automatically.
|
||||||
|
let lock_for_signal = lock.clone();
|
||||||
|
lock.connect_monitor(glib::clone!(
|
||||||
|
#[strong]
|
||||||
app,
|
app,
|
||||||
|
#[strong]
|
||||||
|
config,
|
||||||
|
#[strong]
|
||||||
|
bg_texture,
|
||||||
|
#[strong]
|
||||||
|
unlock_callback,
|
||||||
|
#[strong]
|
||||||
|
blur_cache,
|
||||||
|
#[strong]
|
||||||
|
avatar_cache,
|
||||||
|
#[strong]
|
||||||
|
all_handles,
|
||||||
|
#[strong]
|
||||||
|
shared_fp,
|
||||||
|
move |_instance, monitor| {
|
||||||
|
log::debug!("Monitor signal: creating lockscreen window");
|
||||||
|
let handles = lockscreen::create_lockscreen_window(
|
||||||
|
bg_texture.as_ref().as_ref(),
|
||||||
|
&config,
|
||||||
|
&app,
|
||||||
unlock_callback.clone(),
|
unlock_callback.clone(),
|
||||||
&blur_cache,
|
&blur_cache,
|
||||||
&avatar_cache,
|
&avatar_cache,
|
||||||
);
|
);
|
||||||
lock.assign_window_to_monitor(&handles.window, &monitor);
|
lock_for_signal.assign_window_to_monitor(&handles.window, monitor);
|
||||||
handles.window.present();
|
handles.window.present();
|
||||||
all_handles.push(handles);
|
|
||||||
created_any = true;
|
// If fingerprint is already initialized, wire up the label
|
||||||
}
|
if let Some(ref fp_rc) = *shared_fp.borrow() {
|
||||||
|
lockscreen::show_fingerprint_label(&handles, fp_rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !created_any {
|
all_handles.borrow_mut().push(handles);
|
||||||
log::error!("No lockscreen windows created — screen stays locked (compositor policy)");
|
|
||||||
std::process::exit(1);
|
|
||||||
}
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
lock.lock();
|
||||||
|
|
||||||
// Async fprintd initialization — runs after windows are visible
|
// Async fprintd initialization — runs after windows are visible
|
||||||
if config.fingerprint_enabled {
|
if config.fingerprint_enabled {
|
||||||
init_fingerprint_async(all_handles);
|
init_fingerprint_async(all_handles, shared_fp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Initialize fprintd asynchronously after windows are visible.
|
/// Initialize fprintd asynchronously after windows are visible.
|
||||||
/// Uses a single FingerprintListener shared across all monitors —
|
/// Uses a single FingerprintListener shared across all monitors —
|
||||||
/// only the first monitor's handles get the fingerprint UI wired up.
|
/// only the first monitor's handles get the fingerprint verification wired up.
|
||||||
fn init_fingerprint_async(all_handles: Vec<lockscreen::LockscreenHandles>) {
|
/// The `shared_fp` is set after init so that the connect_monitor handler can
|
||||||
|
/// wire up FP labels on monitors that appear after initialization.
|
||||||
|
fn init_fingerprint_async(
|
||||||
|
all_handles: Rc<RefCell<Vec<lockscreen::LockscreenHandles>>>,
|
||||||
|
shared_fp: Rc<RefCell<Option<Rc<RefCell<FingerprintListener>>>>>,
|
||||||
|
) {
|
||||||
glib::spawn_future_local(async move {
|
glib::spawn_future_local(async move {
|
||||||
let mut listener = FingerprintListener::new();
|
let mut listener = FingerprintListener::new();
|
||||||
listener.init_async().await;
|
listener.init_async().await;
|
||||||
|
|
||||||
// Use the first monitor's username to check enrollment
|
// Extract username without holding a borrow across the await below —
|
||||||
let username = &all_handles[0].username;
|
// otherwise a concurrent connect_monitor signal (hotplug / suspend-resume)
|
||||||
if username.is_empty() {
|
// that tries to borrow_mut() panics at runtime.
|
||||||
|
let username = {
|
||||||
|
let handles = all_handles.borrow();
|
||||||
|
if handles.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
let u = handles[0].username.clone();
|
||||||
|
if u.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
u
|
||||||
|
};
|
||||||
|
|
||||||
if !listener.is_available_async(username).await {
|
if !listener.is_available_async(&username).await {
|
||||||
log::debug!("fprintd not available or no enrolled fingers");
|
log::debug!("fprintd not available or no enrolled fingers");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fp_rc = Rc::new(RefCell::new(listener));
|
let fp_rc = Rc::new(RefCell::new(listener));
|
||||||
|
|
||||||
// Show fingerprint label on all monitors
|
// Re-borrow after the await — no further awaits in this scope, so it is
|
||||||
for handles in &all_handles {
|
// safe to hold the borrow briefly while wiring up the labels.
|
||||||
lockscreen::show_fingerprint_label(handles, &fp_rc);
|
{
|
||||||
|
let handles = all_handles.borrow();
|
||||||
|
for h in handles.iter() {
|
||||||
|
lockscreen::show_fingerprint_label(h, &fp_rc);
|
||||||
|
}
|
||||||
|
lockscreen::start_fingerprint(&handles[0], &fp_rc);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start verification listener on the first monitor only
|
// Publish the listener so hotplugged monitors get FP labels too
|
||||||
lockscreen::start_fingerprint(&all_handles[0], &fp_rc);
|
*shared_fp.borrow_mut() = Some(fp_rc);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,7 +233,9 @@ fn activate_without_lock(
|
|||||||
|
|
||||||
// Async fprintd initialization for development mode
|
// Async fprintd initialization for development mode
|
||||||
if config.fingerprint_enabled {
|
if config.fingerprint_enabled {
|
||||||
init_fingerprint_async(vec![handles]);
|
let all_handles = Rc::new(RefCell::new(vec![handles]));
|
||||||
|
let shared_fp = Rc::new(RefCell::new(None));
|
||||||
|
init_fingerprint_async(all_handles, shared_fp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user