63 lines
1.7 KiB
Python
63 lines
1.7 KiB
Python
"""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)
|