aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/auth.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/src/meshbay_hub/auth.py')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/auth.py31
1 files changed, 23 insertions, 8 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/auth.py b/packages/meshbay-hub/src/meshbay_hub/auth.py
index 58e2310..7045197 100644
--- a/packages/meshbay-hub/src/meshbay_hub/auth.py
+++ b/packages/meshbay-hub/src/meshbay_hub/auth.py
@@ -31,10 +31,17 @@ _ARGON2_KEY_LEN = 32
_ARGON2_VERSIONS = {
1: {"iterations": 3, "memory_cost": 65536}, # 64 MB — initial
- 2: {"iterations": 3, "memory_cost": 262144}, # 256 MB — production target
+ 2: {"iterations": 3, "memory_cost": 262144}, # 256 MB — raw password (legacy)
3: {"iterations": 3, "memory_cost": 262144}, # 256 MB — auth_key input (password split)
+ # 64 MiB, t=3, p=4 — RFC 9106's second recommended setting. What this hashes
+ # is already PBKDF2-SHA512 at 600 000 iterations of the passphrase, done by
+ # the client, and online guessing is bounded by the sign-in lockout, so the
+ # memory above this bought a constant factor against an offline attacker
+ # with the database, at four times the cost of every sign-in. Versions 3 and
+ # above are the auth_key scheme; a v3 hash is rewritten at its next sign-in.
+ 4: {"iterations": 3, "memory_cost": 65536},
}
-_ARGON2_CURRENT_VERSION = 3
+_ARGON2_CURRENT_VERSION = 4
# Module-level hub keypair (loaded once at startup)
_hub_sk_pem: bytes | None = None
@@ -112,10 +119,16 @@ def hub_id() -> str:
# ── Password ──────────────────────────────────────────────────────────────────
-def hash_password(password: str) -> tuple[bytes, bytes]:
- """Hash a password with Argon2id (current version). Returns (hash, salt)."""
+def hash_password(password: str, version: int = _ARGON2_CURRENT_VERSION) -> tuple[bytes, bytes]:
+ """Hash with the parameters of `version`, which is what the caller stores.
+
+ The version is an argument because the stored `pw_version` is what
+ verification reads the parameters from: hashing at one version's parameters
+ and recording another makes an account nobody can sign in to. That mistake
+ sat unseen while versions 2 and 3 shared their parameters.
+ """
salt = os.urandom(16)
- params = _ARGON2_VERSIONS[_ARGON2_CURRENT_VERSION]
+ params = _ARGON2_VERSIONS[version]
pw_hash = Argon2id(
salt=salt,
length=_ARGON2_KEY_LEN,
@@ -143,7 +156,8 @@ def verify_password(password: str, pw_hash: bytes, salt: bytes, version: int = 2
# ── 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
+# One derivation is 64 MiB and ~0.1 s on meshbay.org (0.45 s for an old 256 MB
+# hash, until its account next signs in). 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.
@@ -164,9 +178,10 @@ def verify_password(password: str, pw_hash: bytes, salt: bytes, version: int = 2
_argon2_executor = ThreadPoolExecutor(max_workers=1, thread_name_prefix="argon2")
-async def hash_password_off_loop(password: str) -> tuple[bytes, bytes]:
+async def hash_password_off_loop(password: str,
+ version: int = _ARGON2_CURRENT_VERSION) -> tuple[bytes, bytes]:
loop = asyncio.get_running_loop()
- return await loop.run_in_executor(_argon2_executor, hash_password, password)
+ return await loop.run_in_executor(_argon2_executor, hash_password, password, version)
async def verify_password_off_loop(password: str, pw_hash: bytes, salt: bytes,