06bb1c1664
FZA Einzelstunden:
- Absence.fza_hours (Numeric 5,2) — FZA in Stunden statt Tagen
- Migration 0032: fza_hours Spalte in absences
- AbsenceCreate/AbsenceOut Schema um fza_hours erweitert
- absence_service: _deduct/_refund_overtime nutzt fza_hours direkt wenn gesetzt
- Frontend: Tage/Stunden-Toggle im FZA-Antrag-Modal
Security K-1: Privilege Escalation via PATCH /users/{id}.role
- user_service: Whitelist für Rollenänderungen, SUPER_ADMIN nur durch SUPER_ADMIN
- Letzter COMPANY_ADMIN gegen Selbst-Demotion gesichert
Security K-2: Kiosk-IP-Whitelist hinter nginx
- kiosk_security: _get_client_ip() liest X-Real-IP statt request.client.host
Security K-3: Kiosk-PIN Brute-Force-Schutz
- kiosk_auth_service: Redis-Lockout nach 5 Fehlversuchen (15 min)
Security K-4: TOTP-Setup-Hijacking
- auth router: /totp/setup abgelehnt wenn TOTP bereits aktiv
Security K-5: Separater Fernet-Key
- config: SECRET_KEY_DATA Feld (optional, Fallback auf SECRET_KEY)
- crypto: get_fernet_key() mit Warning bei fehlendem SECRET_KEY_DATA
Security H-2: Vacation Balance nur HR/Admin
- absences router: PATCH /balance nur noch HR/COMPANY_ADMIN/SUPER_ADMIN + AuditLog
Security H-3: Rate-Limits auf /auth/refresh + /auth/logout
- auth router: 30/min auf refresh, 60/min auf logout
Security H-4: Login-Failure-Logging + Lockout
- auth_service: Redis-Counter, Lockout nach 10 Versuchen (15 min)
- AuditLog für login_success und login_failed
Security M-1: Nginx Security-Header
- nginx.conf: X-Frame-Options, X-Content-Type-Options, CSP, Referrer-Policy, X-XSS-Protection, Permissions-Policy
Security M-3: AuditLog bei Rollenänderungen
- user_service: action=role_changed mit old/new role
Security M-6: create_all nur in Development
- main.py: Base.metadata.create_all nur wenn not settings.is_production
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
69 lines
2.2 KiB
Python
69 lines
2.2 KiB
Python
"""
|
||
Zentrale Krypto-Hilfsfunktionen für TimeMaster.
|
||
|
||
Verwendet Fernet-Verschlüsselung (AES-128-CBC + HMAC-SHA256).
|
||
Der Schlüssel wird per SHA-256 abgeleitet aus:
|
||
- SECRET_KEY_DATA (empfohlen, separater Key für Datenverschlüsselung)
|
||
- SECRET_KEY (Fallback wenn SECRET_KEY_DATA nicht gesetzt – Warnung beim Start)
|
||
|
||
Verwendung:
|
||
from app.core.crypto import encrypt_value, decrypt_value
|
||
|
||
stored = encrypt_value("geheimes-passwort")
|
||
plain = decrypt_value(stored)
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import base64
|
||
import hashlib
|
||
import logging
|
||
|
||
from cryptography.fernet import Fernet, InvalidToken
|
||
|
||
from app.core.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_fernet_key() -> bytes:
|
||
"""Gibt den Fernet-Key zurück.
|
||
|
||
Bevorzugt SECRET_KEY_DATA (separater Datenschlüssel).
|
||
Fällt auf SECRET_KEY zurück wenn SECRET_KEY_DATA nicht gesetzt ist,
|
||
und gibt dabei eine Warnung aus (JWT- und Datenschlüssel identisch).
|
||
|
||
Der Key wird per SHA-256 auf 32 Bytes normiert und dann base64url-kodiert.
|
||
"""
|
||
if settings.secret_key_data:
|
||
key_material = settings.secret_key_data
|
||
else:
|
||
logger.warning(
|
||
"SECRET_KEY_DATA nicht gesetzt — JWT-Key wird auch für Datenverschlüsselung "
|
||
"verwendet. Bitte SECRET_KEY_DATA in .env setzen für verbesserte Sicherheit."
|
||
)
|
||
key_material = settings.secret_key
|
||
|
||
key_bytes = hashlib.sha256(key_material.encode()).digest()
|
||
return base64.urlsafe_b64encode(key_bytes)
|
||
|
||
|
||
def _fernet() -> Fernet:
|
||
"""Erstellt eine Fernet-Instanz aus dem konfigurierten Datenschlüssel."""
|
||
return Fernet(get_fernet_key())
|
||
|
||
|
||
def encrypt_value(plain: str) -> str:
|
||
"""Verschlüsselt einen Klartext-String per Fernet. Gibt den chiffrierten String zurück."""
|
||
return _fernet().encrypt(plain.encode()).decode()
|
||
|
||
|
||
def decrypt_value(encrypted: str) -> str:
|
||
"""
|
||
Entschlüsselt einen Fernet-verschlüsselten String.
|
||
Wirft ValueError bei ungültigem Token oder falschem Schlüssel.
|
||
"""
|
||
try:
|
||
return _fernet().decrypt(encrypted.encode()).decode()
|
||
except InvalidToken as exc:
|
||
raise ValueError("Entschlüsselung fehlgeschlagen – ungültiger Token oder falscher Schlüssel.") from exc
|