from __future__ import annotations import base64 import hashlib import hmac import json import os import time from typing import Any def _b64encode(value: bytes) -> str: return base64.urlsafe_b64encode(value).rstrip(b"=").decode("ascii") def _b64decode(value: str) -> bytes: return base64.urlsafe_b64decode(value + ("=" * (-len(value) % 4))) def hash_password(password: str) -> str: salt = os.urandom(16) digest = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, 240_000) return f"{_b64encode(salt)}${_b64encode(digest)}" def verify_password(password: str, password_hash: str) -> bool: try: salt_value, digest_value = password_hash.split("$", 1) except ValueError: return False digest = hashlib.pbkdf2_hmac( "sha256", password.encode(), _b64decode(salt_value), 240_000, ) return hmac.compare_digest(digest, _b64decode(digest_value)) def create_token(payload: dict[str, Any], *, secret: str) -> str: now = int(time.time()) body = {**payload, "iat": now, "exp": now + 60 * 60 * 24 * 7} header = {"alg": "HS256", "typ": "JWT"} signing_input = ".".join( [ _b64encode(json.dumps(header, separators=(",", ":")).encode()), _b64encode(json.dumps(body, separators=(",", ":")).encode()), ], ) signature = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest() return f"{signing_input}.{_b64encode(signature)}" def decode_token(token: str, *, secret: str) -> dict[str, Any] | None: try: header, payload, signature = token.split(".", 2) signing_input = f"{header}.{payload}" expected = hmac.new(secret.encode(), signing_input.encode(), hashlib.sha256).digest() if not hmac.compare_digest(expected, _b64decode(signature)): return None data = json.loads(_b64decode(payload)) except (ValueError, json.JSONDecodeError): return None if int(data.get("exp", 0)) < int(time.time()): return None return data