Files
ssh-jumphost/app/security/totp.py
2026-08-19 22:33:19 +02:00

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()