import hashlib import hmac import secrets from itsdangerous import BadSignature, URLSafeTimedSerializer from app import config _serializer = URLSafeTimedSerializer(config.SECRET_KEY, salt="gw-session") _PBKDF2_ITERATIONS = 600_000 # ---- password hashing (PBKDF2-SHA256) ---- def hash_password(password: str) -> str: salt = secrets.token_hex(16) digest = hashlib.pbkdf2_hmac( "sha256", password.encode(), bytes.fromhex(salt), _PBKDF2_ITERATIONS ).hex() return f"pbkdf2${_PBKDF2_ITERATIONS}${salt}${digest}" def verify_password(password: str, stored: str) -> bool: try: _, iterations, salt, digest = stored.split("$") computed = hashlib.pbkdf2_hmac( "sha256", password.encode(), bytes.fromhex(salt), int(iterations) ).hex() return hmac.compare_digest(computed, digest) except (ValueError, TypeError): return False # ---- API keys ---- def generate_api_key() -> tuple[str, str, str]: """Returns (plain_key, prefix, key_hash). Plain key is shown once.""" plain = "gw_" + secrets.token_urlsafe(32) return plain, plain[:11], hash_api_key(plain) def hash_api_key(plain: str) -> str: return hashlib.sha256(plain.encode()).hexdigest() # ---- session cookies ---- def create_session_token(user_id: int) -> str: return _serializer.dumps({"uid": user_id}) def read_session_token(token: str) -> int | None: try: data = _serializer.loads(token, max_age=config.SESSION_MAX_AGE) return int(data["uid"]) except (BadSignature, KeyError, ValueError, TypeError): return None