Milestone 3

This commit is contained in:
Knacky
2026-05-11 06:05:27 +02:00
commit 4c25e198fc
125 changed files with 13489 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
"""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)