Compare commits

...

15 Commits

Author SHA1 Message Date
nevaforget f6f33a13ab fix: audit fixes — power timeout, timing mitigation, release profile, GREETD_SOCK cache (v0.7.1)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- Add 30s timeout with SIGKILL to power actions (adapted from moonset)
- Add 500ms minimum login response time against timing enumeration
- Cache GREETD_SOCK in GreeterState at startup
- Add [profile.release] with LTO, codegen-units=1, strip
- Add compressed="true" to GResource CSS/SVG entries
- Add SYNC comments to duplicated blur/background functions
- Add nix dependency for signal handling in power timeout
2026-03-31 11:08:40 +02:00
nevaforget 60d294fa37 docs: update README, fix build.rs comment, correct gtk-theme in config
README: replace LD_PRELOAD with MOONGREET_NO_LAYER_SHELL env var,
add missing features (GPU blur, journal logging, password wiping).
build.rs: remove wallpaper.jpg reference.
moongreet.toml: correct gtk-theme to Colloid-Grey-Dark-Catppuccin.
2026-03-31 09:36:19 +02:00
nevaforget 1d557ea135 fix: audit fixes — password zeroize, blur downscale, symlink hardening, error filtering (v0.7.0)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
- Add zeroize dependency, wrap password in Zeroizing<String> from entry extraction
  through to login_worker (prevents heap-resident plaintext)
- Add MAX_BLUR_DIMENSION (1920px) downscale before GPU blur to reduce 4K workload
- Wallpaper: use symlink_metadata + is_symlink rejection in greeter.rs and config.rs
- Avatar: add is_file() check, swap lookup order to ~/.face first (consistent with
  moonlock/moonset)
- greetd errors: show generic fallback in UI, log raw PAM details at debug level only
- fprintd: validate device path prefix before creating D-Bus proxy
- Locale: cache detected locale via OnceLock (avoid repeated env/file reads)
2026-03-30 16:03:04 +02:00
nevaforget a2dc89854d fix: security hardening, blur geometry, and performance audit fixes (v0.6.2)
Update PKGBUILD version / update-pkgver (push) Successful in 2s
Security: cache dirs now 0o700 via DirBuilder::mode(), blur config
validated (finite + clamp 0–200), TOCTOU socket pre-check removed.

Quality: GPU blur geometry fixed (texture shifted instead of stretched),
is_valid_username hardened, is_valid_gtk_theme extracted as testable fn,
save_last_session error handling consistent with save_last_user.

Performance: blurred texture cached across monitors (1x GPU renderpass
instead of N), FingerprintProbe device proxy cached in GreeterState with
generation counter to prevent race condition on fast user-switch.

Clippy: all 7 warnings resolved (collapsible if-let, redundant closure,
manual_range_contains, too_many_arguments suppressed for GTK widget fns).

Tests: 109 → 118 (GTK theme validation, Unicode usernames, cache dir
permissions, unwritable dir handling, blur config edge cases).
2026-03-30 14:31:28 +02:00
nevaforget f3f4db1ab1 ci: also update .SRCINFO in pkgver workflow
Update PKGBUILD version / update-pkgver (push) Successful in 2s
paru reads .SRCINFO (not PKGBUILD) for version comparison during
sysupgrade. Without updating .SRCINFO, paru never detects upgrades
for PKGBUILD repository packages.
2026-03-30 13:49:09 +02:00
nevaforget a61fa4e145 ci: add workflow to auto-update pkgver in moonarch-pkgbuilds
Update PKGBUILD version / update-pkgver (push) Successful in 2s
2026-03-29 23:05:16 +02:00
nevaforget f09a04a115 fix: elevate CSS priority to override GTK4 user theme (v0.6.1)
Colloid-Catppuccin theme loaded via ~/.config/gtk-4.0/gtk.css at
PRIORITY_USER (800) was overriding moongreet's PRIORITY_APPLICATION (600),
causing avatar to lose its circular border-radius.

- Use STYLE_PROVIDER_PRIORITY_USER for app CSS provider
- Replace border-radius: 50% with 9999px (GTK4 CSS percentage quirk)
- Include missed Cargo.lock and PKGBUILD updates from v0.6.0
2026-03-29 14:26:19 +02:00
nevaforget a462b2cf06 feat: add fprintd fingerprint authentication via greetd multi-stage PAM (v0.6.0)
Fingerprint auth was missing because moongreet rejected multi-stage
auth_message sequences from greetd. With pam_fprintd.so in the PAM
stack, greetd sends non-secret prompts for fingerprint and secret
prompts for password — moongreet now handles both in a loop.

- Replace single-pass auth with multi-stage auth_message loop
- fprintd D-Bus probe (gio::DBusProxy) for UI feedback only
- Fingerprint label shown when device available and fingers enrolled
- 60s socket timeout when fingerprint available (pam_fprintd scan time)
- Config option: [appearance] fingerprint-enabled (default: true)
- Fix: password entry focus loss after auth error (grab_focus while
  widget was insensitive — now re-enable before grab_focus)
2026-03-29 13:47:57 +02:00
nevaforget 77b94a560d fix: prevent edge darkening on GPU-blurred wallpaper (v0.5.3)
GskBlurNode samples pixels outside texture bounds as transparent,
causing visible darkening at wallpaper edges. Fix renders the texture
with 3x-sigma padding before blur, then clips back to original size.
Symmetric fix with moonset v0.7.1.
2026-03-28 23:28:39 +01:00
nevaforget b06b02faac refactor: remove embedded wallpaper from binary (v0.5.2)
Wallpaper is installed by moonarch to /usr/share/moonarch/wallpaper.jpg.
Embedding a 374K JPEG in the binary was redundant. Without a wallpaper
file, GTK background color (Catppuccin Mocha base) shows through and
wallpaper-only windows on secondary monitors are skipped.
2026-03-28 23:26:33 +01:00
nevaforget 9a89da8b13 docs: update for wallpaper removal from binary
Sync documentation with greetd-moongreet wallpaper removal.
2026-03-28 23:23:10 +01:00
nevaforget d5e431d37e fix: make setup_logging() resilient to journal logger failure (v0.5.1)
Replace unwrap() calls with match-based error handling that falls back
to eprintln — prevents panic when running outside a systemd session.
Consistent with moonlock's logging init pattern.
2026-03-28 22:56:39 +01:00
nevaforget 7c10516473 fix: re-audit findings — avatar path safety, persistence logging, tests
- Reject non-UTF-8 avatar paths early instead of passing empty string to GDK
- Log persistence write failures with warn! instead of silently discarding
- Reduce API surface: create_background_picture pub→fn
- Add boundary test for MAX_USERNAME_LENGTH and socket connect failure test
2026-03-28 22:47:21 +01:00
nevaforget 09371b5fd2 fix+perf: audit fixes and GPU blur migration (v0.5.0)
Address all findings from quality, performance, and security audits:
- Filter greetd error descriptions consistently (security)
- Re-enable power buttons after failed action (UX bug)
- Narrow TOCTOU window in avatar loading via symlink_metadata (security)
- Allow @ in usernames for LDAP compatibility
- Eliminate unnecessary Vec allocation in passwd parsing
- Remove dead i18n field, annotate retained-for-future struct fields
- Fix if/if→if/else and noisy test output in power.rs

Replace CPU blur (image crate + disk cache + async orchestration) with
GPU blur via GskBlurNode — symmetric with moonlock and moonset.
Removes ~15 transitive dependencies and ~200 lines of caching code.
2026-03-28 22:34:12 +01:00
nevaforget 3c39467508 perf: cache blurred wallpaper to disk to avoid re-blur on startup
First launch with blur blurs and saves to /var/cache/moongreet/.
Subsequent starts load the cached PNG directly. Cache invalidates
when wallpaper path, size, mtime, or sigma changes.
2026-03-28 21:23:36 +01:00
20 changed files with 1036 additions and 425 deletions
+43
View File
@@ -0,0 +1,43 @@
# ABOUTME: Updates pkgver in moonarch-pkgbuilds after a push to main.
# ABOUTME: Ensures paru detects new versions of this package.
name: Update PKGBUILD version
on:
push:
branches:
- main
jobs:
update-pkgver:
runs-on: moonarch
steps:
- name: Checkout source repo
run: |
git clone --bare http://gitea:3000/nevaforget/greetd-moongreet.git source.git
cd source.git
PKGVER=$(git describe --long --tags | sed 's/^v//;s/-/.r/;s/-/./')
echo "New pkgver: $PKGVER"
echo "$PKGVER" > /tmp/pkgver
- name: Update PKGBUILD
run: |
PKGVER=$(cat /tmp/pkgver)
git clone http://gitea:3000/nevaforget/moonarch-pkgbuilds.git pkgbuilds
cd pkgbuilds
OLD_VER=$(grep '^pkgver=' moongreet-git/PKGBUILD | cut -d= -f2)
if [ "$OLD_VER" = "$PKGVER" ]; then
echo "pkgver already up to date ($PKGVER)"
exit 0
fi
sed -i "s/^pkgver=.*/pkgver=$PKGVER/" moongreet-git/PKGBUILD
sed -i "s/^\tpkgver = .*/\tpkgver = $PKGVER/" moongreet-git/.SRCINFO
echo "Updated pkgver: $OLD_VER → $PKGVER"
git config user.name "pkgver-bot"
git config user.email "gitea@moonarch.de"
git add moongreet-git/PKGBUILD moongreet-git/.SRCINFO
git commit -m "chore(moongreet-git): bump pkgver to $PKGVER"
git -c http.extraHeader="Authorization: token ${{ secrets.PKGBUILD_TOKEN }}" push
+9 -6
View File
@@ -17,7 +17,7 @@ Teil des Moonarch-Ökosystems.
## Projektstruktur ## Projektstruktur
- `src/` — Rust-Quellcode (main.rs, greeter.rs, ipc.rs, config.rs, users.rs, sessions.rs, i18n.rs, power.rs) - `src/` — Rust-Quellcode (main.rs, greeter.rs, ipc.rs, config.rs, users.rs, sessions.rs, i18n.rs, power.rs)
- `resources/` — GResource-Assets (style.css, wallpaper.jpg, default-avatar.svg) - `resources/` — GResource-Assets (style.css, default-avatar.svg)
- `config/` — Beispiel-Konfigurationsdateien für `/etc/moongreet/` und `/etc/greetd/` - `config/` — Beispiel-Konfigurationsdateien für `/etc/moongreet/` und `/etc/greetd/`
- `pkg/` — PKGBUILD für Arch-Linux-Paketierung (`makepkg -sf`) - `pkg/` — PKGBUILD für Arch-Linux-Paketierung (`makepkg -sf`)
@@ -44,8 +44,9 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
- `sessions.rs` — Wayland/X11 Sessions aus .desktop Files - `sessions.rs` — Wayland/X11 Sessions aus .desktop Files
- `power.rs` — Reboot/Shutdown via loginctl - `power.rs` — Reboot/Shutdown via loginctl
- `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen - `i18n.rs` — Locale-Erkennung (LANG / /etc/locale.conf) und String-Tabellen (DE/EN), alle UI- und Login-Fehlermeldungen
- `config.rs` — TOML-Config ([appearance] background, gtk-theme) + Wallpaper-Fallback - `fingerprint.rs` — fprintd D-Bus Probe (gio::DBusProxy) — Geräteerkennung und Enrollment-Check für UI-Feedback
- `greeter.rs`GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC, Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o600 Permissions) - `config.rs`TOML-Config ([appearance] background, gtk-theme, fingerprint-enabled) + Wallpaper-Fallback + Blur-Validierung (finite, clamp 0200)
- `greeter.rs` — GTK4 UI (Overlay-Layout), Login-Flow via greetd IPC (Multi-Stage-Auth für fprintd), Faillock-Warnung, Avatar-Cache, Last-User/Last-Session Persistence (0o700 Dirs, 0o600 Files)
- `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger - `main.rs` — Entry Point, GTK App, Layer Shell Setup, Multi-Monitor, systemd-journal-logger
- `resources/style.css` — Catppuccin-inspiriertes Theme - `resources/style.css` — Catppuccin-inspiriertes Theme
@@ -56,12 +57,14 @@ cd pkg && makepkg -sf && sudo pacman -U moongreet-git-<version>-x86_64.pkg.tar.z
- **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads - **Async Login**: `glib::spawn_future_local` + `gio::spawn_blocking` statt raw Threads
- **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche - **Socket-Cancellation**: `Arc<Mutex<Option<UnixStream>>>` + `AtomicBool` für saubere Abbrüche
- **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>` - **Avatar-Cache**: `HashMap<String, gdk::Texture>` in `Rc<RefCell<GreeterState>>`
- **Symmetrie mit moonset**: Gleiche Patterns (i18n, config, users, power, GResource) - **GPU-Blur via GskBlurNode**: `Snapshot::push_blur()` + `GskRenderer::render_texture()` im `connect_realize` Callback — kein CPU-Blur, kein Disk-Cache, kein `image`-Crate. Blurred Texture wird per `Rc<RefCell<Option<gdk::Texture>>>` über alle Monitore gecacht (1x GPU-Renderpass statt N).
- **Fingerprint via greetd Multi-Stage PAM**: fprintd D-Bus nur als Probe (Gerät/Enrollment), eigentliche Verifizierung läuft über PAM im greetd-Auth-Loop. `auth_message_type: "secret"` → Passwort, alles andere → `None` (PAM entscheidet). 60s Socket-Timeout bei fprintd. Device-Proxy in `GreeterState` gecacht, Generation-Counter gegen Race Conditions bei schnellem User-Switch.
- **Symmetrie mit moonlock/moonset**: Gleiche Patterns (i18n, config, users, power, GResource, GPU-Blur)
- **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt - **Session-Validierung**: Relative Pfade erlaubt (greetd löst PATH auf), nur `..`/Null-Bytes werden abgelehnt
- **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config - **GTK-Theme-Validierung**: Nur alphanumerisch + `_-+.` erlaubt, verhindert Path-Traversal über Config
- **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var - **Journal-Logging**: `systemd-journal-logger` statt File-Logging — `journalctl -t moongreet`, Debug-Level per `MOONGREET_DEBUG` Env-Var
- **File Permissions**: Cache-Dateien 0o600 - **File Permissions**: Cache-Verzeichnisse 0o700 via `DirBuilder::mode()`, Cache-Dateien 0o600
- **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests - **Testbare Persistence**: `save_*_to`/`load_*_from` Varianten mit konfigurierbarem Pfad für Unit-Tests
- **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster (Greeter + Wallpaper-Windows) geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor - **Shared Wallpaper Texture**: `gdk::Texture` wird einmal in `load_background_texture()` dekodiert und per Ref-Count an alle Fenster geteilt — vermeidet redundante JPEG-Dekodierung pro Monitor
- **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None` - **Wallpaper-Validierung**: GResource-Zweig via `resources_lookup_data()` + `from_bytes()` (kein Abort bei fehlendem Pfad), Dateigröße-Limit 50 MB, non-UTF-8-Pfade → `None`
- **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal - **Error-Detail-Filterung**: GDK/greetd-Fehlerdetails nur auf `debug!`-Level, `warn!` ohne interne Details — verhindert Systeminfo-Leak ins Journal
Generated
+20 -124
View File
@@ -2,12 +2,6 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "adler2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.102" version = "1.0.102"
@@ -26,18 +20,6 @@ version = "2.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
[[package]]
name = "bytemuck"
version = "1.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec"
[[package]]
name = "byteorder-lite"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
[[package]] [[package]]
name = "cairo-rs" name = "cairo-rs"
version = "0.22.0" version = "0.22.0"
@@ -78,13 +60,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]] [[package]]
name = "crc32fast" name = "cfg_aliases"
version = "1.5.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "equivalent" name = "equivalent"
@@ -108,15 +87,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
[[package]]
name = "fdeflate"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
dependencies = [
"simd-adler32",
]
[[package]] [[package]]
name = "field-offset" name = "field-offset"
version = "0.3.6" version = "0.3.6"
@@ -127,16 +97,6 @@ dependencies = [
"rustc_version", "rustc_version",
] ]
[[package]]
name = "flate2"
version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "foldhash" name = "foldhash"
version = "0.1.5" version = "0.1.5"
@@ -550,21 +510,6 @@ version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
[[package]]
name = "image"
version = "0.25.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [
"bytemuck",
"byteorder-lite",
"moxcms",
"num-traits",
"png",
"zune-core",
"zune-jpeg",
]
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@@ -628,53 +573,38 @@ dependencies = [
"autocfg", "autocfg",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
dependencies = [
"adler2",
"simd-adler32",
]
[[package]] [[package]]
name = "moongreet" name = "moongreet"
version = "0.4.1" version = "0.7.1"
dependencies = [ dependencies = [
"gdk-pixbuf", "gdk-pixbuf",
"gdk4", "gdk4",
"gio", "gio",
"glib", "glib",
"glib-build-tools", "glib-build-tools",
"graphene-rs",
"gtk4", "gtk4",
"gtk4-layer-shell", "gtk4-layer-shell",
"image",
"log", "log",
"nix",
"serde", "serde",
"serde_json", "serde_json",
"systemd-journal-logger", "systemd-journal-logger",
"tempfile", "tempfile",
"toml 0.8.23", "toml 0.8.23",
"zeroize",
] ]
[[package]] [[package]]
name = "moxcms" name = "nix"
version = "0.8.1" version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [ dependencies = [
"num-traits", "bitflags",
"pxfm", "cfg-if",
] "cfg_aliases",
"libc",
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
] ]
[[package]] [[package]]
@@ -719,19 +649,6 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "png"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]] [[package]]
name = "prettyplease" name = "prettyplease"
version = "0.2.37" version = "0.2.37"
@@ -760,12 +677,6 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pxfm"
version = "0.1.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d"
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.45" version = "1.0.45"
@@ -870,12 +781,6 @@ dependencies = [
"serde_core", "serde_core",
] ]
[[package]]
name = "simd-adler32"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.12" version = "0.4.12"
@@ -1239,23 +1144,14 @@ version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
[[package]]
name = "zeroize"
version = "1.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.21" version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
[[package]]
name = "zune-core"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
[[package]]
name = "zune-jpeg"
version = "0.5.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
dependencies = [
"zune-core",
]
+9 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "moongreet" name = "moongreet"
version = "0.4.0" version = "0.7.1"
edition = "2024" edition = "2024"
description = "A greetd greeter for Wayland with GTK4 and Layer Shell" description = "A greetd greeter for Wayland with GTK4 and Layer Shell"
license = "MIT" license = "MIT"
@@ -15,12 +15,19 @@ gio = "0.22"
toml = "0.8" toml = "0.8"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1" serde_json = "1"
image = { version = "0.25", default-features = false, features = ["jpeg", "png"] } graphene-rs = { version = "0.22", package = "graphene-rs" }
nix = { version = "0.29", features = ["signal"] }
zeroize = { version = "1", features = ["std"] }
log = "0.4" log = "0.4"
systemd-journal-logger = "2.2" systemd-journal-logger = "2.2"
[dev-dependencies] [dev-dependencies]
tempfile = "3" tempfile = "3"
[profile.release]
lto = "thin"
codegen-units = 1
strip = true
[build-dependencies] [build-dependencies]
glib-build-tools = "0.22" glib-build-tools = "0.22"
+36 -1
View File
@@ -1,6 +1,41 @@
# Decisions # Decisions
## 2026-03-28 Optional background blur via `image` crate ## 2026-03-31 Fourth audit: power timeout, timing mitigation, release profile, GREETD_SOCK caching
- **Who**: Ragnar, Dom
- **Why**: Fourth triple audit found moongreet power.rs had no timeout on loginctl (greeter could freeze), username enumeration via timing differential, GREETD_SOCK re-read on every login, missing release profile, and missing GResource compression.
- **Tradeoffs**: 500ms minimum login response time adds slight delay on fast auth but prevents timing-based username enumeration. Power timeout (30s + SIGKILL) matches moonset pattern — aggressive but prevents greeter freeze.
- **How**: (1) power.rs adapted from moonset with 30s timeout + SIGKILL (nix dependency added). (2) 500ms min response floor in attempt_login via Instant + glib::timeout_future. (3) GREETD_SOCK cached in GreeterState at startup. (4) `[profile.release]` with LTO, codegen-units=1, strip. (5) `compressed="true"` on GResource entries. (6) SYNC comments on duplicated blur/background functions.
## 2026-03-30 Full audit fix: security, quality, performance (v0.6.2)
- **Who**: Ragnar, Dom
- **Why**: Three parallel audits (security, code quality, performance) identified 10 actionable findings across the codebase — from world-readable cache dirs to a GPU blur geometry bug to a race condition in fingerprint probing.
- **Tradeoffs**: `too_many_arguments` Clippy warnings suppressed with `#[allow]` rather than introducing a `UiWidgets` struct — GTK's `clone!` macro with `#[weak]` refs requires individual widget parameters, a struct would fight the idiom. Async avatar loading skipped because `Pixbuf` is `!Send`; cache already prevents repeat loads. TOCTOU socket pre-check removed entirely — `connect()` in login_worker already handles errors, the `metadata()` check gave false security guarantees.
- **How**: Cache dirs use `DirBuilder::mode(0o700)` instead of `create_dir_all`. Blur config clamped to `0.0..=200.0` with `is_finite()` guard. Blur texture cached in `Rc<RefCell<Option<gdk::Texture>>>` across monitors. FingerprintProbe device proxy cached in `GreeterState` with generation counter to prevent stale async writes. GPU blur geometry fixed (`-pad` origin shift instead of texture stretching). `is_valid_gtk_theme` extracted as testable function. 9 new tests.
## 2026-03-29 Fingerprint authentication via greetd multi-stage PAM
- **Who**: Ragnar, Dom
- **Why**: moonlock supports fprintd but moongreet rejected multi-stage auth. Users with enrolled fingerprints couldn't use them at the login screen.
- **Tradeoffs**: Direct fprintd D-Bus verification (like moonlock) can't start a greetd session — greetd controls session creation via PAM. Using greetd multi-stage means PAM decides the auth order (fingerprint first, then password fallback), not truly parallel. Acceptable — matches standard pam_fprintd behavior.
- **How**: Replace single-pass auth with a loop over auth_message rounds. Secret prompts get the password, non-secret prompts (fprintd) get None and block until PAM resolves. fprintd D-Bus probe (gio::DBusProxy) only for UI — detecting device availability and enrolled fingers. 60s socket timeout when fingerprint available. Config option `fingerprint-enabled` (default true).
## 2026-03-28 Remove embedded wallpaper from binary
- **Who**: Selene, 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.
- **Tradeoffs**: Without moonarch installed AND without config, greeter 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 wallpaper window creation and background picture when no path available.
## 2026-03-28 GPU blur via GskBlurNode replaces CPU blur
- **Who**: Ragnar, Dom
- **Why**: CPU-side Gaussian blur (`image` crate) blocked the GTK main thread for 500ms2s on 4K wallpapers at cold cache. Disk cache and async orchestration added significant complexity.
- **Tradeoffs**: GPU blur quality is slightly different (box-blur approximation vs true Gaussian), acceptable for wallpaper backgrounds. Removes `image` crate dependency entirely (~15 transitive crates eliminated). 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. Symmetric with moonlock and moonset.
## 2026-03-28 Optional background blur via `image` crate (superseded)
- **Who**: Selene, Dom - **Who**: Selene, Dom
- **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens - **Why**: Blurred wallpaper as greeter background is a common UX pattern for login screens
+6 -2
View File
@@ -13,8 +13,12 @@ Part of the Moonarch ecosystem.
- **Power actions** — Reboot / Shutdown via `loginctl` - **Power actions** — Reboot / Shutdown via `loginctl`
- **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer) - **Layer Shell** — Fullscreen via gtk4-layer-shell (TOP layer)
- **Multi-monitor** — Greeter on primary, wallpaper on all monitors - **Multi-monitor** — Greeter on primary, wallpaper on all monitors
- **GPU blur** — Background blur via GskBlurNode (shared cache across monitors)
- **i18n** — German and English (auto-detected from system locale) - **i18n** — German and English (auto-detected from system locale)
- **Faillock warning** — Warns after 2 failed attempts, locked message after 3 - **Faillock warning** — Warns after 2 failed attempts, locked message after 3
- **Fingerprint** — fprintd support via greetd multi-stage PAM (configurable)
- **Journal logging** — `journalctl -t moongreet`, debug level via `MOONGREET_DEBUG` env var
- **Password wiping** — Zeroize on drop
## Requirements ## Requirements
@@ -65,8 +69,8 @@ cargo test
# Build release # Build release
cargo build --release cargo build --release
# Run locally (without greetd, needs LD_PRELOAD for layer-shell) # Run locally (without greetd, disables layer-shell)
LD_PRELOAD=/usr/lib/libgtk4-layer-shell.so ./target/release/moongreet MOONGREET_NO_LAYER_SHELL=1 ./target/release/moongreet
``` ```
## License ## License
+1 -1
View File
@@ -1,5 +1,5 @@
// ABOUTME: Build script for compiling GResource bundle. // ABOUTME: Build script for compiling GResource bundle.
// ABOUTME: Bundles style.css, wallpaper.jpg, and default-avatar.svg into the binary. // ABOUTME: Bundles style.css and default-avatar.svg into the binary.
fn main() { fn main() {
glib_build_tools::compile_resources( glib_build_tools::compile_resources(
+1 -1
View File
@@ -5,4 +5,4 @@
# Absolute path to wallpaper image # Absolute path to wallpaper image
background = "/usr/share/backgrounds/wallpaper.jpg" background = "/usr/share/backgrounds/wallpaper.jpg"
# GTK theme for the greeter UI # GTK theme for the greeter UI
gtk-theme = "Colloid-Catppuccin" gtk-theme = "Colloid-Grey-Dark-Catppuccin"
+1 -1
View File
@@ -4,7 +4,7 @@
# Maintainer: Dominik Kressler # Maintainer: Dominik Kressler
pkgname=moongreet-git pkgname=moongreet-git
pkgver=0.3.1.r5.g4c9b436 pkgver=0.4.0.r7.g77b94a5
pkgrel=1 pkgrel=1
pkgdesc="A greetd greeter for Wayland with GTK4 and Layer Shell" pkgdesc="A greetd greeter for Wayland with GTK4 and Layer Shell"
arch=('x86_64') arch=('x86_64')
+2 -3
View File
@@ -1,8 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<gresources> <gresources>
<gresource prefix="/dev/moonarch/moongreet"> <gresource prefix="/dev/moonarch/moongreet">
<file>style.css</file> <file compressed="true">style.css</file>
<file>wallpaper.jpg</file> <file compressed="true">default-avatar.svg</file>
<file>default-avatar.svg</file>
</gresource> </gresource>
</gresources> </gresources>
+8 -1
View File
@@ -22,7 +22,7 @@ window.wallpaper {
/* Round avatar image — size is set via set_size_request() in code */ /* Round avatar image — size is set via set_size_request() in code */
.avatar { .avatar {
border-radius: 50%; border-radius: 9999px;
min-width: 128px; min-width: 128px;
min-height: 128px; min-height: 128px;
background-color: @theme_selected_bg_color; background-color: @theme_selected_bg_color;
@@ -54,6 +54,13 @@ window.wallpaper {
font-size: 14px; font-size: 14px;
} }
/* Fingerprint prompt label */
.fingerprint-label {
color: alpha(white, 0.6);
font-size: 13px;
margin-top: 8px;
}
/* User list on the bottom left */ /* User list on the bottom left */
.user-list { .user-list {
background-color: transparent; background-color: transparent;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 366 KiB

+102 -22
View File
@@ -6,7 +6,6 @@ use std::fs;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg"; const MOONARCH_WALLPAPER: &str = "/usr/share/moonarch/wallpaper.jpg";
const GRESOURCE_PREFIX: &str = "/dev/moonarch/moongreet";
/// Default config search path: system-wide config. /// Default config search path: system-wide config.
fn default_config_paths() -> Vec<PathBuf> { fn default_config_paths() -> Vec<PathBuf> {
@@ -26,14 +25,28 @@ struct Appearance {
background_blur: Option<f32>, background_blur: Option<f32>,
#[serde(rename = "gtk-theme")] #[serde(rename = "gtk-theme")]
gtk_theme: Option<String>, gtk_theme: Option<String>,
#[serde(rename = "fingerprint-enabled")]
fingerprint_enabled: Option<bool>,
} }
/// Greeter configuration. /// Greeter configuration.
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone)]
pub struct Config { pub struct Config {
pub background_path: Option<String>, pub background_path: Option<String>,
pub background_blur: Option<f32>, pub background_blur: Option<f32>,
pub gtk_theme: Option<String>, pub gtk_theme: Option<String>,
pub fingerprint_enabled: bool,
}
impl Default for Config {
fn default() -> Self {
Config {
background_path: None,
background_blur: None,
gtk_theme: None,
fingerprint_enabled: true,
}
}
} }
/// Load config from TOML files. Later paths override earlier ones. /// Load config from TOML files. Later paths override earlier ones.
@@ -59,12 +72,19 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
Some(parent.join(&bg).to_string_lossy().to_string()); Some(parent.join(&bg).to_string_lossy().to_string());
} }
} }
if appearance.background_blur.is_some() { if let Some(blur) = appearance.background_blur {
merged.background_blur = appearance.background_blur; if blur.is_finite() {
merged.background_blur = Some(blur.clamp(0.0, 200.0));
} else {
log::warn!("Ignoring non-finite background-blur value");
}
} }
if appearance.gtk_theme.is_some() { if appearance.gtk_theme.is_some() {
merged.gtk_theme = appearance.gtk_theme; merged.gtk_theme = appearance.gtk_theme;
} }
if let Some(fp) = appearance.fingerprint_enabled {
merged.fingerprint_enabled = fp;
}
} }
} }
Err(e) => { Err(e) => {
@@ -78,38 +98,39 @@ pub fn load_config(config_paths: Option<&[PathBuf]>) -> Config {
} }
} }
log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}", merged.background_path, merged.background_blur, merged.gtk_theme); log::debug!("Config result: background={:?}, blur={:?}, gtk_theme={:?}, fingerprint={}", merged.background_path, merged.background_blur, merged.gtk_theme, merged.fingerprint_enabled);
merged merged
} }
/// Resolve the wallpaper path using the fallback hierarchy. /// Resolve the wallpaper path using the fallback hierarchy.
/// ///
/// Priority: config background_path > Moonarch system default > gresource fallback. /// Priority: config background_path > Moonarch system default > None (GTK background color).
pub fn resolve_background_path(config: &Config) -> PathBuf { pub fn resolve_background_path(config: &Config) -> Option<PathBuf> {
resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER)) resolve_background_path_with(config, Path::new(MOONARCH_WALLPAPER))
} }
/// Resolve with configurable moonarch wallpaper path (for testing). /// Resolve with configurable moonarch wallpaper path (for testing).
pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> PathBuf { pub fn resolve_background_path_with(config: &Config, moonarch_wallpaper: &Path) -> Option<PathBuf> {
// User-configured path // User-configured path — reject symlinks to prevent path traversal
if let Some(ref bg) = config.background_path { if let Some(ref bg) = config.background_path {
let path = PathBuf::from(bg); let path = PathBuf::from(bg);
if path.is_file() { if let Ok(meta) = path.symlink_metadata() {
log::debug!("Wallpaper: using config path {}", path.display()); if meta.is_file() && !meta.file_type().is_symlink() {
return path; log::debug!("Wallpaper: using config path {}", path.display());
return Some(path);
}
} }
log::debug!("Wallpaper: config path {} not found, trying fallbacks", path.display()); log::debug!("Wallpaper: config path {} not usable, trying fallbacks", path.display());
} }
// Moonarch ecosystem default // Moonarch ecosystem default
if moonarch_wallpaper.is_file() { if moonarch_wallpaper.is_file() {
log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display()); log::debug!("Wallpaper: using moonarch default {}", moonarch_wallpaper.display());
return moonarch_wallpaper.to_path_buf(); return Some(moonarch_wallpaper.to_path_buf());
} }
// GResource fallback path (loaded from compiled resources at runtime) log::debug!("Wallpaper: no wallpaper found, using GTK background color");
log::debug!("Wallpaper: using GResource fallback"); None
PathBuf::from(format!("{GRESOURCE_PREFIX}/wallpaper.jpg"))
} }
#[cfg(test)] #[cfg(test)]
@@ -122,6 +143,7 @@ mod tests {
assert!(config.background_path.is_none()); assert!(config.background_path.is_none());
assert!(config.background_blur.is_none()); assert!(config.background_blur.is_none());
assert!(config.gtk_theme.is_none()); assert!(config.gtk_theme.is_none());
assert!(config.fingerprint_enabled);
} }
#[test] #[test]
@@ -218,7 +240,7 @@ mod tests {
}; };
assert_eq!( assert_eq!(
resolve_background_path_with(&config, Path::new("/nonexistent")), resolve_background_path_with(&config, Path::new("/nonexistent")),
wallpaper Some(wallpaper)
); );
} }
@@ -229,7 +251,7 @@ mod tests {
..Config::default() ..Config::default()
}; };
let result = resolve_background_path_with(&config, Path::new("/nonexistent")); let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
assert!(result.to_str().unwrap().contains("moongreet")); assert!(result.is_none());
} }
#[test] #[test]
@@ -240,14 +262,72 @@ mod tests {
let config = Config::default(); let config = Config::default();
assert_eq!( assert_eq!(
resolve_background_path_with(&config, &moonarch_wp), resolve_background_path_with(&config, &moonarch_wp),
moonarch_wp Some(moonarch_wp)
); );
} }
#[test] #[test]
fn resolve_uses_gresource_fallback_as_last_resort() { fn resolve_returns_none_when_no_wallpaper_found() {
let config = Config::default(); let config = Config::default();
let result = resolve_background_path_with(&config, Path::new("/nonexistent")); let result = resolve_background_path_with(&config, Path::new("/nonexistent"));
assert!(result.to_str().unwrap().contains("wallpaper.jpg")); assert!(result.is_none());
}
#[test]
fn load_config_fingerprint_enabled_default_true() {
let paths = vec![PathBuf::from("/nonexistent/moongreet.toml")];
let config = load_config(Some(&paths));
assert!(config.fingerprint_enabled);
}
#[test]
fn load_config_fingerprint_disabled() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(&conf, "[appearance]\nfingerprint-enabled = false\n").unwrap();
let paths = vec![conf];
let config = load_config(Some(&paths));
assert!(!config.fingerprint_enabled);
}
// -- Blur validation tests --
#[test]
fn load_config_blur_clamped_to_max() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(&conf, "[appearance]\nbackground-blur = 999.0\n").unwrap();
let config = load_config(Some(&[conf]));
assert_eq!(config.background_blur, Some(200.0));
}
#[test]
fn load_config_blur_negative_clamped_to_zero() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(&conf, "[appearance]\nbackground-blur = -5.0\n").unwrap();
let config = load_config(Some(&[conf]));
assert_eq!(config.background_blur, Some(0.0));
}
#[test]
fn load_config_blur_nan_rejected() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
// TOML doesn't support NaN literals, but the parser may return NaN for nan
fs::write(&conf, "[appearance]\nbackground-blur = nan\n").unwrap();
let config = load_config(Some(&[conf]));
// nan is not valid TOML float, so the whole config parse fails → no blur
assert!(config.background_blur.is_none());
}
#[test]
fn load_config_blur_inf_rejected() {
let dir = tempfile::tempdir().unwrap();
let conf = dir.path().join("moongreet.toml");
fs::write(&conf, "[appearance]\nbackground-blur = inf\n").unwrap();
let config = load_config(Some(&[conf]));
// inf is valid TOML → parsed as f32::INFINITY → rejected by is_finite() guard
assert!(config.background_blur.is_none());
} }
} }
+142
View File
@@ -0,0 +1,142 @@
// ABOUTME: fprintd D-Bus probe for fingerprint device availability.
// ABOUTME: Checks if fprintd is running and the user has enrolled fingerprints.
use gio::prelude::*;
use gtk4::gio;
const FPRINTD_BUS_NAME: &str = "net.reactivated.Fprint";
const FPRINTD_MANAGER_PATH: &str = "/net/reactivated/Fprint/Manager";
const FPRINTD_MANAGER_IFACE: &str = "net.reactivated.Fprint.Manager";
const FPRINTD_DEVICE_IFACE: &str = "net.reactivated.Fprint.Device";
const DBUS_TIMEOUT_MS: i32 = 3000;
const FPRINTD_DEVICE_PREFIX: &str = "/net/reactivated/Fprint/Device/";
/// Lightweight fprintd probe — detects device availability and finger enrollment.
/// Does NOT perform verification (that happens through greetd/PAM).
pub struct FingerprintProbe {
device_proxy: Option<gio::DBusProxy>,
}
impl FingerprintProbe {
/// Create a probe without any D-Bus connections.
/// Call `init_async().await` to connect to fprintd.
pub fn new() -> Self {
FingerprintProbe {
device_proxy: None,
}
}
/// Connect to fprintd on the system bus and discover the default device.
pub async fn init_async(&mut self) {
let manager = match gio::DBusProxy::for_bus_future(
gio::BusType::System,
gio::DBusProxyFlags::NONE,
None,
FPRINTD_BUS_NAME,
FPRINTD_MANAGER_PATH,
FPRINTD_MANAGER_IFACE,
)
.await
{
Ok(m) => m,
Err(e) => {
log::debug!("fprintd manager not available: {e}");
return;
}
};
let result = match manager
.call_future("GetDefaultDevice", None, gio::DBusCallFlags::NONE, DBUS_TIMEOUT_MS)
.await
{
Ok(r) => r,
Err(e) => {
log::debug!("fprintd GetDefaultDevice failed: {e}");
return;
}
};
let device_path = match result.child_value(0).get::<String>() {
Some(p) => p,
None => {
log::debug!("fprintd: unexpected GetDefaultDevice response type");
return;
}
};
if device_path.is_empty() {
return;
}
if !device_path.starts_with(FPRINTD_DEVICE_PREFIX) {
log::warn!("Unexpected fprintd device path: {device_path}");
return;
}
match gio::DBusProxy::for_bus_future(
gio::BusType::System,
gio::DBusProxyFlags::NONE,
None,
FPRINTD_BUS_NAME,
&device_path,
FPRINTD_DEVICE_IFACE,
)
.await
{
Ok(proxy) => {
self.device_proxy = Some(proxy);
}
Err(e) => {
log::debug!("fprintd device proxy failed: {e}");
}
}
}
/// Check if the user has enrolled fingerprints on the default device.
/// Returns false if fprintd is unavailable or the user has no enrollments.
pub async fn is_available_async(&self, username: &str) -> bool {
let proxy = match &self.device_proxy {
Some(p) => p,
None => return false,
};
let args = glib::Variant::from((&username,));
match proxy
.call_future(
"ListEnrolledFingers",
Some(&args),
gio::DBusCallFlags::NONE,
DBUS_TIMEOUT_MS,
)
.await
{
Ok(result) => match result.child_value(0).get::<Vec<String>>() {
Some(fingers) => !fingers.is_empty(),
None => {
log::debug!("fprintd: unexpected ListEnrolledFingers response type");
false
}
},
Err(_) => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_probe_has_no_device() {
let probe = FingerprintProbe::new();
assert!(probe.device_proxy.is_none());
}
#[test]
fn constants_are_defined() {
assert!(!FPRINTD_BUS_NAME.is_empty());
assert!(!FPRINTD_MANAGER_PATH.is_empty());
assert!(!FPRINTD_MANAGER_IFACE.is_empty());
assert!(!FPRINTD_DEVICE_IFACE.is_empty());
assert!(DBUS_TIMEOUT_MS > 0);
}
}
+521 -178
View File
File diff suppressed because it is too large Load Diff
+11 -15
View File
@@ -4,6 +4,7 @@
use std::env; use std::env;
use std::fs; use std::fs;
use std::path::Path; use std::path::Path;
use std::sync::OnceLock;
const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf"; const DEFAULT_LOCALE_CONF: &str = "/etc/locale.conf";
@@ -19,16 +20,13 @@ pub struct Strings {
pub no_session_selected: &'static str, pub no_session_selected: &'static str,
pub greetd_sock_not_set: &'static str, pub greetd_sock_not_set: &'static str,
pub greetd_sock_not_absolute: &'static str, pub greetd_sock_not_absolute: &'static str,
pub greetd_sock_not_socket: &'static str,
pub greetd_sock_unreachable: &'static str,
pub auth_failed: &'static str, pub auth_failed: &'static str,
pub wrong_password: &'static str, pub wrong_password: &'static str,
pub multi_stage_unsupported: &'static str, pub fingerprint_prompt: &'static str,
pub invalid_session_command: &'static str, pub invalid_session_command: &'static str,
pub session_start_failed: &'static str, pub session_start_failed: &'static str,
pub reboot_failed: &'static str, pub reboot_failed: &'static str,
pub shutdown_failed: &'static str, pub shutdown_failed: &'static str,
pub connection_error: &'static str,
pub socket_error: &'static str, pub socket_error: &'static str,
pub unexpected_greetd_response: &'static str, pub unexpected_greetd_response: &'static str,
@@ -44,16 +42,13 @@ const STRINGS_DE: Strings = Strings {
no_session_selected: "Keine Session ausgewählt", no_session_selected: "Keine Session ausgewählt",
greetd_sock_not_set: "GREETD_SOCK nicht gesetzt", greetd_sock_not_set: "GREETD_SOCK nicht gesetzt",
greetd_sock_not_absolute: "GREETD_SOCK ist kein absoluter Pfad", greetd_sock_not_absolute: "GREETD_SOCK ist kein absoluter Pfad",
greetd_sock_not_socket: "GREETD_SOCK zeigt nicht auf einen Socket",
greetd_sock_unreachable: "GREETD_SOCK nicht erreichbar",
auth_failed: "Authentifizierung fehlgeschlagen", auth_failed: "Authentifizierung fehlgeschlagen",
wrong_password: "Falsches Passwort", wrong_password: "Falsches Passwort",
multi_stage_unsupported: "Mehrstufige Authentifizierung wird nicht unterstützt", fingerprint_prompt: "Fingerabdruck auflegen oder Passwort eingeben",
invalid_session_command: "Ungültiger Session-Befehl", invalid_session_command: "Ungültiger Session-Befehl",
session_start_failed: "Session konnte nicht gestartet werden", session_start_failed: "Session konnte nicht gestartet werden",
reboot_failed: "Neustart fehlgeschlagen", reboot_failed: "Neustart fehlgeschlagen",
shutdown_failed: "Herunterfahren fehlgeschlagen", shutdown_failed: "Herunterfahren fehlgeschlagen",
connection_error: "Verbindungsfehler",
socket_error: "Socket-Fehler", socket_error: "Socket-Fehler",
unexpected_greetd_response: "Unerwartete Antwort von greetd", unexpected_greetd_response: "Unerwartete Antwort von greetd",
faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!", faillock_attempts_remaining: "Noch {n} Versuch(e) vor Kontosperrung!",
@@ -67,16 +62,13 @@ const STRINGS_EN: Strings = Strings {
no_session_selected: "No session selected", no_session_selected: "No session selected",
greetd_sock_not_set: "GREETD_SOCK not set", greetd_sock_not_set: "GREETD_SOCK not set",
greetd_sock_not_absolute: "GREETD_SOCK is not an absolute path", greetd_sock_not_absolute: "GREETD_SOCK is not an absolute path",
greetd_sock_not_socket: "GREETD_SOCK does not point to a socket",
greetd_sock_unreachable: "GREETD_SOCK unreachable",
auth_failed: "Authentication failed", auth_failed: "Authentication failed",
wrong_password: "Wrong password", wrong_password: "Wrong password",
multi_stage_unsupported: "Multi-stage authentication is not supported", fingerprint_prompt: "Place finger on reader or enter password",
invalid_session_command: "Invalid session command", invalid_session_command: "Invalid session command",
session_start_failed: "Failed to start session", session_start_failed: "Failed to start session",
reboot_failed: "Reboot failed", reboot_failed: "Reboot failed",
shutdown_failed: "Shutdown failed", shutdown_failed: "Shutdown failed",
connection_error: "Connection error",
socket_error: "Socket error", socket_error: "Socket error",
unexpected_greetd_response: "Unexpected response from greetd", unexpected_greetd_response: "Unexpected response from greetd",
faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!", faillock_attempts_remaining: "{n} attempt(s) remaining before lockout!",
@@ -138,14 +130,17 @@ pub fn detect_locale() -> String {
result result
} }
/// Cached locale — detected once, reused for the lifetime of the process.
static CACHED_LOCALE: OnceLock<String> = OnceLock::new();
/// Return the string table for the given locale, defaulting to English. /// Return the string table for the given locale, defaulting to English.
pub fn load_strings(locale: Option<&str>) -> &'static Strings { pub fn load_strings(locale: Option<&str>) -> &'static Strings {
let locale = match locale { let locale = match locale {
Some(l) => l.to_string(), Some(l) => l,
None => detect_locale(), None => CACHED_LOCALE.get_or_init(detect_locale),
}; };
match locale.as_str() { match locale {
"de" => &STRINGS_DE, "de" => &STRINGS_DE,
_ => &STRINGS_EN, _ => &STRINGS_EN,
} }
@@ -285,6 +280,7 @@ mod tests {
assert!(!s.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set"); assert!(!s.greetd_sock_not_set.is_empty(), "{locale}: greetd_sock_not_set");
assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed"); assert!(!s.auth_failed.is_empty(), "{locale}: auth_failed");
assert!(!s.wrong_password.is_empty(), "{locale}: wrong_password"); assert!(!s.wrong_password.is_empty(), "{locale}: wrong_password");
assert!(!s.fingerprint_prompt.is_empty(), "{locale}: fingerprint_prompt");
assert!(!s.reboot_failed.is_empty(), "{locale}: reboot_failed"); assert!(!s.reboot_failed.is_empty(), "{locale}: reboot_failed");
assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed"); assert!(!s.shutdown_failed.is_empty(), "{locale}: shutdown_failed");
assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining"); assert!(!s.faillock_attempts_remaining.is_empty(), "{locale}: faillock_attempts_remaining");
+20 -14
View File
@@ -2,6 +2,7 @@
// ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows. // ABOUTME: Sets up GTK Application, Layer Shell, CSS, and multi-monitor windows.
mod config; mod config;
mod fingerprint;
mod greeter; mod greeter;
mod i18n; mod i18n;
mod ipc; mod ipc;
@@ -19,7 +20,7 @@ fn load_css(display: &gdk::Display) {
gtk::style_context_add_provider_for_display( gtk::style_context_add_provider_for_display(
display, display,
&css_provider, &css_provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION, gtk::STYLE_PROVIDER_PRIORITY_USER,
); );
} }
@@ -51,20 +52,19 @@ fn activate(app: &gtk::Application) {
// Load config and resolve wallpaper // Load config and resolve wallpaper
let config = config::load_config(None); let config = config::load_config(None);
let bg_path = config::resolve_background_path(&config); let bg_texture = config::resolve_background_path(&config)
log::debug!("Background path: {}", bg_path.display()); .and_then(|path| {
log::debug!("Background path: {}", path.display());
greeter::load_background_texture(&path)
});
// Load background texture once — shared across all windows let blur_cache = std::rc::Rc::new(std::cell::RefCell::new(None));
let bg_texture = greeter::load_background_texture(&bg_path, config.background_blur);
if bg_texture.is_none() {
log::error!("Failed to load background texture — greeter will start without wallpaper");
}
let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err(); let use_layer_shell = std::env::var("MOONGREET_NO_LAYER_SHELL").is_err();
log::debug!("Layer shell: {use_layer_shell}"); log::debug!("Layer shell: {use_layer_shell}");
// Main greeter window (login UI) — compositor picks focused monitor // Main greeter window (login UI) — compositor picks focused monitor
let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, app); let greeter_window = greeter::create_greeter_window(bg_texture.as_ref(), &config, &blur_cache, app);
if use_layer_shell { if use_layer_shell {
setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top); setup_layer_shell(&greeter_window, true, gtk4_layer_shell::Layer::Top);
} }
@@ -81,7 +81,7 @@ fn activate(app: &gtk::Application) {
.item(i) .item(i)
.and_then(|obj| obj.downcast::<gdk::Monitor>().ok()) .and_then(|obj| obj.downcast::<gdk::Monitor>().ok())
{ {
let wallpaper = greeter::create_wallpaper_window(texture, app); let wallpaper = greeter::create_wallpaper_window(texture, config.background_blur, &blur_cache, app);
setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom); setup_layer_shell(&wallpaper, false, gtk4_layer_shell::Layer::Bottom);
wallpaper.set_monitor(Some(&monitor)); wallpaper.set_monitor(Some(&monitor));
wallpaper.present(); wallpaper.present();
@@ -91,10 +91,16 @@ fn activate(app: &gtk::Application) {
} }
fn setup_logging() { fn setup_logging() {
systemd_journal_logger::JournalLog::new() match systemd_journal_logger::JournalLog::new() {
.unwrap() Ok(logger) => {
.install() if let Err(e) = logger.install() {
.unwrap(); eprintln!("Failed to install journal logger: {e}");
}
}
Err(e) => {
eprintln!("Failed to create journal logger: {e}");
}
}
let level = if std::env::var("MOONGREET_DEBUG").is_ok() { let level = if std::env::var("MOONGREET_DEBUG").is_ok() {
log::LevelFilter::Debug log::LevelFilter::Debug
} else { } else {
+70 -20
View File
@@ -2,11 +2,18 @@
// ABOUTME: Wrappers around system commands for the greeter UI. // ABOUTME: Wrappers around system commands for the greeter UI.
use std::fmt; use std::fmt;
use std::process::Command; use std::io::Read;
use std::process::{Command, Stdio};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Duration;
const POWER_TIMEOUT: Duration = Duration::from_secs(30);
#[derive(Debug)] #[derive(Debug)]
pub enum PowerError { pub enum PowerError {
CommandFailed { action: &'static str, message: String }, CommandFailed { action: &'static str, message: String },
Timeout { action: &'static str },
} }
impl fmt::Display for PowerError { impl fmt::Display for PowerError {
@@ -15,42 +22,79 @@ impl fmt::Display for PowerError {
PowerError::CommandFailed { action, message } => { PowerError::CommandFailed { action, message } => {
write!(f, "{action} failed: {message}") write!(f, "{action} failed: {message}")
} }
PowerError::Timeout { action } => {
write!(f, "{action} timed out")
}
} }
} }
} }
impl std::error::Error for PowerError {} impl std::error::Error for PowerError {}
/// Run a command and return a PowerError on failure. /// Run a command with timeout and return a PowerError on failure.
///
/// Uses blocking `child.wait()` with a separate timeout thread that sends
/// SIGKILL after POWER_TIMEOUT. This runs inside `gio::spawn_blocking`,
/// so blocking is expected.
fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> { fn run_command(action: &'static str, program: &str, args: &[&str]) -> Result<(), PowerError> {
log::debug!("Power action: {action} ({program} {args:?})"); log::debug!("Power action: {action} ({program} {args:?})");
let child = Command::new(program) let mut child = Command::new(program)
.args(args) .args(args)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn() .spawn()
.map_err(|e| PowerError::CommandFailed { .map_err(|e| PowerError::CommandFailed {
action, action,
message: e.to_string(), message: e.to_string(),
})?; })?;
let output = child let child_pid = nix::unistd::Pid::from_raw(child.id() as i32);
.wait_with_output() let done = Arc::new(AtomicBool::new(false));
.map_err(|e| PowerError::CommandFailed { let done_clone = done.clone();
action,
message: e.to_string(),
})?;
if output.status.success() { let timeout_thread = std::thread::spawn(move || {
log::debug!("Power action {action} completed successfully"); let interval = Duration::from_millis(100);
} let mut elapsed = Duration::ZERO;
if !output.status.success() { while elapsed < POWER_TIMEOUT {
let stderr = String::from_utf8_lossy(&output.stderr); std::thread::sleep(interval);
return Err(PowerError::CommandFailed { if done_clone.load(Ordering::Relaxed) {
action, return;
message: format!("exit code {}: {}", output.status, stderr.trim()), }
}); elapsed += interval;
} }
// ESRCH if the process already exited — harmless
let _ = nix::sys::signal::kill(child_pid, nix::sys::signal::Signal::SIGKILL);
});
Ok(()) let status = child.wait().map_err(|e| PowerError::CommandFailed {
action,
message: e.to_string(),
})?;
done.store(true, Ordering::Relaxed);
let _ = timeout_thread.join();
if status.success() {
log::debug!("Power action {action} completed");
Ok(())
} else {
#[cfg(unix)]
{
use std::os::unix::process::ExitStatusExt;
if status.signal() == Some(9) {
return Err(PowerError::Timeout { action });
}
}
let mut stderr_buf = String::new();
if let Some(mut stderr) = child.stderr.take() {
let _ = stderr.read_to_string(&mut stderr_buf);
}
Err(PowerError::CommandFailed {
action,
message: format!("exit code {}: {}", status, stderr_buf.trim()),
})
}
} }
/// Reboot the system via loginctl. /// Reboot the system via loginctl.
@@ -76,6 +120,12 @@ mod tests {
assert_eq!(err.to_string(), "reboot failed: No such file or directory"); assert_eq!(err.to_string(), "reboot failed: No such file or directory");
} }
#[test]
fn power_error_timeout_display() {
let err = PowerError::Timeout { action: "shutdown" };
assert_eq!(err.to_string(), "shutdown timed out");
}
#[test] #[test]
fn run_command_returns_error_for_missing_binary() { fn run_command_returns_error_for_missing_binary() {
let result = run_command("test", "nonexistent-binary-xyz", &[]); let result = run_command("test", "nonexistent-binary-xyz", &[]);
+9 -8
View File
@@ -12,6 +12,7 @@ const DEFAULT_XSESSION_DIRS: &[&str] = &["/usr/share/xsessions"];
pub struct Session { pub struct Session {
pub name: String, pub name: String,
pub exec_cmd: String, pub exec_cmd: String,
#[allow(dead_code)] // Retained for future Wayland-only filtering
pub session_type: String, pub session_type: String,
} }
@@ -35,14 +36,14 @@ fn parse_desktop_file(path: &Path, session_type: &str) -> Option<Session> {
continue; continue;
} }
if let Some(value) = line.strip_prefix("Name=") { if let Some(value) = line.strip_prefix("Name=")
if name.is_none() { && name.is_none()
name = Some(value.to_string()); {
} name = Some(value.to_string());
} else if let Some(value) = line.strip_prefix("Exec=") { } else if let Some(value) = line.strip_prefix("Exec=")
if exec_cmd.is_none() { && exec_cmd.is_none()
exec_cmd = Some(value.to_string()); {
} exec_cmd = Some(value.to_string());
} }
} }
+25 -26
View File
@@ -23,9 +23,11 @@ const NOLOGIN_SHELLS: &[&str] = &[
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct User { pub struct User {
pub username: String, pub username: String,
#[allow(dead_code)] // Retained for debugging and future UID-based features
pub uid: u32, pub uid: u32,
pub gecos: String, pub gecos: String,
pub home: PathBuf, pub home: PathBuf,
#[allow(dead_code)] // Retained for debugging and future shell-based filtering
pub shell: String, pub shell: String,
} }
@@ -55,23 +57,20 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
let mut users = Vec::new(); let mut users = Vec::new();
for line in content.lines() { for line in content.lines() {
let parts: Vec<&str> = line.split(':').collect(); let mut fields = line.splitn(7, ':');
if parts.len() < 7 { let (Some(username), Some(_pw), Some(uid_str), Some(_gid), Some(gecos), Some(home), Some(shell)) =
(fields.next(), fields.next(), fields.next(), fields.next(),
fields.next(), fields.next(), fields.next())
else {
continue; continue;
} };
let username = parts[0];
let uid_str = parts[2];
let gecos = parts[4];
let home = parts[5];
let shell = parts[6];
let uid = match uid_str.parse::<u32>() { let uid = match uid_str.parse::<u32>() {
Ok(u) => u, Ok(u) => u,
Err(_) => continue, Err(_) => continue,
}; };
if uid < MIN_UID || uid > MAX_UID { if !(MIN_UID..=MAX_UID).contains(&uid) {
continue; continue;
} }
if NOLOGIN_SHELLS.contains(&shell) { if NOLOGIN_SHELLS.contains(&shell) {
@@ -95,7 +94,7 @@ pub fn get_users(passwd_path: Option<&Path>) -> Vec<User> {
users users
} }
/// Find avatar for a user: AccountsService icon > ~/.face > None. /// Find avatar for a user: ~/.face > AccountsService icon > None.
/// Rejects symlinks to prevent path traversal. /// Rejects symlinks to prevent path traversal.
pub fn get_avatar_path(username: &str, home: &Path) -> Option<PathBuf> { pub fn get_avatar_path(username: &str, home: &Path) -> Option<PathBuf> {
get_avatar_path_with(username, home, Path::new(DEFAULT_ACCOUNTSSERVICE_DIR)) get_avatar_path_with(username, home, Path::new(DEFAULT_ACCOUNTSSERVICE_DIR))
@@ -107,30 +106,30 @@ pub fn get_avatar_path_with(
home: &Path, home: &Path,
accountsservice_dir: &Path, accountsservice_dir: &Path,
) -> Option<PathBuf> { ) -> Option<PathBuf> {
// AccountsService icon takes priority // ~/.face takes priority (consistent with moonlock/moonset)
let face = home.join(".face");
if let Ok(meta) = face.symlink_metadata() {
if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar for {username}: {}", face.display());
} else if meta.is_file() {
log::debug!("Avatar for {username}: ~/.face {}", face.display());
return Some(face);
}
}
// AccountsService icon fallback
if accountsservice_dir.exists() { if accountsservice_dir.exists() {
let icon = accountsservice_dir.join(username); let icon = accountsservice_dir.join(username);
if let Ok(meta) = icon.symlink_metadata() { if let Ok(meta) = icon.symlink_metadata() {
if meta.file_type().is_symlink() { if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar for {username}: {}", icon.display()); log::warn!("Rejecting symlink avatar for {username}: {}", icon.display());
} else { } else if meta.is_file() {
log::debug!("Avatar for {username}: AccountsService {}", icon.display()); log::debug!("Avatar for {username}: AccountsService {}", icon.display());
return Some(icon); return Some(icon);
} }
} }
} }
// ~/.face fallback
let face = home.join(".face");
if let Ok(meta) = face.symlink_metadata() {
if meta.file_type().is_symlink() {
log::warn!("Rejecting symlink avatar for {username}: {}", face.display());
} else {
log::debug!("Avatar for {username}: ~/.face {}", face.display());
return Some(face);
}
}
log::debug!("No avatar found for {username}"); log::debug!("No avatar found for {username}");
None None
} }
@@ -249,7 +248,7 @@ mod tests {
} }
#[test] #[test]
fn accountsservice_icon_takes_priority() { fn face_file_takes_priority_over_accountsservice() {
let dir = tempfile::tempdir().unwrap(); let dir = tempfile::tempdir().unwrap();
let icons_dir = dir.path().join("icons"); let icons_dir = dir.path().join("icons");
fs::create_dir(&icons_dir).unwrap(); fs::create_dir(&icons_dir).unwrap();
@@ -262,7 +261,7 @@ mod tests {
fs::write(&face, "fake face").unwrap(); fs::write(&face, "fake face").unwrap();
let path = get_avatar_path_with("testuser", &home, &icons_dir); let path = get_avatar_path_with("testuser", &home, &icons_dir);
assert_eq!(path, Some(icon)); assert_eq!(path, Some(face));
} }
#[test] #[test]