diff options
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py')
| -rw-r--r-- | packages/meshbay-hub/src/meshbay_hub/auth.py | 37 |
1 files changed, 37 insertions, 0 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py index c5ea34d..58e2310 100644 --- a/packages/meshbay-hub/src/meshbay_hub/auth.py +++ b/packages/meshbay-hub/src/meshbay_hub/auth.py @@ -7,11 +7,13 @@ MeshBay Hub — authentication helpers. - Hub keypair: loaded from PEM file on startup """ +import asyncio import base64 import hashlib import os import time import uuid +from concurrent.futures import ThreadPoolExecutor from pathlib import Path import blake3 @@ -139,6 +141,41 @@ def verify_password(password: str, pw_hash: bytes, salt: bytes, version: int = 2 return False +# ── Argon2 off the event loop, one at a time ───────────────────────────────── +# +# One derivation is 256 MB and a quarter to half a second of CPU. Called from an +# async handler it stops the whole hub for that long — every request, every node +# socket, every offer relayed — once per sign-in, passphrase change, reset and +# registration. +# +# It cannot simply go to a thread pool: **two concurrent derivations with +# `lanes=4` deadlock inside OpenSSL** and never return, at no CPU (measured +# 2026-09-14 on cryptography 50.0.x / OpenSSL 4.0.x, here and on meshbay.org; +# `lanes=1` does not, and `lanes` is part of every stored hash, so it is not +# ours to change). Inline on the loop they could never overlap, which is the only +# reason this never hung in production. +# +# So: a dedicated executor with exactly one worker. The loop is free while a +# derivation runs, and derivations still never overlap — including when the +# request that queued one is cancelled mid-way, which a semaphore around +# `to_thread` would get wrong (the permit is released while the thread is still +# deriving, and the next one starts beside it). It also bounds Argon2's memory +# to one derivation, whatever the number of callers. +_argon2_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="argon2") + + +async def hash_password_off_loop(password: str) -> tuple[bytes, bytes]: + loop = asyncio.get_running_loop() + return await loop.run_in_executor(_argon2_executor, hash_password, password) + + +async def verify_password_off_loop(password: str, pw_hash: bytes, salt: bytes, + version: int = 2) -> bool: + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + _argon2_executor, verify_password, password, pw_hash, salt, version) + + def pw_needs_rehash(version: int) -> bool: return version < _ARGON2_CURRENT_VERSION |