summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_argon2_off_loop.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-14 03:01:58 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-17 12:43:09 +0200
commit0370d001a4e74d2af0809a3e1eb5ada699b1bca2 (patch)
tree4eca51060cc94c04856c59765886523b70583b7c /packages/meshbay-hub/tests/test_argon2_off_loop.py
parent9d1d4a7af5844f50f168ff00818382ffe57c3452 (diff)
downloadmeshbay-0370d001a4e74d2af0809a3e1eb5ada699b1bca2.tar.gz
fix(hub): the password verifier is Argon2id 64 MiB, and a hash's version names its parameters
`pw_version` 4: Argon2id 64 MiB, t=3, lanes=4 — RFC 9106's second recommended setting. A v3 hash (256 MB) still verifies at its own parameters and is rewritten at the new ones on the next sign-in, through the rehash path that already existed. Why not more. The verifier matters against an offline attacker holding the database; online guessing is bounded by the sign-in lockout. That attacker pays the client's 600 000 PBKDF2-SHA512 iterations and the hub's Argon2id per guess, since `auth_key` is 256 bits and cannot be searched directly. Memory above 64 MiB multiplies that cost by a constant — at most 16 at 256 MB, less with PBKDF2 counted — while the hub pays the same memory at every sign-in, one derivation at a time. Measured on meshbay.org: 450 ms at 256 MB, 105 ms at 64 MiB, so a burst of sign-ins clears about four times faster. Changing the current version exposed a latent lockout. `hash_password` always used the current version's parameters, while the raw-password scheme recorded `pw_version = 2` — harmless while versions 2 and 3 shared their parameters, and with version 4 every legacy registration and v1→v2 rehash would have stored a 64 MiB hash labelled 256 MB, which nothing could then verify. Seventeen tests caught it. `hash_password` now takes the version it is hashing for. The OpenSSL deadlock between two concurrent `lanes=4` derivations is the same at 64 MiB, so Argon2 stays on its single worker. The loop-stall test measures against a v3 hash, because half of a 45 ms inline derivation is too close to scheduling noise to be a reliable bound. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LcF3QKWii7uQ2kSyXErzCt
Diffstat (limited to 'packages/meshbay-hub/tests/test_argon2_off_loop.py')
-rw-r--r--packages/meshbay-hub/tests/test_argon2_off_loop.py20
1 files changed, 12 insertions, 8 deletions
diff --git a/packages/meshbay-hub/tests/test_argon2_off_loop.py b/packages/meshbay-hub/tests/test_argon2_off_loop.py
index b156ebc..5c25a8e 100644
--- a/packages/meshbay-hub/tests/test_argon2_off_loop.py
+++ b/packages/meshbay-hub/tests/test_argon2_off_loop.py
@@ -1,10 +1,11 @@
"""
Argon2 runs off the event loop, and never two at a time.
-One derivation is 256 MB and a quarter to half a second of CPU. On the loop it
-stopped the whole hub for that long at every sign-in. In a thread pool it would
-have been worse: two concurrent `lanes=4` derivations deadlock inside OpenSSL
-and never return (`auth._argon2_executor`). These pin both halves.
+A derivation is 64 MiB and ~0.1 s at the current version, 256 MB and ~0.45 s
+for a v3 hash not yet rewritten. On the loop it stopped the whole hub for that
+long at every sign-in. In a thread pool it would have been worse: two concurrent
+`lanes=4` derivations deadlock inside OpenSSL and never return, at 64 MiB as at
+256 MB (`auth._argon2_executor`). These pin both halves.
"""
import asyncio
@@ -51,9 +52,13 @@ async def test_concurrent_derivations_all_return():
@pytest.mark.asyncio
async def test_the_loop_keeps_turning_while_argon2_runs():
- pw_hash, salt = auth.hash_password("k" * 44)
+ # A v3 (256 MB) hash, the heaviest the hub still verifies: at 64 MiB an
+ # inline derivation is ~45 ms and half of it is too close to scheduling
+ # noise to tell a stalled loop from a busy machine.
+ version = 3
+ pw_hash, salt = auth.hash_password("k" * 44, version)
started = time.perf_counter()
- auth.verify_password("k" * 44, pw_hash, salt, auth.current_pw_version())
+ auth.verify_password("k" * 44, pw_hash, salt, version)
inline = time.perf_counter() - started
gaps, done = [], asyncio.Event()
@@ -68,8 +73,7 @@ async def test_the_loop_keeps_turning_while_argon2_runs():
task = asyncio.create_task(ticker())
await asyncio.sleep(0.02)
- assert await auth.verify_password_off_loop(
- "k" * 44, pw_hash, salt, auth.current_pw_version())
+ assert await auth.verify_password_off_loop("k" * 44, pw_hash, salt, version)
done.set()
await task
# Inline, the loop stalls for the whole derivation; off it, for scheduling noise.