kundenplattform/encryption.py
Thomas Stallinger 19b39af0ce feat: Selfservice-UI fuer Organisation + Installationsmedium (Phase 5)
Zwei neue Menuepunkte fuer eingeloggte Kunden (nicht der
organisationsuebergreifende /admin/-Bereich):

- "Organisation": Name bearbeiten (ruft Phase-3-PATCH auf
  provisioning-server auf).
- "Installationsmedium": WLAN-SSID/-Passwort, Vorlagenwahl, Kontingent
  und Gueltigkeitsdauer speichern, personalisierte Kunden-ISO per
  Klick erstellen (Phase-4-Endpoints), Fortschritt/Status-Anzeige
  (Meta-Refresh waehrend pending/running), Download + dd/Rufus-
  Anleitung. Getrennte "aufladen"-Aktion erhoeht Kontingent/
  Gueltigkeit der bestehenden Enrollment Session (neuer PATCH-
  Endpoint), ohne eine neue ISO zu erzeugen - die bereits ausgeteilte
  bleibt gueltig.

Neue Tabelle installationsmedium_konfiguration (Migration 0004, eigene
DB, kein Foreign Key auf provisioning-server-Ressourcen, siehe
ADR-0011) haelt SSID/verschluesselten PSK/Vorlage/Kontingent-Defaults
sowie die IDs der aktuell gueltigen Enrollment Session und des
zugehoerigen ISO-Baus.

WLAN-PSK wird Fernet-verschluesselt gespeichert (encryption.py, neuer
KUNDENPLATTFORM_ENCRYPTION_KEY) - echte Zugangsdaten, kein
Passwort-Hash-Fall. Download laeuft ueber einen Streaming-Proxy
(anode_stream() in anode_client.py) statt eines direkten Links, weil
der Browser kein KUNDENPLATTFORM_TOKEN besitzt.

Lokal end-to-end verifiziert (Wegwerf-Postgres fuer beide Services,
echter build_customer_iso.sh-Lauf ueber die Kundenplattform-UI):
Formular -> Enrollment Session + ISO-Bau -> Status-Polling ->
Download-Proxy liefert byte-identische, korrekt personalisierte Datei
(SSID/PSK/Aktivierungscode per xorriso-Extraktion gegengeprueft) ->
Aufladen erhoeht Kontingent/Gueltigkeit der bestehenden Session ohne
neuen Bau -> "neues Installationsmedium erstellen" erzeugt neue
Session + ISO, alte Session bleibt unangetastet nutzbar (alter
ISO-Bau wird laut Phase-4-Cleanup automatisch entfernt), leeres
PSK-Feld uebernimmt das zuvor gespeicherte PSK korrekt.
Organisation-Name-Aenderung ueber PATCH verifiziert. Unauthentifizierter
Zugriff auf beide neuen Routen leitet korrekt zu /login um.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-07 01:36:46 +02:00

35 lines
1.1 KiB
Python

import os
from cryptography.fernet import Fernet, InvalidToken
# Phase 5: verschluesselt den WLAN-PSK at-rest (echte Zugangsdaten, kein
# Passwort-Hash-Fall - muss beim ISO-Bau wieder im Klartext verfuegbar
# sein). Eigener Schluessel statt Ableitung aus SESSION_SECRET, um
# Cookie/JWT-Signing und At-rest-Verschluesselung nicht zu vermischen.
# Erzeugen: python3 -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
ENCRYPTION_KEY = os.environ.get("KUNDENPLATTFORM_ENCRYPTION_KEY", "")
def _fernet() -> Fernet:
if not ENCRYPTION_KEY:
raise RuntimeError("KUNDENPLATTFORM_ENCRYPTION_KEY ist nicht gesetzt.")
return Fernet(ENCRYPTION_KEY.encode("ascii"))
def encrypt_psk(psk: str) -> bytes:
return _fernet().encrypt(psk.encode("utf-8"))
def decrypt_psk(psk_encrypted) -> str | None:
if psk_encrypted is None:
return None
try:
return _fernet().decrypt(bytes(psk_encrypted)).decode("utf-8")
except InvalidToken:
# Sollte nur bei Schluesselwechsel/Datenkorruption passieren - dann
# lieber "kein PSK gespeichert" anzeigen als mit 500 abzubrechen.
return None