2026-11-01 15:01:25 +01:00
|
|
|
# Anmeldung und Bücher-Schlüssel
|
|
|
|
|
|
|
|
|
|
## Zwei Wege hinein
|
|
|
|
|
|
|
|
|
|
**Benutzername und Passwort** (`accounts/views.py` `login_view`): Das Login-Formular leitet im
|
|
|
|
|
Browser aus dem Passwort den Bücher-Schlüssel ab (PBKDF2-SHA256, Salt `diora:<username>`,
|
|
|
|
|
200 000 Runden) und legt ihn bis zur nächsten Seite in `localStorage['diora_pending_enc_key']`
|
|
|
|
|
ab. Die App-Seite übernimmt ihn als `diora_enc_key_<userId>` (`32-crypto.js`, `99-init.js`).
|
|
|
|
|
|
|
|
|
|
**Authelia (OpenID Connect)** (`accounts/oidc.py`, `accounts/oidc_views.py`): als zusätzlicher
|
|
|
|
|
Weg, die Anmeldung mit Passwort bleibt. Der Ablauf ist Authorization Code mit PKCE (S256) und
|
|
|
|
|
einem vertraulichen Client (`client_secret_basic`). Das id_token wird hier geprüft (RS256 gegen
|
|
|
|
|
die JWKS des Anbieters, `iss`, `aud`, `exp`, `nonce`), dafür reicht `cryptography`.
|
|
|
|
|
|
|
|
|
|
- Ein Konto wird **einmal verknüpft**: in den Einstellungen „Mit Authelia verknüpfen“, während
|
|
|
|
|
man normal angemeldet ist (`/accounts/oidc/start/?link=1`). Gespeichert wird `OidcIdentity`
|
|
|
|
|
(`issuer`, `sub`; Name und Mail nur zur Anzeige). Danach führt „Mit Authelia anmelden“ auf der
|
|
|
|
|
Login-Seite in dieses Konto.
|
|
|
|
|
- Eine nicht verknüpfte Authelia-Identität wird abgewiesen. Es entstehen **keine neuen
|
|
|
|
|
Konten** über Authelia, und es wird auch nicht über Name oder Mail automatisch verknüpft.
|
|
|
|
|
- Wer hinein darf, regelt Authelia (Policy `mitglieder` = Gruppen `admins` oder `freunde`).
|
|
|
|
|
- Einstellungen per Umgebung: `DIORA_OIDC_ISSUER`, `DIORA_OIDC_CLIENT_ID`,
|
|
|
|
|
`DIORA_OIDC_CLIENT_SECRET`, `DIORA_OIDC_REDIRECT_URI` (fest, weil hinter dem Proxy kein
|
|
|
|
|
https erkannt wird), optional `DIORA_OIDC_PROVIDER_NAME`. Fehlt eins der ersten drei, gibt es
|
|
|
|
|
keinen Knopf. Auf prod stehen sie in `~/docker-compose/diora-prod/.env` (Strato), das
|
|
|
|
|
Gegenstück liegt auf Hetzner in `/opt/authelia/oidc-clients.env`.
|
|
|
|
|
- Redirect-URI im Authelia-Client: `https://diora.creamfresh.xyz/accounts/oidc/callback/`.
|
|
|
|
|
|
|
|
|
|
## Der Schlüssel nach einer Authelia-Anmeldung
|
|
|
|
|
|
2026-11-01 19:35:21 +01:00
|
|
|
### Normalfall: über Authelia, ohne Nachfrage
|
|
|
|
|
|
|
|
|
|
Authelia hält je Nutzer ein geheimes Attribut `diora_key` (32 Byte, base64). Es geht nur an den
|
|
|
|
|
öffentlichen Client `diora-app` (`DIORA_OIDC_APP_CLIENT_ID`, PKCE, Scope `diora_key`) und nur
|
|
|
|
|
über userinfo, nicht im id_token. Nach jedem Authelia-Login (und nach dem Verknüpfen) führt
|
|
|
|
|
diora auf `/accounts/oidc/key/` (`templates/accounts/oidc_key.html`). Die Seite meldet sich
|
|
|
|
|
**im Browser** noch einmal mit `prompt=none` als `diora-app` an, unsichtbar, weil die
|
|
|
|
|
Authelia-Sitzung noch besteht, und holt per CORS Token und userinfo. Daraus leitet sie mit
|
|
|
|
|
HKDF-SHA256 (Salt = `sub`, info `diora authelia key v1`) einen AES-GCM-Schlüssel ab:
|
|
|
|
|
|
|
|
|
|
- Hat das Gerät den Bücher-Schlüssel (und passt er zu einem Buch), wird er damit verschlüsselt
|
|
|
|
|
und im Vault `authelia-key` abgelegt, bei jedem Authelia-Login neu. So heilt ein geänderter
|
|
|
|
|
`diora_key` von selbst.
|
|
|
|
|
- Hat es keinen, öffnet sie den Vault-Eintrag damit und legt den Schlüssel ab.
|
|
|
|
|
- Es zählt nur das Authelia-Konto, das mit diesem diora-Konto verknüpft ist (`sub`).
|
|
|
|
|
|
|
|
|
|
Der diora-Server sieht `diora_key` nie. **Grenze, ehrlich:** Wer Authelia (Hetzner-Root) oder
|
|
|
|
|
dein Authelia-Konto (Passwort, Mail-Reset) übernimmt, kommt damit an die Bücher. Wer Strato
|
|
|
|
|
übernimmt, kann im Browser ohnehin manipuliertes JS ausliefern. Geschützt ist gegen geleakte
|
|
|
|
|
Daten **einer** Seite (diora-DB/Backup ohne Authelia-Nutzerdatei oder umgekehrt). Geht
|
|
|
|
|
`diora_key` verloren, sind die Bücher nicht weg: Der Schlüssel kommt weiter aus dem Passwort.
|
|
|
|
|
Entschieden 2026-11-01 vom Nutzer (Komfort vor stärkerer Trennung).
|
|
|
|
|
|
|
|
|
|
### Notweg: Code oder Passwort
|
|
|
|
|
|
2026-11-01 15:01:25 +01:00
|
|
|
Ohne Passwort gibt es keinen Schlüssel. Deshalb erzeugt `getOrCreateEncKey()` **nie mehr
|
|
|
|
|
selbst einen neuen Schlüssel**: Früher machte es einen zufälligen, wenn keiner da war, und damit
|
|
|
|
|
waren die vorhandenen Bücher auf diesem Gerät verloren. Jetzt fragt es `dioraUnlock()`
|
|
|
|
|
(`static/js/app/33-unlock.js`). Es gibt zwei Wege:
|
|
|
|
|
|
|
|
|
|
1. **Von einem entsperrten Gerät übergeben** (`accounts/keyshare.py`, Modell `KeyShare`). Das
|
|
|
|
|
neue Gerät schickt einen ECDH-P-256-Public-Key und zeigt einen 6-stelligen Code, der aus
|
|
|
|
|
diesem Key berechnet wird. Entsperrte Geräte fragen alle 15 s
|
|
|
|
|
`/accounts/keyshare/pending/` ab und zeigen denselben Code mit „Freigeben/Ablehnen“. Die
|
|
|
|
|
Freigabe verschlüsselt den Bücher-Schlüssel mit AES-GCM unter dem gemeinsamen Geheimnis
|
|
|
|
|
(HKDF-SHA256, info `diora keyshare v1`). Der Server sieht nur Public Keys und Chiffrat und
|
|
|
|
|
gibt die Antwort genau einmal heraus. Anträge laufen nach 10 min ab. Angeboten werden nur
|
|
|
|
|
solche, deren Gerät in den letzten 20 s nachgefragt hat, und je Konto höchstens 5, die
|
|
|
|
|
ältesten weichen.
|
|
|
|
|
2. **Das diora-Passwort einmal eingeben**: dieselbe Ableitung wie beim Login.
|
|
|
|
|
|
|
|
|
|
In beiden Fällen wird der Schlüssel an einem Buch geprüft, bevor er gespeichert wird. Mit
|
|
|
|
|
„Später“ bleiben Bücher gesperrt, und die Bibliothek zeigt einen Knopf „Entsperren“. Die
|
|
|
|
|
Einstellungsseite erzeugt ebenfalls keinen Schlüssel mehr.
|
|
|
|
|
|
|
|
|
|
## Apps
|
|
|
|
|
|
|
|
|
|
Android und Desktop melden sich bisher nur mit Passwort an (eigene Login-Seiten in
|
|
|
|
|
`diora-android/web-native/login.html` und `diora-desktop/src/web/login.js`, die einen
|
2026-11-01 19:35:21 +01:00
|
|
|
API-Token holen). Authelia für die Apps folgt als eigener Schritt: Login im Systembrowser
|
|
|
|
|
als `diora-app` mit PKCE, Redirect `xyz.creamfresh.diora:/oauth`,
|
|
|
|
|
`xyz.creamfresh.diora.reader:/oauth` bzw. Loopback `http://127.0.0.1:<port>/oauth` (jeder Port).
|