217 lines
5.9 KiB
Markdown
217 lines
5.9 KiB
Markdown
|
|
# STEP 6 – TOTP-ZWEI-FAKTOR-AUTHENTIFIZIERUNG
|
|||
|
|
# Status: ABGESCHLOSSEN
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Architektur
|
|||
|
|
|
|||
|
|
### Komponenten
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
aza_totp.py – Zentrales TOTP-Modul (RFC 6238)
|
|||
|
|
basis14.py – GUI-Integration (Login, Profil, Setup)
|
|||
|
|
pyotp (2.9.0) – TOTP-Implementierung (RFC 6238 kompatibel)
|
|||
|
|
qrcode[pil] (8.2) – QR-Code-Generierung
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Flow
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
Login-Dialog
|
|||
|
|
│
|
|||
|
|
▼
|
|||
|
|
Passwort prüfen ──► FAIL → Fehlermeldung
|
|||
|
|
│
|
|||
|
|
▼ OK
|
|||
|
|
2FA aktiv? ──► NEIN + 2FA_REQUIRED → Erzwinge 2FA-Setup
|
|||
|
|
│ │
|
|||
|
|
▼ JA ▼
|
|||
|
|
TOTP-Dialog ──────────────────────── 2FA-Setup-Dialog
|
|||
|
|
│ │
|
|||
|
|
▼ ▼
|
|||
|
|
Code prüfen QR-Code scannen
|
|||
|
|
│ Code validieren
|
|||
|
|
├─ TOTP OK → Login │
|
|||
|
|
├─ Backup-Code OK → Login Backup-Codes anzeigen
|
|||
|
|
└─ FAIL → Fehlermeldung │
|
|||
|
|
2FA aktiviert
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Datenmodell
|
|||
|
|
|
|||
|
|
### Profil (kg_diktat_user_profile.json)
|
|||
|
|
|
|||
|
|
```json
|
|||
|
|
{
|
|||
|
|
"name": "Dr. Beispiel",
|
|||
|
|
"specialty": "Dermatologie",
|
|||
|
|
"clinic": "Praxis XY",
|
|||
|
|
"password_hash": "$2b$12$...",
|
|||
|
|
"totp_active": true,
|
|||
|
|
"totp_secret_enc": "Base64-verschlüsselter-TOTP-Secret",
|
|||
|
|
"backup_codes": [
|
|||
|
|
"sha256-hash-code-1",
|
|||
|
|
"sha256-hash-code-2",
|
|||
|
|
"",
|
|||
|
|
"sha256-hash-code-4",
|
|||
|
|
...
|
|||
|
|
]
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Feld | Typ | Beschreibung |
|
|||
|
|
|------|-----|-------------|
|
|||
|
|
| `totp_active` | boolean | 2FA aktiviert/deaktiviert |
|
|||
|
|
| `totp_secret_enc` | string | TOTP-Secret, XOR-verschlüsselt mit Passwort-Hash, Base64 |
|
|||
|
|
| `backup_codes` | string[] | SHA-256-Hashes der Backup-Codes; leer = verbraucht |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## TOTP-Parameter
|
|||
|
|
|
|||
|
|
| Parameter | Wert |
|
|||
|
|
|-----------|------|
|
|||
|
|
| Algorithmus | TOTP (RFC 6238) |
|
|||
|
|
| Hash | SHA-1 (Standard per RFC) |
|
|||
|
|
| Zeitschritt | 30 Sekunden |
|
|||
|
|
| Code-Länge | 6 Ziffern |
|
|||
|
|
| Gültigkeitsfenster | +-1 (90 Sekunden total) |
|
|||
|
|
| Secret-Länge | 160 bit (32 Base32-Zeichen) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Sicherheitsmassnahmen
|
|||
|
|
|
|||
|
|
### Secret-Verschlüsselung
|
|||
|
|
|
|||
|
|
- TOTP-Secret wird NIE im Klartext gespeichert
|
|||
|
|
- Verschlüsselung: XOR mit SHA-256(Benutzer-Passwort)
|
|||
|
|
- Encoding: Base64
|
|||
|
|
- Entschlüsselung nur mit korrektem Passwort möglich
|
|||
|
|
- Bei falschem Passwort: leerer String (kein Crash)
|
|||
|
|
|
|||
|
|
### Backup-Codes
|
|||
|
|
|
|||
|
|
- 8 Einmal-Codes (8 Zeichen, hex, uppercase)
|
|||
|
|
- Gespeichert als SHA-256-Hashes
|
|||
|
|
- Verbrauchte Codes werden zu leerem String
|
|||
|
|
- Case-insensitive Eingabe
|
|||
|
|
- HMAC-basierter Vergleich (timing-safe)
|
|||
|
|
|
|||
|
|
### Rate-Limiting
|
|||
|
|
|
|||
|
|
- Max. 5 TOTP-Versuche pro 5 Minuten pro Benutzer
|
|||
|
|
- Nach Erreichen: auch korrekter Code wird blockiert
|
|||
|
|
- Im RAM gehalten (Reset bei Neustart)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ENV-Variablen
|
|||
|
|
|
|||
|
|
| Variable | Default | Beschreibung |
|
|||
|
|
|----------|---------|-------------|
|
|||
|
|
| `AZA_2FA_ENABLED` | `1` | 2FA-Feature verfügbar |
|
|||
|
|
| `AZA_2FA_REQUIRED` | `0` | 2FA ist Pflicht für alle Benutzer |
|
|||
|
|
|
|||
|
|
### Verhalten
|
|||
|
|
|
|||
|
|
| AZA_2FA_ENABLED | AZA_2FA_REQUIRED | Ergebnis |
|
|||
|
|
|:---:|:---:|---|
|
|||
|
|
| 0 | 0 | 2FA komplett deaktiviert, kein Button im Profil |
|
|||
|
|
| 1 | 0 | 2FA optional, Aktivierung im Profil |
|
|||
|
|
| 1 | 1 | 2FA Pflicht, Setup nach erstem Login erzwungen |
|
|||
|
|
| 0 | 1 | Feature deaktiviert, Required ignoriert |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Recovery
|
|||
|
|
|
|||
|
|
- Wiederherstellung NUR über Backup-Codes
|
|||
|
|
- Kein Admin-Reset ohne Prüfung
|
|||
|
|
- Kein E-Mail-Recovery
|
|||
|
|
- Backup-Codes werden nach 2FA-Aktivierung einmalig angezeigt
|
|||
|
|
- Benutzer muss Codes selbst sichern (Kopieren/Ausdrucken)
|
|||
|
|
- Bei aufgebrauchten Backup-Codes: Profil-Datei manuell bereinigen (Admin-Eingriff)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Geänderte / Neue Dateien
|
|||
|
|
|
|||
|
|
| Datei | Änderung |
|
|||
|
|
|-------|----------|
|
|||
|
|
| `aza_totp.py` | NEU: Zentrales TOTP-Modul |
|
|||
|
|
| `basis14.py` | Import: pyotp, qrcode, PIL.ImageTk, aza_totp |
|
|||
|
|
| `basis14.py` | `_show_password_login()`: 2FA-Check nach Passwort |
|
|||
|
|
| `basis14.py` | `_show_totp_login()`: NEU – TOTP-Code-Dialog |
|
|||
|
|
| `basis14.py` | `_show_2fa_setup()`: NEU – QR-Code + Erst-Validierung |
|
|||
|
|
| `basis14.py` | `_show_backup_codes()`: NEU – Backup-Code-Anzeige |
|
|||
|
|
| `basis14.py` | `_show_profile_editor()`: 2FA-Aktivierung/Deaktivierung |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Dependencies
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
pyotp==2.9.0 # TOTP RFC 6238
|
|||
|
|
qrcode[pil]==8.2 # QR-Code-Generierung
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Testergebnis
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
24/24 PASS, 0/24 FAIL
|
|||
|
|
GESAMTBEWERTUNG: PASS
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Test | Ergebnis |
|
|||
|
|
|------|----------|
|
|||
|
|
| Secret-Generierung (Base32, 32 Zeichen) | PASS |
|
|||
|
|
| Secrets sind einzigartig | PASS |
|
|||
|
|
| Provisioning URI (otpauth://) | PASS |
|
|||
|
|
| URI enthält Issuer | PASS |
|
|||
|
|
| URI enthält Secret | PASS |
|
|||
|
|
| Korrekter Code akzeptiert | PASS |
|
|||
|
|
| Falscher Code abgelehnt | PASS |
|
|||
|
|
| Leerer Code abgelehnt | PASS |
|
|||
|
|
| Abgelaufener Code (90s) abgelehnt | PASS |
|
|||
|
|
| 8 Backup-Codes generiert | PASS |
|
|||
|
|
| Code-Format (8 hex chars) | PASS |
|
|||
|
|
| Codes einzigartig | PASS |
|
|||
|
|
| Hashes SHA-256 | PASS |
|
|||
|
|
| Backup-Code Lookup | PASS |
|
|||
|
|
| Falscher Backup-Code abgelehnt | PASS |
|
|||
|
|
| Case-insensitive | PASS |
|
|||
|
|
| Einmalnutzung | PASS |
|
|||
|
|
| Invalidierter Code abgelehnt | PASS |
|
|||
|
|
| Secret-Verschlüsselung | PASS |
|
|||
|
|
| Verschlüsselt != Klartext | PASS |
|
|||
|
|
| Entschlüsselung korrekt | PASS |
|
|||
|
|
| Falsches PW = falsches Secret | PASS |
|
|||
|
|
| Rate-Limit greift nach 5 Versuchen | PASS |
|
|||
|
|
| Rate-Limit blockiert auch korrekte Codes | PASS |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Risiken
|
|||
|
|
|
|||
|
|
1. **XOR-Verschlüsselung:** Für eine Desktop-App akzeptabel, aber kryptographisch
|
|||
|
|
schwächer als AES. Upgrade auf Fernet/AES bei Bedarf.
|
|||
|
|
|
|||
|
|
2. **Secret an Passwort gebunden:** Bei Passwortänderung muss das TOTP-Secret
|
|||
|
|
neu verschlüsselt werden. Aktuell nicht automatisch – 2FA muss nach
|
|||
|
|
Passwortänderung neu eingerichtet werden.
|
|||
|
|
|
|||
|
|
3. **Rate-Limit im RAM:** Wird bei Neustart zurückgesetzt. Für Desktop-App
|
|||
|
|
akzeptabel (kein Netzwerk-Angriff möglich).
|
|||
|
|
|
|||
|
|
4. **Kein TOTP für workforce_planner:** Die 2FA ist nur im Desktop-Client
|
|||
|
|
(basis14.py) implementiert. Der workforce_planner (Web-API) hat noch
|
|||
|
|
keine 2FA-Integration.
|
|||
|
|
|
|||
|
|
5. **SHA-1 in TOTP:** RFC 6238 spezifiziert SHA-1 als Standard. Alle gängigen
|
|||
|
|
Authenticator-Apps (Google, Microsoft, Authy) erwarten SHA-1.
|