Compare commits

...

2 Commits

Author SHA1 Message Date
Amir Alexander Abdelbaki 88f3b72343 fix(hyprlua): support howdy 2.x in enroll-biometrics
howdy 2.x installs its CLI under root-only /usr/lib/security/howdy/ with
/usr/bin/howdy symlinked into it, so `command -v howdy` reports "not found"
for a normal user — the script wrongly thought howdy wasn't installed and
tried to reinstall it. Detect via the symlink and pacman -Qq instead; all
ops already run through `sudo howdy`.

This version also has no pam_howdy.so — it authenticates via pam_python.so
loading /usr/lib/security/howdy/pam.py. Switch the emitted PAM block to that
module and add pam_python_require() to install the AUR `pam-python`
dependency, replacing the dead pam_howdy.so existence check.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:13:26 +02:00
Amir Alexander Abdelbaki dc3ffb68b7 fix(sysupdate): make hypr config sync atomic
The hypr/ sync removed each live item with `rm -rf` before `cp`. For
hyprland.lua this opened a window where the file was absent; if Hyprland's
live config watcher reloaded during it, it errored ("cannot open
hyprland.lua") and wrote out its fallback hyprland.conf stub, which then
leaked into the repo.

Stage each item into a temp dir on the same filesystem and `mv` it into
place — for files this is an atomic rename(2), so the watcher never sees
hyprland.lua missing. Directories are cleared first (mv can't replace a
non-empty dir), but those aren't watched config files.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-13 22:13:26 +02:00
2 changed files with 76 additions and 17 deletions

View File

@ -14,7 +14,10 @@ PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
# Both factors required, with the normal password stack kept as a fallback so a
# dead camera or absent key can never lock you out.
PAM_TARGETS=(/etc/pam.d/sudo /etc/pam.d/hyprlock /etc/pam.d/login)
PAM_HOWDY_SO="/usr/lib/security/pam_howdy.so"
# howdy 2.x has no pam_howdy.so — it authenticates through pam_python.so loading
# its own entrypoint script. pam_python.so ships in the AUR package `pam-python`.
PAM_PYTHON_SO="/usr/lib/security/pam_python.so"
HOWDY_PAM_PY="/usr/lib/security/howdy/pam.py"
PAM_U2F_SO="/usr/lib/security/pam_u2f.so"
U2F_MAP="/etc/u2f_mappings"
PAM_MARK_BEGIN="# enroll-biometrics:begin howdy+u2f 2fa"
@ -143,7 +146,16 @@ presence_test_camera() {
}
# ── Howdy helpers ─────────────────────────────────────────────────────────────
howdy_installed() { command -v howdy &>/dev/null; }
# Detection must survive howdy 2.x's layout: its CLI lives under root-only
# /usr/lib/security/howdy/ with /usr/bin/howdy symlinked into it, so a normal
# user can neither resolve nor run the link — `command -v howdy` reports "not
# found" even when it is installed. Every op here invokes it via `sudo howdy`
# anyway, so fall back to the symlink itself and the package DB.
howdy_installed() {
command -v howdy &>/dev/null && return 0
[[ -L /usr/bin/howdy || -e /usr/bin/howdy ]] && return 0
command -v pacman &>/dev/null && pacman -Qq howdy &>/dev/null
}
howdy_require() {
howdy_installed && return 0
@ -243,6 +255,42 @@ howdy_test() {
fi
}
# ── howdy PAM module (pam_python.so) ──────────────────────────────────────────
# howdy's PAM entrypoint is a Python script loaded by pam_python.so. The module
# is /usr/lib/security/pam_python.so directly (world-traversable, unlike howdy's
# own root-only dir), so a plain existence test is reliable here; pacman -Qq is
# the fallback in case it lands elsewhere.
pam_python_installed() {
[[ -e "$PAM_PYTHON_SO" ]] && return 0
command -v pacman &>/dev/null && pacman -Qq pam-python &>/dev/null
}
pam_python_require() {
pam_python_installed && return 0
# pam-python is AUR-only, so it needs an AUR helper just like howdy.
local helper
for h in yay paru; do
command -v "$h" &>/dev/null && { helper="$h"; break; }
done
if [[ -z "$helper" ]]; then
msg "No AUR Helper" \
"howdy's PAM module needs the AUR package 'pam-python', which requires an AUR helper (yay or paru).\n\nInstall yay first, then re-run." \
11 62
return 1
fi
dialog --backtitle "$BACKTITLE" --title " pam-python Needed " \
--yesno "\nhowdy authenticates via pam_python.so, provided by the AUR package 'pam-python'.\n\nInstall it now via ${helper}?" 10 62 || return 1
clear
printf "\nInstalling pam-python via %s...\n\n" "$helper"
"$helper" -S --noconfirm --needed pam-python
pam_python_installed || {
msg "Install Failed" "pam-python installation failed.\nInstall it manually: ${helper} -S pam-python" 8 58
return 1
}
}
# ── FIDO U2F helpers ────────────────────────────────────────────────────────────
fido_installed() { [[ -e "$PAM_U2F_SO" ]] && command -v pamu2fcfg &>/dev/null; }
@ -291,7 +339,7 @@ fido_register() {
# existing (password) auth lines below.
pam_emit_block() {
printf '%s\n' "$PAM_MARK_BEGIN"
printf 'auth [success=ignore default=1] pam_howdy.so\n'
printf 'auth [success=ignore default=1] pam_python.so %s\n' "$HOWDY_PAM_PY"
printf 'auth sufficient pam_u2f.so authfile=%s cue\n' "$U2F_MAP"
printf '%s\n' "$PAM_MARK_END"
}
@ -362,13 +410,9 @@ pam_remove_hyprlock() {
}
pam_setup() {
howdy_require || return
fido_require || return
if [[ ! -e "$PAM_HOWDY_SO" ]]; then
msg "Missing PAM Module" "pam_howdy.so not found at:\n$PAM_HOWDY_SO\n\nReinstall howdy and try again." 10 60
return
fi
howdy_require || return
pam_python_require || return
fido_require || return
dialog --backtitle "$BACKTITLE" --title " Set Up PAM 2FA " --yesno \
"\nThis will require BOTH face (howdy) and a FIDO key for:\n\n ${PAM_TARGETS[*]}\n\nYour password still works if a factor is unavailable.\nBackups of each file are saved as *.bak.<timestamp>.\n\nContinue?" 15 64 || return

View File

@ -616,15 +616,30 @@ sync_configs() {
[[ "$skip" == true ]] && continue
if [[ "$name" == "hypr" ]]; then
# Copy hypr/ contents but preserve ~/.config/hypr/usr/
# Copy hypr/ contents but preserve ~/.config/hypr/usr/.
# Stage each item into a temp dir on the same filesystem, then mv it
# into place — for files this is an atomic rename(2), so Hyprland's
# live config watcher never sees hyprland.lua momentarily missing
# (which otherwise makes it emit its fallback hyprland.conf stub).
mkdir -p "$target/hypr"
local hypr_ok=true
while IFS= read -r -d '' hitem; do
local hname; hname="$(basename "$hitem")"
[[ "$hname" == "usr" ]] && continue
rm -rf "${target}/hypr/${hname}" && cp -r "$hitem" "$target/hypr/" \
|| hypr_ok=false
done < <(find "$item" -maxdepth 1 -mindepth 1 -print0 | sort -z)
local stage; stage="$(mktemp -d "${target}/hypr/.sync.XXXXXX")" || hypr_ok=false
if $hypr_ok; then
while IFS= read -r -d '' hitem; do
local hname; hname="$(basename "$hitem")"
[[ "$hname" == "usr" ]] && continue
if cp -r "$hitem" "$stage/"; then
# mv can't atomically replace a non-empty dir, so clear
# an existing directory target first; files swap atomically.
[[ -d "$stage/$hname" && -d "${target}/hypr/${hname}" ]] \
&& rm -rf "${target}/hypr/${hname}"
mv -f "$stage/$hname" "${target}/hypr/${hname}" || hypr_ok=false
else
hypr_ok=false
fi
done < <(find "$item" -maxdepth 1 -mindepth 1 -print0 | sort -z)
rm -rf "$stage"
fi
if $hypr_ok; then
ok "synced hypr ${DI}(usr/ preserved)${RS}"
(( synced++ )) || true