espilon-source/tools/C3PO/web/routes/pages.py
Eun0us 8b6c1cd53d ε - ChaCha20-Poly1305 AEAD + HKDF crypto upgrade + C3PO rewrite + docs
Crypto:
- Replace broken ChaCha20 (static nonce) with ChaCha20-Poly1305 AEAD
- HKDF-SHA256 key derivation from per-device factory NVS master keys
- Random 12-byte nonce per message (ESP32 hardware RNG)
- crypto_init/encrypt/decrypt API with mbedtls legacy (ESP-IDF v5.3.2)
- Custom partition table with factory NVS (fctry at 0x10000)

Firmware:
- crypto.c full rewrite, messages.c device_id prefix + AEAD encrypt
- crypto_init() at boot with esp_restart() on failure
- Fix command_t initializations across all modules (sub/help fields)
- Clean CMakeLists dependencies for ESP-IDF v5.3.2

C3PO (C2):
- Rename tools/c2 + tools/c3po -> tools/C3PO
- Per-device CryptoContext with HKDF key derivation
- KeyStore (keys.json) for master key management
- Transport parses device_id:base64(...) wire format

Tools:
- New tools/provisioning/provision.py for factory NVS key generation
- Updated flasher with mbedtls config for v5.3.2

Docs:
- Update all READMEs for new crypto, C3PO paths, provisioning
- Update roadmap, architecture diagrams, security sections
- Update CONTRIBUTING.md project structure
2026-02-10 21:28:45 +01:00

97 lines
3.1 KiB
Python

"""Page routes (login, dashboard, cameras, mlat)."""
import os
import secrets
from flask import Blueprint, render_template, redirect, url_for, request, session
def create_pages_blueprint(server_config):
"""
Create the pages blueprint.
Args:
server_config: Dict with keys:
- username: Login username
- password: Login password
- image_dir: Camera images directory
- c2_root: C2 root directory path
- require_login: Auth decorator
"""
bp = Blueprint("pages", __name__)
username = server_config["username"]
password = server_config["password"]
image_dir = server_config["image_dir"]
c2_root = server_config["c2_root"]
require_login = server_config["require_login"]
@bp.route("/login", methods=["GET", "POST"])
def login():
error = None
if request.method == "POST":
# CSRF validation
token = request.form.get("csrf_token", "")
if token != session.get("csrf_token", ""):
error = "Invalid request. Please try again."
else:
form_user = request.form.get("username")
form_pass = request.form.get("password")
if form_user == username and form_pass == password:
session["logged_in"] = True
return redirect(url_for("pages.dashboard"))
else:
error = "Invalid credentials."
# Generate CSRF token for the form
session["csrf_token"] = secrets.token_hex(32)
return render_template("login.html", error=error, csrf_token=session["csrf_token"])
@bp.route("/logout")
def logout():
session.pop("logged_in", None)
return redirect(url_for("pages.login"))
@bp.route("/")
@require_login
def index():
return redirect(url_for("pages.dashboard"))
@bp.route("/dashboard")
@require_login
def dashboard():
return render_template("dashboard.html", active_page="dashboard")
@bp.route("/cameras")
@require_login
def cameras():
full_image_dir = os.path.join(c2_root, image_dir)
try:
image_files = sorted([
f for f in os.listdir(full_image_dir)
if f.endswith(".jpg")
])
except FileNotFoundError:
image_files = []
return render_template("cameras.html", active_page="cameras", image_files=image_files)
@bp.route("/mlat")
@require_login
def mlat():
return render_template("mlat.html", active_page="mlat")
@bp.route("/streams/<filename>")
@require_login
def stream_image(filename):
from flask import send_from_directory
full_image_dir = os.path.join(c2_root, image_dir)
return send_from_directory(full_image_dir, filename)
@bp.route("/recordings/<filename>")
@require_login
def download_recording(filename):
from flask import send_from_directory
recordings_dir = os.path.join(c2_root, "static", "recordings")
return send_from_directory(recordings_dir, filename, as_attachment=True)
return bp