"""Password hashing and constant-time secret hashing.""" from __future__ import annotations import hashlib import hmac import secrets from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError # Argon2id with moderate cost. `time_cost=2`, `memory_cost=64MiB`, `parallelism=2` # is well above OWASP minimums while staying snappy on a Debian small VM. _hasher = PasswordHasher( time_cost=2, memory_cost=64 * 1024, parallelism=2, hash_len=32, salt_len=16, ) def hash_password(plaintext: str) -> str: return _hasher.hash(plaintext) def verify_password(stored_hash: str, plaintext: str) -> bool: """Constant-time verification. Returns False on mismatch, never raises.""" try: return _hasher.verify(stored_hash, plaintext) except VerifyMismatchError: return False except Exception: # corrupted hash or unsupported parameters return False def needs_rehash(stored_hash: str) -> bool: """True when Argon2 parameters have evolved since the hash was created.""" try: return _hasher.check_needs_rehash(stored_hash) except Exception: return True # === Opaque-token helpers (refresh tokens, invitation tokens) === # # We never store the raw token in DB — only its SHA-256. Comparison uses # `hmac.compare_digest` to dodge timing attacks. Tokens are URL-safe base64. TOKEN_BYTES = 48 # 384 bits of entropy → 64 chars b64url def generate_opaque_token() -> str: return secrets.token_urlsafe(TOKEN_BYTES) def hash_opaque_token(token: str) -> str: return hashlib.sha256(token.encode("utf-8")).hexdigest() def verify_opaque_token(token: str, stored_hash: str) -> bool: return hmac.compare_digest(hash_opaque_token(token), stored_hash)