Switch auth password hashing to Argon2

This commit is contained in:
tliu93 committed 2026-04-20 15:26:36 +02:00
1 parent e1aad408ab
commit 3f7c9e43d9
7 files changed
+34 -54

No files matched your search

+11 -45
View File
@@ -1,12 +1,13 @@
from __future__ import annotations
import base64
import hashlib
import logging
import secrets
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from argon2 import PasswordHasher
from argon2.exceptions import InvalidHashError, VerificationError, VerifyMismatchError
from sqlalchemy import Select, select
from sqlalchemy.orm import Session
@@ -14,11 +15,7 @@ from app.config import Settings
from app.models.auth import AuthSession, AuthUser
logger = logging.getLogger(__name__)
SCRYPT_N = 2**14
SCRYPT_R = 8
SCRYPT_P = 1
SCRYPT_DKLEN = 64
password_hasher = PasswordHasher()
class AuthBootstrapError(RuntimeError):
@@ -62,52 +59,17 @@ def initialize_auth_schema(session: Session, settings: Settings) -> None:
def hash_password(password: str) -> str:
salt = secrets.token_bytes(16)
derived_key = hashlib.scrypt(
password.encode("utf-8"),
salt=salt,
n=SCRYPT_N,
r=SCRYPT_R,
p=SCRYPT_P,
dklen=SCRYPT_DKLEN,
)
return "$".join(
[
"scrypt",
str(SCRYPT_N),
str(SCRYPT_R),
str(SCRYPT_P),
base64.b64encode(salt).decode("ascii"),
base64.b64encode(derived_key).decode("ascii"),
]
)
return password_hasher.hash(password)
def verify_password(password: str, stored_hash: str) -> bool:
try:
algorithm, n, r, p, encoded_salt, encoded_key = stored_hash.split("$")
except ValueError:
return password_hasher.verify(stored_hash, password)
except VerifyMismatchError:
return False
if algorithm != "scrypt":
except (InvalidHashError, VerificationError):
return False
try:
salt = base64.b64decode(encoded_salt.encode("ascii"))
expected_key = base64.b64decode(encoded_key.encode("ascii"))
derived_key = hashlib.scrypt(
password.encode("utf-8"),
salt=salt,
n=int(n),
r=int(r),
p=int(p),
dklen=len(expected_key),
)
except (ValueError, TypeError):
return False
return secrets.compare_digest(derived_key, expected_key)
def authenticate_user(session: Session, *, username: str, password: str) -> AuthUser | None:
user = session.scalar(select(AuthUser).where(AuthUser.username == username).limit(1))
@@ -156,6 +118,10 @@ def get_authenticated_session(session: Session, *, raw_token: str | None) -> Aut
now = _utc_now()
expires_at = _as_utc(auth_session.expires_at)
revoked_at = _as_utc(auth_session.revoked_at)
if expires_at is None:
logger.warning("Auth session %s has no expires_at; treating it as invalid", auth_session.id)
return None
if revoked_at is not None or expires_at <= now or not user.is_active:
if revoked_at is None and expires_at <= now:
auth_session.revoked_at = now