From 8a04ca8a4341103875e142f386f05683bdb1c061 Mon Sep 17 00:00:00 2001 From: ale <ale@incal.net> Date: Sat, 22 Feb 2025 16:01:40 +0000 Subject: [PATCH] Support modern Argon2 hash formats Switch to argon2_cffi in order to use an API that allows both high-level and low-level (for legacy $a2$ hashes) uses. --- jupyterhub_authenticator/password.py | 38 ++++++++++++---------- jupyterhub_authenticator/test/conftest.py | 2 ++ jupyterhub_authenticator/test/test_auth.py | 10 ++++++ pyproject.toml | 4 +-- 4 files changed, 35 insertions(+), 19 deletions(-) diff --git a/jupyterhub_authenticator/password.py b/jupyterhub_authenticator/password.py index dc7017e..9b82a6e 100644 --- a/jupyterhub_authenticator/password.py +++ b/jupyterhub_authenticator/password.py @@ -1,6 +1,6 @@ +import argon2 import binascii import hmac -from pyargon2 import hash_bytes class PasswordError(Exception): @@ -8,21 +8,25 @@ class PasswordError(Exception): def checkpw(password, encrypted_password): - if not encrypted_password.startswith("$a2$"): - raise PasswordError('not an argon2i password') - parts = encrypted_password[4:].split('$', 4) - if len(parts) != 5: - raise PasswordError('password format error') + if encrypted_password.startswith("$a2$"): + parts = encrypted_password[4:].split('$', 4) + if len(parts) != 5: + raise PasswordError('password format error') - params = { - 'time_cost': int(parts[0]), - 'memory_cost': int(parts[1]), - 'parallelism': int(parts[2]), - 'salt': binascii.unhexlify(parts[3]), - 'variant': 'i', - 'encoding': 'hex', - } + params = { + 'time_cost': int(parts[0]), + 'memory_cost': int(parts[1]), + 'parallelism': int(parts[2]), + 'hash_len': 32, + 'salt': binascii.unhexlify(parts[3]), + 'type': argon2.low_level.Type.I, + } + return hmac.compare_digest( + argon2.low_level.hash_secret_raw(password.encode('utf-8'), **params), + binascii.unhexlify(parts[4])) - return hmac.compare_digest( - hash_bytes(password.encode('utf-8'), **params), - parts[4]) + try: + return argon2.PasswordHasher().verify(encrypted_password, password) + except argon2.exceptions.InvalidHashError: + # Rewrite exception to maintain API compatibility. + return False diff --git a/jupyterhub_authenticator/test/conftest.py b/jupyterhub_authenticator/test/conftest.py index d4521f2..f981fe3 100644 --- a/jupyterhub_authenticator/test/conftest.py +++ b/jupyterhub_authenticator/test/conftest.py @@ -8,6 +8,8 @@ from jupyterhub_authenticator import LabPopAIAuthenticator TEST_USERS = [ {"name": "testuser", "password": "$a2$3$32768$4$86eb596d2ea359f231344189bdd8b283$0850c1f9194dc2932890ec2aaab015d98405cf5ef72989d1b1455bdb4b4ffec6"}, + {"name": "testuser2", + "password": "$argon2id$v=19$m=102400,t=2,p=8$ReJ4KEpaZ+A4+LFt+j+w2Q$A124QWocPFU+C7ItKbuoNQ"}, ] diff --git a/jupyterhub_authenticator/test/test_auth.py b/jupyterhub_authenticator/test/test_auth.py index 6d635de..7e442fc 100644 --- a/jupyterhub_authenticator/test/test_auth.py +++ b/jupyterhub_authenticator/test/test_auth.py @@ -11,6 +11,16 @@ async def test_auth_ok(authenticator): assert result["name"] == "testuser" +@pytest.mark.asyncio +async def test_auth_ok_argon2id(authenticator): + result = await authenticator.authenticate(None, { + "username": "testuser2", + "password": "password", + }) + assert result + assert result["name"] == "testuser2" + + @pytest.mark.asyncio async def test_auth_fail_wrong_password(authenticator): result = await authenticator.authenticate(None, { diff --git a/pyproject.toml b/pyproject.toml index 8d2abcc..5ec29a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "jupyterhub_authenticator" -version = "0.1.0" +version = "0.1.1" authors = [ {name = "A/I", email = "info@autistici.org"} ] @@ -14,7 +14,7 @@ dependencies = [ "jupyterhub", "traitlets", "traitlets_paths", - "pyargon2", + "argon2_cffi", ] [project.optional-dependencies] -- GitLab