#!/bin/sh
# keyboot-keys — userland key management for a keyboot system: operator SSH keys
# for the unlock environment, and the LUKS keyfile slots.
#
#   keyboot-keys ssh  list                      show operator keys in the ESP overlay
#   keyboot-keys ssh  add  <pubkey|file>        authorize an operator key (unlock env)
#   keyboot-keys ssh  remove <pattern>          de-authorize keys matching a substring
#   keyboot-keys luks <op> [--keyfile F] [...]  manage LUKS keyfile slots (delegates)
#
# SSH: the unlock env's keys are baked into the image AND merged with an editable
# overlay on the (vfat) ESP at /esp/keyboot/authorized_keys (init/stage-3-ssh).
# This tool edits that overlay, so operator keys rotate WITHOUT rebuilding the
# image. Run it on the installed OS (it mounts the keyboot ESP) or in the keyboot
# env. New keys take effect on the next boot into the keyboot unlock environment.
#
# LUKS: a thin, discoverable front-end over the proven keyfile tools — read-only
# ops go to `keyboot keyfile`, mutations to `keyboot-install keyfile`. The keyfile
# container must be present (default /etc/keyboot/keyfile.luks, as in the keyboot
# provision/rescue env; else pass --keyfile). luks ops:
#   list | info | add-recovery | enroll-automation | passwd | rekey | rotate

set -u
PROG=keyboot-keys
ESP_AK_REL=keyboot/authorized_keys
KEYFILE_DEFAULT=/etc/keyboot/keyfile.luks

die()  { printf '%s: error: %s\n' "$PROG" "$*" >&2; exit 2; }
warn() { printf '%s: %s\n' "$PROG" "$*" >&2; }
usage(){ awk 'NR==1{next} /^#/{print; next} {exit}' "$0" | sed 's/^# \{0,1\}//'; }

# ---- ESP discovery + bounded rw bracket (mirrors init/lib/slot.sh) ----
esp_dev() {
    for d in /dev/md/keyboot-esp /dev/md/esp; do [ -e "$d" ] && { echo "$d"; return; }; done
    for d in /sys/block/md*; do [ -e "$d" ] && [ -e "/dev/${d##*/}" ] && { echo "/dev/${d##*/}"; return; }; done
    command -v findfs >/dev/null 2>&1 && findfs LABEL=KEYBOOT-ESP 2>/dev/null && return
    command -v blkid  >/dev/null 2>&1 && blkid -L KEYBOOT-ESP 2>/dev/null && return
}
ESP_MNT=""; ESP_REMOUNTED=0
esp_open() {  # $1 = rw|ro ; sets ESP_MNT
    _mode="$1"
    if grep -q ' /esp ' /proc/mounts 2>/dev/null; then
        ESP_MNT=/esp
        [ "$_mode" = rw ] && { mount -o remount,rw /esp 2>/dev/null || die "cannot remount /esp rw"; ESP_REMOUNTED=1; }
        return
    fi
    _dev="$(esp_dev)"; [ -n "$_dev" ] || die "no keyboot ESP found (mdraid1 vfat)"
    ESP_MNT=/run/keyboot-keys-esp; mkdir -p "$ESP_MNT"
    mount -t vfat -o "$_mode" "$_dev" "$ESP_MNT" 2>/dev/null || die "mount $_dev failed"
}
esp_close() {
    sync 2>/dev/null || true
    if [ "$ESP_MNT" = /esp ]; then
        [ "$ESP_REMOUNTED" = 1 ] && mount -o remount,ro /esp 2>/dev/null || true
    elif [ -n "$ESP_MNT" ]; then umount "$ESP_MNT" 2>/dev/null || true; fi
}

# Key body (type+base64), ignoring options/comment — for dedup + matching.
key_body() { awk '{for(i=1;i<=NF;i++) if($i ~ /^(ssh|ecdsa|sk)-/){print $i" "$(i+1); exit}}'; }

ssh_list() {
    esp_open ro; _f="$ESP_MNT/$ESP_AK_REL"
    if [ -r "$_f" ] && [ -s "$_f" ]; then
        printf 'operator keys in the ESP overlay (%s):\n' "$_f"
        grep -vE '^\s*($|#)' "$_f" 2>/dev/null
    else
        printf 'no ESP authorized_keys overlay yet (only image-baked keys are active).\n'
    fi
    esp_close
}
ssh_add() {
    [ -n "${1:-}" ] || die "ssh add needs a pubkey string or file"
    if [ -f "$1" ]; then _new="$(cat "$1")"; else _new="$1"; fi
    printf '%s' "$_new" | key_body | grep -q . || die "not an SSH public key: ${1%% *}..."
    _b="$(printf '%s' "$_new" | key_body)"
    esp_open rw; _f="$ESP_MNT/$ESP_AK_REL"; mkdir -p "$(dirname "$_f")"
    if [ -r "$_f" ] && grep -qF "$_b" "$_f" 2>/dev/null; then warn "key already authorized"; esp_close; return 0; fi
    printf '%s\n' "$_new" >> "$_f"; chmod 600 "$_f" 2>/dev/null || true
    printf '%s: authorized %s (effective next keyboot-env boot)\n' "$PROG" "$_b" >&2
    esp_close
}
ssh_remove() {
    [ -n "${1:-}" ] || die "ssh remove needs a substring (comment or key fragment)"
    esp_open rw; _f="$ESP_MNT/$ESP_AK_REL"
    [ -r "$_f" ] || { warn "no overlay to edit"; esp_close; return 0; }
    _before="$(wc -l < "$_f" 2>/dev/null)"
    grep -vF "$1" "$_f" > "$_f.tmp" 2>/dev/null && mv "$_f.tmp" "$_f"
    _after="$(wc -l < "$_f" 2>/dev/null)"
    printf '%s: removed %s line(s) matching %s\n' "$PROG" "$(( ${_before:-0} - ${_after:-0} ))" "$1" >&2
    esp_close
}

luks() {
    [ $# -ge 1 ] || die "luks needs an op (list|info|add-recovery|enroll-automation|passwd|rekey|rotate)"
    _op="$1"; shift
    # Pull an explicit --keyfile if given; else default.
    _kf="$KEYFILE_DEFAULT"; _rest=""
    while [ $# -gt 0 ]; do
        case "$1" in --keyfile) _kf="$2"; shift 2 ;; --keyfile=*) _kf="${1#*=}"; shift ;; *) _rest="$_rest $1"; shift ;; esac
    done
    [ -r "$_kf" ] || die "keyfile $_kf not readable — run in the keyboot provision/rescue env, or pass --keyfile <container>"
    case "$_op" in
        list)   exec keyboot keyfile list-slots "$_kf" ;;
        info)   exec keyboot keyfile info "$_kf" ;;
        add-recovery|enroll-automation|passwd|rekey|rotate)
            command -v keyboot-install >/dev/null 2>&1 || die "keyboot-install not present (rescue/provision env only)"
            # shellcheck disable=SC2086
            exec keyboot-install keyfile "$_op" "$_kf" $_rest ;;
        *) die "unknown luks op: $_op" ;;
    esac
}

[ $# -ge 1 ] || { usage; exit 2; }
domain="$1"; shift
case "$domain" in
    ssh)
        [ $# -ge 1 ] || die "ssh needs list|add|remove"
        op="$1"; shift
        case "$op" in
            list)   ssh_list ;;
            add)    ssh_add "${1:-}" ;;
            remove) ssh_remove "${1:-}" ;;
            *) die "unknown ssh op: $op (list|add|remove)" ;;
        esac ;;
    luks) luks "$@" ;;
    -h|--help|help) usage ;;
    *) die "unknown domain: $domain (ssh|luks)" ;;
esac
