Commit Graph
3 Commits
Author SHA1 Message Date
Florian Wilde c38c988c3c feat: Anmelden mit Passkey
Passkeys (WebAuthn/FIDO2) als zweiter Anmeldeweg - auf der Anmeldeseite
und bei der erneuten Bestaetigung vor sensiblen Aktionen. Verwaltet unter
"Mein Konto".

Der Passkey ERSETZT den zweiten Faktor, statt zu ihm hinzuzukommen. Das
ist tragbar, weil er zwei Faktoren in sich vereint: Besitz des Geraets
und dessen Entsperrung. Damit das keine Annahme bleibt, wird die
Nutzerverifikation angefordert (userVerification: 'required') und
serverseitig nachgeprueft - eine Antwort ohne sie wird abgewiesen.

TOTP muss vorher eingerichtet sein. Ein Passkey haengt an einem Geraet;
waere er der einzige Weg hinein, koennte ein verlorenes Handy die Akte
schliessen.

Aufbau:
- Die Pruefung laeuft vollstaendig im Credentials-Provider "passkey",
  nicht in der Server Action davor. Ein "war schon geprueft" ueber die
  Grenze zu reichen waere die Stelle, an der sich Anmeldungen faelschen
  lassen.
- Challenges liegen serverseitig und werden per DELETE genau einmal
  verbraucht - die Datenbank entscheidet, wer sie bekommt.
- Bereichskennung und Herkunft kommen aus AUTH_URL, nicht aus einer
  zweiten Konfigurationsquelle.
- Ohne https wird der Knopf gar nicht erst angeboten.
- Rate-Limiting, Kontosperre und Audit-Log gelten unveraendert; die
  Anmeldung nennt den verwendeten Passkey namentlich.

Zwei Anmerkungen zur Migration:
- "prisma migrate diff" wollte zusaetzlich die Trigramm-Indizes aus
  prisma/sql/suche.sql loeschen, die Prisma nicht kennt. Die Zeilen sind
  entfernt und im Migrationskopf vermerkt - ein DROP haette die unscharfe
  Suche stillgelegt, ohne dass etwas fehlgeschlagen waere.
- "prisma format" hat das Schema durchgaengig neu ausgerichtet. Ohne
  Whitespace betrachtet sind es 88 hinzugefuegte Zeilen und keine
  geloeschte.

Geprueft: 21 Einheitstests, drei End-to-End-Tests gegen Chromiums
virtuellen Authenticator (einrichten, anmelden, bestaetigen, entfernen,
Geraet ohne Verifikation abgewiesen), beide Geraeteprofile. 496 Tests,
null Typfehler, Produktionsbau steht.

Nebenbei: Die Zahlen im README waren veraltet (24 statt 33 Modelle, 46
statt 79 Seiten). Sie stehen jetzt mit dem Befehl daneben, mit dem man
sie nachzaehlt.
2026-08-22 18:44:56 +02:00
Florian Wilde 232543e2aa fix: Leere Schluesselvariable legte die Verschluesselung lahm
docker-compose.yml reicht ENC_KEY_V2 immer an den Behaelter durch - mit
leerem Wert, wenn in der .env keiner steht. src/lib/env.ts erkannte
Schluessel aber am Variablennamen statt am Inhalt, fand also ein leeres
ENC_KEY_V2 und brach ab mit "Umgebungsvariable ENC_KEY_V2 fehlt".

Die Meldung war doppelt irrefuehrend: Sie nannte einen Schluessel, den
niemand wollte, und sie erschien an einer Stelle, die damit nichts zu tun
hat - beim Einrichten des zweiten Faktors, als Serverfehler ohne Text.

Betroffen war jede Stelle, die verschluesselte Felder schreibt oder
liest, also praktisch alles ausser der Anmeldung selbst. Die kam durch,
weil sie ohne Verschluesselung auskommt - der Fehler trat deshalb erst
auf der naechsten Seite auf, und der Zusammenhang war nicht zu sehen.

Leere Schluesselvariablen werden jetzt uebersprungen. Sieben Tests decken
die Auswertung ab, darunter genau diese Konstellation.

Die Meldung beim Schluesselwechsel nennt jetzt ausserdem, welche
Versionen vorhanden sind, und die richtige Reihenfolge: erst den neuen
Schluessel eintragen, dann ENC_KEY_AKTIV umstellen.

Dazu scripts/anmeldung-pruefen.mjs. Das Anmeldeformular sagt bewusst
nicht, woran eine Anmeldung scheiterte - sonst liesse sich daran ablesen,
welche Adressen es gibt. Auf dem Server darf man es wissen: Jeder Versuch
wird mit Grund protokolliert, und das Skript liest ihn vor, zusammen mit
dem Zustand des Zugangs, dem Stand der Sperre und der AUTH_URL.

Etappe: 8
Phase: Haertung
2026-08-22 14:00:25 +02:00
Florian Wilde c49810d092 feat: Feldverschluesselung, Argon2id und TOTP-Zweitfaktor
Die Sicherheitsschicht, auf der alles Weitere aufsetzt.

Feldverschluesselung (AES-256-GCM):
- Umschlagformat bc1.<version>.<iv>.<tag>.<ct>. Die Schluesselversion
  erlaubt Rotation, ohne alte Daten unlesbar zu machen.
- Der Feldname wird als AAD mitauthentifiziert. Ein Geheimtext laesst
  sich damit nicht von diagnosen nach notizen umhaengen.
- Leere Werte werden zu NULL statt zu einem Umschlag - sonst verriete
  die Datenbank, dass ueberhaupt etwas eingetragen war.
- VERSCHLUESSELTE_FELDER ist der einzige Ort, an dem steht, was
  verschluesselt gehoert. sealen()/oeffnen() arbeiten darauf.

Argon2id nach OWASP-Empfehlung (19 MiB, t=2, p=1), mit hashVeraltet()
fuer spaetere Parametererhoehung ohne Migration.

Blindindex per HMAC-SHA256 mit eigenem Schluessel: erlaubt die Frage
"kenne ich diese IBAN schon?", ohne sie im Klartext zu halten.
Derselbe Mechanismus pseudonymisiert IP-Adressen fuers Audit-Log.

TOTP mit otpauth, QR-Code serverseitig als Data-URI - kein externer
QR-Dienst, kein CDN. Wiederherstellungscodes aus einem Alphabet ohne
0/O und 1/I, einzeln Argon2id-gehasht, Pruefung ohne fruehen Abbruch.

37 Tests decken Rundlauf, Manipulation, Feldbindung, Rotation,
Salzung und Normalisierung ab.

Etappe: 1
Phase: Fundament
2026-08-22 01:42:13 +02:00