update
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
# INCIDENT REPORT – CREDENTIAL EXPOSURE
|
||||
# Datum: 2026-02-22
|
||||
# Klassifikation: SICHERHEITSVORFALL (intern)
|
||||
|
||||
---
|
||||
|
||||
## 1. Zusammenfassung
|
||||
|
||||
Im Rahmen der systematischen Sicherheitsanalyse (STEP 4.4 – Klartext-Credentials
|
||||
entfernen) wurde ein produktives E-Mail-Passwort im Klartext in einer
|
||||
Konfigurationsdatei des Projekts entdeckt.
|
||||
|
||||
---
|
||||
|
||||
## 2. Betroffene Komponente
|
||||
|
||||
- **Datei:** `aza_email_config.json`
|
||||
- **Modul:** AZA E-Mail (aza_email.py)
|
||||
- **Feld:** `password` im `accounts`-Array
|
||||
- **Betroffener Account:** Produktiver Mail-Account (Details bewusst nicht aufgeführt)
|
||||
|
||||
---
|
||||
|
||||
## 3. Entdeckung
|
||||
|
||||
- **Entdeckt durch:** Automatisierte Security Gap-Analyse (STEP 3) und
|
||||
bestätigt bei der Implementierung von STEP 4.4
|
||||
- **Entdeckungsdatum:** 2026-02-22
|
||||
- **Entdeckungsmethode:** Grep-Suche nach `password`, `secret`, `token`
|
||||
in allen JSON/YAML/Config-Dateien des Projekts
|
||||
|
||||
---
|
||||
|
||||
## 4. Risikoanalyse
|
||||
|
||||
### Worst-Case-Szenarien
|
||||
|
||||
| Szenario | Risiko | Schwere |
|
||||
|----------|--------|---------|
|
||||
| Unbefugter Zugriff auf das Dateisystem | Passwort-Exfiltration | HOCH |
|
||||
| Account-Übernahme (Mail) | Lesen/Senden von E-Mails im Namen des Kontoinhabers | HOCH |
|
||||
| Mail-Missbrauch | Spam, Phishing, Social Engineering über legitimen Account | HOCH |
|
||||
| Laterale Bewegung | Falls Passwort wiederverwendet: Zugriff auf weitere Systeme | KRITISCH |
|
||||
| Datenschutzverletzung | Zugriff auf medizinische Kommunikation (Patientendaten) | KRITISCH |
|
||||
| Backup-/Export-Exposition | Passwort in Backups, Cloud-Syncs oder Transfers vorhanden | MITTEL |
|
||||
|
||||
### Besondere Faktoren
|
||||
|
||||
- **Medizinischer Kontext:** E-Mail-Account einer Arztpraxis – potenziell
|
||||
Patientendaten in Mails (DSG/DSGVO-relevant)
|
||||
- **Expositionsdauer:** Unbekannt. Das Passwort war seit der Erstellung
|
||||
der Konfigurationsdatei im Klartext gespeichert.
|
||||
- **Zugriffskreis:** Alle Personen/Systeme mit Lesezugriff auf das
|
||||
Projektverzeichnis, Backups oder Transfers dieser Datei.
|
||||
|
||||
---
|
||||
|
||||
## 5. Sofortmassnahmen
|
||||
|
||||
| Nr | Massnahme | Status | Verantwortlich |
|
||||
|----|-----------|--------|----------------|
|
||||
| 1 | Passwort aus Konfigurationsdatei entfernt | ERLEDIGT (STEP 4.4) | Security Engineer |
|
||||
| 2 | Code umgestellt auf ENV-basierte Credentials | ERLEDIGT (STEP 4.4) | Security Engineer |
|
||||
| 3 | Migrationswarnung implementiert | ERLEDIGT (STEP 4.4) | Security Engineer |
|
||||
| 4 | **Passwort beim Mail-Provider rotieren** | **OFFEN – DRINGEND** | Projektinhaber |
|
||||
| 5 | Prüfung ob Account kompromittiert wurde | OFFEN | Projektinhaber |
|
||||
|
||||
---
|
||||
|
||||
## 6. Empfohlene Folgeaktionen
|
||||
|
||||
### Sofort (innerhalb 24h)
|
||||
|
||||
- [ ] **Passwort beim Mail-Provider ändern** (neues, starkes Passwort,
|
||||
mindestens 16 Zeichen, keine Wiederverwendung)
|
||||
- [ ] **2FA aktivieren**, falls der Mail-Provider dies unterstützt
|
||||
- [ ] **Login-Protokolle prüfen**: Gibt es unbekannte Zugriffe auf den
|
||||
betroffenen Mail-Account?
|
||||
|
||||
### Kurzfristig (innerhalb 1 Woche)
|
||||
|
||||
- [ ] **Passwort-Wiederverwendung prüfen**: Wurde dasselbe Passwort für
|
||||
andere Dienste/Systeme verwendet? Falls ja: dort ebenfalls rotieren.
|
||||
- [ ] **Backups durchsuchen**: Prüfen, ob Kopien der Datei
|
||||
`aza_email_config.json` in Backups, Cloud-Speicher, USB-Sticks,
|
||||
E-Mail-Anhängen oder anderen Transfers vorhanden sind.
|
||||
Falls ja: Passwort dort ebenfalls als exponiert betrachten.
|
||||
- [ ] **Projekt-Kopien prüfen**: Alle `backup *`-Ordner und Kopien
|
||||
des Projektverzeichnisses auf Klartext-Passwörter durchsuchen.
|
||||
|
||||
### Mittelfristig (Empfehlung)
|
||||
|
||||
- [ ] **Secret-Scanning** in CI/CD-Pipeline einführen (z.B. git-secrets,
|
||||
truffleHog, GitHub Secret Scanning), um zukünftige Klartext-Credentials
|
||||
automatisch zu erkennen.
|
||||
- [ ] **OS-Keychain-Integration** evaluieren (Windows Credential Manager),
|
||||
um Passwörter sicher und benutzerfreundlich zu speichern.
|
||||
|
||||
---
|
||||
|
||||
## 7. Lessons Learned
|
||||
|
||||
### Ursache
|
||||
|
||||
Das Passwort wurde direkt im GUI-Dialog eingegeben und ohne
|
||||
Schutzmassnahmen in eine JSON-Datei auf der Festplatte geschrieben.
|
||||
Es gab keine Trennung zwischen Konfiguration und Credentials.
|
||||
|
||||
### Präventionsmassnahmen (implementiert)
|
||||
|
||||
1. **Credentials werden NIE mehr in Dateien geschrieben**
|
||||
(`_strip_passwords()` in `save_email_config()`)
|
||||
2. **Credentials kommen ausschliesslich aus ENV-Variablen**
|
||||
(`AZA_EMAIL_PASSWORD_0`, `_1`, ...)
|
||||
3. **Migrationswarnung** bei bestehenden Klartext-Passwörtern in der Config
|
||||
4. **Klartext-Passwörter in JSON werden ignoriert** (kein Auto-Migrate)
|
||||
|
||||
### Präventionsmassnahmen (empfohlen, nicht implementiert)
|
||||
|
||||
1. Secret-Scanning in CI/CD
|
||||
2. Pre-Commit-Hooks für Secret-Detection
|
||||
3. OS-Keychain als sicherer Credential-Store
|
||||
4. Regelmässige Audits der Konfigurationsdateien
|
||||
|
||||
---
|
||||
|
||||
## 8. Referenzen
|
||||
|
||||
- STEP 3 – GAP-Analyse: Lücke #28 identifiziert
|
||||
- STEP 4.4 – Implementierung der Credential-Entfernung
|
||||
- STEP 4.4 Handover: `/security/handovers/STEP_4_4_CREDENTIALS.md`
|
||||
|
||||
---
|
||||
|
||||
*Erstellt: 2026-02-22 im Rahmen des AZA/MedWork Security & Compliance Projects*
|
||||
*Klassifikation: Intern – Nicht zur externen Weitergabe bestimmt*
|
||||
32
AzA march 2026/security/handovers/STEP_01_SUMMARY.md
Normal file
32
AzA march 2026/security/handovers/STEP_01_SUMMARY.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# STEP 1 – HIN TECHNICAL REFERENCE COLLECTION
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Was gemacht wurde
|
||||
|
||||
- Ordner /security/reference/hin_docs/ erstellt
|
||||
- Öffentlich verfügbare technische Dokumentation von HIN gesammelt
|
||||
- 16 Originalquellen (hin.ch, support.hin.ch, cdn.hin.ch, msxfaq.de) ausgewertet
|
||||
- Technische Referenzdatei erstellt: hin_architektur.md
|
||||
- Quellenverzeichnis erstellt: hin_quellen.md
|
||||
- Support-Anfrage vorbereitet: QUELLEN_UND_ANLEITUNG.md
|
||||
|
||||
## Was geändert wurde
|
||||
|
||||
- NEU: /security/reference/hin_docs/hin_architektur.md
|
||||
- NEU: /security/reference/hin_docs/hin_quellen.md
|
||||
- NEU: /security/reference/hin_docs/QUELLEN_UND_ANLEITUNG.md
|
||||
|
||||
Keine Codeänderungen. Keine Architekturänderungen.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- Offizielle HIN-PDFs sind nicht frei downloadbar
|
||||
- Support-Anfrage an support@hin.ch ist vorbereitet aber NICHT gesendet (muss User tun)
|
||||
- HIN Gateway Dokumentation (ZIP) unter https://download.hin.ch/gw/hin-gateway.zip nicht heruntergeladen
|
||||
|
||||
## Risiken
|
||||
|
||||
- Alle Daten stammen aus öffentlichen Quellen, nicht aus offizieller HIN-Vertragsdokumentation
|
||||
- Einige technische Details (TLS-Versionen, Cipher Suites) sind in öffentlichen Quellen nicht verfügbar
|
||||
70
AzA march 2026/security/handovers/STEP_02_SUMMARY.md
Normal file
70
AzA march 2026/security/handovers/STEP_02_SUMMARY.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# STEP 2 – TECHNISCHE ZERLEGUNG VON HIN
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Was gemacht wurde
|
||||
|
||||
Analyse der HIN-Dokumente aus /security/reference/hin_docs/.
|
||||
Strukturierte Extraktion des HIN Security Stacks in 5 Bereiche.
|
||||
|
||||
## Ergebnis (Kurzfassung)
|
||||
|
||||
### 1. Transport
|
||||
- TLS: optional, nicht erzwungen
|
||||
- S/MIME ist primärer Transportschutz (Legacy)
|
||||
- Wireguard für Stargate (neue Generation)
|
||||
- SCION für SSHN-Netzwerk
|
||||
- TLS-Version, Cipher Suites, PFS: NICHT BELEGT
|
||||
|
||||
### 2. Netzwerk
|
||||
- Geschlossener Vertrauensraum (Gated Community)
|
||||
- SCION/SSHN mit Schweizer-only Routing
|
||||
- DDoS-Schutz durch Architektur
|
||||
- 1 Instanz pro Organisation (Segmentierung)
|
||||
- Zero Trust, IP-Whitelisting: NICHT BELEGT
|
||||
|
||||
### 3. PKI
|
||||
- Eigene CA: "HIN MGW Issuing CA 2022"
|
||||
- RSA 4096-bit, SHA256withRSA
|
||||
- Domain-Zertifikate (10 Jahre Gültigkeit)
|
||||
- SSHN-Zertifikate (72h Gültigkeit)
|
||||
- Zentrale Verteilung, geschlossenes System
|
||||
- Hardware-Token, Smartcard: NICHT BELEGT
|
||||
|
||||
### 4. Authentifizierung
|
||||
- 2FA in drei Varianten (Client, Gateway, Mobil)
|
||||
- Identitätsprüfung via Videoidentifikation
|
||||
- SSO für HIN-geschützte Anwendungen
|
||||
- eID-Typen: Persönlich, Organisation, Team
|
||||
- SAML/OAuth/OIDC: NICHT BELEGT
|
||||
|
||||
### 5. Mail
|
||||
- S/MIME Gateway-zu-Gateway (nicht Ende-zu-Ende)
|
||||
- Domain-Zertifikate, automatische Verschlüsselung
|
||||
- HIN Mail Global: GINA/SEPPMail, SMS-Auth, 30 Tage
|
||||
- Betreffzeile bei HIN Mail Global: UNVERSCHLÜSSELT
|
||||
- Backend: SEPPMail-Appliance, Zimbra (IMAP)
|
||||
|
||||
## Was geändert wurde
|
||||
|
||||
Keine Dateien geändert. Analyse wurde im Chat ausgegeben.
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
1. TLS-Version, Cipher Suites, PFS – nicht dokumentiert
|
||||
2. mTLS – unklar
|
||||
3. SSO-Protokoll (SAML/OAuth/OIDC) – nicht dokumentiert
|
||||
4. Hardware-Token / Smartcard – nicht dokumentiert
|
||||
5. Client-Zertifikate vs. Software-Token – nicht spezifiziert
|
||||
6. Zero Trust Modell – nicht explizit
|
||||
7. Post-Quanten-Kryptografie – nur "vorbereitet", keine Details
|
||||
8. Wireguard-Konfiguration – keine Details
|
||||
9. Ende-zu-Ende Option – nicht dokumentiert
|
||||
10. IP-Whitelisting – nicht dokumentiert
|
||||
|
||||
## Risiken
|
||||
|
||||
- 10 von 20+ technischen Parametern sind in öffentlichen Quellen NICHT BELEGT
|
||||
- Gap-Analyse wird in diesen Bereichen auf konservative Annahmen angewiesen sein
|
||||
- Ohne offizielle HIN-Dokumentation bleiben diese Lücken bestehen
|
||||
209
AzA march 2026/security/handovers/STEP_03_GAP_ANALYSE.md
Normal file
209
AzA march 2026/security/handovers/STEP_03_GAP_ANALYSE.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# =====================================================================
|
||||
# STEP 3 – FORMELLE GAP-ANALYSE (HIN vs. AZA)
|
||||
# =====================================================================
|
||||
# Datenquellen: /security/reference/hin_docs/ + Projekt-Code
|
||||
# Methode: Nur technisch belegbare Fakten
|
||||
# =====================================================================
|
||||
|
||||
## 1. Transport Security
|
||||
|
||||
### HIN:
|
||||
- S/MIME als primärer Transportschutz (nicht TLS)
|
||||
- TLS optional, nicht erzwungen auf SMTP-Ebene
|
||||
- Wireguard-Protokoll für Stargate (App-zu-App, kein VPN)
|
||||
- SCION-Netzwerk für SSHN (any-to-any Verschlüsselung)
|
||||
- Routing ausschliesslich über Schweizer Netze
|
||||
|
||||
### AZA:
|
||||
- SMTP mit STARTTLS (Port 587) – aza_email.py Zeilen 1433–1436, 2994–2997
|
||||
- IMAP über SSL/TLS (Port 993, IMAP4_SSL) – aza_email.py Zeilen 1470, 1705
|
||||
- Keine eigene TLS-Konfiguration (Python-Defaults)
|
||||
- Keine Zertifikatsverifikation im Code gesetzt
|
||||
- todo_server.py: reiner HTTP-Server, kein HTTPS – Zeile 228
|
||||
- transcribe_server.py: kein TLS
|
||||
- backend_main.py: kein TLS
|
||||
- Supabase-Zugriff über HTTPS – aza_config.py Zeile 176
|
||||
|
||||
### GAP:
|
||||
| Aspekt | HIN | AZA | Lücke |
|
||||
|--------|-----|-----|-------|
|
||||
| Mail-Verschlüsselung | S/MIME (RSA 4096) | Nur STARTTLS (opportunistisch) | JA |
|
||||
| App-zu-App Transport | Wireguard | Kein verschlüsselter Kanal | JA |
|
||||
| Netzwerk-Routing | SCION, nur CH | Internet, global | JA |
|
||||
| Server-Kommunikation | Verschlüsselt | HTTP (Klartext) | JA |
|
||||
| TLS-Konfiguration | UNBEKANNT | Python-Defaults | UNBEKANNT |
|
||||
|
||||
### Kritikalität: **HIGH**
|
||||
|
||||
---
|
||||
|
||||
## 2. Netzwerkmodell
|
||||
|
||||
### HIN:
|
||||
- Geschlossener Vertrauensraum (Gated Community)
|
||||
- SCION/SSHN mit AS-Nummern und Zertifikaten
|
||||
- DDoS-Schutz durch Architektur
|
||||
- 1 Instanz pro Organisation (keine Mandantenfähigkeit)
|
||||
- Redundante Transportwege, automatischer Fail-over
|
||||
- Governance: HIN, FMH, pharmaSuisse, BAG, SWITCH
|
||||
|
||||
### AZA:
|
||||
- Kein Netzwerk-Perimeter definiert
|
||||
- Kein VPN
|
||||
- Kein DDoS-Schutz
|
||||
- Keine Netzwerk-Segmentierung
|
||||
- Einzige Netzwerk-Massnahme: Windows-Firewall-Regel für Todo-Server-Port – todo_server.py Zeilen 209–220
|
||||
- Backend-Services auf localhost (HTTP)
|
||||
- Supabase als externer Cloud-Service (HTTPS)
|
||||
|
||||
### GAP:
|
||||
| Aspekt | HIN | AZA | Lücke |
|
||||
|--------|-----|-----|-------|
|
||||
| Vertrauensraum | Geschlossen, verifiziert | Offen, kein Perimeter | JA |
|
||||
| DDoS-Schutz | SCION-basiert | Keiner | JA |
|
||||
| Segmentierung | 1 Instanz/Organisation | Keine | JA |
|
||||
| Redundanz | Multi-Path, Fail-over | Keine | JA |
|
||||
| Governance | 5 Organisationen | Keine | JA |
|
||||
| VPN | Wireguard (Stargate) | Keiner | JA |
|
||||
|
||||
### Kritikalität: **HIGH**
|
||||
|
||||
---
|
||||
|
||||
## 3. PKI
|
||||
|
||||
### HIN:
|
||||
- Eigene CA: "HIN MGW Issuing CA 2022"
|
||||
- RSA 4096-bit, SHA256withRSAEncryption
|
||||
- Domain-Zertifikate (10 Jahre Gültigkeit)
|
||||
- SSHN-Zertifikate (72h Gültigkeit, automatische Erneuerung)
|
||||
- Zentrale Zertifikatsverteilung über HIN-Backend
|
||||
- Geschlossenes System
|
||||
|
||||
### AZA:
|
||||
- Keine PKI
|
||||
- Keine eigene CA
|
||||
- Keine Zertifikate (weder Client- noch Server-Zertifikate)
|
||||
- Kein Zertifikatsmanagement
|
||||
- Kein CSR-Prozess
|
||||
- Keine Signierung von Daten oder Kommunikation
|
||||
|
||||
### GAP:
|
||||
| Aspekt | HIN | AZA | Lücke |
|
||||
|--------|-----|-----|-------|
|
||||
| Eigene CA | HIN MGW Issuing CA 2022 | Keine | JA |
|
||||
| Zertifikate | RSA 4096, Domain-Zerts | Keine | JA |
|
||||
| Zertifikatsverteilung | Zentral, automatisch | Nicht vorhanden | JA |
|
||||
| Schlüsselmanagement | Zentral über Backend | Nicht vorhanden | JA |
|
||||
| Hardware-Token | UNBEKANNT | Nicht vorhanden | UNBEKANNT |
|
||||
|
||||
### Kritikalität: **HIGH**
|
||||
|
||||
---
|
||||
|
||||
## 4. Authentifizierung
|
||||
|
||||
### HIN:
|
||||
- 2FA in drei Varianten:
|
||||
- HIN Client: Passwort + kryptographischer Geräteschlüssel
|
||||
- HIN Gateway: mTAN/Auth-App + Active Directory
|
||||
- Mobil: mTAN/Auth-App + HIN Login+Passwort
|
||||
- Identitätsprüfung via Videoidentifikation
|
||||
- eID-Typen: Persönlich, Organisation, Team
|
||||
- SSO für HIN-geschützte Anwendungen
|
||||
- Berufsqualifikation über Register/Verbände verifiziert
|
||||
|
||||
### AZA:
|
||||
- basis14.py: SHA-256 Passwort-Hash OHNE Salt – Zeilen 1298–1300
|
||||
- workforce_planner: bcrypt mit Salt – auth.py Zeilen 23–28
|
||||
- workforce_planner: JWT mit HS256 – auth.py Zeilen 31–37
|
||||
- workforce_planner: Secret Key hardcoded als Fallback "dev-secret-change-in-production" – config.py Zeile 19
|
||||
- workforce_planner: Token-Ablauf 480 Minuten (8h) – config.py Zeile 20
|
||||
- backend_main.py: API-Token via Header "X-Api-Token" – Zeilen 154–157
|
||||
- backend_main.py: User-Identifikation via Header "X-User" – Zeilen 162–164
|
||||
- Rollen: ADMIN, MANAGER, EMPLOYEE – enums.py Zeilen 33–36
|
||||
- Rollenbasierte Zugriffskontrolle mit require_role() – auth.py Zeilen 59–66
|
||||
- Keine 2FA / MFA / OTP
|
||||
- Keine zertifikatsbasierte Authentifizierung
|
||||
- Keine Identitätsprüfung (Videoidentifikation o.ä.)
|
||||
- Kein SSO-Protokoll (SAML/OAuth/OIDC)
|
||||
|
||||
### GAP:
|
||||
| Aspekt | HIN | AZA | Lücke |
|
||||
|--------|-----|-----|-------|
|
||||
| 2FA | Ja (3 Varianten) | Nein | JA |
|
||||
| Passwort-Hashing | UNBEKANNT | SHA-256 ohne Salt (basis14) / bcrypt (workforce) | JA (basis14) |
|
||||
| Secret Key | UNBEKANNT | Hardcoded Fallback | JA |
|
||||
| Identitätsprüfung | Videoidentifikation | Keine | JA |
|
||||
| SSO | Ja | Nein | JA |
|
||||
| Zertifikats-Auth | Ja (Geräteschlüssel) | Nein | JA |
|
||||
| Token-Ablauf | UNBEKANNT | 480 min (8h) | UNBEKANNT |
|
||||
| Rollen | eID-basiert (3 Typen) | RBAC (3 Rollen) | TEILWEISE |
|
||||
|
||||
### Kritikalität: **HIGH**
|
||||
|
||||
---
|
||||
|
||||
## 5. Mail-Sicherheit
|
||||
|
||||
### HIN:
|
||||
- S/MIME Gateway-zu-Gateway mit Domain-Zertifikaten (RSA 4096)
|
||||
- Automatische Verschlüsselung/Entschlüsselung durch Gateway
|
||||
- HIN Mail Global: GINA/SEPPMail für externe Empfänger
|
||||
- SMS-Code-Authentifizierung für externe Empfänger
|
||||
- Domain-Prüfung gegen HIN-Domainliste
|
||||
- Betreffzeile bei HIN Mail Global: UNVERSCHLÜSSELT
|
||||
- Header "X-HIN-Encrypted" für Steuerung
|
||||
|
||||
### AZA:
|
||||
- SMTP mit STARTTLS (opportunistisch, nicht erzwungen) – aza_email.py
|
||||
- IMAP über SSL (IMAP4_SSL) – aza_email.py
|
||||
- Keine S/MIME-Implementierung
|
||||
- Keine PGP/GPG-Implementierung
|
||||
- Keine Gateway-Verschlüsselung
|
||||
- Keine Ende-zu-Ende-Verschlüsselung
|
||||
- Keine Signierung von E-Mails
|
||||
- Passwörter im Klartext in aza_email_config.json gespeichert
|
||||
- Keine Zertifikatsverifikation im SMTP/IMAP-Code
|
||||
|
||||
### GAP:
|
||||
| Aspekt | HIN | AZA | Lücke |
|
||||
|--------|-----|-----|-------|
|
||||
| Mail-Verschlüsselung | S/MIME (RSA 4096) | Keine | JA |
|
||||
| Gateway-Verschlüsselung | Automatisch | Keine | JA |
|
||||
| Externe Empfänger | HIN Mail Global (GINA) | Keine Lösung | JA |
|
||||
| Signierung | S/MIME Signatur | Keine | JA |
|
||||
| Credential-Speicherung | UNBEKANNT | Klartext JSON | JA |
|
||||
| Zertifikatsverifikation | Domain-Zertifikate | Python-Defaults | JA |
|
||||
|
||||
### Kritikalität: **HIGH**
|
||||
|
||||
---
|
||||
|
||||
## OFFENE TECHNISCHE FRAGEN
|
||||
|
||||
1. HIN: TLS-Version, Cipher Suites, PFS – nicht in hin_docs belegt
|
||||
2. HIN: Hardware-Token / Smartcard – nicht in hin_docs belegt
|
||||
3. HIN: SSO-Protokoll (SAML/OAuth/OIDC) – nicht in hin_docs belegt
|
||||
4. HIN: Passwort-Hashing-Verfahren – nicht in hin_docs belegt
|
||||
5. HIN: Token-Ablaufzeiten – nicht in hin_docs belegt
|
||||
6. HIN: Credential-Speicherung – nicht in hin_docs belegt
|
||||
7. AZA: Python-Default-TLS-Verhalten hängt von Systemkonfiguration ab – nicht geprüft
|
||||
8. AZA: Supabase-Sicherheitskonfiguration – nicht im Scope dieser Analyse
|
||||
9. AZA: Ob STARTTLS erzwungen oder opportunistisch ist – Code zeigt keine Prüfung auf Erfolg
|
||||
|
||||
---
|
||||
|
||||
## ZUSAMMENFASSUNG
|
||||
|
||||
| Bereich | Kritikalität | Anzahl Lücken |
|
||||
|---------|-------------|---------------|
|
||||
| 1. Transport Security | HIGH | 5 |
|
||||
| 2. Netzwerkmodell | HIGH | 6 |
|
||||
| 3. PKI | HIGH | 5 |
|
||||
| 4. Authentifizierung | HIGH | 7 |
|
||||
| 5. Mail-Sicherheit | HIGH | 6 |
|
||||
| **GESAMT** | **HIGH** | **29 identifizierte Lücken** |
|
||||
|
||||
Alle 5 Bereiche haben Kritikalität HIGH.
|
||||
9 Punkte konnten mangels HIN-Dokumentation nicht vollständig verglichen werden.
|
||||
54
AzA march 2026/security/handovers/STEP_03_SUMMARY.md
Normal file
54
AzA march 2026/security/handovers/STEP_03_SUMMARY.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# STEP 3 – FORMELLE GAP-ANALYSE (HIN vs. AZA)
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Ziel des Schrittes
|
||||
|
||||
Formale Gegenüberstellung des HIN Security Stacks mit dem aktuellen
|
||||
AZA/MedWork-Sicherheitszustand. Nur faktenbasierter Vergleich,
|
||||
keine Empfehlungen, keine Implementierung.
|
||||
|
||||
## Konkrete Änderungen
|
||||
|
||||
- NEU: /security/handovers/STEP_03_GAP_ANALYSE.md (vollständige Gap-Analyse)
|
||||
|
||||
## Betroffene Dateien
|
||||
|
||||
Keine Dateien geändert. Nur Analyse-Dokument erstellt.
|
||||
|
||||
Analysierte Quelldateien:
|
||||
- basis14.py (Passwort-Hashing, Login)
|
||||
- aza_email.py (SMTP/IMAP, Mail-Sicherheit)
|
||||
- aza_email_config.json (Credential-Speicherung)
|
||||
- aza_config.py (Supabase-Zugriff)
|
||||
- backend_main.py (API-Token, User-Header)
|
||||
- todo_server.py (HTTP-Server, Firewall)
|
||||
- transcribe_server.py (kein TLS)
|
||||
- workforce_planner/api/auth.py (JWT, bcrypt)
|
||||
- workforce_planner/config.py (Secret Key)
|
||||
- workforce_planner/core/enums.py (Rollen)
|
||||
- workforce_planner/core/models.py (Employee-Modell)
|
||||
|
||||
## Sicherheitsauswirkung
|
||||
|
||||
29 Lücken identifiziert, alle 5 Bereiche mit Kritikalität HIGH:
|
||||
- Transport: 5 Lücken (kein S/MIME, kein verschlüsselter App-Kanal, HTTP-Server)
|
||||
- Netzwerk: 6 Lücken (kein Perimeter, kein VPN, kein DDoS-Schutz)
|
||||
- PKI: 5 Lücken (keine CA, keine Zertifikate, kein Schlüsselmanagement)
|
||||
- Authentifizierung: 7 Lücken (keine 2FA, SHA-256 ohne Salt, hardcoded Secret)
|
||||
- Mail: 6 Lücken (keine Verschlüsselung, Klartext-Passwörter in Config)
|
||||
|
||||
## Offene Punkte
|
||||
|
||||
- 9 Vergleichspunkte nicht vollständig bewertbar (HIN-Dokumentation fehlt)
|
||||
- Supabase-Sicherheitskonfiguration nicht im Scope
|
||||
- Python-Default-TLS-Verhalten systemabhängig
|
||||
|
||||
## Risiken
|
||||
|
||||
- Alle 5 Bereiche zeigen fundamentale Lücken zum HIN-Standard
|
||||
- Besonders kritisch: Klartext-Passwörter in aza_email_config.json
|
||||
- Besonders kritisch: SHA-256 ohne Salt in basis14.py
|
||||
- Besonders kritisch: Hardcoded Secret Key "dev-secret-change-in-production"
|
||||
- Besonders kritisch: HTTP-Server ohne TLS (todo_server, transcribe_server, backend)
|
||||
139
AzA march 2026/security/handovers/STEP_10_CONSENT_LOGGING.md
Normal file
139
AzA march 2026/security/handovers/STEP_10_CONSENT_LOGGING.md
Normal file
@@ -0,0 +1,139 @@
|
||||
# STEP 10 – Technische Einwilligungs-Protokollierung
|
||||
|
||||
## Ziel
|
||||
Jede KI-bezogene Datenverarbeitung muss technisch nachweisbar
|
||||
durch eine Einwilligung gedeckt sein.
|
||||
|
||||
---
|
||||
|
||||
## 1. Datenmodell
|
||||
|
||||
Jeder Consent-Eintrag enthält:
|
||||
|
||||
| Feld | Typ | Beschreibung |
|
||||
|---|---|---|
|
||||
| user_id | string | Benutzername aus Profil |
|
||||
| consent_type | string | Immer "ai_processing" |
|
||||
| consent_version | string | Stand-Datum aus ai_consent.md |
|
||||
| timestamp | string | UTC ISO-8601 |
|
||||
| source | string | "ui", "test", "admin" |
|
||||
| action | string | "grant" oder "revoke" |
|
||||
| prev_hash | string | SHA-256 des vorherigen Eintrags |
|
||||
| hash | string | SHA-256 dieses Eintrags (Integritaetskette) |
|
||||
|
||||
## 2. Speicherort
|
||||
|
||||
- Datei: `aza_consent_log.json` (Projekt-Root)
|
||||
- Format: JSON-Array (Append-only)
|
||||
- Manipulationssicherheit: SHA-256-Hash-Kette
|
||||
(jeder Eintrag referenziert den Hash des vorherigen)
|
||||
- Kein Überschreiben: Widerruf erzeugt neuen Eintrag,
|
||||
alter Grant bleibt in der Historie
|
||||
|
||||
## 3. Enforcement-Punkte
|
||||
|
||||
KI-Funktionen werden an folgenden Stellen blockiert, wenn
|
||||
keine gueltige Einwilligung vorliegt:
|
||||
|
||||
| Stelle | Datei | Methode |
|
||||
|---|---|---|
|
||||
| Chat Completion (alle KI-Texte) | basis14.py | call_chat_completion() |
|
||||
| Transkription | basis14.py | transcribe_wav() |
|
||||
| Aufnahme starten | basis14.py | toggle_record() |
|
||||
| Korrektur-Aufnahme | basis14.py | _toggle_record_append() |
|
||||
| Diktat in Widget | basis14.py | _diktat_into_widget() |
|
||||
| KI-Prüfung | basis14.py | open_ki_pruefen() |
|
||||
| Interaktionscheck | basis14.py | do_interaktion() |
|
||||
|
||||
Bei fehlendem Consent:
|
||||
- UI-Einstiegspunkte: Consent-Dialog wird angezeigt
|
||||
- API-Gateways: RuntimeError wird geworfen
|
||||
|
||||
## 4. UI-Elemente
|
||||
|
||||
### 4.1 Consent-Dialog (vor erster KI-Nutzung)
|
||||
- Zeigt den kompletten Text aus legal/ai_consent.md
|
||||
- Checkbox: "Ich habe den Text gelesen und stimme zu"
|
||||
- Buttons: "Zustimmen" / "Ablehnen"
|
||||
- Wird nur angezeigt, wenn kein gueltiger Consent vorliegt
|
||||
|
||||
### 4.2 Einstellungsfenster (Datenschutz & Recht)
|
||||
- Status-Anzeige: "Erteilt" / "Nicht erteilt / widerrufen"
|
||||
- Button: "KI-Einwilligung widerrufen" / "erteilen"
|
||||
- Button: "Consent-Log exportieren"
|
||||
- Bestehende Buttons: Datenschutzerklaerung, KI-Einwilligung
|
||||
|
||||
## 5. Versions-Handling
|
||||
|
||||
- Die Consent-Version wird aus dem "Stand:"-Feld in
|
||||
legal/ai_consent.md extrahiert
|
||||
- Bei Aenderung des Consent-Textes (neues Datum) wird
|
||||
ein bestehender Consent ungueltig
|
||||
- Benutzer muss erneut zustimmen
|
||||
|
||||
## 6. Audit-Workflow
|
||||
|
||||
### Export
|
||||
```
|
||||
python -c "from aza_consent import export_consent_log; print(export_consent_log())"
|
||||
```
|
||||
Oder ueber Einstellungen -> "Consent-Log exportieren"
|
||||
|
||||
### Integritaetspruefung
|
||||
```
|
||||
python -c "from aza_consent import verify_chain_integrity; ok, e = verify_chain_integrity(); print('OK' if ok else e)"
|
||||
```
|
||||
|
||||
### Export-Format
|
||||
```json
|
||||
{
|
||||
"export_timestamp": "2026-02-22T...",
|
||||
"total_entries": 3,
|
||||
"current_consent_version": "Februar 2026",
|
||||
"entries": [
|
||||
{
|
||||
"user_id": "...",
|
||||
"consent_type": "ai_processing",
|
||||
"consent_version": "Februar 2026",
|
||||
"timestamp": "2026-02-22T...",
|
||||
"source": "ui",
|
||||
"action": "grant",
|
||||
"prev_hash": "0000...0000",
|
||||
"hash": "a1b2c3..."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## 7. Geaenderte / Neue Dateien
|
||||
|
||||
| Datei | Aktion |
|
||||
|---|---|
|
||||
| aza_consent.py | NEU – Consent-Modul (Datenmodell, Storage, Enforcement, Export) |
|
||||
| basis14.py | GEAENDERT – Import aza_consent, Consent-Check an 7 Stellen |
|
||||
| aza_settings_mixin.py | GEAENDERT – Widerruf/Erteil-Button, Status, Export im Einstellungsfenster |
|
||||
| _test_consent.py | NEU – Testskript (14 Tests) |
|
||||
|
||||
## 8. Test-Ergebnisse (22.02.2026)
|
||||
|
||||
| Test | Ergebnis |
|
||||
|---|---|
|
||||
| Ohne Consent -> KI blockiert | PASS |
|
||||
| Consent erteilen -> KI erlaubt | PASS |
|
||||
| Widerruf -> KI blockiert | PASS |
|
||||
| Erneuter Consent -> KI erlaubt | PASS |
|
||||
| Hash-Kette intakt | PASS |
|
||||
| Status-Abfrage korrekt | PASS |
|
||||
| Version-Match | PASS |
|
||||
| Export erstellt gueltige Datei | PASS |
|
||||
| User-Historie korrekt | PASS |
|
||||
| Anderer User -> kein Consent | PASS |
|
||||
| Gesamt: 14/14 | ALLE BESTANDEN |
|
||||
|
||||
## 9. Risiken
|
||||
|
||||
| Risiko | Bewertung | Massnahme |
|
||||
|---|---|---|
|
||||
| JSON-Datei manuell editierbar | NIEDRIG | Hash-Kette erkennt Manipulation |
|
||||
| Kein digitaler Signatur-Mechanismus | MITTEL | Fuer Audit: Export + Integritaetspruefung |
|
||||
| Consent nur auf App-Ebene, nicht Patient | NIEDRIG | Papier-Einwilligung ergaenzt dies |
|
||||
@@ -0,0 +1,209 @@
|
||||
# STEP 10a – Consent Audit-Proof (Nachweis + Checkliste)
|
||||
|
||||
Datum: 22.02.2026
|
||||
Pruefung durch: Internes Audit-Skript (_test_consent_audit.py)
|
||||
|
||||
---
|
||||
|
||||
## 1. Speicherort + Schema
|
||||
|
||||
Datei: aza_consent_log.json (Projekt-Root)
|
||||
Format: JSON Array (Append-only, kein Ueberschreiben)
|
||||
|
||||
Schema pro Eintrag:
|
||||
|
||||
Feld Typ Beschreibung
|
||||
-----------------------------------------------
|
||||
user_id string Benutzer-ID aus Profil
|
||||
consent_type string Immer "ai_processing"
|
||||
consent_version string Stand-Datum aus ai_consent.md
|
||||
timestamp string UTC ISO-8601
|
||||
source string "ui" / "test" / "admin"
|
||||
action string "grant" oder "revoke"
|
||||
prev_hash string SHA-256 des vorherigen Eintrags
|
||||
hash string SHA-256 dieses Eintrags
|
||||
|
||||
Versions-Mechanismus:
|
||||
consent_version wird aus legal/ai_consent.md extrahiert.
|
||||
Die Funktion _get_consent_version() liest die Zeile, die mit
|
||||
"Stand:" beginnt, und gibt den Wert nach dem Doppelpunkt zurueck.
|
||||
Aktuell: "Februar 2026"
|
||||
Bei Aenderung des Datums in ai_consent.md wird jede bestehende
|
||||
Einwilligung ungueltig und muss erneuert werden.
|
||||
|
||||
---
|
||||
|
||||
## 2. Beispiel-Logeintraege (sanitized)
|
||||
|
||||
2a) GRANT (Zustimmung):
|
||||
|
||||
{
|
||||
"user_id": "user_1",
|
||||
"consent_type": "ai_processing",
|
||||
"consent_version": "Februar 2026",
|
||||
"timestamp": "2026-02-22T21:13:25.419060+00:00",
|
||||
"source": "ui",
|
||||
"action": "grant",
|
||||
"prev_hash": "000000000000000000000000000000000000000000000000...",
|
||||
"hash": "6988dcc3fb7215af69eb1fcdd35afaf7aab37262b3ed..."
|
||||
}
|
||||
|
||||
2b) REVOKE (Widerruf):
|
||||
|
||||
{
|
||||
"user_id": "user_1",
|
||||
"consent_type": "ai_processing",
|
||||
"consent_version": "Februar 2026",
|
||||
"timestamp": "2026-02-22T21:13:25.419060+00:00",
|
||||
"source": "ui",
|
||||
"action": "revoke",
|
||||
"prev_hash": "6988dcc3fb7215af69eb1fcdd35afaf7aab37262b3ed...",
|
||||
"hash": "2a60ef4a43cefc8e3c39dc155530c0f443bbb0b47b0e..."
|
||||
}
|
||||
|
||||
2c) RE-GRANT (erneute Zustimmung):
|
||||
|
||||
{
|
||||
"user_id": "user_1",
|
||||
"consent_type": "ai_processing",
|
||||
"consent_version": "Februar 2026",
|
||||
"timestamp": "2026-02-22T21:13:25.421171+00:00",
|
||||
"source": "ui",
|
||||
"action": "grant",
|
||||
"prev_hash": "2a60ef4a43cefc8e3c39dc155530c0f443bbb0b47b0e...",
|
||||
"hash": "2cb429a75c3d02f8861c6fd6fbec1216acb6a453926a..."
|
||||
}
|
||||
|
||||
Timestamp-Pruefung (UTC-Nachweis):
|
||||
Alle Timestamps enden auf "+00:00" -> UTC bestaetigt.
|
||||
|
||||
---
|
||||
|
||||
## 3. Integritaetsbeweis
|
||||
|
||||
3a) Intakte Log-Datei:
|
||||
|
||||
verify_chain_integrity() -> PASS
|
||||
Alle Hash-Werte stimmen mit der Berechnung ueberein.
|
||||
Alle prev_hash-Referenzen sind korrekt verkettet.
|
||||
|
||||
3b) Manipulierte Log-Datei:
|
||||
|
||||
Manipulation: timestamp von Eintrag 0 um ein Zeichen geaendert
|
||||
("...+00:00" -> "...+00:0X")
|
||||
|
||||
verify_chain_integrity() -> FAIL
|
||||
Fehlermeldung:
|
||||
"Eintrag 0: Hash stimmt nicht
|
||||
(erwartet 8b92439ca6d1b926..., gefunden 6988dcc3fb7215af...)"
|
||||
|
||||
Ergebnis: Manipulation wird zuverlaessig erkannt.
|
||||
|
||||
3c) Nach Restore der Originaldatei:
|
||||
|
||||
verify_chain_integrity() -> PASS
|
||||
|
||||
---
|
||||
|
||||
## 4. Enforcement-Beweis
|
||||
|
||||
4a) Ohne Consent:
|
||||
has_valid_consent("user_1") -> False
|
||||
KI-Funktion: BLOCKIERT
|
||||
Verhalten: RuntimeError "KI-Einwilligung fehlt oder wurde widerrufen."
|
||||
UI: Consent-Dialog wird angezeigt (Checkbox + Zustimmen-Button)
|
||||
|
||||
4b) Nach Consent:
|
||||
record_consent("user_1") ausgefuehrt
|
||||
has_valid_consent("user_1") -> True
|
||||
KI-Funktion: ERLAUBT
|
||||
|
||||
4c) Nach Widerruf:
|
||||
record_revoke("user_1") ausgefuehrt
|
||||
has_valid_consent("user_1") -> False
|
||||
KI-Funktion: BLOCKIERT
|
||||
|
||||
4d) Version-Change (consent_version geaendert):
|
||||
Vor Aenderung: has_valid_consent -> True
|
||||
consent_version im Log manuell auf "Januar 2025" gesetzt
|
||||
Aktuelle Version in ai_consent.md: "Februar 2026"
|
||||
has_valid_consent -> False
|
||||
Ergebnis: Erneute Zustimmung erforderlich (KORREKT)
|
||||
|
||||
Enforcement-Punkte im Code:
|
||||
|
||||
Datei Methode Typ
|
||||
-------------------------------------------------------
|
||||
basis14.py call_chat_completion() API-Gateway
|
||||
basis14.py transcribe_wav() API-Gateway
|
||||
basis14.py toggle_record() UI-Einstieg
|
||||
basis14.py _toggle_record_append() UI-Einstieg
|
||||
basis14.py _diktat_into_widget() UI-Einstieg
|
||||
basis14.py open_ki_pruefen() UI-Einstieg
|
||||
basis14.py do_interaktion() UI-Einstieg
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Minimization Check
|
||||
|
||||
Gespeicherte Felder im Consent-Log (vollstaendige Liste):
|
||||
|
||||
action, consent_type, consent_version, hash,
|
||||
prev_hash, source, timestamp, user_id
|
||||
|
||||
Pruefung auf sensible Daten:
|
||||
|
||||
Transkript-Inhalte: NICHT gespeichert
|
||||
KG-Inhalte: NICHT gespeichert
|
||||
Prompts / KI-Antworten: NICHT gespeichert
|
||||
API-Keys / Secrets: NICHT gespeichert
|
||||
Passwoerter: NICHT gespeichert
|
||||
Patientennamen: NICHT gespeichert
|
||||
Diagnosen: NICHT gespeichert
|
||||
Audio-Daten: NICHT gespeichert
|
||||
|
||||
Kein Feldwert ueberschreitet 200 Zeichen
|
||||
(ausser hash/prev_hash mit genau 64 Hex-Zeichen).
|
||||
|
||||
Data Minimization: PASS
|
||||
|
||||
---
|
||||
|
||||
## 6. Gesamtbewertung
|
||||
|
||||
Pruefpunkt Ergebnis
|
||||
-------------------------------------------------------
|
||||
Speicherort + Schema PASS
|
||||
Beispiel-Logeintraege PASS
|
||||
Timestamp UTC PASS
|
||||
Integritaet intakt PASS
|
||||
Integritaet manipuliert erkannt PASS
|
||||
Enforcement ohne Consent PASS
|
||||
Enforcement mit Consent PASS
|
||||
Enforcement nach Widerruf PASS
|
||||
Version-Change erfordert Neu-Consent PASS
|
||||
Data Minimization PASS
|
||||
|
||||
GESAMTBEWERTUNG: PASS (10/10)
|
||||
|
||||
---
|
||||
|
||||
## 7. Reproduzierbarkeit
|
||||
|
||||
Der Nachweis kann jederzeit reproduziert werden mit:
|
||||
|
||||
python _test_consent_audit.py
|
||||
|
||||
Das Skript erzeugt temporaere Testdaten, fuehrt alle Pruefungen
|
||||
durch und raeumt danach auf (keine persistenten Aenderungen).
|
||||
|
||||
---
|
||||
|
||||
## 8. Offene Punkte
|
||||
|
||||
- Kein digitaler Signatur-Mechanismus (nur Hash-Kette):
|
||||
Fuer hoechste Anforderungen waere eine kryptografische
|
||||
Signatur (z.B. RSA/ECDSA) empfehlenswert.
|
||||
- Consent bezieht sich auf den Anwendungsbenutzer (Arzt),
|
||||
nicht auf den Patienten. Die Patienten-Einwilligung erfolgt
|
||||
ueber das Papierformular (legal/ai_consent.md).
|
||||
151
AzA march 2026/security/handovers/STEP_11_AUDIT_LOGGING.md
Normal file
151
AzA march 2026/security/handovers/STEP_11_AUDIT_LOGGING.md
Normal file
@@ -0,0 +1,151 @@
|
||||
# STEP 11 – Audit-Logging (Minimal & DSG-konform)
|
||||
|
||||
## Ziel
|
||||
Sicherheitsrelevante Ereignisse nachvollziehbar protokollieren,
|
||||
ohne sensible Daten (Patientendaten, Prompts, Passwoerter) zu speichern.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architektur
|
||||
|
||||
Neues Modul: aza_audit_log.py
|
||||
|
||||
Log-Format: Pipe-separierte Textdatei (eine Zeile pro Ereignis)
|
||||
|
||||
TIMESTAMP | EVENT | USER | STATUS | SOURCE | DETAIL
|
||||
|
||||
Beispiel:
|
||||
2026-02-22T21:13:25.419+00:00 | LOGIN_OK | Dr. Mueller | OK | desktop |
|
||||
|
||||
Alle Timestamps sind UTC (ISO-8601).
|
||||
|
||||
---
|
||||
|
||||
## 2. Protokollierte Ereignisse
|
||||
|
||||
Event-Typ Beschreibung Wann
|
||||
------------------------------------------------------------------
|
||||
APP_START Anwendung gestartet main()
|
||||
APP_STOP Anwendung beendet nach mainloop()
|
||||
LOGIN_OK Erfolgreicher Login do_login()
|
||||
LOGIN_FAIL Fehlgeschlagener Login do_login()
|
||||
2FA_OK 2FA-Verifizierung erfolgreich do_verify()
|
||||
2FA_FAIL 2FA-Verifizierung fehlgeschlagen do_verify()
|
||||
CONSENT_GRANT KI-Einwilligung erteilt on_accept()
|
||||
CONSENT_REVOKE KI-Einwilligung widerrufen toggle_consent()
|
||||
AI_TRANSCRIBE KI-Transkription gestartet transcribe_wav()
|
||||
AI_CHAT KI-Chat-Completion aufgerufen call_chat_completion()
|
||||
AI_BLOCKED KI-Aufruf ohne Consent blockiert transcribe/chat
|
||||
PASSWORD_REHASH Legacy-Hash auf bcrypt migriert do_login()
|
||||
EXPORT Log-Export durchgefuehrt do_export()
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Minimization (DSG Art. 6)
|
||||
|
||||
Was NICHT im Audit-Log gespeichert wird:
|
||||
- Patientennamen / Patientendaten
|
||||
- Transkripte / KG-Inhalte
|
||||
- KI-Prompts / KI-Antworten
|
||||
- Passwoerter / Passwort-Hashes
|
||||
- API-Keys / Secrets
|
||||
- E-Mail-Inhalte
|
||||
|
||||
Detail-Feld: Maximal 200 Zeichen, nur Metadaten
|
||||
(z.B. "model=gpt-5.2", "2FA backup-code").
|
||||
|
||||
---
|
||||
|
||||
## 4. Log-Rotation
|
||||
|
||||
Parameter Standard ENV-Variable
|
||||
--------------------------------------------------
|
||||
Max. Dateigroesse 10 MB AZA_AUDIT_ROTATE_MB
|
||||
Rotierte Dateien 12 AZA_AUDIT_KEEP
|
||||
Logdatei-Pfad aza_audit.log AZA_AUDIT_LOG
|
||||
|
||||
Bei Ueberschreitung der Maximalgroesse:
|
||||
aza_audit.log -> aza_audit.1.log -> ... -> aza_audit.12.log
|
||||
Aelteste Datei wird ueberschrieben.
|
||||
|
||||
---
|
||||
|
||||
## 5. Export
|
||||
|
||||
Ueber Einstellungen -> "Logs exportieren (Audit)":
|
||||
Exportiert sowohl Consent-Log als auch Audit-Log als JSON.
|
||||
|
||||
Programmatisch:
|
||||
python -c "from aza_audit_log import export_audit_log; print(export_audit_log())"
|
||||
|
||||
Export-Format:
|
||||
{
|
||||
"export_timestamp": "2026-02-22T...",
|
||||
"total_entries": 12,
|
||||
"source_file": ".../aza_audit.log",
|
||||
"entries": [
|
||||
{
|
||||
"timestamp": "2026-02-22T...",
|
||||
"event": "LOGIN_OK",
|
||||
"user_id": "...",
|
||||
"status": "OK",
|
||||
"source": "desktop",
|
||||
"detail": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
---
|
||||
|
||||
## 6. Integration bestehender Audit-Systeme
|
||||
|
||||
Komponente Audit-System Speicherort
|
||||
-----------------------------------------------------------
|
||||
Desktop-App (basis14) aza_audit_log.py aza_audit.log
|
||||
Backend (backend_main) _audit_write() medwork_audit.log
|
||||
Workforce Planner log_action() + DB workforce_planner.db
|
||||
|
||||
Alle drei Systeme sind unabhaengig und koennen
|
||||
separat exportiert werden.
|
||||
|
||||
---
|
||||
|
||||
## 7. Geaenderte / Neue Dateien
|
||||
|
||||
Datei Aktion
|
||||
--------------------------------------------------
|
||||
aza_audit_log.py NEU – Audit-Log-Modul
|
||||
basis14.py GEAENDERT – Import + 10 log_event()-Aufrufe
|
||||
aza_settings_mixin.py GEAENDERT – Import + Widerruf-Log + Export
|
||||
_test_audit_log.py NEU – Testskript (23 Tests)
|
||||
|
||||
---
|
||||
|
||||
## 8. Test-Ergebnisse (22.02.2026)
|
||||
|
||||
Test Ergebnis
|
||||
-------------------------------------------
|
||||
Events schreiben PASS
|
||||
12 Zeilen korrekt PASS
|
||||
6 Felder pro Zeile PASS
|
||||
Timestamp UTC PASS
|
||||
Event-Typ korrekt PASS
|
||||
User korrekt PASS
|
||||
Status OK/FAIL korrekt PASS
|
||||
Source = desktop PASS
|
||||
Statistiken korrekt PASS
|
||||
Export JSON korrekt PASS
|
||||
Data Minimization PASS
|
||||
Pipe-Sanitierung PASS
|
||||
Gesamt: 23/23 ALLE BESTANDEN
|
||||
|
||||
---
|
||||
|
||||
## 9. Risiken
|
||||
|
||||
Risiko Bewertung Massnahme
|
||||
--------------------------------------------------------
|
||||
Log-Datei loeschbar NIEDRIG Backup sichert mit
|
||||
Kein zentrales SIEM MITTEL Export fuer manuelle Pruefung
|
||||
Keine Echtzeit-Alarmierung MITTEL Manueller Review empfohlen
|
||||
Log-Rotation verliert alte Daten NIEDRIG 12 Dateien a 10 MB = 120 MB
|
||||
@@ -0,0 +1,197 @@
|
||||
STEP 11a – AUDIT-LOG INTEGRITAET (HASH-KETTE)
|
||||
Datum: 2026-02-22
|
||||
Status: ABGESCHLOSSEN
|
||||
|
||||
=====================================================================
|
||||
1. ZIEL
|
||||
=====================================================================
|
||||
|
||||
Audit-Log (aza_audit_log.py) manipulations-erkennbar machen
|
||||
durch SHA-256-Hash-Kette, analog zum Consent-Log.
|
||||
|
||||
=====================================================================
|
||||
2. AENDERUNGEN
|
||||
=====================================================================
|
||||
|
||||
Datei: aza_audit_log.py
|
||||
|
||||
a) Neues Format (8 Felder statt 6):
|
||||
TIMESTAMP | EVENT | USER | STATUS | SOURCE | DETAIL | PREV_HASH | ENTRY_HASH
|
||||
|
||||
b) Hash-Berechnung:
|
||||
entry_hash = SHA-256(prev_hash + payload)
|
||||
payload = "TS | EVENT | USER | STATUS | SOURCE | DETAIL"
|
||||
Startwert (GENESIS): 64x "0"
|
||||
|
||||
c) Rotation mit Ketten-Uebergabe:
|
||||
Bei Rotation wird der letzte Hash des rotierten Files
|
||||
als Header in der neuen Datei gespeichert:
|
||||
#CHAIN_FROM=<letzter_entry_hash>
|
||||
Neue Eintraege setzen prev_hash = Chain-Header-Hash.
|
||||
Ein Auditor kann die Kette ueber Dateigrenzen pruefen.
|
||||
|
||||
d) Neue Funktionen:
|
||||
- verify_integrity(path) -> (bool, list[str])
|
||||
Prueft eine einzelne Logdatei auf Ketten-Integritaet.
|
||||
- verify_all_rotations() -> (bool, dict)
|
||||
Prueft alle rotierten Dateien.
|
||||
|
||||
e) CLI-Interface:
|
||||
python -m aza_audit_log verify -> Aktuelle Datei pruefen
|
||||
python -m aza_audit_log verify --all -> Alle Rotationsdateien
|
||||
python -m aza_audit_log verify --file X -> Bestimmte Datei
|
||||
python -m aza_audit_log stats -> Statistiken + Integritaet
|
||||
python -m aza_audit_log export -> JSON-Export mit Integritaet
|
||||
|
||||
f) Export enthaelt jetzt:
|
||||
- integrity: PASS/FAIL
|
||||
- integrity_errors: []
|
||||
- Jeder Entry hat prev_hash und entry_hash
|
||||
|
||||
g) get_log_stats() enthaelt jetzt:
|
||||
- integrity: PASS/FAIL
|
||||
|
||||
=====================================================================
|
||||
3. ROTATIONS-STRATEGIE
|
||||
=====================================================================
|
||||
|
||||
Ablauf bei Rotation (Datei > AZA_AUDIT_ROTATE_MB):
|
||||
|
||||
1. Letzten entry_hash der aktuellen Datei auslesen
|
||||
2. Bestehende .1, .2, ... Dateien hochschieben
|
||||
3. Aktuelle Datei -> .1 verschieben
|
||||
4. Neue leere Datei anlegen mit Header:
|
||||
#CHAIN_FROM=<letzter_hash>
|
||||
5. Neue Eintraege nutzen diesen Hash als prev_hash
|
||||
|
||||
Verifizierung:
|
||||
- verify_integrity() pro Datei: prueft interne Kette
|
||||
- Header #CHAIN_FROM verbindet die Dateien logisch
|
||||
- verify_all_rotations() prueft alle Dateien der Reihe nach
|
||||
|
||||
=====================================================================
|
||||
4. BEISPIEL LOG-ZEILE
|
||||
=====================================================================
|
||||
|
||||
2026-02-22T21:42:27.160+00:00 | APP_START | user_1 | OK | desktop | test | 000000...0000 | a3f8e1...
|
||||
|
||||
Felder:
|
||||
[0] Timestamp (UTC, ISO-8601)
|
||||
[1] Event-Typ
|
||||
[2] User-ID
|
||||
[3] Status (OK/FAIL)
|
||||
[4] Source
|
||||
[5] Detail (max 200 Zeichen, keine sensiblen Daten)
|
||||
[6] prev_hash (SHA-256 des vorherigen Eintrags)
|
||||
[7] entry_hash (SHA-256 ueber prev_hash + Felder 0-5)
|
||||
|
||||
=====================================================================
|
||||
5. VERIFY-ANLEITUNG + BEISPIELAUSGABEN
|
||||
=====================================================================
|
||||
|
||||
a) Integritaet pruefen (CLI):
|
||||
|
||||
$ python -m aza_audit_log verify
|
||||
Datei: C:\...\aza_audit.log
|
||||
Integritaet: PASS
|
||||
|
||||
b) Manipulation erkennen:
|
||||
|
||||
$ python -m aza_audit_log verify
|
||||
Datei: C:\...\aza_audit.log
|
||||
Integritaet: FAIL
|
||||
Zeile 2: entry_hash stimmt nicht (erwartet 104ebe9a..., gefunden 32e00f09...)
|
||||
|
||||
c) Alle Rotationsdateien:
|
||||
|
||||
$ python -m aza_audit_log verify --all
|
||||
aza_audit.1.log: PASS
|
||||
aza_audit.log: PASS
|
||||
GESAMT: PASS
|
||||
|
||||
d) Statistiken:
|
||||
|
||||
$ python -m aza_audit_log stats
|
||||
Datei: aza_audit.log
|
||||
Existiert: True
|
||||
Groesse: 0.0 MB
|
||||
Eintraege: 2
|
||||
Integritaet: PASS
|
||||
|
||||
=====================================================================
|
||||
6. TEST-ERGEBNIS (PROOF)
|
||||
=====================================================================
|
||||
|
||||
Testskript: _test_audit_integrity.py
|
||||
21 Tests, 0 Fehler.
|
||||
|
||||
Tests:
|
||||
1. Hash-Kette schreiben + verifizieren:
|
||||
- 5 Eintraege geschrieben, verify PASS
|
||||
- 8 Felder pro Zeile
|
||||
- prev_hash[0] = GENESIS
|
||||
- prev_hash[n] = entry_hash[n-1]
|
||||
|
||||
2. Manipulation erkennen:
|
||||
- 1 Zeichen geaendert (LOGIN_OK -> LOGIN_XX)
|
||||
- verify -> FAIL, Fehlerstelle gemeldet
|
||||
- Restore -> PASS
|
||||
|
||||
3. Rotation mit Ketten-Uebergabe:
|
||||
- Rotierte Datei (.1) intakt: PASS
|
||||
- Aktuelle Datei mit Chain-Header: PASS
|
||||
- Header-Hash == letzter Hash der rotierten Datei: PASS
|
||||
- prev_hash im neuen File == Chain-Header: PASS
|
||||
- verify_all_rotations: PASS
|
||||
|
||||
4. Stats + Export:
|
||||
- Integritaet in Stats: PASS
|
||||
- Export integrity: PASS
|
||||
- Entries mit entry_hash: PASS
|
||||
|
||||
5. Data Minimization:
|
||||
- Kein Passwort, kein API-Key, kein Transkript: PASS
|
||||
|
||||
=====================================================================
|
||||
7. DATA MINIMIZATION
|
||||
=====================================================================
|
||||
|
||||
Unveraendert gegenueber STEP 11:
|
||||
- Keine Patientennamen
|
||||
- Keine Transkripte/Prompts
|
||||
- Keine Passwoerter/API-Keys
|
||||
- Detail max. 200 Zeichen, nur Metadaten
|
||||
|
||||
=====================================================================
|
||||
8. BETROFFENE DATEIEN
|
||||
=====================================================================
|
||||
|
||||
Geaendert:
|
||||
- aza_audit_log.py (Format erweitert, Hash-Kette, verify, CLI)
|
||||
|
||||
Neu:
|
||||
- _test_audit_integrity.py (Proof-Skript)
|
||||
- security/handovers/STEP_11a_AUDIT_LOG_INTEGRITY.md (dieses Dokument)
|
||||
|
||||
Nicht geaendert:
|
||||
- basis14.py (Aufrufe bleiben kompatibel, neue Felder transparent)
|
||||
- aza_settings_mixin.py (unveraendert)
|
||||
- aza_consent.py (unveraendert)
|
||||
|
||||
=====================================================================
|
||||
9. RISIKEN
|
||||
=====================================================================
|
||||
|
||||
- Performance: _get_last_hash() liest die gesamte Datei.
|
||||
Bei sehr grossen Dateien (kurz vor Rotation) kann dies langsam sein.
|
||||
Akzeptabel, da Rotation bei 10 MB greift.
|
||||
|
||||
- Rueckwaertskompatibilitaet: Bestehende 6-Feld-Logdateien werden
|
||||
von verify_integrity() als ungueltig gemeldet (< 8 Felder).
|
||||
Loesung: Bestehende Logdatei vor erstem Start archivieren/loeschen.
|
||||
|
||||
=====================================================================
|
||||
10. OFFENE PUNKTE
|
||||
=====================================================================
|
||||
|
||||
Keine.
|
||||
211
AzA march 2026/security/handovers/STEP_12_MONITORING.md
Normal file
211
AzA march 2026/security/handovers/STEP_12_MONITORING.md
Normal file
@@ -0,0 +1,211 @@
|
||||
STEP 12 – MONITORING & HEALTH CHECKS (MINIMAL)
|
||||
Datum: 2026-02-22
|
||||
Status: ABGESCHLOSSEN
|
||||
|
||||
=====================================================================
|
||||
1. ZIEL
|
||||
=====================================================================
|
||||
|
||||
Minimaler Betriebsnachweis:
|
||||
- Services leben (Health-Checks)
|
||||
- Metriken aus bestehenden Logs (kein externer Dienst)
|
||||
- Integritaetspruefung automatisierbar
|
||||
- Scheduling dokumentiert
|
||||
|
||||
Keine Patientendaten erfasst.
|
||||
|
||||
=====================================================================
|
||||
2. HEALTH-CHECK ENDPOINTS
|
||||
=====================================================================
|
||||
|
||||
Alle Services liefern jetzt unter /health:
|
||||
|
||||
{
|
||||
"status": "ok",
|
||||
"version": "<app-version>",
|
||||
"uptime_s": <seconds>,
|
||||
"tls": true/false
|
||||
}
|
||||
|
||||
a) backend_main.py
|
||||
URL: https://127.0.0.1:8000/health
|
||||
Vorher: {"ok": true}
|
||||
Nachher: {"status": "ok", "version": "0.1.0", "uptime_s": ..., "tls": ...}
|
||||
|
||||
b) transcribe_server.py
|
||||
URL: https://127.0.0.1:8090/health
|
||||
Vorher: {"status": "ok"}
|
||||
Nachher: {"status": "ok", "version": "0.1.0", "uptime_s": ..., "tls": ...}
|
||||
|
||||
c) todo_server.py
|
||||
URL: https://127.0.0.1:5111/health
|
||||
Vorher: Kein /health Endpoint
|
||||
Nachher: {"status": "ok", "version": "1.0.0", "uptime_s": ..., "tls": ...}
|
||||
|
||||
d) Desktop-App (basis14.py)
|
||||
Kein HTTP-Endpoint (Desktop-App ohne eigenen Server).
|
||||
Betriebsnachweis ueber Audit-Log:
|
||||
- APP_START / APP_STOP Events
|
||||
- LOGIN_OK / LOGIN_FAIL Events
|
||||
|
||||
=====================================================================
|
||||
3. MONITORING-METRIKEN (aza_monitoring.py)
|
||||
=====================================================================
|
||||
|
||||
Quellen: Nur bestehende lokale Logs/Dateien. Kein Cloud-Dienst.
|
||||
|
||||
a) Audit-Log Metriken (aus aza_audit_log.py):
|
||||
- Anzahl LOGIN_FAIL
|
||||
- Anzahl AI_CHAT + AI_TRANSCRIBE (KI-Calls Zaehler)
|
||||
- Anzahl AI_BLOCKED
|
||||
- Anzahl 2FA_FAIL
|
||||
- Integritaets-Status (PASS/FAIL)
|
||||
|
||||
b) Consent-Log Metriken (aus aza_consent.py):
|
||||
- Anzahl Eintraege
|
||||
- Integritaets-Status
|
||||
|
||||
c) Backup-Metriken (aus backups/ Verzeichnis):
|
||||
- Anzahl vorhandener Backups
|
||||
- Letztes Backup (Name + Zeitpunkt)
|
||||
|
||||
d) Alert-Severity-Stufen:
|
||||
- INFO: Zaehler ohne Handlungsbedarf
|
||||
- WARN: Erhoehte Werte (z.B. > 0 login_fail)
|
||||
- HIGH: Kritische Schwellen (z.B. >= 10 login_fail)
|
||||
- CRITICAL: Integritaets-Verletzung
|
||||
|
||||
=====================================================================
|
||||
4. INTEGRITAETS-CHECKS
|
||||
=====================================================================
|
||||
|
||||
Automatisierte Pruefung ueber:
|
||||
python aza_monitoring.py integrity
|
||||
|
||||
Prueft:
|
||||
- aza_audit_log: verify_all_rotations() (SHA-256 Hash-Kette)
|
||||
- aza_consent_log: verify_chain_integrity() (SHA-256 Hash-Kette)
|
||||
|
||||
Bei Fehler:
|
||||
- Klarer Log-Eintrag (INTEGRITY_FAIL Event ins Audit-Log)
|
||||
- Exit-Code 1 (fuer Scheduler-Alerting)
|
||||
|
||||
=====================================================================
|
||||
5. CLI-KOMMANDOS
|
||||
=====================================================================
|
||||
|
||||
python aza_monitoring.py health Health-Checks aller Services
|
||||
python aza_monitoring.py metrics Metriken aus Logs
|
||||
python aza_monitoring.py integrity Integritaetspruefung (Exit 0/1)
|
||||
python aza_monitoring.py alerts Sicherheits-Alerts
|
||||
python aza_monitoring.py nightly Naechtlicher Gesamtcheck + JSON-Report
|
||||
python aza_monitoring.py all Alles anzeigen
|
||||
|
||||
=====================================================================
|
||||
6. SCHEDULING-BEISPIELE
|
||||
=====================================================================
|
||||
|
||||
a) Linux (cron):
|
||||
|
||||
# Naechtlicher Gesamtcheck um 02:00
|
||||
0 2 * * * cd /pfad/zu/aza && python aza_monitoring.py nightly >> /var/log/aza_monitoring.log 2>&1
|
||||
|
||||
# Stuendlicher Health-Check
|
||||
0 * * * * cd /pfad/zu/aza && python aza_monitoring.py health >> /var/log/aza_health.log 2>&1
|
||||
|
||||
# Integritaet alle 6 Stunden
|
||||
0 */6 * * * cd /pfad/zu/aza && python aza_monitoring.py integrity || echo "INTEGRITY FAIL" | mail admin@example.com
|
||||
|
||||
b) Windows (Task Scheduler):
|
||||
|
||||
Aktion: Programm starten
|
||||
Programm: python
|
||||
Argumente: aza_monitoring.py nightly
|
||||
Starten in: C:\Users\surov\Documents\AZA\backup 19.2.26
|
||||
|
||||
Trigger: Taeglich, 02:00 Uhr
|
||||
|
||||
Alternativ via PowerShell:
|
||||
|
||||
$action = New-ScheduledTaskAction `
|
||||
-Execute "python" `
|
||||
-Argument "aza_monitoring.py nightly" `
|
||||
-WorkingDirectory "C:\Users\surov\Documents\AZA\backup 19.2.26"
|
||||
$trigger = New-ScheduledTaskTrigger -Daily -At "02:00"
|
||||
Register-ScheduledTask -TaskName "AZA Nightly Monitor" `
|
||||
-Action $action -Trigger $trigger -Description "AZA Nightly Monitoring"
|
||||
|
||||
=====================================================================
|
||||
7. DATENSCHUTZ-HINWEIS (DATA MINIMIZATION)
|
||||
=====================================================================
|
||||
|
||||
Das Monitoring erfasst KEINE:
|
||||
- Patientennamen oder -daten
|
||||
- Transkript-Inhalte oder Prompts
|
||||
- Passwoerter oder API-Keys
|
||||
- KI-Antworten
|
||||
|
||||
Es werden ausschliesslich Zaehler und Status-Informationen erhoben:
|
||||
- Anzahl Events pro Typ
|
||||
- PASS/FAIL Status
|
||||
- Dateigeroessen und Zeitstempel
|
||||
- Service-Version und Uptime
|
||||
|
||||
Health-Check-Responses enthalten nur technische Metadaten.
|
||||
|
||||
=====================================================================
|
||||
8. TEST-ERGEBNIS
|
||||
=====================================================================
|
||||
|
||||
Testskript: _test_monitoring.py
|
||||
27 Tests, 0 Fehler.
|
||||
|
||||
Tests:
|
||||
1. Metriken: Audit-Log Eintraege, Integritaet, Backup-Count
|
||||
2. Alerts: login_fail, ai_calls_total, ai_blocked, 2fa_fail erkannt
|
||||
3. Integritaets-Check: PASS bei intaktem Log
|
||||
4. Manipulation: FAIL bei manipuliertem Log, PASS nach Restore
|
||||
5. Nightly-Report: JSON-Struktur korrekt, Datei geschrieben
|
||||
6. Data Minimization: Keine Passwoerter/Keys/Transkripte
|
||||
7. Health-Check Format: _APP_VERSION und _START_TIME vorhanden
|
||||
|
||||
=====================================================================
|
||||
9. BETROFFENE DATEIEN
|
||||
=====================================================================
|
||||
|
||||
Geaendert:
|
||||
- backend_main.py (/health erweitert: version, uptime_s, tls)
|
||||
- transcribe_server.py (/health erweitert: version, uptime_s, tls)
|
||||
- todo_server.py (/health neu hinzugefuegt)
|
||||
|
||||
Neu:
|
||||
- aza_monitoring.py (Monitoring, Metriken, Integrity, CLI)
|
||||
- _test_monitoring.py (Proof-Skript)
|
||||
|
||||
Nicht geaendert:
|
||||
- basis14.py
|
||||
- aza_audit_log.py (unveraendert, wird nur gelesen)
|
||||
- aza_consent.py (unveraendert, wird nur gelesen)
|
||||
|
||||
=====================================================================
|
||||
10. RISIKEN
|
||||
=====================================================================
|
||||
|
||||
- Health-Checks sind unauthentifiziert.
|
||||
Risiko: Gering (nur Status-Info, keine sensiblen Daten).
|
||||
Massnahme: Bei Bedarf hinter API-Token schuetzen.
|
||||
|
||||
- Monitoring laeuft lokal, kein externes Alerting.
|
||||
Risiko: Alerts werden nur im JSON-Report gespeichert.
|
||||
Massnahme: Nightly-Report per Mail weiterleiten (Empfehlung).
|
||||
|
||||
- Kein Real-Time-Monitoring.
|
||||
Risiko: Zwischenzeitliche Ausfaelle werden erst beim naechsten
|
||||
Scheduled-Check erkannt.
|
||||
Massnahme: Health-Check-Intervall anpassen (z.B. alle 5 Min).
|
||||
|
||||
=====================================================================
|
||||
11. OFFENE PUNKTE
|
||||
=====================================================================
|
||||
|
||||
Keine.
|
||||
100
AzA march 2026/security/handovers/STEP_4_1_TLS.md
Normal file
100
AzA march 2026/security/handovers/STEP_4_1_TLS.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# STEP 4.1 – TLS-PFLICHT FÜR ALLE BACKEND-SERVICES
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
GAP #4: Alle Backend-Services von HTTP auf HTTPS/TLS umstellen.
|
||||
Fail-Start bei fehlendem Zertifikat im Produktionsmodus.
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| **aza_tls.py** | NEU – Zentrale TLS-Konfiguration |
|
||||
| **todo_server.py** | TLS-Integration via create_ssl_context() |
|
||||
| **transcribe_server.py** | TLS-Integration via uvicorn SSL-Kwargs |
|
||||
| **backend_main.py** | TLS-Integration via uvicorn SSL-Kwargs |
|
||||
|
||||
## TLS-Parameter
|
||||
|
||||
| Parameter | Wert |
|
||||
|-----------|------|
|
||||
| Minimale TLS-Version | 1.2 (konfigurierbar via ENV) |
|
||||
| Maximale TLS-Version | 1.3 |
|
||||
| Cipher Suites | ECDHE+AESGCM, ECDHE+CHACHA20, DHE+AESGCM, DHE+CHACHA20 |
|
||||
| Ausgeschlossen | aNULL, eNULL, MD5, DSS, RC4, 3DES |
|
||||
| Perfect Forward Secrecy | Ja (nur ECDHE/DHE Cipher erlaubt) |
|
||||
| DEV-Zertifikat | RSA 4096-bit, SHA-256, 365 Tage, Self-Signed |
|
||||
| PROD-Vorbereitung | Pfade über ENV-Variablen konfigurierbar (ACME/Let's Encrypt) |
|
||||
|
||||
## Umgebungsvariablen
|
||||
|
||||
| Variable | Default | Beschreibung |
|
||||
|----------|---------|--------------|
|
||||
| AZA_TLS_CERTFILE | (leer) | Pfad zum Zertifikat (PEM) |
|
||||
| AZA_TLS_KEYFILE | (leer) | Pfad zum Private Key (PEM) |
|
||||
| AZA_TLS_MIN_VERSION | "1.2" | Minimale TLS-Version ("1.2" oder "1.3") |
|
||||
| AZA_TLS_REQUIRE | "1" | "1" = Fail-Start ohne Zertifikat |
|
||||
|
||||
## Betroffene Server
|
||||
|
||||
| Server | Port | Typ | TLS-Methode |
|
||||
|--------|------|-----|-------------|
|
||||
| todo_server | 5111 | stdlib HTTPServer | ssl.wrap_socket() |
|
||||
| transcribe_server | 8090 | FastAPI/Uvicorn | uvicorn ssl_certfile/ssl_keyfile |
|
||||
| backend_main | 8000 | FastAPI/Uvicorn | uvicorn ssl_certfile/ssl_keyfile |
|
||||
| workforce_planner | variabel | FastAPI/Uvicorn | Nicht geändert (kein __main__-Block) |
|
||||
|
||||
## Testmethode
|
||||
|
||||
### DEV-Zertifikat erstellen:
|
||||
```
|
||||
python aza_tls.py
|
||||
```
|
||||
|
||||
### Server starten:
|
||||
```
|
||||
set AZA_TLS_CERTFILE=dev-cert.pem
|
||||
set AZA_TLS_KEYFILE=dev-key.pem
|
||||
python backend_main.py
|
||||
```
|
||||
|
||||
### Testen:
|
||||
```
|
||||
curl https://localhost:8000/health --insecure
|
||||
```
|
||||
|
||||
### Fail-Start testen (ohne Zertifikat):
|
||||
```
|
||||
set AZA_TLS_REQUIRE=1
|
||||
python backend_main.py
|
||||
→ Muss mit Fehlermeldung abbrechen
|
||||
```
|
||||
|
||||
### DEV-Modus ohne TLS:
|
||||
```
|
||||
set AZA_TLS_REQUIRE=0
|
||||
python backend_main.py
|
||||
→ Startet auf HTTP mit Warnung
|
||||
```
|
||||
|
||||
## Rest-Risiken
|
||||
|
||||
1. **workforce_planner**: Hat keinen eigenen __main__-Block. TLS muss beim
|
||||
Start via uvicorn CLI konfiguriert werden:
|
||||
`uvicorn workforce_planner.api.app:app --ssl-certfile=... --ssl-keyfile=...`
|
||||
|
||||
2. **Self-Signed Zertifikat**: Browser und Clients zeigen Warnungen.
|
||||
Für Produktion muss ein CA-signiertes Zertifikat verwendet werden.
|
||||
|
||||
3. **Client-Seite**: Die Clients (basis14.py, aza_diktat_mixin.py) müssen
|
||||
ihre Server-URLs von http:// auf https:// ändern – das ist ein separater
|
||||
Schritt.
|
||||
|
||||
4. **Zertifikatsrotation**: Kein automatischer Erneuerungsmechanismus
|
||||
implementiert. Für PROD wird ACME/certbot empfohlen.
|
||||
|
||||
5. **todo_server PWA**: Service Worker erfordert HTTPS für vollständige
|
||||
Funktionalität auf iPhones – dies ist jetzt möglich.
|
||||
157
AzA march 2026/security/handovers/STEP_4_1a_TLS_PROOF.md
Normal file
157
AzA march 2026/security/handovers/STEP_4_1a_TLS_PROOF.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# STEP 4.1a – TLS-ENFORCEMENT VERIFIKATION (BEWEIS)
|
||||
# Status: ABGESCHLOSSEN
|
||||
# Datum: 2026-02-22
|
||||
|
||||
---
|
||||
|
||||
## 1. Listener-Tabelle
|
||||
|
||||
| Server | Port | Host | Protokoll (mit TLS) | Protokoll (ohne TLS) |
|
||||
|--------|------|------|---------------------|----------------------|
|
||||
| backend_main | 8000 (ENV PORT) | 0.0.0.0 | HTTPS only | Fail-Start (AZA_TLS_REQUIRE=1) |
|
||||
| transcribe_server | 8090 (ENV TRANSCRIBE_PORT) | 0.0.0.0 | HTTPS only | Fail-Start (AZA_TLS_REQUIRE=1) |
|
||||
| todo_server | 5111 | 0.0.0.0 | HTTPS only | Fail-Start (AZA_TLS_REQUIRE=1) |
|
||||
| workforce_planner | variabel | variabel | Über uvicorn CLI | Kein eigener __main__ |
|
||||
|
||||
---
|
||||
|
||||
## 2. Test-Kommandos und Ergebnisse
|
||||
|
||||
### TEST 1: Fail-Start ohne Zertifikat (AZA_TLS_REQUIRE=1)
|
||||
|
||||
**Kommando:**
|
||||
```
|
||||
$env:AZA_TLS_CERTFILE = ""
|
||||
$env:AZA_TLS_KEYFILE = ""
|
||||
$env:AZA_TLS_REQUIRE = "1"
|
||||
$env:MEDWORK_API_TOKEN = "test123"
|
||||
python backend_main.py
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
```
|
||||
FEHLER: TLS ist erforderlich (AZA_TLS_REQUIRE=1), aber
|
||||
AZA_TLS_CERTFILE und/oder AZA_TLS_KEYFILE sind nicht gesetzt.
|
||||
```
|
||||
Exit Code: 1
|
||||
|
||||
**Bewertung: PASS** – Server startet nicht ohne Zertifikat.
|
||||
|
||||
---
|
||||
|
||||
### TEST 2: HTTPS-Verbindung
|
||||
|
||||
**Kommando:**
|
||||
```python
|
||||
ctx = ssl.create_default_context()
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
r = urllib.request.urlopen('https://127.0.0.1:8444/health', context=ctx)
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
```
|
||||
HTTPS: 200 {"ok": true}
|
||||
```
|
||||
|
||||
**Bewertung: PASS** – HTTPS funktioniert.
|
||||
|
||||
---
|
||||
|
||||
### TEST 3: HTTP-Verbindung (muss fehlschlagen)
|
||||
|
||||
**Kommando:**
|
||||
```python
|
||||
r = urllib.request.urlopen('http://127.0.0.1:8444/health', timeout=5)
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
```
|
||||
http.client.RemoteDisconnected: Remote end closed connection without response
|
||||
```
|
||||
Exit Code: 1
|
||||
|
||||
**Bewertung: PASS** – HTTP wird abgelehnt. Kein HTTP-Fallback.
|
||||
|
||||
---
|
||||
|
||||
### TEST 4: TLS-Version und Cipher
|
||||
|
||||
**Kommando:**
|
||||
```python
|
||||
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
with ctx.wrap_socket(socket.socket(), server_hostname='localhost') as s:
|
||||
s.connect(('127.0.0.1', 8444))
|
||||
print(s.version(), s.cipher())
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
```
|
||||
TLS Version: TLSv1.3
|
||||
Cipher: TLS_AES_256_GCM_SHA384
|
||||
Protocol: TLSv1.3
|
||||
Bits: 256
|
||||
```
|
||||
|
||||
**Bewertung: PASS** – TLS 1.3 mit AES-256-GCM, PFS aktiv.
|
||||
|
||||
---
|
||||
|
||||
### TEST 5: Zertifikat-Details
|
||||
|
||||
**Kommando:**
|
||||
```python
|
||||
cert = s.getpeercert(binary_form=True)
|
||||
c = x509.load_der_x509_certificate(cert)
|
||||
```
|
||||
|
||||
**Ergebnis:**
|
||||
```
|
||||
Subject: CN=localhost,O=AZA MedWork DEV
|
||||
Issuer: CN=localhost,O=AZA MedWork DEV
|
||||
Key: 4096 bit RSA
|
||||
Algo: SHA-256
|
||||
Valid: 2026-02-22 -> 2027-02-22
|
||||
```
|
||||
|
||||
**Bewertung: PASS** – RSA 4096-bit, SHA-256, Self-Signed (DEV).
|
||||
|
||||
---
|
||||
|
||||
### TEST 6: ENV-Hardcoding-Prüfung
|
||||
|
||||
**Kommando:** Grep nach "dev-cert.pem" / "dev-key.pem" in *.py
|
||||
|
||||
**Ergebnis:**
|
||||
Nur in `aza_tls.py` Zeilen 114-115 (Generierungsfunktion) und Docstring.
|
||||
NICHT in todo_server.py, transcribe_server.py, backend_main.py.
|
||||
Alle Server lesen Zertifikatspfade ausschliesslich aus ENV-Variablen.
|
||||
|
||||
**Bewertung: PASS** – Keine hardcodierten Zertifikatspfade.
|
||||
|
||||
---
|
||||
|
||||
## 3. Fix während Verifikation
|
||||
|
||||
| Datei | Zeile | Problem | Fix |
|
||||
|-------|-------|---------|-----|
|
||||
| aza_tls.py | 93-97 | `ssl_version: ssl.TLSVersion.TLSv1_2` inkompatibel mit uvicorn (ValueError: invalid protocol version 771) | Ersetzt durch `ssl_ciphers: _STRONG_CIPHERS` |
|
||||
|
||||
---
|
||||
|
||||
## 4. Gesamtbewertung
|
||||
|
||||
| Test | Ergebnis |
|
||||
|------|----------|
|
||||
| Fail-Start ohne Cert | **PASS** |
|
||||
| HTTPS-Verbindung | **PASS** |
|
||||
| HTTP-Ablehnung | **PASS** |
|
||||
| TLS >= 1.2 | **PASS** (TLS 1.3 verhandelt) |
|
||||
| Starke Cipher | **PASS** (AES-256-GCM) |
|
||||
| PFS | **PASS** (ECDHE/DHE only) |
|
||||
| Zertifikat RSA 4096 | **PASS** |
|
||||
| Keine hardcodierten Pfade | **PASS** |
|
||||
|
||||
**GESAMTERGEBNIS: PASS (8/8)**
|
||||
99
AzA march 2026/security/handovers/STEP_4_2_HASHING.md
Normal file
99
AzA march 2026/security/handovers/STEP_4_2_HASHING.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# STEP 4.2 – SICHERES PASSWORT-HASHING
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Ziel
|
||||
|
||||
GAP #18: SHA-256 ohne Salt durch moderne KDF ersetzen.
|
||||
Transparente Migration bestehender Hashes ohne Forced Reset.
|
||||
|
||||
## Alte vs. neue Methode
|
||||
|
||||
| Parameter | ALT (unsicher) | NEU (sicher) |
|
||||
|-----------|----------------|--------------|
|
||||
| Algorithmus | SHA-256 | bcrypt |
|
||||
| Salt | Keines | Automatisch (eingebettet) |
|
||||
| Cost-Faktor | n/a | 12 Runden |
|
||||
| Hash-Format | 64 Hex-Zeichen | $2b$12$... (60 Zeichen) |
|
||||
| Brute-Force-Resistenz | Minimal (GPU-optimiert) | ~250ms pro Versuch |
|
||||
| Rainbow-Table-Schutz | Keiner | Salt schützt |
|
||||
|
||||
## Parameter
|
||||
|
||||
```
|
||||
bcrypt.gensalt(rounds=12)
|
||||
```
|
||||
|
||||
- Cost: 12 (2^12 = 4096 Iterationen)
|
||||
- Salt: 16 Bytes, automatisch generiert
|
||||
- Output: 60-Zeichen-String mit eingebettetem Salt + Params
|
||||
- Format: $2b$12$<22-char-salt><31-char-hash>
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
| Datei | Zeile | Änderung |
|
||||
|-------|-------|----------|
|
||||
| basis14.py | 16-17 | `import bcrypt` hinzugefügt |
|
||||
| basis14.py | 1298-1300 | `_hash_password()`: SHA-256 → bcrypt |
|
||||
| basis14.py | 1302-1311 | NEU: `_verify_password()` mit Legacy-Erkennung |
|
||||
| basis14.py | 1313-1317 | NEU: `_is_legacy_hash()` |
|
||||
| basis14.py | 1355-1362 | Login: `_verify_password()` + automatischer Rehash |
|
||||
| basis14.py | 1459 | Registration: nutzt neuen `_hash_password()` (bcrypt) |
|
||||
| basis14.py | 1545 | Profil: `_verify_password()` für altes Passwort |
|
||||
| basis14.py | 1554 | Profil: `_hash_password()` für neues Passwort (bcrypt) |
|
||||
|
||||
## Migrationslogik
|
||||
|
||||
```
|
||||
Login-Versuch:
|
||||
1. _verify_password(pw, stored_hash) aufrufen
|
||||
2. Prüfe: beginnt stored_hash mit "$2b$" oder "$2a$"?
|
||||
JA → bcrypt.checkpw()
|
||||
NEIN → SHA-256 Legacy-Vergleich
|
||||
3. Bei erfolgreicher Anmeldung:
|
||||
_is_legacy_hash(stored_hash)?
|
||||
JA → Rehash mit bcrypt, Profil speichern
|
||||
NEIN → Nichts tun
|
||||
```
|
||||
|
||||
- Kein Forced Reset
|
||||
- Alter Hash wird beim nächsten Login automatisch ersetzt
|
||||
- Neuer Hash sofort bei Registration und Passwortänderung
|
||||
|
||||
## Legacy-Erkennung
|
||||
|
||||
Ein Hash ist Legacy (SHA-256) wenn:
|
||||
- Er NICHT mit "$2" beginnt (kein bcrypt-Prefix)
|
||||
- Er exakt 64 Zeichen lang ist (SHA-256 Hex-Output)
|
||||
|
||||
## Tests
|
||||
|
||||
| Test | Ergebnis |
|
||||
|------|----------|
|
||||
| Neuer bcrypt-Hash erstellen | PASS ($2b$12$...) |
|
||||
| bcrypt verify (korrektes PW) | PASS (True) |
|
||||
| bcrypt verify (falsches PW) | PASS (False) |
|
||||
| Legacy SHA-256 erkennen | PASS (is_legacy=True) |
|
||||
| bcrypt als nicht-Legacy erkennen | PASS (is_legacy=False) |
|
||||
| Legacy verify (korrektes PW) | PASS (True) |
|
||||
| Legacy verify (falsches PW) | PASS (False/rejected) |
|
||||
| Legacy rehash zu bcrypt | PASS (verify nach rehash True) |
|
||||
|
||||
## Nicht geändert
|
||||
|
||||
- workforce_planner/api/auth.py – nutzt bereits bcrypt (cost=default)
|
||||
- aza_persistence.py – importiert hashlib, nutzt es aber nicht für Passwörter
|
||||
- Backup-Kopien (basis14 - Kopie*.py) – bewusst nicht geändert
|
||||
|
||||
## Rest-Risiken
|
||||
|
||||
1. **bcrypt als Dependency**: Muss installiert sein (`pip install bcrypt`).
|
||||
Ist bereits Dependency von workforce_planner.
|
||||
|
||||
2. **Timing**: Legacy-SHA-256-Vergleich ist schneller als bcrypt.
|
||||
Ein Timing-Seitenkanalangriff könnte erkennen, ob ein Nutzer
|
||||
noch einen alten Hash hat. Risiko: minimal (lokale Desktop-App).
|
||||
|
||||
3. **workforce_planner cost**: Nutzt bcrypt mit Default-Rounds (10).
|
||||
Empfehlung: Auf 12 angleichen (separater Schritt).
|
||||
194
AzA march 2026/security/handovers/STEP_4_2a_HASHING_PROOF.md
Normal file
194
AzA march 2026/security/handovers/STEP_4_2a_HASHING_PROOF.md
Normal file
@@ -0,0 +1,194 @@
|
||||
# STEP 4.2a – HASH-MIGRATION PROOF (AUDIT-NACHWEIS)
|
||||
# Status: PASS
|
||||
|
||||
---
|
||||
|
||||
## Test-Setup
|
||||
|
||||
- **Datei:** `basis14.py` (Zeilen 1298–1316)
|
||||
- **Speicherort:** `kg_diktat_user_profile.json` (JSON, Feld `password_hash`)
|
||||
- **Testmethode:** Automatisiertes Python-Skript mit Backup/Restore des Original-Profils
|
||||
- **KDF:** bcrypt, cost=12, Salt eingebettet
|
||||
- **Legacy:** SHA-256, 64 Hex-Zeichen, kein Salt
|
||||
- **Testpasswörter:** `AuditTest2026!` (korrekt), `FalschesPasswort` (falsch)
|
||||
|
||||
---
|
||||
|
||||
## Test 1: Legacy-SHA-256-User anlegen
|
||||
|
||||
```
|
||||
Passwort: AuditTest2026!
|
||||
SHA-256 Hash: e99f0f4a91440e4a7d89ebd1db548df0eed586dd16e1b3c0e9bb6356220e0f3b
|
||||
Speicherort: kg_diktat_user_profile.json
|
||||
```
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---------|----------|
|
||||
| Profil gespeichert | PASS |
|
||||
| Hash ist Legacy (64 hex chars) | PASS |
|
||||
| Hash Länge = 64 | PASS |
|
||||
| Hash beginnt NICHT mit $2b$ | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Test 2: Login mit falschem Passwort
|
||||
|
||||
```
|
||||
Passwort: FalschesPasswort
|
||||
Ergebnis: REJECTED
|
||||
```
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---------|----------|
|
||||
| Falsches PW abgelehnt | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Test 3: Login mit korrektem Passwort + Rehash
|
||||
|
||||
### VORHER (Legacy SHA-256):
|
||||
```
|
||||
e99f0f4a91440e4a7d89ebd1db548df0eed586dd16e1b3c0e9bb6356220e0f3b
|
||||
```
|
||||
|
||||
### NACHHER (bcrypt):
|
||||
```
|
||||
$2b$12$mM1wNUuQJfYMZ23xJRfsJO/JVI0G4ucm9lT.BnTK/LVK8m/gw6CEK
|
||||
```
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---------|----------|
|
||||
| Korrektes PW akzeptiert | PASS |
|
||||
| Hash hat sich geändert | PASS |
|
||||
| Neuer Hash beginnt mit $2b$ | PASS |
|
||||
| Neuer Hash ist NICHT Legacy | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Test 4: Login nach Rehash (bcrypt-Hash)
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---------|----------|
|
||||
| Korrektes PW nach Rehash | PASS |
|
||||
| Falsches PW nach Rehash abgelehnt | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Test 5: Neue Registrierung → direkt bcrypt
|
||||
|
||||
```
|
||||
Neues Passwort: NeuerUser2026!
|
||||
bcrypt Hash: $2b$12$MdhU4H6CihlPMecPxRGgHuZ0rRJw.QXd1B0tqr4A8xK...
|
||||
```
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---------|----------|
|
||||
| Hash ist bcrypt | PASS |
|
||||
| Hash ist NICHT Legacy | PASS |
|
||||
| Verify korrekt | PASS |
|
||||
| Verify falsch abgelehnt | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Test 6: Passwortänderung → bcrypt
|
||||
|
||||
```
|
||||
Neuer Hash: $2b$12$62I3tJxk09DaPmmu.6SsEeTgSp6UJReaJtSzs57SFO6...
|
||||
```
|
||||
|
||||
| Prüfung | Ergebnis |
|
||||
|---------|----------|
|
||||
| Hash ist bcrypt | PASS |
|
||||
| Altes PW abgelehnt | PASS |
|
||||
| Neues PW akzeptiert | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Code-Verifikation
|
||||
|
||||
### _hash_password() – Zeile 1298–1301
|
||||
```python
|
||||
@staticmethod
|
||||
def _hash_password(pw: str) -> str:
|
||||
return bcrypt.hashpw(pw.encode("utf-8"), bcrypt.gensalt(rounds=12)).decode("utf-8")
|
||||
```
|
||||
→ Erzeugt bcrypt-Hash mit cost=12 und eingebettetem Salt.
|
||||
|
||||
### _verify_password() – Zeile 1303–1309
|
||||
```python
|
||||
@staticmethod
|
||||
def _verify_password(pw: str, stored_hash: str) -> bool:
|
||||
if stored_hash.startswith("$2b$") or stored_hash.startswith("$2a$"):
|
||||
return bcrypt.checkpw(pw.encode("utf-8"), stored_hash.encode("utf-8"))
|
||||
legacy = hashlib.sha256(pw.encode("utf-8")).hexdigest()
|
||||
return legacy == stored_hash
|
||||
```
|
||||
→ Erkennt bcrypt-Hashes am Prefix. Fällt auf SHA-256-Vergleich zurück.
|
||||
|
||||
### _is_legacy_hash() – Zeile 1311–1316
|
||||
```python
|
||||
@staticmethod
|
||||
def _is_legacy_hash(stored_hash: str) -> bool:
|
||||
if not stored_hash:
|
||||
return False
|
||||
return not stored_hash.startswith("$2") and len(stored_hash) == 64
|
||||
```
|
||||
→ Erkennt alte 64-Zeichen SHA-256-Hashes.
|
||||
|
||||
### Login mit Rehash – Zeile 1363–1367
|
||||
```python
|
||||
stored = self._user_profile.get("password_hash", "")
|
||||
if self._verify_password(pw, stored):
|
||||
if self._is_legacy_hash(stored):
|
||||
self._user_profile["password_hash"] = self._hash_password(pw)
|
||||
save_user_profile(self._user_profile)
|
||||
dlg.destroy()
|
||||
```
|
||||
→ Bei erfolgreichem Login: Legacy-Hash automatisch durch bcrypt ersetzt.
|
||||
|
||||
### Registrierung – Zeile 1465
|
||||
```python
|
||||
"password_hash": self._hash_password(pw),
|
||||
```
|
||||
→ Nutzt direkt bcrypt.
|
||||
|
||||
### Profiländerung (altes PW prüfen) – Zeile 1545
|
||||
```python
|
||||
if old_hash and not self._verify_password(pw_old_e.get(), old_hash):
|
||||
```
|
||||
→ Verifiziert mit _verify_password (bcrypt + Legacy-kompatibel).
|
||||
|
||||
### Profiländerung (neues PW setzen) – Zeile 1554
|
||||
```python
|
||||
pw_hash = self._hash_password(new_pw)
|
||||
```
|
||||
→ Nutzt direkt bcrypt.
|
||||
|
||||
---
|
||||
|
||||
## Gesamtergebnis
|
||||
|
||||
| Nr | Testfall | Ergebnis |
|
||||
|----|----------|----------|
|
||||
| 1 | Legacy-Hash anlegen & erkennen | PASS |
|
||||
| 2 | Falsches PW → abgelehnt | PASS |
|
||||
| 3 | Korrektes PW → akzeptiert + Rehash | PASS |
|
||||
| 4 | Login nach Rehash funktioniert | PASS |
|
||||
| 5 | Registrierung schreibt bcrypt | PASS |
|
||||
| 6 | Passwortänderung schreibt bcrypt | PASS |
|
||||
|
||||
**18/18 Einzelprüfungen: PASS**
|
||||
**0 Fixes notwendig**
|
||||
**Original-Profil wiederhergestellt**
|
||||
|
||||
---
|
||||
|
||||
## Bewertung: PASS
|
||||
|
||||
Alle Anforderungen erfüllt:
|
||||
- Legacy SHA-256 wird erkannt
|
||||
- Login mit korrektem/falschem PW korrekt
|
||||
- Rehash passiert automatisch beim Login
|
||||
- Nach Rehash ist der gespeicherte Hash bcrypt
|
||||
- Neue Registrierungen und Passwortänderungen verwenden direkt bcrypt
|
||||
- Kein Forced Reset notwendig
|
||||
162
AzA march 2026/security/handovers/STEP_4_3_SECRET_KEY.md
Normal file
162
AzA march 2026/security/handovers/STEP_4_3_SECRET_KEY.md
Normal file
@@ -0,0 +1,162 @@
|
||||
# STEP 4.3 – HARDCODED SECRET KEY ENTFERNT (LÜCKE #19)
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Fundstellen
|
||||
|
||||
| Datei | Zeile | Problem |
|
||||
|-------|-------|---------|
|
||||
| `workforce_planner/config.py` | 18 | `SECRET_KEY = os.getenv("WP_SECRET_KEY", "dev-secret-change-in-production")` – Hardcoded Fallback-Secret |
|
||||
| `workforce_planner/api/auth.py` | 35, 47 | Konsument: JWT encode/decode mit `SECRET_KEY` |
|
||||
|
||||
Keine weiteren hardcoded Secrets gefunden:
|
||||
- `basis14.py`: OPENAI_API_KEY kommt aus ENV, kein Fallback
|
||||
- `basis14.py`: MEDWORK_API_TOKEN kommt aus ENV oder Datei, kein hardcoded Wert
|
||||
- `aza_email_config.json`: Passwörter im Klartext (separates GAP, nicht in diesem Schritt)
|
||||
|
||||
---
|
||||
|
||||
## Neue ENV-Variablen
|
||||
|
||||
| Variable | Pflicht | Beschreibung |
|
||||
|----------|---------|-------------|
|
||||
| `AZA_SECRET_KEY` | Ja (ausser DEV) | JWT-Signing-Key, min. 32 Zeichen |
|
||||
| `AZA_ENV` | Nein | `dev` = erlaubt auto-generierten Key |
|
||||
|
||||
Alte Variable `WP_SECRET_KEY` wird **nicht mehr verwendet**.
|
||||
|
||||
---
|
||||
|
||||
## Implementierung
|
||||
|
||||
### workforce_planner/config.py (komplett neu)
|
||||
|
||||
**ALT:**
|
||||
```python
|
||||
SECRET_KEY = os.getenv("WP_SECRET_KEY", "dev-secret-change-in-production")
|
||||
```
|
||||
|
||||
**NEU:**
|
||||
```python
|
||||
def _load_secret_key() -> str:
|
||||
key = os.getenv("AZA_SECRET_KEY", "").strip()
|
||||
env_mode = os.getenv("AZA_ENV", "").strip().lower()
|
||||
|
||||
# Kein Key: DEV auto-gen oder Fail-Start
|
||||
if not key:
|
||||
if env_mode == "dev":
|
||||
return secrets.token_hex(64) # temporär, nur diese Session
|
||||
sys.exit(1) # Fehlermeldung + Exit
|
||||
|
||||
# Zu kurz (< 32 Zeichen): Fail-Start
|
||||
if len(key) < 32:
|
||||
sys.exit(1)
|
||||
|
||||
# Triviales Muster: Fail-Start
|
||||
for pattern in ("dev", "test", "password", "secret", ...):
|
||||
if key.startswith(pattern) und nicht-alphanumerisches Folgezeichen:
|
||||
sys.exit(1)
|
||||
|
||||
return key
|
||||
|
||||
SECRET_KEY = _load_secret_key()
|
||||
```
|
||||
|
||||
### workforce_planner/api/auth.py
|
||||
|
||||
Keine Änderung nötig – importiert weiterhin `SECRET_KEY` aus `config.py`.
|
||||
Die Variable hat denselben Namen, der Wert kommt jetzt validiert aus ENV.
|
||||
|
||||
---
|
||||
|
||||
## Fail-Start Verhalten
|
||||
|
||||
| Szenario | Ergebnis |
|
||||
|----------|----------|
|
||||
| Kein `AZA_SECRET_KEY`, kein `AZA_ENV=dev` | Exit 1 + Fehlermeldung |
|
||||
| `AZA_ENV=dev`, kein Key | OK (auto-gen, Warnung auf stderr) |
|
||||
| Key < 32 Zeichen | Exit 1 + "zu kurz" |
|
||||
| Key mit trivialem Muster ("dev-...", "test-...", "password...") | Exit 1 + "triviales Muster" |
|
||||
| Gültiger Key >= 32 Zeichen | OK |
|
||||
| Nur `WP_SECRET_KEY` gesetzt (alt) | Exit 1 (wird ignoriert) |
|
||||
|
||||
---
|
||||
|
||||
## Testkommandos + erwartete Outputs
|
||||
|
||||
### 1. Fail-Start (kein Key)
|
||||
```bash
|
||||
python -c "from workforce_planner.config import SECRET_KEY"
|
||||
# → Exit 1: FEHLER: AZA_SECRET_KEY ist nicht gesetzt.
|
||||
```
|
||||
|
||||
### 2. DEV-Modus (auto-gen)
|
||||
```bash
|
||||
AZA_ENV=dev python -c "from workforce_planner.config import SECRET_KEY; print(len(SECRET_KEY))"
|
||||
# → 128 (hex-encoded 64 Bytes) + Warnung auf stderr
|
||||
```
|
||||
|
||||
### 3. Zu kurzer Key
|
||||
```bash
|
||||
AZA_SECRET_KEY=short123 python -c "from workforce_planner.config import SECRET_KEY"
|
||||
# → Exit 1: FEHLER: AZA_SECRET_KEY ist zu kurz (8 Zeichen, Minimum: 32)
|
||||
```
|
||||
|
||||
### 4. Triviales Muster
|
||||
```bash
|
||||
AZA_SECRET_KEY="test-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" python -c "from workforce_planner.config import SECRET_KEY"
|
||||
# → Exit 1: FEHLER: AZA_SECRET_KEY enthält triviales Muster ('test')
|
||||
```
|
||||
|
||||
### 5. Gültiger Key
|
||||
```bash
|
||||
AZA_SECRET_KEY=$(python -c "import secrets; print(secrets.token_hex(64))") python -c "from workforce_planner.config import SECRET_KEY; print('OK:', len(SECRET_KEY), 'chars')"
|
||||
# → OK: 128 chars
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testergebnis (automatisiert)
|
||||
|
||||
```
|
||||
12/12 PASS, 0/12 FAIL
|
||||
GESAMTBEWERTUNG: PASS
|
||||
```
|
||||
|
||||
| Test | Ergebnis |
|
||||
|------|----------|
|
||||
| Fail-Start ohne Key | PASS |
|
||||
| Fehlermeldung enthält 'AZA_SECRET_KEY' | PASS |
|
||||
| DEV auto-gen Key | PASS |
|
||||
| Key geladen (128 chars) | PASS |
|
||||
| Warnung ausgegeben | PASS |
|
||||
| Fail-Start kurzer Key | PASS |
|
||||
| Fehlermeldung 'zu kurz' | PASS |
|
||||
| Fail-Start alter Fallback | PASS |
|
||||
| Fail-Start triviales Pattern (lang) | PASS |
|
||||
| Start mit gültigem Key | PASS |
|
||||
| Key geladen (64 chars) | PASS |
|
||||
| WP_SECRET_KEY allein -> Fail-Start | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Session/Token-Invalidierung
|
||||
|
||||
**Impact:** Bestehende JWT-Tokens (HS256), die mit dem alten Fallback-Key
|
||||
`"dev-secret-change-in-production"` signiert wurden, werden nach dem
|
||||
Setzen eines neuen `AZA_SECRET_KEY` **ungültig**.
|
||||
|
||||
- Betroffene Nutzer müssen sich **einmalig neu anmelden**.
|
||||
- Dies betrifft nur den `workforce_planner` (Arbeitsplan-Modul).
|
||||
- `basis14.py` verwendet keine JWT-Tokens (nur lokales Passwort-Hashing).
|
||||
- **Keine komplexe Rotation** implementiert (nicht im Scope dieses Schrittes).
|
||||
- Bei Produktivbetrieb: Key einmalig setzen und dauerhaft beibehalten.
|
||||
|
||||
---
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `workforce_planner/config.py` | Hardcoded Fallback entfernt, `_load_secret_key()` mit Validierung + Fail-Start implementiert |
|
||||
166
AzA march 2026/security/handovers/STEP_4_4_CREDENTIALS.md
Normal file
166
AzA march 2026/security/handovers/STEP_4_4_CREDENTIALS.md
Normal file
@@ -0,0 +1,166 @@
|
||||
# STEP 4.4 – KLARTEXT-CREDENTIALS ENTFERNT (LÜCKE #28)
|
||||
# Status: ABGESCHLOSSEN
|
||||
|
||||
---
|
||||
|
||||
## Fundstellen
|
||||
|
||||
| Datei | Problem |
|
||||
|-------|---------|
|
||||
| `aza_email_config.json` | Klartext-Passwort im `password`-Feld: `"password": "Asdfasdf22@@As96k324"` |
|
||||
|
||||
Keine weiteren Klartext-Credentials in JSON/YAML/INI-Dateien gefunden.
|
||||
|
||||
**Hinweis:** Das hier entfernte Passwort war ein echtes Produktivpasswort.
|
||||
Es sollte umgehend beim Mail-Provider geändert werden.
|
||||
|
||||
---
|
||||
|
||||
## Neue ENV-Variablen
|
||||
|
||||
| Variable | Beschreibung |
|
||||
|----------|-------------|
|
||||
| `AZA_EMAIL_PASSWORD_0` | Passwort für das erste E-Mail-Konto |
|
||||
| `AZA_EMAIL_PASSWORD_1` | Passwort für das zweite E-Mail-Konto (falls vorhanden) |
|
||||
| `AZA_EMAIL_PASSWORD_N` | Passwort für das N-te Konto (0-basiert) |
|
||||
|
||||
### Setzen unter Windows:
|
||||
```cmd
|
||||
set AZA_EMAIL_PASSWORD_0=MeinSicheresPasswort
|
||||
```
|
||||
|
||||
### Setzen unter Linux/Mac:
|
||||
```bash
|
||||
export AZA_EMAIL_PASSWORD_0="MeinSicheresPasswort"
|
||||
```
|
||||
|
||||
### Dauerhaft (Windows PowerShell):
|
||||
```powershell
|
||||
[Environment]::SetEnvironmentVariable("AZA_EMAIL_PASSWORD_0", "MeinPasswort", "User")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geänderte Dateien
|
||||
|
||||
| Datei | Änderung |
|
||||
|-------|----------|
|
||||
| `aza_email.py` | Neue Funktionen: `_get_account_password()`, `_strip_passwords()`, `_check_plaintext_migration()` |
|
||||
| `aza_email.py` | `load_email_config()`: Lädt Passwörter aus ENV, ignoriert Klartext in JSON, gibt Warnung |
|
||||
| `aza_email.py` | `save_email_config()`: Strippt `password` und `_password` vor dem Schreiben |
|
||||
| `aza_email.py` | Alle IMAP/SMTP-Stellen: `acc.get("password")` → `acc.get("_password")` |
|
||||
| `aza_email.py` | Account-Dialog: Passwort wird als `_password` (RAM-only) gespeichert |
|
||||
| `aza_email_config.json` | Klartext-Passwort entfernt |
|
||||
|
||||
---
|
||||
|
||||
## Architektur
|
||||
|
||||
### Passwort-Fluss (NEU):
|
||||
|
||||
```
|
||||
ENV (AZA_EMAIL_PASSWORD_0)
|
||||
│
|
||||
▼
|
||||
load_email_config() ──► acc["_password"] (nur RAM)
|
||||
│
|
||||
▼
|
||||
IMAP/SMTP Login ──► acc.get("_password", "")
|
||||
│
|
||||
▼
|
||||
save_email_config() ──► _strip_passwords() ──► JSON OHNE Passwort
|
||||
```
|
||||
|
||||
### Passwort-Fluss (GUI-Eingabe):
|
||||
|
||||
```
|
||||
Account-Dialog (Passwort-Feld)
|
||||
│
|
||||
▼
|
||||
save_account() ──► acc["_password"] = pw_input (nur RAM)
|
||||
│
|
||||
▼
|
||||
_save_config() ──► save_email_config() ──► _strip_passwords() ──► JSON OHNE Passwort
|
||||
```
|
||||
|
||||
Das `_password`-Feld existiert NUR im RAM. Es wird NIE in JSON geschrieben.
|
||||
Das `password`-Feld wird beim Laden aus JSON ignoriert und beim Speichern entfernt.
|
||||
|
||||
---
|
||||
|
||||
## Migrationsverhalten
|
||||
|
||||
Wenn `aza_email_config.json` noch ein `password`-Feld enthält:
|
||||
|
||||
1. **Warnung auf stderr:**
|
||||
```
|
||||
SICHERHEITSWARNUNG: Klartext-Passwoerter in aza_email_config.json gefunden!
|
||||
Betroffene Konten: andre.surovy@haut-winterthur.ch
|
||||
Bitte entfernen und stattdessen ENV-Variablen setzen:
|
||||
AZA_EMAIL_PASSWORD_0
|
||||
Die Passwoerter in der Datei werden IGNORIERT.
|
||||
```
|
||||
|
||||
2. **Klartext-Passwort wird NICHT übernommen** (kein Auto-Migrate)
|
||||
3. **Benutzer muss ENV-Variable setzen**, damit E-Mail funktioniert
|
||||
4. **Beim nächsten Speichern** wird das `password`-Feld automatisch entfernt
|
||||
|
||||
---
|
||||
|
||||
## Fail-Start Verhalten
|
||||
|
||||
| Szenario | Ergebnis |
|
||||
|----------|----------|
|
||||
| `AZA_EMAIL_PASSWORD_0` gesetzt | E-Mail funktioniert |
|
||||
| `AZA_EMAIL_PASSWORD_0` nicht gesetzt | E-Mail-Abruf zeigt: "Konto-Daten unvollständig" |
|
||||
| Klartext-PW in JSON | Warnung + wird ignoriert |
|
||||
| GUI-Passworteingabe | Funktioniert für Sitzung (nicht persistent) |
|
||||
|
||||
---
|
||||
|
||||
## Testergebnis
|
||||
|
||||
```
|
||||
12/12 PASS, 0/12 FAIL
|
||||
GESAMTBEWERTUNG: PASS
|
||||
```
|
||||
|
||||
| Test | Ergebnis |
|
||||
|------|----------|
|
||||
| Kein Passwort in JSON | PASS |
|
||||
| Warnung auf stderr bei Klartext | PASS |
|
||||
| Passwort NICHT in geladenen Daten | PASS |
|
||||
| ENV-PW in _password Feld | PASS |
|
||||
| Kein password Feld nach Laden | PASS |
|
||||
| Kein password in gespeicherter Datei | PASS |
|
||||
| Kein _password in gespeicherter Datei | PASS |
|
||||
| password entfernt (Konto 1) | PASS |
|
||||
| password entfernt (Konto 2) | PASS |
|
||||
| _password entfernt (nie auf Disk) | PASS |
|
||||
| Andere Felder erhalten | PASS |
|
||||
| Kein print mit password-Wert | PASS |
|
||||
|
||||
---
|
||||
|
||||
## Rest-Risiken
|
||||
|
||||
1. **Passwort im RAM:** Während der Sitzung liegt das Passwort im RAM
|
||||
(Python-String). Für eine Desktop-App akzeptabel.
|
||||
|
||||
2. **ENV-Variable sichtbar:** `AZA_EMAIL_PASSWORD_0` kann in Prozesslisten
|
||||
sichtbar sein. Empfehlung für Zukunft: OS-Keychain-Integration
|
||||
(Windows Credential Manager / macOS Keychain).
|
||||
|
||||
3. **Altes Passwort exponiert:** Das Passwort `Asdfasdf22@@As96k324` war
|
||||
in der JSON-Datei gespeichert. Es sollte beim Provider geändert werden.
|
||||
|
||||
4. **Backup-Dateien:** Die Kopie-Dateien (`aza_email - Kopie.py`, etc.)
|
||||
enthalten noch den alten Code. Diese sind Backups und nicht aktiv.
|
||||
|
||||
---
|
||||
|
||||
## Repo-Hygiene
|
||||
|
||||
- Kein Git-Repository vorhanden → kein `.gitignore`-Eintrag nötig
|
||||
- Kein History-Rewrite nötig
|
||||
- `aza_email_config.json` wurde direkt bereinigt (Passwort entfernt)
|
||||
216
AzA march 2026/security/handovers/STEP_6_2FA.md
Normal file
216
AzA march 2026/security/handovers/STEP_6_2FA.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# 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.
|
||||
59
AzA march 2026/security/handovers/STEP_8_LEGAL.md
Normal file
59
AzA march 2026/security/handovers/STEP_8_LEGAL.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# STEP 8 – Datenschutzerklärung & KI-Einwilligung
|
||||
|
||||
## Ziel
|
||||
Erstellung rechtlich belastbarer Texte für DSG (Schweiz), DSGVO (EU),
|
||||
medizinische Daten und KI-Verarbeitung.
|
||||
|
||||
## Erstellte Dateien
|
||||
|
||||
### /legal/privacy_policy.md
|
||||
Vollständige Datenschutzerklärung mit:
|
||||
- Verantwortlicher (Platzhalter für Praxisdaten)
|
||||
- 4 Verarbeitungszwecke (Dokumentation, Kommunikation, Organisation, KI)
|
||||
- Datenarten inkl. expliziter Nennung von Gesundheitsdaten (Art. 5 DSG)
|
||||
- Rechtsgrundlagen DSG + DSGVO
|
||||
- Speicherorte (lokal + Supabase + OpenAI API)
|
||||
- Auftragsverarbeiter: OpenAI (USA, SCCs), Supabase (EU-Hosting, USA-Sitz)
|
||||
- Drittlandübertragung dokumentiert
|
||||
- Betroffenenrechte DSG + DSGVO
|
||||
- Beschwerderecht (EDÖB + EU-Aufsicht)
|
||||
- Kontaktstelle
|
||||
|
||||
### /legal/ai_consent.md
|
||||
KI-Einwilligungserklärung mit:
|
||||
- Erklärung aller 4 KI-Einsatzgebiete (Transkription, KG, Interaktion, Textprüfung)
|
||||
- Konkrete Angabe der übermittelten Daten pro Funktion
|
||||
- Benennung des Anbieters (OpenAI) und der Modelle
|
||||
- Drittlandhinweis (USA, SCCs)
|
||||
- Klare Abgrenzung: Was KI NICHT entscheidet (keine Diagnosen, keine Therapie)
|
||||
- Ärztliche Verantwortung explizit festgehalten
|
||||
- Widerrufsrecht mit konkreten Folgen
|
||||
- Dokumentationspflicht
|
||||
- Unterschriftenfelder (Einwilligung + Widerruf)
|
||||
|
||||
## Betroffene Dateien
|
||||
| Datei | Aktion |
|
||||
|-------|--------|
|
||||
| /legal/privacy_policy.md | NEU erstellt |
|
||||
| /legal/ai_consent.md | NEU erstellt |
|
||||
|
||||
## Datengrundlage
|
||||
Alle Aussagen basieren auf dem tatsächlichen Code-Stand:
|
||||
- OpenAI API-Nutzung: Transkription (gpt-4o-mini-transcribe), Text (gpt-5.2/5-mini/5-nano)
|
||||
- Supabase Cloud-Sync (eu-central-1)
|
||||
- Lokale Datenspeicherung (JSON, SQLite)
|
||||
- Implementierte Sicherheitsmassnahmen aus STEP 4.x und 6
|
||||
|
||||
## Offene Punkte
|
||||
1. **Verantwortlicher:** Platzhalter – muss mit echten Praxisdaten ausgefüllt werden
|
||||
2. **Juristische Prüfung:** Beide Dokumente müssen vor Produktiveinsatz von
|
||||
einer rechtskundigen Person geprüft werden
|
||||
3. **AV-Vertrag OpenAI:** DPA/AVV mit OpenAI muss geprüft/abgeschlossen werden
|
||||
4. **AV-Vertrag Supabase:** DPA/AVV mit Supabase muss geprüft/abgeschlossen werden
|
||||
5. **Einwilligungsformular:** Muss in druckbare Form gebracht werden (PDF/Papier)
|
||||
6. **Kantonale Besonderheiten:** Aufbewahrungsfristen variieren je nach Kanton
|
||||
|
||||
## Risiken
|
||||
- Ohne juristische Prüfung sind die Texte nicht rechtsverbindlich
|
||||
- AV-Verträge mit OpenAI und Supabase fehlen noch (RED im Audit)
|
||||
- Drittlandübertragung USA ist datenschutzrechtlich sensibel
|
||||
233
AzA march 2026/security/handovers/STEP_9_BACKUP_RECOVERY.md
Normal file
233
AzA march 2026/security/handovers/STEP_9_BACKUP_RECOVERY.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# STEP 9 – Backup, Recovery & Löschkonzept
|
||||
|
||||
## Ziel
|
||||
Sicherstellung von Datenverlustminimierung, Wiederherstellbarkeit
|
||||
und Erfüllung der DSG-Löschpflichten.
|
||||
|
||||
---
|
||||
|
||||
## 1. Datenübersicht
|
||||
|
||||
### 1.1 Medizinische / Personenbezogene Daten
|
||||
|
||||
| Datei / Speicher | Format | Inhalt | Sensibilität |
|
||||
|---|---|---|---|
|
||||
| `kg_diktat_user_profile.json` | JSON | Arztname, Fachgebiet, Passwort-Hash, TOTP-Secret | HOCH |
|
||||
| `kg_diktat_ablage/KG/` | Dateien | Krankengeschichten | HOCH |
|
||||
| `kg_diktat_ablage/Briefe/` | Dateien | Arztbriefe | HOCH |
|
||||
| `kg_diktat_ablage/Rezepte/` | Dateien | Rezepte | HOCH |
|
||||
| `kg_diktat_ablage/Kostengutsprachen/` | Dateien | Kostengutsprachen | HOCH |
|
||||
| `kg_diktat_ablage/Diktat/` | Dateien | Diktattexte | HOCH |
|
||||
| `kg_diktat_notes.json` | JSON | Notizen (ev. Patientenbezug) | MITTEL |
|
||||
| `kg_diktat_todos.json` | JSON | Aufgaben (ev. Patientenbezug) | MITTEL |
|
||||
| `kg_diktat_todo_inbox.json` | JSON | Eingehende Aufgaben | MITTEL |
|
||||
| `kg_diktat_medwork_contacts.json` | JSON | Praxiskontakte | MITTEL |
|
||||
| `aza_email_contacts.json` | JSON | E-Mail-Kontakte | MITTEL |
|
||||
| `aza_medwork_messages.json` | JSON | Nachrichten | MITTEL |
|
||||
| `workforce_planner.db` | SQLite | Mitarbeiter, Abwesenheiten, Audit-Log | MITTEL |
|
||||
|
||||
### 1.2 Konfigurationsdaten (nicht personenbezogen)
|
||||
|
||||
| Datei | Inhalt |
|
||||
|---|---|
|
||||
| `kg_diktat_config.txt` | Grundeinstellungen |
|
||||
| `kg_diktat_signature.txt` | Arzt-Signatur |
|
||||
| `kg_diktat_korrekturen.json` | Auto-Korrekturen |
|
||||
| `kg_diktat_textbloecke.json` | Textbausteine |
|
||||
| `kg_diktat_autotext.json` | Autotext-Vorlagen |
|
||||
| `kg_diktat_soap_presets.json` | SOAP-Profile |
|
||||
| `kg_diktat_brief_presets.json` | Brief-Profile |
|
||||
| `aza_email_config.json` | E-Mail-Konfiguration (ohne Passwort) |
|
||||
| Diverse `*_window.txt` | Fensterpositionen / UI-State |
|
||||
|
||||
### 1.3 Cloud-Daten
|
||||
|
||||
| Dienst | Daten | Speicherort |
|
||||
|---|---|---|
|
||||
| Supabase | Synchronisierte Praxisdaten | AWS eu-central-1 |
|
||||
| OpenAI API | Keine persistente Speicherung (API-Modus, max. 30 Tage) | USA |
|
||||
|
||||
---
|
||||
|
||||
## 2. Backup-Konzept
|
||||
|
||||
### 2.1 Implementierung
|
||||
|
||||
Neues Skript: **`aza_backup.py`**
|
||||
|
||||
```
|
||||
python aza_backup.py backup # Backup erstellen
|
||||
python aza_backup.py list # Backups auflisten
|
||||
python aza_backup.py verify <f> # Integrität prüfen
|
||||
python aza_backup.py restore <f> # Wiederherstellen
|
||||
python aza_backup.py cleanup # Alte Backups entfernen
|
||||
```
|
||||
|
||||
### 2.2 Was wird gesichert
|
||||
|
||||
- Alle medizinischen JSON-Dateien (17 Dateien)
|
||||
- Alle Konfigurationsdateien (12 Dateien)
|
||||
- Alle UI-State-Dateien (11 Dateien)
|
||||
- Komplettes Ablage-Verzeichnis (KG, Briefe, Rezepte, etc.)
|
||||
- Lernmodus-Export
|
||||
- Workforce-Planner-Datenbank (SQLite)
|
||||
|
||||
### 2.3 Backup-Parameter
|
||||
|
||||
| Parameter | Wert | Konfiguration |
|
||||
|---|---|---|
|
||||
| Format | ZIP (Deflate, Level 9) | Fest |
|
||||
| Intervall | Täglich empfohlen | Manuell oder Cron/Task Scheduler |
|
||||
| Aufbewahrung | 90 Tage (Standard) | `AZA_BACKUP_KEEP_DAYS` |
|
||||
| Zielverzeichnis | `./backups/` | `AZA_BACKUP_DIR` |
|
||||
| Integrität | SHA-256 pro Datei + Manifest | Automatisch |
|
||||
| Benennung | `aza_backup_YYYY-MM-DD_HH-MM-SS.zip` | Automatisch |
|
||||
|
||||
### 2.4 Verschlüsselung
|
||||
|
||||
- **Transport:** ZIP-Dateien können auf verschlüsseltem Medium gespeichert werden
|
||||
- **Empfehlung:** Backup-Zielverzeichnis auf BitLocker-verschlüsseltem
|
||||
Laufwerk oder verschlüsseltem NAS ablegen
|
||||
- **ZIP-eigene Verschlüsselung:** Nicht implementiert (Python-zipfile
|
||||
unterstützt kein starkes AES; Laufwerksverschlüsselung wird empfohlen)
|
||||
|
||||
### 2.5 Offsite-Kopie
|
||||
|
||||
- Backup-Verzeichnis (`AZA_BACKUP_DIR`) kann auf externes Medium
|
||||
oder Netzlaufwerk zeigen
|
||||
- Empfohlen: Regelmässige Kopie auf externes verschlüsseltes Medium
|
||||
- Keine automatische Cloud-Sicherung implementiert
|
||||
|
||||
### 2.6 Automatisierung (Windows Task Scheduler)
|
||||
|
||||
```
|
||||
schtasks /create /tn "AZA_Backup" /tr "python C:\...\aza_backup.py backup" /sc daily /st 22:00
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Recovery-Konzept
|
||||
|
||||
### 3.1 Recovery Point Objective (RPO)
|
||||
|
||||
| Szenario | RPO |
|
||||
|---|---|
|
||||
| Tägliches Backup | Max. 24 Stunden Datenverlust |
|
||||
| Stündliches Backup (optional) | Max. 1 Stunde |
|
||||
|
||||
### 3.2 Recovery Time Objective (RTO)
|
||||
|
||||
| Szenario | RTO |
|
||||
|---|---|
|
||||
| Einzelne Datei | < 5 Minuten |
|
||||
| Komplettes System | < 30 Minuten |
|
||||
|
||||
### 3.3 Restore-Anleitung
|
||||
|
||||
#### Schritt 1: Verfügbare Backups anzeigen
|
||||
```
|
||||
python aza_backup.py list
|
||||
```
|
||||
|
||||
#### Schritt 2: Backup-Integrität prüfen
|
||||
```
|
||||
python aza_backup.py verify backups/aza_backup_YYYY-MM-DD_HH-MM-SS.zip
|
||||
```
|
||||
|
||||
#### Schritt 3: Dry-Run (was wird wiederhergestellt?)
|
||||
```
|
||||
python aza_backup.py restore backups/aza_backup_YYYY-MM-DD_HH-MM-SS.zip --dry-run
|
||||
```
|
||||
|
||||
#### Schritt 4: Tatsächliche Wiederherstellung
|
||||
```
|
||||
python aza_backup.py restore backups/aza_backup_YYYY-MM-DD_HH-MM-SS.zip
|
||||
```
|
||||
|
||||
**Sicherheitsmechanismus:** Vor dem Restore wird automatisch eine
|
||||
Pre-Restore-Sicherung der aktuellen Daten erstellt (in `backups/.pre_restore_*/`),
|
||||
sodass der Restore rückgängig gemacht werden kann.
|
||||
|
||||
### 3.4 Test-Ergebnisse (22.02.2026)
|
||||
|
||||
| Test | Ergebnis |
|
||||
|---|---|
|
||||
| Backup erstellen | PASS – 44 Dateien, 0.06 MB |
|
||||
| Backup verifizieren (SHA-256) | PASS – alle Checksummen korrekt |
|
||||
| Restore Dry-Run | PASS – 44 Dateien korrekt aufgelistet |
|
||||
| Backup auflisten | PASS – Datum, Grösse, Name korrekt |
|
||||
| Löschung Dry-Run | PASS – Warnung zu Backups korrekt |
|
||||
|
||||
---
|
||||
|
||||
## 4. Löschkonzept
|
||||
|
||||
### 4.1 Löschfristen
|
||||
|
||||
| Datenkategorie | Aufbewahrungsfrist | Rechtsgrundlage |
|
||||
|---|---|---|
|
||||
| Krankengeschichten | 10–20 Jahre (kantonal) | Kantonale Gesundheitsgesetze |
|
||||
| Arztbriefe | 10–20 Jahre | Kantonale Gesundheitsgesetze |
|
||||
| Rezepte | 10 Jahre | OR Art. 958f |
|
||||
| Mitarbeiterdaten | Anstellungsdauer + 5 Jahre | OR Art. 128 |
|
||||
| E-Mail-Kontakte | Bis Löschung durch Benutzer | DSG Art. 6 |
|
||||
| Backups | 90 Tage (konfigurierbar) | Intern |
|
||||
|
||||
### 4.2 Technische Umsetzung
|
||||
|
||||
#### Automatische Backup-Bereinigung
|
||||
```
|
||||
python aza_backup.py cleanup
|
||||
```
|
||||
Entfernt Backups älter als `AZA_BACKUP_KEEP_DAYS` (Standard: 90 Tage).
|
||||
|
||||
#### Patientendaten löschen (Recht auf Vergessenwerden)
|
||||
```
|
||||
# Dry-Run (nur prüfen, was gelöscht wird)
|
||||
python aza_backup.py delete-patient "Nachname Vorname"
|
||||
|
||||
# Tatsächliche Löschung
|
||||
python aza_backup.py delete-patient "Nachname Vorname" --execute
|
||||
```
|
||||
|
||||
### 4.3 Lösch-Workflow
|
||||
|
||||
1. **Antrag:** Patient stellt Löschanfrage (schriftlich empfohlen)
|
||||
2. **Prüfung:** Arzt prüft, ob gesetzliche Aufbewahrungspflicht besteht
|
||||
3. **Dry-Run:** `delete-patient --dry-run` zeigt betroffene Dateien
|
||||
4. **Dokumentation:** Löschanfrage und Ergebnis im Audit-Log festhalten
|
||||
5. **Ausführung:** `delete-patient --execute`
|
||||
6. **Backups:** Bestehende Backups enthalten noch Daten – nach Ablauf
|
||||
der Aufbewahrungsfrist (90 Tage) werden diese automatisch entfernt
|
||||
7. **Cloud:** Supabase-Daten manuell löschen (aktuell kein API-Endpunkt)
|
||||
|
||||
### 4.4 Einschränkungen
|
||||
|
||||
- Löschung in bestehenden Backups nur durch Warten auf Ablauf
|
||||
oder manuelles Löschen der betroffenen Backups
|
||||
- Supabase-Cloud-Daten müssen manuell gelöscht werden
|
||||
- OpenAI-API-Daten werden gemäss OpenAI nach max. 30 Tagen gelöscht
|
||||
- Namensbasierte Suche – funktioniert nur bei Dateinamen / JSON-Inhalt
|
||||
mit Patientenname
|
||||
|
||||
---
|
||||
|
||||
## 5. Geänderte / Neue Dateien
|
||||
|
||||
| Datei | Aktion |
|
||||
|---|---|
|
||||
| `aza_backup.py` | NEU – Backup, Verify, Restore, Cleanup, Löschung |
|
||||
| `backups/` | NEU – Backup-Verzeichnis (automatisch erstellt) |
|
||||
|
||||
---
|
||||
|
||||
## 6. Risiken
|
||||
|
||||
| Risiko | Bewertung | Massnahme |
|
||||
|---|---|---|
|
||||
| Kein automatischer Backup-Schedule | MITTEL | Task Scheduler einrichten |
|
||||
| ZIP nicht AES-verschlüsselt | MITTEL | Laufwerksverschlüsselung nutzen |
|
||||
| Keine Offsite-Kopie konfiguriert | HOCH | `AZA_BACKUP_DIR` auf NAS setzen |
|
||||
| Supabase-Löschung manuell | MITTEL | API-Endpunkt implementieren |
|
||||
| Namensbasierte Löschung unvollständig | NIEDRIG | Erweiterte Suche implementieren |
|
||||
| Kein inkrementelles Backup | NIEDRIG | Bei Datenwachstum nachrüsten |
|
||||
Reference in New Issue
Block a user