#!/usr/bin/env bash
set -euo pipefail

# 0) Create dedicated user if it doesn't exist
if ! id -u styx >/dev/null 2>&1; then
  sudo useradd -m -s /bin/bash styx
  echo "Created user 'styx'"
fi

# 1) App directory
sudo mkdir -p /opt/styx-kiosk/keys
sudo chown -R styx:styx /opt/styx-kiosk

# 2) Drop the kiosk app (written below) into place
sudo tee /opt/styx-kiosk/kiosk.py >/dev/null <<'PY'
#!/usr/bin/env python3
import base64, json, os, subprocess, threading, tempfile
from datetime import datetime
import tkinter as tk
from tkinter import ttk, messagebox

APP_TITLE = "STYX Airgap Signer"
CAMERA_DEV = os.environ.get("ZBAR_DEV", "/dev/video0")
KEY_PATH   = os.environ.get("STYX_KEY", "/vault/keys/signer_ed25519.pem")  # in the LUKS vault
ALGO       = os.environ.get("STYX_ALGO", "ed25519")  # or 'secp256r1'
QR_TMP     = "/tmp/styx_signed.png"

def zbar_scan_oneshot():
    # --raw -> data only; --nodisplay -> no preview window; --oneshot -> exit after first code
    # (zbarcam supports --oneshot; prints one code and exits). :contentReference[oaicite:2]{index=2}
    cmd = ["zbarcam", "--raw", "--nodisplay", "--oneshot", CAMERA_DEV]
    try:
        out = subprocess.check_output(cmd, text=True, timeout=30)
        out = out.strip()
        return out if out else None
    except Exception as e:
        return None

def openssl_pub_der_b64(key_path):
    der = subprocess.check_output(["openssl","pkey","-in",key_path,"-pubout","-outform","DER"])
    return base64.b64encode(der).decode()

def sign_bytes(msg: bytes, key_path: str, algo: str) -> bytes:
    with tempfile.NamedTemporaryFile(delete=False) as f:
        f.write(msg)
        msg_path = f.name
    try:
        if algo.lower() == "ed25519":
            # Ed25519 expects raw message; OpenSSL handles hashing internally.
            sig = subprocess.check_output(
                ["openssl","pkeyutl","-sign","-inkey",key_path,"-rawin","-in",msg_path]
            )
            return sig
        elif algo.lower() in ("secp256r1","prime256v1","p256"):
            # ECDSA over P-256; hash with SHA-256; OpenSSL returns DER-encoded (r,s)
            sig = subprocess.check_output(
                ["openssl","dgst","-sha256","-sign",key_path,msg_path]
            )
            return sig
        else:
            raise RuntimeError(f"Unsupported algo: {algo}")
    finally:
        try: os.unlink(msg_path)
        except: pass

def make_signed_envelope(scanned_text: str, key_path: str, algo: str) -> dict:
    # Accept either raw string or JSON with 'tx_bytes' (base64) or 'message'
    try:
        obj = json.loads(scanned_text)
        if "tx_bytes" in obj:
            msg = base64.b64decode(obj["tx_bytes"])
        elif "message" in obj:
            msg = obj["message"].encode()
        else:
            # If it's JSON but doesn't carry known fields, sign canonical JSON bytes
            msg = json.dumps(obj, sort_keys=True, separators=(",",":")).encode()
        request_id = obj.get("request_id")
    except Exception:
        # Non-JSON → treat the scanned text as the message to sign
        msg = scanned_text.encode()
        request_id = None

    sig = sign_bytes(msg, key_path, algo)
    env = {
        "algo": algo.lower(),
        "signature_b64": base64.b64encode(sig).decode(),
        "pubkey_spki_der_b64": openssl_pub_der_b64(key_path),
        "payload_sha256_b64": base64.b64encode(subprocess.check_output(["openssl","dgst","-sha256","-binary"], input=msg)).decode(),
        "quote_raw": scanned_text,
        "request_id": request_id,
        "device": os.uname().nodename,
        "ts_utc": datetime.utcnow().isoformat(timespec="seconds") + "Z",
    }
    return env

def qrencode_to_file(text: str, path: str):
    # Use qrencode CLI to render a PNG we can display.
    subprocess.run(["qrencode","-l","M","-s","16","-t","PNG","-o",path], input=text.encode(), check=True)

class App(tk.Tk):
    def __init__(self):
        super().__init__()
        self.title(APP_TITLE)
        self.attributes("-fullscreen", True)
        self.configure(background="black")
        self.bind("<Escape>", lambda e: self.quit())  # for maintenance only

        s = ttk.Style(self)
        s.configure("Big.TButton", font=("DejaVu Sans", 48), padding=24)
        s.configure("Big.TLabel",  font=("DejaVu Sans", 32), foreground="white", background="black")

        self.container = tk.Frame(self, bg="black")
        self.container.pack(expand=True, fill="both")

        self.status = ttk.Label(self.container, text="Ready", style="Big.TLabel")
        self.status.pack(pady=20)

        self.scan_btn = ttk.Button(self.container, text="SCAN", style="Big.TButton", command=self.start_scan)
        self.scan_btn.pack(pady=20)

        self.image_label = tk.Label(self.container, bg="black")
        self.image_label.pack(pady=10)

        self.new_btn = ttk.Button(self.container, text="NEW SCAN", style="Big.TButton", command=self.reset)
        self.new_btn.pack_forget()

        self.note = ttk.Label(self.container, text="", style="Big.TLabel")
        self.note.pack(pady=10)

        if not os.path.exists(KEY_PATH):
            self.status.config(text=f"Key not found at {KEY_PATH}\nInsert/unlock vault to proceed.")

    def reset(self):
        self.image_label.configure(image="")
        self.image_label.image = None
        self.new_btn.pack_forget()
        self.note.config(text="")
        self.status.config(text="Ready")
        self.scan_btn.config(state="normal")

    def start_scan(self):
        if not os.path.exists(KEY_PATH):
            messagebox.showerror("Key missing", f"Signing key not found at:\n{KEY_PATH}\nUnlock your vault.")
            return
        self.status.config(text="Scanning…")
        self.scan_btn.config(state="disabled")
        threading.Thread(target=self._do_scan_and_sign, daemon=True).start()

    def _do_scan_and_sign(self):
        scanned = zbar_scan_oneshot()
        if not scanned:
            self.after(0, self._scan_failed)
            return
        try:
            envelope = make_signed_envelope(scanned, KEY_PATH, ALGO)
            payload = json.dumps(envelope, separators=(",",":"))
            qrencode_to_file(payload, QR_TMP)
            self.after(0, self._show_qr, envelope)
        except Exception as e:
            self.after(0, lambda: self._error(str(e)))

    def _scan_failed(self):
        self.status.config(text="No QR detected. Try again.")
        self.scan_btn.config(state="normal")

    def _show_qr(self, envelope):
        # Display the PNG produced by qrencode
        try:
            img = tk.PhotoImage(file=QR_TMP)
            self.image_label.configure(image=img)
            self.image_label.image = img
        except Exception as e:
            self.status.config(text=f"QR render failed: {e}")
            self.scan_btn.config(state="normal")
            return
        self.status.config(text="Signed. Show this QR to your online box.")
        self.note.config(text=f"Algo: {envelope['algo']}   Host: {envelope['device']}")
        self.new_btn.pack(pady=20)

if __name__ == "__main__":
    App().mainloop()
PY
sudo chmod +x /opt/styx-kiosk/kiosk.py
sudo chown -R styx:styx /opt/styx-kiosk

# 3) Minimal X session: openbox + kiosk; no mouse pointer
sudo -u styx tee /home/styx/.xinitrc >/dev/null <<'XRC'
xset -dpms
xset s off
xset s noblank
# If 'unclutter' is installed, uncomment the next line to hide cursor:
# unclutter -idle 0 -root &
openbox-session &
/opt/styx-kiosk/kiosk.py
XRC
sudo chown styx:styx /home/styx/.xinitrc
sudo chmod 0755 /home/styx/.xinitrc

# 4) Autologin the 'styx' user on tty1, auto-start X
sudo mkdir -p /etc/systemd/system/getty@tty1.service.d
sudo tee /etc/systemd/system/getty@tty1.service.d/override.conf >/dev/null <<'OVR'
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin styx --noclear %I $TERM
Type=idle
OVR

sudo -u styx tee -a /home/styx/.bash_profile >/dev/null <<'BRC'
# Start X on the first tty automatically, headless
if [ -z "$DISPLAY" ] && [ "$(tty)" = "/dev/tty1" ]; then
  exec startx -- -nocursor
fi
BRC

sudo systemctl daemon-reload
sudo systemctl enable getty@tty1.service

echo "Done. Reboot to try the kiosk."
