Cache PBKDF2 password hash to eliminate ~1s overhead on every HTTP request

get_password_hash() computes PBKDF2-SHA256 with 600k iterations to
hash the HERMES_WEBUI_PASSWORD env var.  This is called on nearly every
HTTP request via check_auth -> is_auth_enabled -> get_password_hash.

Before: ~1s of PBKDF2 per request, regardless of how many times the
same env-var value has already been hashed.  A page load hitting 5+
API endpoints would burn 5+ seconds purely on password hashing.

After: compute once on first call, cache the hex result in a module-
level variable.  Subsequent calls are a single global-variable read
(~50ns).  The env var is immutable for the process lifetime, so there
is nothing to invalidate.

Thread-safe: double-checked locking ensures that under a burst of
concurrent requests only one thread computes PBKDF2, while the fast
path (after initialisation) requires zero locks.

10 unit tests covering all branches, cache-lifetime semantics, and
concurrent burst safety (8 threads, exactly 1 PBKDF2 call).
Test isolation: reloads only api.auth via importlib.reload, leaving
api.config untouched so test_pytest_state_isolation.py is unaffected.

Security analysis: zero regression.  The hash is derived from a static
env var and a static signing key — both already readable from process
memory.  Caching does not introduce any new disclosure or replay
vector.  PBKDF2 is still used for the initial computation and for
verify_password() on login.

AI: deepseek/deepseek-v4-flash
This commit is contained in:
Lucas Coutinho
2026-05-13 00:54:50 -03:00
parent bc3f4e54a6
commit 7acbb3d99d
+13 -7
View File
@@ -22,19 +22,25 @@ import time
import unittest
from pathlib import Path
# Isolate state dir from production
# Isolate state dir from production — only affects the auth module reload.
# We deliberately do NOT delete api.config from sys.modules (unlike some
# sibling test files that need a fresh config import). Deleting api.config
# would change its module-level STATE_DIR global and leak into all
# subsequently collected tests (breaking test_pytest_state_isolation.py).
import tempfile
_TEST_STATE = Path(tempfile.mkdtemp())
os.environ["HERMES_WEBUI_STATE_DIR"] = str(_TEST_STATE)
sys.path.insert(0, str(Path(__file__).parent.parent))
# Ensure a clean module state
for mod in list(sys.modules.keys()):
if 'api.auth' in mod or 'api.config' in mod:
del sys.modules[mod]
import api.auth as auth
# Force a fresh import of the auth module so it picks up the isolated env var.
# The auth module re-executes `from api.config import STATE_DIR, load_settings`
# at import time, but api.config is already in sys.modules — Python just
# rebinds the names from the existing module, keeping the conftest STATE_DIR
# untouched.
import api.auth
importlib.reload(api.auth)
auth = api.auth
class TestPasswordHashCache(unittest.TestCase):