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)"
|
||||
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) ────────────────────────────────────────────
|
||||
# Both factors required, with the normal password stack kept as a fallback so a
|
||||
# dead camera or absent key can never lock you out.
|
||||
|
|
@ -95,8 +101,47 @@ set_camera_id() {
|
|||
printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG"
|
||||
}
|
||||
|
||||
# ── Presence detection ────────────────────────────────────────────────────────
|
||||
presence_configure_camera() {
|
||||
# Grab a single still from a v4l2 node into $2. No sudo: /dev/video* is acl/group
|
||||
# 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
|
||||
mapfile -t cam_items < <(list_cameras)
|
||||
|
||||
|
|
@ -104,21 +149,51 @@ presence_configure_camera() {
|
|||
msg "No Cameras Found" \
|
||||
"No video devices found at /dev/video*.\n\nMake sure your webcam is connected." \
|
||||
10 55
|
||||
return
|
||||
return 1
|
||||
fi
|
||||
|
||||
local current; current=$(get_camera_id)
|
||||
local choice
|
||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Select Camera " \
|
||||
--menu "\nCurrent: /dev/video${current}\n\nSelect camera to use for presence detection:" \
|
||||
16 62 6 \
|
||||
"${cam_items[@]}" \
|
||||
3>&1 1>&2 2>&3) || return
|
||||
while true; do
|
||||
local choice
|
||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " $title " \
|
||||
--menu "\nCurrent: ${current}\n\nSelect a camera:" \
|
||||
16 62 6 \
|
||||
"${cam_items[@]}" \
|
||||
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" \
|
||||
"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
|
||||
}
|
||||
|
||||
|
|
@ -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 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_add() {
|
||||
howdy_require || return
|
||||
howdy_ensure_camera || return
|
||||
|
||||
local name
|
||||
name=$(dialog --backtitle "$BACKTITLE" \
|
||||
|
|
@ -449,26 +608,28 @@ main_menu() {
|
|||
local choice
|
||||
choice=$(dialog --backtitle "$BACKTITLE" \
|
||||
--title " Biometric Enrollment " \
|
||||
--menu "\nSelect an option:" 22 70 9 \
|
||||
--menu "\nSelect an option:" 23 70 10 \
|
||||
"1" "Presence detection — configure camera" \
|
||||
"2" "Presence detection — test detection" \
|
||||
"3" "Howdy face auth — add face model" \
|
||||
"4" "Howdy face auth — list enrolled models" \
|
||||
"5" "Howdy face auth — remove face model" \
|
||||
"6" "Howdy face auth — test authentication" \
|
||||
"7" "PAM 2FA — set up howdy + FIDO key" \
|
||||
"8" "PAM 2FA — remove howdy + FIDO key" \
|
||||
"3" "Howdy face auth — configure camera" \
|
||||
"4" "Howdy face auth — add face model" \
|
||||
"5" "Howdy face auth — list enrolled models" \
|
||||
"6" "Howdy face auth — remove face model" \
|
||||
"7" "Howdy face auth — test authentication" \
|
||||
"8" "PAM 2FA — set up howdy + FIDO key" \
|
||||
"9" "PAM 2FA — remove howdy + FIDO key" \
|
||||
3>&1 1>&2 2>&3) || { clear; exit 0; }
|
||||
|
||||
case "$choice" in
|
||||
1) presence_configure_camera ;;
|
||||
2) presence_test_camera ;;
|
||||
3) howdy_add ;;
|
||||
4) howdy_list ;;
|
||||
5) howdy_remove ;;
|
||||
6) howdy_test ;;
|
||||
7) pam_setup ;;
|
||||
8) pam_teardown ;;
|
||||
3) howdy_configure_camera ;;
|
||||
4) howdy_add ;;
|
||||
5) howdy_list ;;
|
||||
6) howdy_remove ;;
|
||||
7) howdy_test ;;
|
||||
8) pam_setup ;;
|
||||
9) pam_teardown ;;
|
||||
esac
|
||||
main_menu
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue