#!/usr/bin/env bash
# ============================================================================
#  Consilium Belli — comms-harden.sh
#  Ships the safe defaults so security doesn't depend on the operator
#  remembering (the Enigma failure mode). Review it, then run it per node.
#
#  Civilian, observation-and-reporting only. Nothing here targets, jams, or
#  weaponises. Operate within NZ ISM/amateur licence terms; keep encryption
#  OFF amateur bands; cap TX power to the GURL limit before transmitting.
#
#  Usage:
#     ./comms-harden.sh handset     # an operator node (gets the private channel key)
#     ./comms-harden.sh relay       # an UNATTENDED router (NO private key — relays ciphertext only)
#     ./comms-harden.sh keys        # just generate PSK + WireGuard keys + one-time pads
#
#  Requires: openssl (always).  Optional: meshtastic CLI, wireguard-tools (wg).
#  Meshtastic flag names drift between firmware versions — every device command
#  is echoed before it runs, and `meshtastic --help` is the source of truth.
# ============================================================================
set -euo pipefail

ROLE="${1:-keys}"
OUT="./consilium-secrets-$(date -u +%Y%m%dT%H%M%SZ)"
mkdir -p "$OUT"
chmod 700 "$OUT"

say(){ printf '\n\033[33m»\033[0m %s\n' "$*"; }
run(){ printf '  \033[36m$ %s\033[0m\n' "$*"; if [ "${APPLY:-0}" = "1" ]; then eval "$*"; fi; }

need(){ command -v "$1" >/dev/null 2>&1; }

# ── 1. Generate key material (CSPRNG) ──────────────────────────────────────
say "Generating a real 256-bit pre-shared key (replaces the public AQ== default)"
PSK_B64="$(openssl rand -base64 32)"
printf '%s\n' "$PSK_B64" > "$OUT/channel.psk"
chmod 600 "$OUT/channel.psk"
echo "  PSK (base64) -> $OUT/channel.psk"

# ── 1b. Rolling-key master + today's derived key (wires in keyschedule.mjs) ──
# The mesh key is DERIVED from this master + the clock, never transmitted. The
# master is the crown jewel: distribute in person, never over the air.
say "Generating the rolling-key master secret (distribute at muster; never transmit)"
MASTER_B64="$(openssl rand -base64 32)"
printf 'base64:%s\n' "$MASTER_B64" > "$OUT/channel.master"
chmod 600 "$OUT/channel.master"
echo "  Master -> $OUT/channel.master"

CHANNEL_PSK="$PSK_B64"   # fallback: the static per-run PSK
if command -v node >/dev/null 2>&1 && [ -f "./keyschedule.mjs" ]; then
  say "Deriving today's rolling key from the master (period=day) via keyschedule.mjs"
  DERIVED="$(CONSILIUM_MASTER="base64:$MASTER_B64" node ./keyschedule.mjs --period day | awk '/^PSK/{print $3}')"
  if [ -n "$DERIVED" ]; then CHANNEL_PSK="$DERIVED"
    echo "  Today's channel key = derived rolling key — rotate on a cron with keyschedule.mjs --apply"; fi
else
  echo "  (node + ./keyschedule.mjs not both present here — using the static PSK; add them for rolling keys)"
fi

if need wg; then
  say "Generating a WireGuard keypair for the encrypted backhaul tunnel"
  wg genkey | tee "$OUT/wg_private.key" | wg pubkey > "$OUT/wg_public.key"
  chmod 600 "$OUT/wg_private.key"
  echo "  WireGuard keys -> $OUT/wg_private.key / wg_public.key"
else
  echo "  (wireguard-tools not installed — skipping WG keys; install 'wg' to enable)"
fi

# ── 2. One-time pads for the crown-jewel command channel ───────────────────
# Truly random, key >= message, used ONCE, destroyed after use. Hand these over
# in person at muster — that is the only genuinely hard step, and it's solved
# by meeting once. 5-char groups, 40 groups/page, N pages.
say "Generating one-time pads (distribute at muster; use once; destroy after use)"
PAD_PAGES="${PAD_PAGES:-5}"
{
  echo "CONSILIUM ONE-TIME PAD  —  USE ONCE, THEN DESTROY"
  echo "Generated $(date -u +%Y-%m-%dT%H:%M:%SZ)  ·  pages: $PAD_PAGES"
  for p in $(seq 1 "$PAD_PAGES"); do
    printf '\n--- PAGE %02d ---\n' "$p"
    openssl rand -hex 100 | tr 'a-f0-9' 'A-Z0-9' | fold -w5 \
      | paste -sd' ' - | fold -sw60
  done
} > "$OUT/one-time-pads.txt"
chmod 600 "$OUT/one-time-pads.txt"
echo "  Pads -> $OUT/one-time-pads.txt"

[ "$ROLE" = "keys" ] && { say "Key material generated in $OUT. Done."; exit 0; }

# ── 3. Apply the Meshtastic safe config ────────────────────────────────────
# Set APPLY=1 to actually run the device commands; default is a dry-run print.
if ! need meshtastic; then
  say "meshtastic CLI not found — printing the intended config (install: pipx install meshtastic)"
fi

say "Safe defaults for a '$ROLE' node  (dry-run unless APPLY=1)"

# Disable position + telemetry broadcasts on EVERY node (handset and relay).
run "meshtastic --set position.position_broadcast_secs 0"
run "meshtastic --set position.position_broadcast_smart_enabled false"
run "meshtastic --pos-fields \"\"                 # broadcast no position fields"
run "meshtastic --set telemetry.device_update_interval 0"
run "meshtastic --set telemetry.environment_update_interval 0"

if [ "$ROLE" = "handset" ]; then
  # Operator node: rotate the primary channel onto the real PSK, name it, no uplink.
  run "meshtastic --ch-set name Ops --ch-index 0"
  run "meshtastic --ch-set psk base64:$CHANNEL_PSK --ch-index 0"
  run "meshtastic --ch-set uplink_enabled false --ch-index 0"
  run "meshtastic --ch-set downlink_enabled false --ch-index 0"
  run "meshtastic --set lora.tx_enabled true"
  say "PKC direct messages: automatic on firmware >= 2.5 (each node has its own X25519 keypair)."
elif [ "$ROLE" = "relay" ]; then
  # UNATTENDED router: forwards ciphertext at the LoRa layer WITHOUT the private
  # channel key. Do NOT load channel.psk onto this node.
  run "meshtastic --set device.role ROUTER"
  say "RELAY: do NOT add the private channel PSK here. It rebroadcasts encrypted"
  say "       packets it cannot read — physical capture of this node yields no key."
else
  echo "Unknown role '$ROLE' (use: handset | relay | keys)"; exit 1
fi

# ── 4. Reminders the operator must not skip ────────────────────────────────
cat <<'EOF'

────────────────────────────────────────────────────────────────────────────
 BEFORE YOU TRANSMIT
  • Cap TX power to the NZ GURL limit (HaLow's 27 dBm is a US figure).
  • Keep encryption OFF amateur/HF bands — brevity codes only there.
  • Never load a private channel key onto a node you leave unattended.
  • Distribute pads in person; use each once; destroy after use.
  • Rotate the PSK on a schedule and after any suspected loss
    (assume anything sent can be recorded now and decrypted later).
  • Add your own end-to-end crypto ABOVE Meshtastic so one compromised
    node never exposes the whole mesh.
 Secrets were written to: THIS run's ./consilium-secrets-*/ directory (chmod 700).
 Move them to encrypted storage and shred the working copies.
────────────────────────────────────────────────────────────────────────────
EOF
