fix(hyprlua): set howdy device_path + add camera chooser with kitty preview
Enrollment failed because howdy's own config.ini kept device_path=none, so VideoCapture aborted before ever opening the lens (generic exit 1). The presence-detect camera setting is separate and never reached howdy. - add a "Howdy face auth — configure camera" menu entry that writes device_path into /usr/lib/security/howdy/config.ini - guard howdy_add: detect an unset/stale device_path and offer to fix it before enrolling, instead of surfacing howdy's opaque exit 1 - shared camera chooser shows a live still from the selected node via kitty's icat (gated on KITTY_WINDOW_ID), used for presence + howdy - support non-IR colour webcams: detect IR vs colour from the preview's saturation and tune recording_plugin (ffmpeg) + dark_threshold per type Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>main
parent
88f3b72343
commit
5f83f10f48
|
|
@ -10,6 +10,12 @@ PRESENCE_CFG="${XDG_CONFIG_HOME:-$HOME/.config}/presence-detect.conf"
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
|
PYTHON_DETECT="$SCRIPT_DIR/python/presence_detect.py"
|
||||||
|
|
||||||
|
# howdy keeps its own camera setting in this INI, entirely separate from the
|
||||||
|
# presence-detect camera. Enrollment aborts in VideoCapture before touching the
|
||||||
|
# lens if `device_path` is left at the package default of `none`, so the howdy
|
||||||
|
# menu has to write a real /dev/video* node in here.
|
||||||
|
HOWDY_CONFIG="/usr/lib/security/howdy/config.ini"
|
||||||
|
|
||||||
# ── PAM 2FA (howdy face + FIDO U2F) ────────────────────────────────────────────
|
# ── PAM 2FA (howdy face + FIDO U2F) ────────────────────────────────────────────
|
||||||
# Both factors required, with the normal password stack kept as a fallback so a
|
# Both factors required, with the normal password stack kept as a fallback so a
|
||||||
# dead camera or absent key can never lock you out.
|
# dead camera or absent key can never lock you out.
|
||||||
|
|
@ -95,8 +101,47 @@ set_camera_id() {
|
||||||
printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG"
|
printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Presence detection ────────────────────────────────────────────────────────
|
# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
|
||||||
presence_configure_camera() {
|
# readable by the user. The first frame off a cold sensor is often black, so pull
|
||||||
|
# a few and keep the last.
|
||||||
|
camera_capture_frame() {
|
||||||
|
local dev="$1" out="$2"
|
||||||
|
command -v ffmpeg &>/dev/null || return 2
|
||||||
|
ffmpeg -hide_banner -loglevel error -f v4l2 -i "$dev" \
|
||||||
|
-frames:v 5 -update 1 -y "$out" </dev/null 2>/dev/null
|
||||||
|
[[ -s "$out" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# Render an image inline via kitty's graphics protocol. KITTY_WINDOW_ID is the
|
||||||
|
# reliable signal — $TERM is frequently xterm-256color even under kitty.
|
||||||
|
preview_frame() {
|
||||||
|
local img="$1"
|
||||||
|
[[ -s "$img" ]] || return 1
|
||||||
|
command -v kitty &>/dev/null || return 1
|
||||||
|
[[ -n "$KITTY_WINDOW_ID" || "$TERM" == *kitty* ]] || return 1
|
||||||
|
kitty +kitten icat --align left --clear "$img"
|
||||||
|
}
|
||||||
|
|
||||||
|
# IR sensors deliver near-zero chroma; colour webcams don't. Returns 0 when the
|
||||||
|
# still looks monochrome (likely IR), 1 otherwise. Assumes colour when it can't
|
||||||
|
# measure — that's the case that needs the extra non-IR tuning anyway.
|
||||||
|
camera_looks_ir() {
|
||||||
|
local img="$1" sat
|
||||||
|
[[ -s "$img" ]] || return 1
|
||||||
|
command -v ffmpeg &>/dev/null || return 1
|
||||||
|
sat=$(ffmpeg -hide_banner -loglevel error -i "$img" \
|
||||||
|
-vf signalstats,metadata=mode=print:file=- -f null - 2>/dev/null \
|
||||||
|
| awk -F= '/SATAVG/{print $2; exit}')
|
||||||
|
[[ -z "$sat" ]] && return 1
|
||||||
|
awk -v s="$sat" 'BEGIN{exit !(s < 12)}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Shared camera chooser: menu → live still preview in kitty → confirm. The pick
|
||||||
|
# is returned in the global PICKED_CAM (numeric id); returns 1 if cancelled.
|
||||||
|
PICKED_CAM=""
|
||||||
|
PICKED_CAM_FRAME=""
|
||||||
|
pick_camera_with_preview() {
|
||||||
|
local title="$1" current="$2"
|
||||||
local -a cam_items
|
local -a cam_items
|
||||||
mapfile -t cam_items < <(list_cameras)
|
mapfile -t cam_items < <(list_cameras)
|
||||||
|
|
||||||
|
|
@ -104,21 +149,51 @@ presence_configure_camera() {
|
||||||
msg "No Cameras Found" \
|
msg "No Cameras Found" \
|
||||||
"No video devices found at /dev/video*.\n\nMake sure your webcam is connected." \
|
"No video devices found at /dev/video*.\n\nMake sure your webcam is connected." \
|
||||||
10 55
|
10 55
|
||||||
return
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
local current; current=$(get_camera_id)
|
while true; do
|
||||||
local choice
|
local choice
|
||||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||||
--title " Select Camera " \
|
--title " $title " \
|
||||||
--menu "\nCurrent: /dev/video${current}\n\nSelect camera to use for presence detection:" \
|
--menu "\nCurrent: ${current}\n\nSelect a camera:" \
|
||||||
16 62 6 \
|
16 62 6 \
|
||||||
"${cam_items[@]}" \
|
"${cam_items[@]}" \
|
||||||
3>&1 1>&2 2>&3) || return
|
3>&1 1>&2 2>&3) || return 1
|
||||||
|
|
||||||
set_camera_id "$choice"
|
local dev="/dev/video${choice}"
|
||||||
|
clear
|
||||||
|
printf "\n\033[1;35m Preview — %s\033[0m\n" "$dev"
|
||||||
|
printf "\033[35m ─────────────────────────────────────────\033[0m\n\n"
|
||||||
|
|
||||||
|
local frame="$TMP_D/cam_preview.png"
|
||||||
|
camera_capture_frame "$dev" "$frame"
|
||||||
|
case $? in
|
||||||
|
0) preview_frame "$frame" \
|
||||||
|
|| printf " (kitty preview unavailable; frame saved to %s)\n" "$frame" ;;
|
||||||
|
2) printf " ffmpeg is not installed — skipping preview.\n" ;;
|
||||||
|
*) printf " Could not capture a frame from %s.\n The device may be busy or not a capture node.\n" "$dev" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
printf "\n"
|
||||||
|
local ans
|
||||||
|
read -rp " Use ${dev}? [Y]es / [n]o, pick another / [c]ancel: " ans
|
||||||
|
case "${ans,,}" in
|
||||||
|
""|y|yes) PICKED_CAM="$choice"; PICKED_CAM_FRAME="$frame"; return 0 ;;
|
||||||
|
c|cancel) return 1 ;;
|
||||||
|
*) continue ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Presence detection ────────────────────────────────────────────────────────
|
||||||
|
presence_configure_camera() {
|
||||||
|
local current; current=$(get_camera_id)
|
||||||
|
pick_camera_with_preview "Select Camera (Presence)" "/dev/video${current}" || return
|
||||||
|
|
||||||
|
set_camera_id "$PICKED_CAM"
|
||||||
msg "Camera Set" \
|
msg "Camera Set" \
|
||||||
"Presence detection will use /dev/video${choice}.\n\nRestart presence-detect.sh for the change to take effect." \
|
"Presence detection will use /dev/video${PICKED_CAM}.\n\nRestart presence-detect.sh for the change to take effect." \
|
||||||
9 62
|
9 62
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -181,9 +256,93 @@ howdy_require() {
|
||||||
howdy_installed || { msg "Install Failed" "howdy installation failed.\nInstall it manually: ${helper} -S howdy" 8 52; return 1; }
|
howdy_installed || { msg "Install Failed" "howdy installation failed.\nInstall it manually: ${helper} -S howdy" 8 52; return 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# ── Howdy camera (device_path) ────────────────────────────────────────────────
|
||||||
|
# config.ini is root-only, so reads and writes both go through sudo. Returns the
|
||||||
|
# bare value (e.g. /dev/video1, or `none` when unconfigured).
|
||||||
|
howdy_get_device_path() {
|
||||||
|
sudo grep -oP '^\s*device_path\s*=\s*\K.*' "$HOWDY_CONFIG" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Generic [video] key writer: replaces the key in place, or inserts it under the
|
||||||
|
# [video] header if absent. Every key we touch (device_path, recording_plugin,
|
||||||
|
# dark_threshold) lives only in that section, so a global replace is safe.
|
||||||
|
howdy_set_cfg() {
|
||||||
|
local key="$1" val="$2"
|
||||||
|
if sudo grep -qE "^\s*${key}\s*=" "$HOWDY_CONFIG"; then
|
||||||
|
sudo sed -i -E "s|^(\s*${key}\s*=).*|\1 ${val}|" "$HOWDY_CONFIG"
|
||||||
|
else
|
||||||
|
sudo sed -i -E "/^\[video\]/a ${key} = ${val}" "$HOWDY_CONFIG"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
howdy_set_device_path() { howdy_set_cfg device_path "$1"; }
|
||||||
|
|
||||||
|
# True when howdy points at a device node that actually exists.
|
||||||
|
howdy_camera_ok() {
|
||||||
|
local dp; dp=$(howdy_get_device_path)
|
||||||
|
[[ -n "$dp" && "$dp" != none && -e "$dp" ]]
|
||||||
|
}
|
||||||
|
|
||||||
|
# howdy's stock tuning assumes a Windows-Hello IR sensor. A regular colour
|
||||||
|
# webcam works too — dlib doesn't care about IR — but needs two tweaks:
|
||||||
|
# • recording_plugin = ffmpeg (opencv mis-decodes many YUYV colour streams to
|
||||||
|
# grey garbage; ffmpeg reads them cleanly — the same path the preview used)
|
||||||
|
# • dark_threshold = 80 (IR's flashing emitter wants a low value; a
|
||||||
|
# colour feed in normal light gets rejected as "too dark" at the default 50)
|
||||||
|
# The camera-type question defaults from the preview's measured colourfulness.
|
||||||
|
howdy_apply_camera_profile() {
|
||||||
|
local frame="$1" dev="$2"
|
||||||
|
local dn=""
|
||||||
|
camera_looks_ir "$frame" && dn="--defaultno" # looks IR ⇒ default the answer to "No"
|
||||||
|
|
||||||
|
dialog --backtitle "$BACKTITLE" --title " Camera Type " $dn --yesno \
|
||||||
|
"\nIs this a regular (non-IR / colour) webcam?\n\n Yes — tune howdy for a colour camera\n (ffmpeg recorder, dark_threshold 80).\n Works in normal light; less secure.\n\n No — IR sensor (Windows Hello style).\n Keep howdy's default IR tuning." 16 62
|
||||||
|
case $? in
|
||||||
|
0) howdy_set_cfg recording_plugin ffmpeg
|
||||||
|
howdy_set_cfg dark_threshold 80
|
||||||
|
msg "Howdy Camera Set" \
|
||||||
|
"howdy will use ${dev}.\n\nTuned for a NON-IR colour webcam:\n recorder = ffmpeg\n dark_threshold = 80\n\nRun 'add face model' next. If frames come out\ngrey or fail, try the other /dev/video node." 14 62 ;;
|
||||||
|
1) howdy_set_cfg recording_plugin opencv
|
||||||
|
howdy_set_cfg dark_threshold 50
|
||||||
|
msg "Howdy Camera Set" \
|
||||||
|
"howdy will use ${dev}.\n\nTuned for an IR sensor (howdy defaults):\n recorder = opencv\n dark_threshold = 50" 12 62 ;;
|
||||||
|
*) msg "Howdy Camera Set" \
|
||||||
|
"howdy will use ${dev}.\n\nCamera-type tuning left unchanged." 9 56 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
howdy_configure_camera() {
|
||||||
|
howdy_require || return
|
||||||
|
local current; current=$(howdy_get_device_path)
|
||||||
|
[[ -z "$current" ]] && current="(unset)"
|
||||||
|
|
||||||
|
pick_camera_with_preview "Select Camera (Howdy)" "$current" || return
|
||||||
|
|
||||||
|
local dev="/dev/video${PICKED_CAM}"
|
||||||
|
howdy_set_device_path "$dev"
|
||||||
|
howdy_apply_camera_profile "$PICKED_CAM_FRAME" "$dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Howdy aborts in VideoCapture when device_path is unset/wrong, surfacing only a
|
||||||
|
# generic exit code 1. Catch that here and offer to fix it before enrolling.
|
||||||
|
howdy_ensure_camera() {
|
||||||
|
howdy_camera_ok && return 0
|
||||||
|
local dp; dp=$(howdy_get_device_path)
|
||||||
|
dialog --backtitle "$BACKTITLE" --title " Camera Not Configured " --yesno \
|
||||||
|
"\nhowdy has no usable camera (device_path = ${dp:-none}).\n\nEnrollment will fail until this is set.\nConfigure the howdy camera now?" 11 60 \
|
||||||
|
|| return 1
|
||||||
|
howdy_configure_camera
|
||||||
|
howdy_camera_ok || {
|
||||||
|
msg "Still Not Configured" \
|
||||||
|
"howdy's camera is still unset, aborting enrollment." 7 52
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
# ── Howdy operations ──────────────────────────────────────────────────────────
|
# ── Howdy operations ──────────────────────────────────────────────────────────
|
||||||
howdy_add() {
|
howdy_add() {
|
||||||
howdy_require || return
|
howdy_require || return
|
||||||
|
howdy_ensure_camera || return
|
||||||
|
|
||||||
local name
|
local name
|
||||||
name=$(dialog --backtitle "$BACKTITLE" \
|
name=$(dialog --backtitle "$BACKTITLE" \
|
||||||
|
|
@ -449,26 +608,28 @@ main_menu() {
|
||||||
local choice
|
local choice
|
||||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||||
--title " Biometric Enrollment " \
|
--title " Biometric Enrollment " \
|
||||||
--menu "\nSelect an option:" 22 70 9 \
|
--menu "\nSelect an option:" 23 70 10 \
|
||||||
"1" "Presence detection — configure camera" \
|
"1" "Presence detection — configure camera" \
|
||||||
"2" "Presence detection — test detection" \
|
"2" "Presence detection — test detection" \
|
||||||
"3" "Howdy face auth — add face model" \
|
"3" "Howdy face auth — configure camera" \
|
||||||
"4" "Howdy face auth — list enrolled models" \
|
"4" "Howdy face auth — add face model" \
|
||||||
"5" "Howdy face auth — remove face model" \
|
"5" "Howdy face auth — list enrolled models" \
|
||||||
"6" "Howdy face auth — test authentication" \
|
"6" "Howdy face auth — remove face model" \
|
||||||
"7" "PAM 2FA — set up howdy + FIDO key" \
|
"7" "Howdy face auth — test authentication" \
|
||||||
"8" "PAM 2FA — remove howdy + FIDO key" \
|
"8" "PAM 2FA — set up howdy + FIDO key" \
|
||||||
|
"9" "PAM 2FA — remove howdy + FIDO key" \
|
||||||
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
||||||
|
|
||||||
case "$choice" in
|
case "$choice" in
|
||||||
1) presence_configure_camera ;;
|
1) presence_configure_camera ;;
|
||||||
2) presence_test_camera ;;
|
2) presence_test_camera ;;
|
||||||
3) howdy_add ;;
|
3) howdy_configure_camera ;;
|
||||||
4) howdy_list ;;
|
4) howdy_add ;;
|
||||||
5) howdy_remove ;;
|
5) howdy_list ;;
|
||||||
6) howdy_test ;;
|
6) howdy_remove ;;
|
||||||
7) pam_setup ;;
|
7) howdy_test ;;
|
||||||
8) pam_teardown ;;
|
8) pam_setup ;;
|
||||||
|
9) pam_teardown ;;
|
||||||
esac
|
esac
|
||||||
main_menu
|
main_menu
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue