#!/bin/sh
# Clawdie first-boot password gate.
#
# Forces the operator to set a root + operator (clawdie) password on the FIRST
# boot, on the text console, BEFORE the GUI (sddm) and BEFORE the colibri daemon
# autospawns an agent. Running before the daemon means the security decision is
# always made before any agent can act — no cross-component interlock needed.
#
# Design:
#   - Idempotent via a persistent success marker (/var is persistent on this
#     image: varmfs="NO"). Marker present -> silent exit.
#   - A countdown to ENGAGE (read -t). If the operator does not engage, boot
#     continues with passwords UNSET and the gate re-prompts next boot (never
#     bricks an unattended/headless boot). Once set, never prompts again.
#   - Passwords are read with echo off (stty) and applied via `pw usermod -h 0`,
#     which reads the new password from STDIN — never in argv/ps, never near the
#     agent/LLM.
#   - On success, writes /var/db/colibri/.secured so the colibri daemon can
#     reflect security state to mother (label the node "unsecured" while absent).
#     The daemon-side consumption of this marker is a separate (colibri) change.
#
# Functions below are sourced and unit-tested on a non-FreeBSD host with
# CLAWDIE_ROOTPW_TEST=1 (which skips the rc.subr handoff). The interactive
# countdown lives only in _start and is not exercised by the logic test.

# PROVIDE: clawdie_firstboot_rootpw
# REQUIRE: FILESYSTEMS devfs
# BEFORE: clawdie_live_gpu LOGIN
# KEYWORD: nojail
#
# Ordering: runs on the plain early boot text console, BEFORE clawdie_live_gpu
# does its KMS/framebuffer mode-switch (so there is no console-flush race) and
# BEFORE LOGIN (so before sddm and before colibri_daemon, which REQUIRE LOGIN).
# Needs only FILESYSTEMS + devfs (console, /etc/master.passwd, /var marker, pw).

if [ -r /etc/rc.subr ]; then
    . /etc/rc.subr
fi

name="clawdie_firstboot_rootpw"
rcvar="${name}_enable"
start_cmd="${name}_start"
stop_cmd=":"

: "${clawdie_firstboot_rootpw_enable:=YES}"

# Overridable for tests.
SECURED_MARKER="${SECURED_MARKER:-/var/db/colibri/.secured}"
PW_BIN="${PW_BIN:-/usr/sbin/pw}"
ROOTPW_CONSOLE="${ROOTPW_CONSOLE:-/dev/console}"
ROOTPW_COUNTDOWN="${ROOTPW_COUNTDOWN:-15}"
ROOTPW_MIN_LEN="${ROOTPW_MIN_LEN:-8}"

# --- pure logic (unit-tested) -----------------------------------------------

# Already secured? (marker present)
_rootpw_secured() {
    [ -e "${SECURED_MARKER}" ]
}

# Validate a password pair. Echoes a reason and returns 1 on failure.
_rootpw_valid() {
    _p1="$1"; _p2="$2"
    if [ -z "${_p1}" ]; then echo "empty password"; return 1; fi
    if [ "${_p1}" != "${_p2}" ]; then echo "passwords do not match"; return 1; fi
    if [ "${#_p1}" -lt "${ROOTPW_MIN_LEN}" ]; then
        echo "too short (minimum ${ROOTPW_MIN_LEN} characters)"; return 1
    fi
    return 0
}

# Apply a password to a user. Reads the password from STDIN (pw usermod -h 0).
_rootpw_apply() {
    "${PW_BIN}" usermod "$1" -h 0
}

# Record success so the daemon sees a secured node and the gate stops prompting.
_rootpw_mark_secured() {
    mkdir -p "$(dirname "${SECURED_MARKER}")" 2>/dev/null || true
    # mirror the daemon's ownership intent; best-effort (no-op under test).
    chown colibri:colibri "$(dirname "${SECURED_MARKER}")" 2>/dev/null || true
    chmod 0750 "$(dirname "${SECURED_MARKER}")" 2>/dev/null || true
    : > "${SECURED_MARKER}"
    chmod 0644 "${SECURED_MARKER}" 2>/dev/null || true
}

# --- interactive (console only; not unit-tested) ----------------------------

# Prompt for one account, echo off, loop until a valid pair is applied.
_rootpw_prompt_and_set() {
    _user="$1"; _label="$2"
    while :; do
        stty -echo 2>/dev/null
        printf '  %s password: ' "${_label}"; IFS= read -r _p1; printf '\n'
        printf '  confirm %s: ' "${_label}"; IFS= read -r _p2; printf '\n'
        stty echo 2>/dev/null
        if _why="$(_rootpw_valid "${_p1}" "${_p2}")"; then
            printf '%s\n' "${_p1}" | _rootpw_apply "${_user}"
            _p1=; _p2=; _why=
            printf '  -> %s password set.\n\n' "${_label}"
            return 0
        fi
        _p1=; _p2=
        printf '  ! %s — try again.\n' "${_why}"
    done
}

# Visible "Continuing in 3s 2s 1s" countdown before boot resumes, so the
# operator can read the result before clawdie_live_gpu repaints the screen.
_rootpw_continue_countdown() {
    _n="${1:-3}"
    printf '  Continuing in '
    while [ "${_n}" -gt 0 ]; do
        printf '%ss ' "${_n}"
        sleep 1
        _n=$((_n - 1))
    done
    printf '... proceeding.\n'
}

clawdie_firstboot_rootpw_start() {
    _rootpw_secured && return 0

    # Talk to the operator on the system console. We run before the GPU/KMS
    # mode-switch, so this is the stable early text console — no settle/clear
    # workaround needed.
    exec < "${ROOTPW_CONSOLE}" > "${ROOTPW_CONSOLE}" 2>&1

    printf '\n================ FIRST BOOT — SECURE THIS NODE ================\n\n'
    printf '  This stick boots with NO root password. Set one now.\n'
    printf '  WRITE BOTH PASSWORDS ON PAPER — there is no recovery.\n\n'
    printf '  Press ENTER within %ss to set passwords' "${ROOTPW_COUNTDOWN}"
    printf ' (otherwise skipped) ... '

    if IFS= read -r -t "${ROOTPW_COUNTDOWN}" _ans; then
        printf '\n\n'
        _rootpw_prompt_and_set root    "ROOT (admin)"
        _rootpw_prompt_and_set clawdie "OPERATOR (clawdie)"
        _rootpw_mark_secured
        printf '  Node secured.\n'
        _rootpw_continue_countdown 3
    else
        printf '\n\n  [skipped] root/operator passwords NOT set — node is UNSECURED.\n'
        printf '  The colibri agent will NOT start or register with mother until a\n'
        printf '  password is set (colibri_daemon_require_secured). Set one to activate\n'
        printf '  this node. You will be prompted again on the next boot.\n'
        _rootpw_continue_countdown 3
    fi
    return 0
}

# On FreeBSD, hand off to rc.subr. Under test, skip so functions can be sourced.
if [ -n "${CLAWDIE_ROOTPW_TEST:-}" ]; then
    :
elif command -v run_rc_command >/dev/null 2>&1; then
    load_rc_config "${name}"
    run_rc_command "$1"
fi
