diff --git a/desktopenvs/hyprlua/scripts/enroll-biometrics.sh b/desktopenvs/hyprlua/scripts/enroll-biometrics.sh index b744974..5901df7 100755 --- a/desktopenvs/hyprlua/scripts/enroll-biometrics.sh +++ b/desktopenvs/hyprlua/scripts/enroll-biometrics.sh @@ -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 + [[ -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 }