feat: argon2 password hashing helpers for the portal
This commit is contained in:
@@ -0,0 +1,27 @@
|
|||||||
|
"""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)
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import pytest
|
||||||
|
from backend.auth_passwords import hash_password, verify_password, generate_password
|
||||||
|
|
||||||
|
|
||||||
|
def test_hash_is_not_plaintext_and_verifies():
|
||||||
|
h = hash_password("hunter2")
|
||||||
|
assert h != "hunter2"
|
||||||
|
assert h.startswith("$argon2")
|
||||||
|
assert verify_password("hunter2", h) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_rejects_wrong_password():
|
||||||
|
h = hash_password("hunter2")
|
||||||
|
assert verify_password("nope", h) is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_verify_is_safe_on_garbage_hash():
|
||||||
|
assert verify_password("anything", "not-a-real-hash") is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_generated_password_is_strong_and_unique():
|
||||||
|
a, b = generate_password(), generate_password()
|
||||||
|
assert a != b
|
||||||
|
assert len(a) >= 12
|
||||||
Reference in New Issue
Block a user