"""Password hashing for the client portal — argon2id via argon2-cffi. Kept separate from portal_auth (cookie signing) so the future operator auth can reuse the same hasher. Never store or log raw passwords.""" import secrets from argon2 import PasswordHasher from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHashError _ph = PasswordHasher() def hash_password(raw: str) -> str: """Return an argon2id hash string for a raw password.""" return _ph.hash(raw) def verify_password(raw: str, hashed: str) -> bool: """True iff raw matches the stored hash. Never raises.""" try: return _ph.verify(hashed, raw) except (VerifyMismatchError, VerificationError, InvalidHashError, Exception): return False def generate_password(n_bytes: int = 12) -> str: """A strong, URL-safe shareable password (~16 chars for n_bytes=12).""" return secrets.token_urlsafe(n_bytes)