#!/bin/bash
#==============================================================================
# NAME
#     fido-token-test.sh — interactive acceptance test for new FIDO2 keys
#
# SYNOPSIS
#     ./fido-token-test.sh                 interactive menu (default)
#     ./fido-token-test.sh --batch         read-only acceptance sweep (machine-readable)
#     ./fido-token-test.sh --batch --report  sweep prefixed with the full identity
#                                          report (option 10) for every token
#     ./fido-token-test.sh --help          print this header and exit
#
# DESCRIPTION
#     Vets a FIDO2 security key before it goes into the FIDO user-auth lab:
#     detects it, dumps capabilities, checks PIN state, runs a live touch/crypto
#     round-trip, and — carefully — sets PINs and factory-resets tokens. Built on
#     libfido2's user-space tools. Runs on the lab box, not a server.
#
# MENU OPTIONS
#     1  Detect / select token .... list attached authenticators (fido2-token -L),
#                                    pick the active one; auto-selects if only one.
#     2  Show capabilities ........ full fido2-token -I dump plus a quick read of
#                                    CTAP versions, resident-key support, UV/PIN,
#                                    and AAGUID presence.
#     3  Check PIN state .......... reports whether a PIN is set and colours the
#                                    remaining-retry counter (healthy/near-lockout/
#                                    locked).
#     4  Round-trip test .......... makes a throwaway NON-resident credential and
#                                    verifies its attestation — proves touch + crypto
#                                    end to end. Writes nothing to the token.
#     5  Set / change PIN ......... fido2-token -S, falling back to -C.   [MODIFIES]
#     6  Factory reset ............ fail-closed wipe for tokens with a unique serial:
#                                    requires exactly one token attached, a resolved
#                                    strong identity, not on the protected list, and
#                                    the serial re-typed by hand.               [WIPES]
#     7  Register PROTECTED ....... fingerprint the active token and store it as an
#                                    un-resettable real auth key (shields it from 6).
#     8  List protected tokens .... show the registered protected fingerprints.
#     9  Same-model wipe .......... controlled hotplug reset for a BLANK lab unit
#                                    that shares its model/serial-less identity with
#                                    your real key: forces removal of all tokens,
#                                    single re-insert, and refuses anything that looks
#                                    configured (PIN/credentials).              [WIPES]
#    10  Full identity report ...... dump EVERY identifying parameter (udev, USB
#                                    descriptors, fido2-token -I, ykman, computed
#                                    fingerprint) for every attached token, plus a
#                                    comparison table; optionally save a timestamped
#                                    copy under FILES for later diffing.
#    11  Touch-to-identify ......... physically map a /dev/hidraw path to the token
#                                    in your hand: each is triggered (LED blinks) and
#                                    the one you TOUCH is claimed. PIN-set tokens are
#                                    flagged (likely real key) and skipped.
#     q  Quit
#
# SAFETY MODEL
#     This box holds a REAL auth token, so all destructive paths fail CLOSED:
#       * Reset (6) refuses any token it cannot uniquely identify, any token on
#         the protected list, and any time more than one token is attached.
#       * Same-model wipe (9) substitutes PHYSICAL verification (hotplug sequence
#         + blank-token check) when no serial can distinguish the tokens.
#       * --batch mode NEVER modifies a token — read-only by construction.
#     Register your real key with option 7 BEFORE using any reset.
#
# DEPENDENCIES
#     Required : fido2-tools (fido2-token, fido2-cred, fido2-assert), openssl
#                Debian/Ubuntu:  sudo apt install fido2-tools
#     Optional : udev (udevadm) for USB-serial reads; ykman for YubiKey serials.
#                Missing udevadm just makes reset fail-closed more often (safe side).
#
# FILES
#     ${XDG_CONFIG_HOME:-~/.config}/fido-lab/protected-tokens
#         Tab-separated: <fingerprint> <label> <ISO-8601 added-date>. chmod 600.
#     ${XDG_CONFIG_HOME:-~/.config}/fido-lab/reports/fido-identity-<ts>.txt
#         Saved identity reports from option 10 (for archival / diffing).
#
# EXIT STATUS (--batch)
#     0 all tokens passed   1 one or more failed   2 no devices found
#==============================================================================

set -uo pipefail

RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'

DEV=""          # selected /dev/hidraw* path
DEV_DESC=""     # human label for the selected device

# ---------------------------------------------------------------- helpers ----
hr()   { printf "${DIM}%s${NC}\n" "----------------------------------------------------------------"; }
ok()   { echo -e "  ${GREEN}[ OK ]${NC} $*"; }
warn() { echo -e "  ${YELLOW}[WARN]${NC} $*"; }
fail() { echo -e "  ${RED}[FAIL]${NC} $*"; }
info() { echo -e "  ${CYAN}[INFO]${NC} $*"; }
pause(){ echo; read -rp "  Press Enter to continue... " _; }

require_dev() {
    if [[ -z "$DEV" ]]; then
        warn "No token selected yet — run [1] Detect / select first."
        return 1
    fi
    if [[ ! -e "$DEV" ]]; then
        fail "Selected device $DEV is gone (unplugged?). Re-detect."
        DEV=""; DEV_DESC=""
        return 1
    fi
    return 0
}

check_deps() {
    local missing=()
    for t in fido2-token fido2-cred fido2-assert; do
        command -v "$t" >/dev/null 2>&1 || missing+=("$t")
    done
    if (( ${#missing[@]} )); then
        fail "Missing required tools: ${missing[*]}"
        echo -e "        Install on Debian/Ubuntu:  ${BOLD}sudo apt install fido2-tools${NC}"
        exit 1
    fi
    command -v udevadm >/dev/null 2>&1 || \
        warn "udevadm not found — serial reads will be limited, so resets will fail-closed more often (that's the safe side)."
}

# ------------------------------------------------------- identity / guards ---
CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/fido-lab"
PROTECTED_FILE="$CONFIG_DIR/protected-tokens"

# How many FIDO authenticators are attached right now.
fido_count() { fido2-token -L 2>/dev/null | grep -c ':'; }

# Block until exactly $1 FIDO tokens are attached (or $2s timeout). Returns 1 on timeout.
wait_for_count() {
    local target="$1" timeout="${2:-60}" waited=0
    printf "  waiting"
    while [[ "$(fido_count)" != "$target" ]]; do
        printf "."; sleep 1; ((waited++))
        (( waited >= timeout )) && { printf " timeout\n"; return 1; }
    done
    printf " ok\n"; return 0
}

# Best-effort STABLE identity for a device ($1, default the selected DEV).
# Echoes: "vid:pid|serial|aaguid|strength"  where strength is strong|weak.
# A device is "strong" only if a usable unique serial was recovered.
token_identity() {
    local dev="${1:-$DEV}"
    local vid pid serial aaguid ud s_norm strength
    ud="$(udevadm info -a -n "$dev" 2>/dev/null || true)"
    vid="$(grep -m1 'ATTRS{idVendor}'  <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
    pid="$(grep -m1 'ATTRS{idProduct}' <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
    serial="$(grep -m1 'ATTRS{serial}' <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"

    # Prefer an authoritative ykman serial, but only when exactly one token is
    # attached (otherwise ykman is ambiguous about which key it's reading).
    if command -v ykman >/dev/null 2>&1 && [[ "$(fido_count)" == "1" ]]; then
        local ys; ys="$(ykman info 2>/dev/null | grep -iE 'serial number' | grep -oE '[0-9]+' | head -1)"
        [[ -n "$ys" ]] && serial="$ys"
    fi

    aaguid="$(fido2-token -I "$dev" 2>/dev/null | grep -oiE 'aaguid:? *[0-9a-f]+' | grep -oiE '[0-9a-f]{8,}' | head -1)"

    # A serial is only trustworthy if it's non-empty, not a placeholder, long enough.
    s_norm="$(tr 'A-F' 'a-f' <<<"$serial" | tr -cd 'a-z0-9')"
    if [[ -n "$s_norm" && ! "$s_norm" =~ ^0+$ && ! "$s_norm" =~ ^f+$ && ${#s_norm} -ge 4 ]]; then
        strength="strong"
    else
        strength="weak"; serial="${serial:-<none>}"
    fi
    printf '%s:%s|%s|%s|%s' "${vid:-????}" "${pid:-????}" "${serial:-<none>}" "${aaguid:-<none>}" "$strength"
}

# SHA-256 over the identity-bearing fields (everything except trailing strength).
token_fingerprint() { printf '%s' "${1%|*}" | sha256sum | awk '{print $1}'; }

is_protected() {
    [[ -f "$PROTECTED_FILE" ]] || return 1
    grep -q "^$1"$'\t' "$PROTECTED_FILE"
}

# ------------------------------------------------------------- [1] detect ----
detect() {
    hr; echo -e "  ${BOLD}Detect FIDO2 authenticators${NC}"; hr
    mapfile -t LINES < <(fido2-token -L 2>/dev/null)

    if (( ${#LINES[@]} == 0 )); then
        fail "No FIDO2 devices found."
        info "Plug the token in, then try again. If it's a fresh USB insert and"
        info "you're on a VM, confirm the USB filter passed it through to the guest."
        info "Permissions: your user needs access to /dev/hidraw* (udev rules /"
        info "plugdev group). Try 'sudo $0' to rule out permissions."
        return
    fi

    echo "  Found ${#LINES[@]} device(s):"; echo
    local i=1
    for l in "${LINES[@]}"; do
        printf "    ${BOLD}%d)${NC} %s\n" "$i" "$l"
        ((i++))
    done
    echo
    if (( ${#LINES[@]} == 1 )); then
        DEV="${LINES[0]%%:*}"
        DEV_DESC="${LINES[0]#*: }"
        ok "Auto-selected: ${DEV} — ${DEV_DESC}"
        return
    fi
    local pick
    read -rp "  Select device # (Enter to cancel): " pick
    [[ -z "$pick" ]] && { warn "Cancelled."; return; }
    if [[ "$pick" =~ ^[0-9]+$ ]] && (( pick >= 1 && pick <= ${#LINES[@]} )); then
        DEV="${LINES[pick-1]%%:*}"
        DEV_DESC="${LINES[pick-1]#*: }"
        ok "Selected: ${DEV} — ${DEV_DESC}"
    else
        warn "Invalid selection."
    fi
}

# --------------------------------------------------------------- [2] info ----
show_info() {
    require_dev || return
    hr; echo -e "  ${BOLD}Capabilities — ${DEV_DESC}${NC}"; hr
    local out
    out="$(fido2-token -I "$DEV" 2>&1)" || { fail "Could not read device info:"; echo "$out"; return; }

    # Full dump, dimmed, for the record.
    echo "$out" | sed 's/^/    /'
    echo; hr; echo -e "  ${BOLD}Quick read${NC}"

    grep -qi 'U2F_V2\|FIDO_2'          <<<"$out" && ok "Speaks CTAP: $(grep -oiE 'U2F_V2|FIDO_2_[0-9_]+' <<<"$out" | paste -sd' ')"
    if grep -qiE '^\s*options:.*\brk\b' <<<"$out" || grep -qiE 'rk:\s*true' <<<"$out"; then
        ok "Resident keys (discoverable credentials) supported — good for passwordless login."
    else
        warn "No resident-key support advertised — usable as 2FA but not passwordless."
    fi
    grep -qiE 'uv:\s*true|clientPin|uv\b' <<<"$out" && ok "User-verification / PIN capable."
    local aaguid; aaguid="$(grep -oiE 'aaguid:? *[0-9a-f]{8}' <<<"$out" | head -1)"
    [[ -n "$aaguid" ]] && info "AAGUID present (model is attestable / identifiable)."
    pause
}

# ---------------------------------------------------------- [3] pin state ----
pin_state() {
    require_dev || return
    hr; echo -e "  ${BOLD}PIN state — ${DEV_DESC}${NC}"; hr
    local out; out="$(fido2-token -I "$DEV" 2>&1)"

    if grep -qiE 'clientPin:\s*true|pin.*set|clientpin.*true' <<<"$out"; then
        ok "A PIN is already set on this token."
    elif grep -qiE 'clientPin:\s*false|no pin' <<<"$out"; then
        warn "No PIN set. For the lab's FIDO2+PIN login you must set one — use [5]."
    else
        info "Could not conclusively parse PIN status; raw 'pin' lines:"
        grep -iE 'pin' <<<"$out" | sed 's/^/      /' || true
    fi

    local retries; retries="$(grep -oiE 'pin retries:? *[0-9]+' <<<"$out" | grep -oE '[0-9]+' | head -1)"
    if [[ -n "$retries" ]]; then
        if   (( retries >= 5 )); then ok   "PIN retries remaining: ${retries} (healthy)."
        elif (( retries >= 1 )); then warn "PIN retries remaining: ${retries} — token near lockout."
        else                          fail "PIN retries: 0 — token is LOCKED, needs reset [6]."
        fi
    fi
    pause
}

# ------------------------------------------------------ [4] round-trip test --
roundtrip() {
    require_dev || return
    hr; echo -e "  ${BOLD}Live touch + crypto round-trip${NC}"; hr
    info "This makes a throwaway (non-resident) credential and verifies its"
    info "signature — proving the token physically works end to end."
    echo -e "  ${YELLOW}${BOLD}>> Touch the token when it blinks. <<${NC}"
    echo -e "  ${DIM}(If a PIN is set, the tool will prompt for it.)${NC}"
    echo

    local tmp cred cdh rpid uname uid
    tmp="$(mktemp -d)"; cred="$tmp/cred"
    cdh="$(openssl rand -base64 32)"
    uid="$(openssl rand -base64 16)"
    rpid="fido-lab.local"
    uname="lab-token-test"

    if printf '%s\n%s\n%s\n%s\n' "$cdh" "$rpid" "$uname" "$uid" \
         | fido2-cred -M -o "$cred" "$DEV" >/dev/null 2>"$tmp/mk.err"; then
        ok "Make-credential succeeded (touch registered, attestation returned)."
    else
        fail "Make-credential failed:"
        sed 's/^/      /' "$tmp/mk.err"
        rm -rf "$tmp"; pause; return
    fi

    # Verify the attestation object we just got back.
    if fido2-cred -V -o "$tmp/pubkey" <"$cred" >/dev/null 2>"$tmp/vf.err"; then
        ok "Attestation verified — credential is cryptographically valid."
    else
        warn "Credential made but self-verification reported:"
        sed 's/^/      /' "$tmp/vf.err"
    fi

    ok "Token PASSED the live round-trip. Safe to enroll in the lab."
    info "Nothing was written to the token (non-resident credential)."
    rm -rf "$tmp"
    pause
}

# ---------------------------------------------------------- [5] set a PIN ----
set_pin() {
    require_dev || return
    hr; echo -e "  ${BOLD}Set / change PIN — ${DEV_DESC}${NC}"; hr
    info "The lab default PIN convention is your call; libfido2 enforces the"
    info "token's own minimum length. You'll be prompted twice."
    echo
    if fido2-token -S "$DEV"; then
        ok "PIN set. Re-check with [3]."
    else
        warn "Set-PIN did not complete (a PIN may already exist — use change below)."
        read -rp "  Try CHANGE PIN instead? [y/N] " a
        [[ "$a" =~ ^[Yy]$ ]] && { fido2-token -C "$DEV" && ok "PIN changed."; }
    fi
    pause
}

# --------------------------------------------------- [6] factory reset -------
# Fail-closed: refuses unless it can PROVE this is not a protected / real key.
factory_reset() {
    require_dev || return
    hr; echo -e "  ${RED}${BOLD}FACTORY RESET — ${DEV_DESC}${NC}"; hr
    fail "This WIPES all credentials and the PIN. Irreversible."
    echo

    # Guard 1 — exactly ONE FIDO device may be attached during a reset.
    local n; n="$(fido_count)"
    if [[ "$n" != "1" ]]; then
        fail "BLOCKED: ${n} FIDO devices attached."
        info "Unplug EVERY token except the single new one you intend to wipe —"
        info "including your real auth key — then re-detect [1] and retry."
        pause; return
    fi

    # Resolve identity.
    local id fp strength serial
    id="$(token_identity)"; fp="$(token_fingerprint "$id")"; strength="${id##*|}"
    echo "  Identity    : $id"
    echo "  Fingerprint : ${fp:0:24}…"; echo

    # Guard 2 — never wipe a registered protected token. No override.
    if is_protected "$fp"; then
        fail "REFUSED: this token is on the PROTECTED list (a registered real key)."
        fail "Not wiping it. If this is wrong, edit $PROTECTED_FILE by hand."
        pause; return
    fi

    # Guard 3 — fail-closed on weak identity (can't be told apart from your key).
    if [[ "$strength" != "strong" ]]; then
        fail "REFUSED (fail-closed): no unique serial could be read for this token."
        info "It cannot be distinguished from another key of the same model, so the"
        info "tool will not risk it. If you are 100% certain, wipe it MANUALLY:"
        echo -e "        ${BOLD}fido2-token -R $DEV${NC}"
        pause; return
    fi

    # Show what's on it — a configured token is a red flag for a 'new' key.
    local ii; ii="$(fido2-token -I "$DEV" 2>/dev/null)"
    grep -qiE 'clientPin:\s*true' <<<"$ii" && \
        warn "This token already has a PIN set — confirm it isn't in use."
    echo
    info "Most tokens only allow reset within ~10s of being plugged in."
    warn "About to wipe:"
    echo -e "        ${BOLD}${id%|*}${NC}"
    echo

    # Guard 4 — re-type the serial by hand, then the word RESET.
    serial="${id#*|}"; serial="${serial%%|*}"
    local typed; read -rp "  Re-type this token's serial to confirm ($serial): " typed
    [[ "$typed" == "$serial" ]] || { warn "Serial mismatch — aborted, nothing wiped."; pause; return; }
    local c; read -rp "  Final confirm — type RESET: " c
    [[ "$c" == "RESET" ]] || { warn "Aborted."; pause; return; }

    if fido2-token -R "$DEV"; then
        ok "Token reset to factory state."
    else
        fail "Reset failed — unplug, replug, and run this immediately after."
    fi
    pause
}

# ----------------------------------------------- [7] register protected key --
register_protected() {
    require_dev || return
    hr; echo -e "  ${BOLD}Register a PROTECTED token (permanently un-resettable here)${NC}"; hr
    local id fp strength; id="$(token_identity)"; fp="$(token_fingerprint "$id")"; strength="${id##*|}"
    echo "  Identity    : $id"
    echo "  Fingerprint : $fp"; echo
    if [[ "$strength" == "weak" ]]; then
        warn "This token exposes NO unique serial — protection is model-level only."
        warn "It will also shield every identical-model token from reset. That is the"
        warn "safe side near a real auth key, but know the guard is broad."
    fi
    if is_protected "$fp"; then ok "Already protected."; pause; return; fi
    local a; read -rp "  Add to protected list? [y/N] " a
    [[ "$a" =~ ^[Yy]$ ]] || { warn "Not added."; pause; return; }
    local label; read -rp "  Label (e.g. 'primary auth key'): " label
    mkdir -p "$CONFIG_DIR"
    printf '%s\t%s\t%s\n' "$fp" "${label:-unlabeled}" "$(date -Is)" >> "$PROTECTED_FILE"
    chmod 600 "$PROTECTED_FILE" 2>/dev/null || true
    ok "Protected. This tool will now refuse to reset this token."
    pause
}

# ----------------------------------------------- [8] list protected keys -----
list_protected() {
    hr; echo -e "  ${BOLD}Protected tokens${NC}"; hr
    if [[ ! -s "$PROTECTED_FILE" ]]; then
        warn "None registered yet. Register your REAL auth key with [7] before"
        warn "you ever use reset [6]."
        pause; return
    fi
    echo -e "  ${DIM}File: $PROTECTED_FILE${NC}"; echo
    local fp label added
    while IFS=$'\t' read -r fp label added; do
        printf "    ${GREEN}%s…${NC}\n      %s  ${DIM}(added %s)${NC}\n" "${fp:0:16}" "$label" "$added"
    done < "$PROTECTED_FILE"
    pause
}

# --------------------------------------------- [9] same-model wipe -----------
# For wiping a BLANK lab token that shares its model (and lack of serial) with
# your real key, where fingerprint guards can't tell them apart. Safety here is
# PHYSICAL: you remove every token (incl. the real key), insert only the one to
# wipe, and the tool refuses anything that looks configured (PIN / credentials).
wipe_same_model() {
    hr; echo -e "  ${RED}${BOLD}SAME-MODEL WIPE — controlled hotplug reset${NC}"; hr
    fail "WIPES the token. Irreversible. Use only for a blank lab unit that shares"
    fail "its model with your real key (so serial guards cannot distinguish them)."
    echo
    info "Safety is physical, not identity-based:"
    info "  1) you remove ALL tokens — including your real auth key,"
    info "  2) you insert ONLY the throwaway token,"
    info "  3) the tool refuses it if it looks configured (PIN or credentials)."
    echo
    local g; read -rp "  Proceed? type SAME-MODEL: " g
    [[ "$g" == "SAME-MODEL" ]] || { warn "Aborted."; pause; return; }

    echo; echo -e "  ${YELLOW}${BOLD}Step 1/3 — UNPLUG EVERY FIDO token now (including your real key).${NC}"
    if ! wait_for_count 0 60; then
        fail "Still detecting a token after 60s — aborted for safety."; pause; return
    fi
    ok "All tokens removed."

    echo; echo -e "  ${YELLOW}${BOLD}Step 2/3 — insert ONLY the throwaway (do it now; the reset window is short).${NC}"
    if ! wait_for_count 1 60; then
        fail "Expected exactly 1 token; saw $(fido_count). Aborted — never wipe when ambiguous."
        pause; return
    fi
    local line; line="$(fido2-token -L 2>/dev/null | head -1)"
    DEV="${line%%:*}"; DEV_DESC="${line#*: }"
    ok "Single token present: $DEV — $DEV_DESC"

    echo; echo -e "  ${BOLD}Step 3/3 — blank-token sanity check${NC}"
    local ii; ii="$(fido2-token -I "$DEV" 2>/dev/null)"
    echo "  Identity: $(token_identity)"
    if grep -qiE 'clientPin:\s*true' <<<"$ii"; then
        fail "REFUSED: this token has a PIN set — it looks CONFIGURED / in use."
        fail "A fresh lab token has none. This may be your REAL key. Not wiping."
        info "If you are certain, wipe manually: ${BOLD}fido2-token -R $DEV${NC}"
        pause; return
    fi
    if grep -qiE 'existing rk\(?s?\)?:? *[1-9]' <<<"$ii"; then
        fail "REFUSED: token reports existing resident credentials — looks in use."
        pause; return
    fi
    ok "Token appears blank (no PIN, no visible resident credentials)."
    echo
    warn "About to WIPE: $DEV ($DEV_DESC)"
    local c; read -rp "  Type WIPE to reset now: " c
    [[ "$c" == "WIPE" ]] || { warn "Aborted."; pause; return; }
    if fido2-token -R "$DEV"; then
        ok "Token reset to factory state."
    else
        fail "Reset failed (often the ~10s window elapsed). Unplug, replug, retry."
    fi
    pause
}

# ------------------------------------------ [10] full identity report --------
# Dumps EVERY identifying parameter for every attached token, then a compact
# comparison table, so you can eyeball which physical unit is which. Optionally
# saves a timestamped copy for the record.
emit_report() {
    local i=1 line dev ud ii id fp
    echo "# FIDO identity report — $(date -Is)"
    echo "# host: $(hostname 2>/dev/null)   user: ${USER:-?}"
    echo
    for line in "${LINES[@]}"; do
        dev="${line%%:*}"
        ud="$(udevadm info -a -n "$dev" 2>/dev/null || true)"
        echo "================================================================"
        echo "TOKEN $i : $dev"
        echo "  L-line       : $line"
        printf "  manufacturer : %s\n" "$(grep -m1 'ATTRS{manufacturer}' <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
        printf "  product      : %s\n" "$(grep -m1 'ATTRS{product}'      <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
        printf "  idVendor     : %s\n" "$(grep -m1 'ATTRS{idVendor}'     <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
        printf "  idProduct    : %s\n" "$(grep -m1 'ATTRS{idProduct}'    <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
        printf "  usb serial   : %s\n" "$(grep -m1 'ATTRS{serial}'       <<<"$ud" | sed -E 's/.*=="?([^"]*)"?.*/\1/')"
        printf "  usb path     : %s\n" "$(udevadm info -q property -n "$dev" 2>/dev/null | grep -m1 '^ID_PATH=' | cut -d= -f2-)"
        if command -v ykman >/dev/null 2>&1 && [[ "$(fido_count)" == "1" ]]; then
            printf "  ykman serial : %s\n" "$(ykman info 2>/dev/null | grep -iE 'serial number' | grep -oE '[0-9]+' | head -1)"
        fi
        echo "  --- fido2-token -I ---"
        fido2-token -I "$dev" 2>&1 | sed 's/^/    /'
        id="$(token_identity "$dev")"; fp="$(token_fingerprint "$id")"
        echo "  identity     : $id"
        echo "  fingerprint  : $fp"
        is_protected "$fp" && echo "  protected    : YES (registered real key)" || echo "  protected    : no"
        echo
        ((i++))
    done
    echo "================================================================"
    echo "COMPARISON TABLE"
    printf "  %-13s %-9s %-18s %-11s %-3s %-6s %s\n" DEV VID:PID SERIAL AAGUID PIN STR FINGERPRINT
    for line in "${LINES[@]}"; do
        dev="${line%%:*}"
        id="$(token_identity "$dev")"; fp="$(token_fingerprint "$id")"
        ii="$(fido2-token -I "$dev" 2>/dev/null)"
        local vp serial aaguid strength pin
        vp="$(cut -d'|' -f1 <<<"$id")"
        serial="$(cut -d'|' -f2 <<<"$id")"
        aaguid="$(cut -d'|' -f3 <<<"$id")"
        strength="$(cut -d'|' -f4 <<<"$id")"
        grep -qiE 'clientPin:\s*true' <<<"$ii" && pin="Y" || pin="N"
        printf "  %-13s %-9s %-18s %-11s %-3s %-6s %s\n" \
            "$dev" "$vp" "${serial:0:18}" "${aaguid:0:11}" "$pin" "$strength" "${fp:0:16}…"
    done
}

identity_dump() {
    hr; echo -e "  ${BOLD}Full identity report — all attached tokens${NC}"; hr
    mapfile -t LINES < <(fido2-token -L 2>/dev/null)
    if (( ${#LINES[@]} == 0 )); then fail "No tokens attached."; pause; return; fi
    info "${#LINES[@]} token(s) attached."
    local save outfile=""
    read -rp "  Also save a timestamped copy? [y/N] " save
    if [[ "$save" =~ ^[Yy]$ ]]; then
        mkdir -p "$CONFIG_DIR/reports"
        outfile="$CONFIG_DIR/reports/fido-identity-$(date +%Y%m%d-%H%M%S).txt"
    fi
    echo
    if [[ -n "$outfile" ]]; then
        emit_report | tee "$outfile"
        echo; ok "Saved: $outfile"
        info "Diff two reports with:  diff <old> <new>"
    else
        emit_report | ${PAGER:-less} -R 2>/dev/null || emit_report
    fi
    pause
}

# ---------------------------------------- [11] touch-to-identify -------------
# Physically maps a /dev/hidraw path to the token in your hand: each token is
# triggered in turn (its LED blinks); touch the one you want and it's identified.
# PIN-set tokens can't be blinked silently, so they're flagged (likely your real
# key) and skipped — which is itself a useful signal.
touch_identify() {
    hr; echo -e "  ${BOLD}Touch-to-identify${NC}"; hr
    command -v timeout >/dev/null 2>&1 || { fail "'timeout' (coreutils) required."; pause; return; }
    mapfile -t LINES < <(fido2-token -L 2>/dev/null)
    if (( ${#LINES[@]} == 0 )); then fail "No tokens attached."; pause; return; fi
    info "Each token is triggered in turn. Touch the blinking one you want to"
    info "identify (20s per token; do nothing to skip to the next)."
    local line dev i=1 matched="" tmp
    for line in "${LINES[@]}"; do
        dev="${line%%:*}"
        echo
        if fido2-token -I "$dev" 2>/dev/null | grep -qiE 'clientPin:\s*true'; then
            echo -e "  ${DIM}Device $i/${#LINES[@]} ($dev): PIN-set — likely a configured/real key; skipping.${NC}"
            ((i++)); continue
        fi
        echo -e "  ${YELLOW}${BOLD}Device $i/${#LINES[@]}: $dev is blinking — TOUCH IT NOW to claim it.${NC}"
        tmp="$(mktemp -d)"
        printf '%s\n%s\n%s\n%s\n' "$(openssl rand -base64 32)" "identify.local" "touch-id" "$(openssl rand -base64 16)" > "$tmp/in"
        if timeout 20 fido2-cred -M -i "$tmp/in" -o "$tmp/out" "$dev" >/dev/null 2>&1; then
            matched="$dev"; DEV="$dev"; DEV_DESC="${line#*: }"
            rm -rf "$tmp"
            ok "Touched — this physical token is ${BOLD}$dev${NC}"
            break
        fi
        rm -rf "$tmp"
        echo -e "  ${DIM}no touch — skipped.${NC}"
        ((i++))
    done
    echo
    if [[ -n "$matched" ]]; then
        local id fp; id="$(token_identity "$matched")"; fp="$(token_fingerprint "$id")"
        echo "  identity    : $id"
        echo "  fingerprint : $fp"
        is_protected "$fp" && warn "This token is on the PROTECTED list (your real key)."
        ok "Active token set to $matched."
    else
        warn "No token was touched — nothing identified."
    fi
    pause
}

# ----------------------------------------------------------------- menu ------
menu() {
    while true; do
        clear
        echo -e "${CYAN}${BOLD}"
        echo "  ==============================================================="
        echo "   FIDO2 TOKEN ACCEPTANCE TEST  ::  cyber-training FIDO auth lab"
        echo "  ==============================================================="
        echo -e "${NC}"
        if [[ -n "$DEV" ]]; then
            echo -e "   Active token: ${GREEN}${DEV}${NC} — ${DEV_DESC}"
        else
            echo -e "   Active token: ${YELLOW}none selected${NC}"
        fi
        echo
        echo "   1) Detect / select token"
        echo "   2) Show capabilities (CTAP, resident keys, AAGUID)"
        echo "   3) Check PIN state + retry counter"
        echo "   4) Live touch + crypto round-trip test"
        echo -e "   5) Set / change PIN         ${DIM}(modifies token)${NC}"
        echo -e "   6) Factory reset            ${RED}(wipes token — fail-closed)${NC}"
        echo -e "   7) Register token as PROTECTED  ${GREEN}(shield real auth key)${NC}"
        echo "   8) List protected tokens"
        echo -e "   9) Same-model wipe (hotplug)    ${RED}(wipes blank same-model unit)${NC}"
        echo "  10) Full identity report (all tokens + compare + save)"
        echo -e "  11) Touch-to-identify           ${CYAN}(map a token by physical touch)${NC}"
        echo "   q) Quit"
        echo
        read -rp "   Choose: " choice
        echo
        case "$choice" in
            1) detect ;;
            2) show_info ;;
            3) pin_state ;;
            4) roundtrip ;;
            5) set_pin ;;
            6) factory_reset ;;
            7) register_protected ;;
            8) list_protected ;;
            9) wipe_same_model ;;
            10) identity_dump ;;
            11) touch_identify ;;
            q|Q) echo "  Bye."; exit 0 ;;
            *) warn "Unknown option: $choice"; sleep 1 ;;
        esac
    done
}

# ----------------------------------------------------------- batch mode ------
# Read-only acceptance sweep over every attached token. NEVER modifies a token.
# $1 == "report" also prints the full identity report (option 10) up front.
batch_mode() {
    local with_report="${1:-}"
    echo "# fido-batch-acceptance $(date -Is)"
    mapfile -t LINES < <(fido2-token -L 2>/dev/null)
    if (( ${#LINES[@]} == 0 )); then echo "RESULT: NO_DEVICES"; return 2; fi
    if [[ "$with_report" == "report" ]]; then
        echo "=================== IDENTITY REPORT ==================="
        emit_report
        echo "=================== ACCEPTANCE SWEEP =================="
    fi
    local rc=0 l
    for l in "${LINES[@]}"; do
        DEV="${l%%:*}"; DEV_DESC="${l#*: }"
        echo "DEVICE: $DEV  ($DEV_DESC)"
        echo "IDENTITY: $(token_identity)"
        if fido2-token -I "$DEV" >/dev/null 2>&1; then echo "INFO: ok"; else echo "INFO: FAIL"; rc=1; fi
        echo ">>> Touch the token to test $DEV ..."
        local tmp; tmp="$(mktemp -d)"
        if printf '%s\n%s\n%s\n%s\n' "$(openssl rand -base64 32)" "fido-lab.local" "batch" "$(openssl rand -base64 16)" \
             | fido2-cred -M -o "$tmp/cred" "$DEV" >/dev/null 2>&1; then
            echo "ROUNDTRIP: PASS"
        else
            echo "ROUNDTRIP: FAIL"; rc=1
        fi
        rm -rf "$tmp"; echo "---"
    done
    echo "RESULT: $([[ $rc -eq 0 ]] && echo ALL_PASS || echo SOME_FAIL)"
    return $rc
}

# --------------------------------------------------------------- entry -------
check_deps
BATCH=0; WANT_REPORT=0
for arg in "$@"; do
    case "$arg" in
        --batch)  BATCH=1 ;;
        --report) WANT_REPORT=1 ;;
        -h|--help) sed -n '2,/^#===/p' "$0" | sed 's/^#\{0,1\} \{0,1\}//'; exit 0 ;;
        *) echo "Unknown argument: $arg (try --help)" >&2; exit 64 ;;
    esac
done
if (( BATCH )); then
    batch_mode "$([[ $WANT_REPORT -eq 1 ]] && echo report)"; exit $?
fi
if (( WANT_REPORT )); then
    echo "--report requires --batch (interactive report is menu option 10)." >&2; exit 64
fi
menu
