from __future__ import annotations import hashlib import secrets import bcrypt # bcrypt hashes at most 72 bytes and bcrypt>=4 raises on longer input, so we # truncate defensively — long passphrases stay valid instead of erroring. _MAX_BCRYPT_BYTES = 72 def hash_password(password: str) -> str: return bcrypt.hashpw(password.encode("utf-8")[:_MAX_BCRYPT_BYTES], bcrypt.gensalt()).decode("utf-8") def verify_password(password: str, password_hash: str) -> bool: try: return bcrypt.checkpw(password.encode("utf-8")[:_MAX_BCRYPT_BYTES], password_hash.encode("utf-8")) except (ValueError, TypeError): return False def generate_token() -> str: """A high-entropy opaque device (bearer) token, URL-safe so it pastes cleanly.""" return secrets.token_urlsafe(32) def hash_token(token: str) -> str: """One-way hash for device-token LOOKUP. A device token is already high-entropy random, so a plain SHA-256 is enough (no slow KDF like passwords need) — which keeps per-request bearer auth cheap. Only this hash is stored server-side.""" return hashlib.sha256(token.encode("utf-8")).hexdigest()