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