#!/bin/bash # enroll-biometrics.sh — TUI for face biometric setup. # # Two subsystems: # 1. Presence detection — configure/test the webcam used by presence-detect.sh # 2. Howdy face auth — enroll/manage/test face models for PAM authentication BACKTITLE="Biometric Enrollment" 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" # ── Dialog theme (Cyberqueer) ───────────────────────────────────────────────── TMP_D=$(mktemp -d) trap 'rm -rf "$TMP_D"' EXIT INT TERM export DIALOGRC="$TMP_D/dialogrc" cat > "$DIALOGRC" << 'RCEOF' use_shadow = ON use_colors = ON screen_color = (BLACK,BLACK,ON) shadow_color = (BLACK,BLACK,ON) title_color = (MAGENTA,BLACK,ON) border_color = (MAGENTA,BLACK,ON) button_active_color = (BLACK,MAGENTA,ON) button_inactive_color = (WHITE,BLACK,OFF) button_key_active_color = (BLACK,CYAN,ON) button_key_inactive_color = (CYAN,BLACK,ON) button_label_active_color = (BLACK,MAGENTA,ON) button_label_inactive_color = (WHITE,BLACK,OFF) inputbox_color = (WHITE,BLACK,OFF) inputbox_border_color = (MAGENTA,BLACK,ON) menubox_color = (WHITE,BLACK,OFF) menubox_border_color = (MAGENTA,BLACK,ON) item_color = (WHITE,BLACK,OFF) item_selected_color = (BLACK,MAGENTA,ON) tag_color = (CYAN,BLACK,ON) tag_selected_color = (BLACK,CYAN,ON) check_color = (WHITE,BLACK,OFF) check_selected_color = (BLACK,MAGENTA,ON) uarrow_color = (MAGENTA,BLACK,ON) darrow_color = (MAGENTA,BLACK,ON) RCEOF # ── Helpers ─────────────────────────────────────────────────────────────────── die() { clear; printf "\nError: %s\n\n" "$1" >&2; exit 1; } msg() { dialog --backtitle "$BACKTITLE" --title " $1 " --msgbox "\n$2\n" "$3" "$4" } require_dialog() { command -v dialog &>/dev/null && return sudo pacman -S --noconfirm dialog || die "dialog not found" } # ── Camera helpers ───────────────────────────────────────────────────────────── list_cameras() { for dev in /dev/video*; do [[ -c "$dev" ]] || continue local id="${dev#/dev/video}" local name name=$(v4l2-ctl --device="$dev" --info 2>/dev/null \ | awk -F': ' '/Card type/{print $2}' | head -1) [[ -z "$name" ]] && name="Camera $id" printf '%s\n%s\n' "$id" "$name" done } get_camera_id() { if [[ -n "$PRESENCE_DETECT_CAMERA" ]]; then echo "$PRESENCE_DETECT_CAMERA" elif [[ -f "$PRESENCE_CFG" ]]; then grep -oP 'CAMERA=\K[0-9]+' "$PRESENCE_CFG" 2>/dev/null || echo 0 else echo 0 fi } set_camera_id() { mkdir -p "$(dirname "$PRESENCE_CFG")" printf 'CAMERA=%s\n' "$1" > "$PRESENCE_CFG" } # ── Presence detection ──────────────────────────────────────────────────────── presence_configure_camera() { local -a cam_items mapfile -t cam_items < <(list_cameras) if [[ ${#cam_items[@]} -eq 0 ]]; then msg "No Cameras Found" \ "No video devices found at /dev/video*.\n\nMake sure your webcam is connected." \ 10 55 return 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 set_camera_id "$choice" msg "Camera Set" \ "Presence detection will use /dev/video${choice}.\n\nRestart presence-detect.sh for the change to take effect." \ 9 62 } presence_test_camera() { local cam; cam=$(get_camera_id) clear printf "\n\033[1;35m Testing presence detection on /dev/video%s...\033[0m\n" "$cam" printf "\033[35m ─────────────────────────────────────────\033[0m\n\n" printf " Look at the camera and stay still.\n\n" python3 "$PYTHON_DETECT" "$cam" 2>/dev/null local rc=$? case $rc in 0) msg "Test Result" "Face detected!\n\nPresence detection is working correctly." 8 52 ;; 1) msg "Test Result" \ "No face detected.\n\nMake sure you are in front of the camera\nand there is adequate lighting." \ 10 56 ;; 2) msg "Camera Error" \ "Could not open /dev/video${cam}.\n\nTry configuring a different camera first." \ 9 56 ;; *) msg "Error" "Unexpected exit code ($rc) from detection script." 7 52 ;; esac } # ── Howdy helpers ───────────────────────────────────────────────────────────── howdy_installed() { command -v howdy &>/dev/null; } howdy_require() { howdy_installed && return 0 dialog --backtitle "$BACKTITLE" --title " Howdy Not Found " \ --yesno "\nhowdy is not installed.\n\nInstall it now via yay?" 8 48 || return 1 clear printf "\nInstalling howdy...\n\n" if command -v yay &>/dev/null; then yay -S --noconfirm --needed howdy else sudo pacman -S --noconfirm --needed howdy fi howdy_installed || { msg "Install Failed" "howdy installation failed.\nInstall it manually: yay -S howdy" 8 52; return 1; } } # ── Howdy operations ────────────────────────────────────────────────────────── howdy_add() { howdy_require || return local name name=$(dialog --backtitle "$BACKTITLE" \ --title " Add Face Model " \ --inputbox "\nEnter a label for this face model:" 8 52 "$USER" \ 3>&1 1>&2 2>&3) || return [[ -z "$name" ]] && return clear printf "\n\033[1;35m Enrolling face model '%s'...\033[0m\n" "$name" printf "\033[35m ─────────────────────────────────────────\033[0m\n\n" printf " Look at the camera and hold still.\n\n" sudo howdy add -n "$name" local rc=$? if [[ $rc -eq 0 ]]; then msg "Enrolled" \ "Face model '$name' added.\n\nYou may need to relogin for PAM changes to take effect." \ 9 58 else msg "Failed" "Enrollment failed (code $rc).\n\nCheck camera and lighting." 8 52 fi } howdy_list() { howdy_require || return local out; out=$(sudo howdy list 2>&1) msg "Enrolled Face Models" "$out" 20 64 } howdy_remove() { howdy_require || return local models; models=$(sudo howdy list 2>&1) if [[ -z "$models" || "$models" == *"no models"* ]]; then msg "No Models" "No face models are currently enrolled." 7 44 return fi local id id=$(dialog --backtitle "$BACKTITLE" \ --title " Remove Face Model " \ --inputbox "\nCurrent models:\n\n${models}\n\nEnter model ID to remove:" 20 64 \ 3>&1 1>&2 2>&3) || return [[ -z "$id" || ! "$id" =~ ^[0-9]+$ ]] && { msg "Invalid ID" "Please enter a numeric model ID." 6 36; return } dialog --backtitle "$BACKTITLE" --title " Confirm " \ --yesno "\nRemove face model ID $id?" 7 40 || return sudo howdy remove -I "$id" msg "Done" "Face model $id removed." 6 38 } howdy_test() { howdy_require || return clear printf "\n\033[1;35m Testing howdy authentication...\033[0m\n" printf "\033[35m ─────────────────────────────────────────\033[0m\n\n" printf " Look at the camera.\n\n" sudo howdy test local rc=$? if [[ $rc -eq 0 ]]; then msg "Result" "Authentication successful." 6 38 else msg "Result" "Authentication failed (code $rc).\n\nCheck enrolled models and camera setup." 9 52 fi } # ── Main menu ───────────────────────────────────────────────────────────────── main_menu() { local choice choice=$(dialog --backtitle "$BACKTITLE" \ --title " Biometric Enrollment " \ --menu "\nSelect an option:" 20 66 8 \ "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" \ 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 ;; esac main_menu } require_dialog main_menu