#!/bin/sh
# OpenBSD 7.9 owner setup, called by /etc/rc.local on every boot until complete.
# Deliberately never executes code supplied through the removable FAT volume.
set -eu
PATH=/bin:/sbin:/usr/bin:/usr/sbin
export PATH LC_ALL=C
umask 077

state=/var/db/rpi-firstboot
share=/usr/local/share/rpi-firstboot
boot=/boot
lock=/var/run/rpi-firstboot.lock
work=
mounted=no
locked=no

[ "$(id -u)" -eq 0 ] || { echo 'rpi-firstboot: root is required' >&2; exit 1; }
[ "$(uname -s)" = OpenBSD ] || { echo 'rpi-firstboot: OpenBSD is required' >&2; exit 1; }
[ ! -L "$state" ] || { echo 'rpi-firstboot: invalid state directory' >&2; exit 1; }
if [ -f "$state/done" ]; then
    exit 0
fi
install -d -o root -g wheel -m 0700 "$state"
exec >> /var/log/rpi-firstboot.log 2>&1

say() {
    printf 'rpi-firstboot: %s\n' "$*"
    printf 'rpi-firstboot: %s\n' "$*" > /dev/console || :
    logger -t rpi-firstboot "$*" || :
}
instructions() {
    say 'Setup is incomplete. Ethernet DHCP is available; SSH remains disabled.'
    say 'Power off, put the SD card in your computer, and copy your SSH .pub file'
    say 'to authorized_keys in the FAT boot partition (never copy a private key).'
    say 'Optional setup.conf: username=openbsd and hostname=openbsd-rpi4b.'
    say 'Safely eject the card and boot again. Details: /var/log/rpi-firstboot.log.'
}
fail() { say "$*"; exit 1; }
cleanup() {
    result=$?
    trap - EXIT HUP INT TERM
    if [ "$result" -ne 0 ] && [ "$locked" = yes ] && [ ! -f "$state/done" ]; then
        rcctl disable sshd >/dev/null 2>&1 || :
        rcctl stop sshd >/dev/null 2>&1 || :
        instructions
    fi
    if [ "$mounted" = yes ]; then
        umount "$boot" || say 'Could not unmount the read-only boot partition.'
    fi
    if [ -n "$work" ]; then rm -rf "$work"; fi
    if [ "$locked" = yes ]; then rmdir "$lock" || :; fi
    exit "$result"
}
trap cleanup EXIT
trap 'exit 1' HUP INT TERM
if mkdir "$lock" 2>/dev/null; then
    locked=yes
else
    say 'Another setup process holds the boot-time lock.'
    exit 1
fi

# A reboot can interrupt setup after SSH was enabled but before its final marker
# was durable. Keep the whole incomplete retry closed to remote login.
rcctl disable sshd
rcctl stop sshd >/dev/null 2>&1 || :
say 'Checking the boot partition for owner setup.'
[ -d "$boot" ] && [ ! -L "$boot" ] || fail 'Missing /boot mount point.'
if mount | awk '$2 == "on" && $3 == "/boot" { found = 1 } END { exit !found }'; then
    fail '/boot is already mounted; setup expects its dedicated noauto fstab entry.'
fi
mount -r "$boot" || fail 'Cannot mount the FAT boot partition; check its fstab entry.'
mounted=yes
work=$(mktemp -d /var/run/rpi-firstboot.XXXXXXXXXX)
mkdir "$work/validated"
/bin/sh "$share/validate.sh" "$boot" "$work/validated" || fail 'Owner input did not pass validation.'
owner=$(sed -n '1p' "$work/validated/settings")
machine=$(sed -n '2p' "$work/validated/settings")
userdir=/home/$owner

# Journal the account reservation before making it. This image reserves uid/gid
# 1000 for its one initial owner. On retry, only that exact account may be reused;
# existing base/system accounts can never be repurposed by a setup.conf name.
if [ -f "$state/owner" ]; then
    [ "$(cat "$state/owner")" = "$owner" ] || fail 'Setup already began for another username; restore that username in setup.conf.'
else
    if id "$owner" >/dev/null 2>&1; then fail 'The requested username already exists.'; fi
    if id 1000 >/dev/null 2>&1; then fail 'The image owner UID 1000 is already in use.'; fi
    if awk -F: -v name="$owner" '$1 == name || $3 == 1000 { found = 1 } END { exit !found }' /etc/group; then
        fail 'The requested owner group or GID 1000 already exists.'
    fi
    [ ! -e "$userdir" ] && [ ! -L "$userdir" ] || fail 'The owner home directory already exists.'
    printf '%s\n' "$owner" > "$state/owner.pending"
    sync
    mv "$state/owner.pending" "$state/owner"
    sync
fi

if awk -F: -v name="$owner" '$1 == name { found = 1 } END { exit !found }' /etc/group; then
    awk -F: -v name="$owner" '
        $1 == name { count++; if ($3 != 1000 || ($4 != "" && $4 != name)) bad = 1 }
        $3 == 1000 && $1 != name { bad = 1 }
        END { exit (count != 1 || bad) }
    ' /etc/group || fail 'The reserved owner group has unexpected properties.'
else
    groupadd -g 1000 "$owner"
fi

if ! id "$owner" >/dev/null 2>&1; then
    # Thirteen asterisks is OpenBSD passwd(5)'s key-only login convention.
    # Do not use an empty password or copy the image builder's credentials.
    useradd -u 1000 -g "$owner" -G wheel -d "$userdir" -s /bin/ksh \
        -p '*************' -e 0 -f 0 -c 'OpenBSD Raspberry Pi owner' "$owner"
fi
awk -F: -v name="$owner" -v home="$userdir" '
    $1 == name {
        count++
        if ($2 != "*************" || $3 != 1000 || $4 != 1000 ||
            ($6 != "" && $6 != "0") || ($7 != "" && $7 != "0") ||
            $8 != "OpenBSD Raspberry Pi owner" || $9 != home || $10 != "/bin/ksh") bad = 1
    }
    $3 == 1000 && $1 != name { bad = 1 }
    END { exit (count != 1 || bad) }
' /etc/master.passwd || fail 'The reserved owner account has unexpected properties.'
case " $(id -Gn "$owner") " in
    *' wheel '*) ;;
    *) usermod -G wheel "$owner" ;;
esac

[ ! -L "$userdir" ] && [ ! -L "$userdir/.ssh" ] || fail 'The owner home or .ssh directory is a symlink.'
install -d -o "$owner" -g "$owner" -m 0700 "$userdir" "$userdir/.ssh"
install -o "$owner" -g "$owner" -m 0600 "$work/validated/authorized_keys" "$userdir/.ssh/authorized_keys.pending"
mv -f "$userdir/.ssh/authorized_keys.pending" "$userdir/.ssh/authorized_keys"

printf 'permit nopass %s as root\n' "$owner" > "$work/doas.conf"
doas -C "$work/doas.conf" || fail 'Generated doas policy did not validate.'
install -o root -g wheel -m 0600 "$work/doas.conf" /etc/doas.conf.rpi-pending
mv -f /etc/doas.conf.rpi-pending /etc/doas.conf

cat "$share/sshd_config" > "$work/sshd_config"
printf '\nAllowUsers %s\n' "$owner" >> "$work/sshd_config"
# Standard rc has already generated fresh host keys before rc.local runs.
# Refuse to enable SSH if this config or those generated host keys are invalid.
sshd -t -f "$work/sshd_config" || fail 'SSH configuration or first-boot host keys did not validate.'
install -o root -g wheel -m 0600 "$work/sshd_config" /etc/ssh/sshd_config.rpi-pending
mv -f /etc/ssh/sshd_config.rpi-pending /etc/ssh/sshd_config

printf '%s\n' "$machine" > "$work/myname"
install -o root -g wheel -m 0644 "$work/myname" /etc/myname.rpi-pending
mv -f /etc/myname.rpi-pending /etc/myname
hostname "$machine"

# Everything needed for owner access is now committed. FAT input is left intact
# for diagnosis/retry and is ignored after done; it is never treated as a login
# recovery back door after normal administration begins.
umount "$boot" || fail 'Cannot unmount the read-only boot partition.'
mounted=no
sync
rcctl enable sshd
rcctl start sshd || fail 'SSH could not start; setup will retry next boot.'
rcctl check sshd || fail 'SSH did not stay running; setup will retry next boot.'
printf 'owner=%s\nhostname=%s\n' "$owner" "$machine" > "$state/done.pending"
sync
mv "$state/done.pending" "$state/done"
sync
say "Owner setup is complete: SSH as $owner; use doas -s for root."
say 'There is no default password. Boot-partition setup files will now be ignored.'
ssh-keygen -l -f /etc/ssh/ssh_host_ed25519_key.pub | while IFS= read -r line; do
    say "SSH host key: $line"
done
