"""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 _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 Exception: # argon2 raises on mismatch/garbage; treat all as "no match" 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)