from __future__ import annotations from base64 import urlsafe_b64encode from datetime import datetime, timezone import hashlib import secrets from cryptography.fernet import Fernet, InvalidToken import pyotp def utc_now() -> datetime: return datetime.now(timezone.utc) def _derive_fernet_key(source: str) -> bytes: digest = hashlib.sha256(source.encode("utf-8")).digest() return urlsafe_b64encode(digest) def build_fernet(secret_source: str) -> Fernet: return Fernet(_derive_fernet_key(secret_source)) def encrypt_secret(fernet: Fernet, value: str) -> str: return fernet.encrypt(value.encode("utf-8")).decode("utf-8") def decrypt_secret(fernet: Fernet, value: str | None) -> str | None: if not value: return None try: return fernet.decrypt(value.encode("utf-8")).decode("utf-8") except InvalidToken: return None def generate_numeric_code(length: int = 6) -> str: if length <= 0: raise ValueError("length must be positive") lower = 10 ** (length - 1) upper = (10**length) - 1 return str(secrets.randbelow(upper - lower + 1) + lower) def hash_token(token: str) -> str: return hashlib.sha256(token.encode("utf-8")).hexdigest() def generate_reset_token() -> str: return secrets.token_urlsafe(48) def normalize_otp_code(code: str) -> str: return "".join(ch for ch in code.strip() if ch.isdigit()) def generate_totp_secret() -> str: return pyotp.random_base32() def build_totp_uri(*, secret: str, account_name: str, issuer: str = "Stundenfuchs") -> str: return pyotp.TOTP(secret).provisioning_uri(name=account_name, issuer_name=issuer) def verify_totp_code(*, secret: str, code: str) -> bool: normalized = normalize_otp_code(code) if len(normalized) != 6: return False return bool(pyotp.TOTP(secret).verify(normalized, valid_window=1))