53 lines
1.7 KiB
Python
53 lines
1.7 KiB
Python
"""
|
|
TOTP-Enrollment und -Verifikation (RFC 6238) inkl. Recovery-Codes.
|
|
|
|
Pflicht-2FA: siehe Konzept 4.5 / 6.2. Das TOTP-Secret wird mit einem eigenen
|
|
AAD-Kontext ("totp") verschluesselt gespeichert -- Schluesseltrennung vom
|
|
SSH-Key-Material ist ueber den associated_data-Parameter realisiert (beide
|
|
nutzen zwar denselben KEK, sind aber durch AAD kontextgebunden und nicht
|
|
gegeneinander austauschbar).
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import secrets
|
|
|
|
import pyotp
|
|
|
|
from app.security.crypto import decrypt_secret, encrypt_secret
|
|
|
|
_TOTP_AAD = b"totp_secret"
|
|
|
|
|
|
def generate_totp_secret() -> str:
|
|
return pyotp.random_base32()
|
|
|
|
|
|
def encrypt_totp_secret(secret: str) -> bytes:
|
|
return encrypt_secret(secret.encode(), associated_data=_TOTP_AAD)
|
|
|
|
|
|
def decrypt_totp_secret(blob: bytes) -> str:
|
|
return decrypt_secret(blob, associated_data=_TOTP_AAD).decode()
|
|
|
|
|
|
def provisioning_uri(secret: str, username: str, issuer: str = "Jumphost") -> str:
|
|
return pyotp.TOTP(secret).provisioning_uri(name=username, issuer_name=issuer)
|
|
|
|
|
|
def verify_totp_code(secret: str, code: str) -> bool:
|
|
"""Verifiziert mit +-1 Zeitfenster Toleranz gegen Clock-Drift."""
|
|
totp = pyotp.TOTP(secret)
|
|
return totp.verify(code, valid_window=1)
|
|
|
|
|
|
def generate_recovery_codes(count: int = 10) -> list[str]:
|
|
"""Erzeugt Einmal-Recovery-Codes im Klartext (nur zur einmaligen Anzeige)."""
|
|
return [secrets.token_hex(5) for _ in range(count)]
|
|
|
|
|
|
def hash_recovery_code(code: str) -> str:
|
|
# Recovery-Codes sind hochentropisch (40 Bit hex) -- ein schneller,
|
|
# gesalzener Hash reicht hier aus; dennoch SHA-256 mit Pfeffer aus KEK-Kontext.
|
|
return hashlib.sha256(code.encode() + b"recovery_code_pepper").hexdigest()
|