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") # A real bcrypt hash of a value nobody can present, used only to spend the time a # verification would have. Computed once at import — generating it per call would # cost a gensalt+hash on top of the checkpw and make the "no such user" path SLOWER # than the real one, which is the same oracle pointing the other way. _DUMMY_HASH = bcrypt.hashpw(secrets.token_bytes(32), bcrypt.gensalt()) def dummy_verify(password: str) -> None: """Burn one password verification against a throwaway hash. For the sign-in path when the email has no account: it makes "no such user" cost what "wrong password" costs, so response time stops answering the question of which emails are registered here. """ bcrypt.checkpw(password.encode("utf-8")[:_MAX_BCRYPT_BYTES], _DUMMY_HASH) 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()