ai-interview-coach / app /security.py
LaelaZ's picture
Deploy InterviewCoach to HF Spaces (Docker)
473a23b verified
"""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