Spaces:
Sleeping
Sleeping
| """Password hashing and signed session cookies, using only the stdlib. | |
| Avoiding bcrypt/passlib keeps the dependency surface minimal and guarantees the | |
| app runs anywhere Python does. Passwords are hashed with PBKDF2-HMAC-SHA256 | |
| (310k iterations) and a per-password salt; sessions are stateless, signed cookies | |
| (itsdangerous-style) so no server-side session store is needed. | |
| """ | |
| from __future__ import annotations | |
| import base64 | |
| import hashlib | |
| import hmac | |
| import json | |
| import os | |
| import time | |
| from typing import Optional | |
| from app.config import get_settings | |
| _PBKDF2_ROUNDS = 310_000 | |
| _ALGO = "pbkdf2_sha256" | |
| # -- password hashing -------------------------------------------------------- | |
| def hash_password(password: str) -> str: | |
| salt = os.urandom(16) | |
| dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, _PBKDF2_ROUNDS) | |
| return f"{_ALGO}${_PBKDF2_ROUNDS}${salt.hex()}${dk.hex()}" | |
| def verify_password(password: str, stored: str) -> bool: | |
| try: | |
| algo, rounds_s, salt_hex, hash_hex = stored.split("$") | |
| if algo != _ALGO: | |
| return False | |
| rounds = int(rounds_s) | |
| salt = bytes.fromhex(salt_hex) | |
| expected = bytes.fromhex(hash_hex) | |
| except (ValueError, AttributeError): | |
| return False | |
| dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, rounds) | |
| return hmac.compare_digest(dk, expected) | |
| # -- signed session tokens --------------------------------------------------- | |
| def _b64e(raw: bytes) -> str: | |
| return base64.urlsafe_b64encode(raw).rstrip(b"=").decode("ascii") | |
| def _b64d(s: str) -> bytes: | |
| pad = "=" * (-len(s) % 4) | |
| return base64.urlsafe_b64decode(s + pad) | |
| def _sign(payload_b64: str) -> str: | |
| key = get_settings().secret_key.encode("utf-8") | |
| sig = hmac.new(key, payload_b64.encode("ascii"), hashlib.sha256).digest() | |
| return _b64e(sig) | |
| def make_session_token(user_id: int) -> str: | |
| payload = {"uid": user_id, "iat": int(time.time())} | |
| payload_b64 = _b64e(json.dumps(payload, separators=(",", ":")).encode("utf-8")) | |
| return f"{payload_b64}.{_sign(payload_b64)}" | |
| def read_session_token(token: Optional[str]) -> Optional[int]: | |
| """Return the user id if the token is valid and unexpired, else None.""" | |
| if not token or "." not in token: | |
| return None | |
| payload_b64, sig = token.rsplit(".", 1) | |
| if not hmac.compare_digest(sig, _sign(payload_b64)): | |
| return None | |
| try: | |
| payload = json.loads(_b64d(payload_b64)) | |
| except (ValueError, json.JSONDecodeError): | |
| return None | |
| iat = payload.get("iat", 0) | |
| if time.time() - iat > get_settings().session_max_age: | |
| return None | |
| uid = payload.get("uid") | |
| return int(uid) if isinstance(uid, int) else None | |