From 30f1f45d686641d5c49ee8bda3b1e3d2d39209a6 Mon Sep 17 00:00:00 2001 From: Joachim Hummel Date: Thu, 13 Nov 2025 13:16:23 +0000 Subject: [PATCH] feat: implement Argon2 algorithm --- algorithms.py | 1 + argon2_algorithm.py | 54 ++++++++++++++++++++++++++++++++++++++++ tests/test_algorithms.py | 14 +++++++++++ 3 files changed, 69 insertions(+) create mode 100644 argon2_algorithm.py diff --git a/algorithms.py b/algorithms.py index e5451af..c8f4b27 100644 --- a/algorithms.py +++ b/algorithms.py @@ -42,3 +42,4 @@ def list_algorithms() -> list[str]: # Import to auto-register algorithms import pbkdf2_algorithm # noqa: E402, F401 +import argon2_algorithm # noqa: E402, F401 diff --git a/argon2_algorithm.py b/argon2_algorithm.py new file mode 100644 index 0000000..50c770b --- /dev/null +++ b/argon2_algorithm.py @@ -0,0 +1,54 @@ +"""Argon2 algorithm implementation.""" + +from __future__ import annotations + +import base64 +import binascii + +from argon2 import PasswordHasher +from argon2.exceptions import VerifyMismatchError, InvalidHashError + + +class Argon2Algorithm: + """Argon2id password hashing algorithm.""" + + identifier = "argon2" + + def __init__(self): + """Initialize with default Argon2 parameters.""" + self._hasher = PasswordHasher() + + def hash(self, password: str, **kwargs) -> tuple[str, str]: + """Hash a password using Argon2id. + + Note: Argon2 generates its own salt internally and returns + a complete hash string that includes the salt and parameters. + We return empty string for salt_b64 and the full hash as hash_b64. + """ + hash_string = self._hasher.hash(password) + # Return empty salt since Argon2 embeds salt in hash + return ("", base64.b64encode(hash_string.encode("utf-8")).decode("utf-8")) + + def verify( + self, + password: str, + salt_b64: str, + hash_b64: str, + **kwargs, + ) -> bool: + """Verify a password against Argon2 hash. + + Note: salt_b64 is ignored since Argon2 embeds salt in the hash. + """ + try: + hash_string = base64.b64decode(hash_b64, validate=True).decode("utf-8") + self._hasher.verify(hash_string, password) + return True + except (VerifyMismatchError, InvalidHashError, binascii.Error, ValueError, UnicodeDecodeError): + return False + + +from algorithms import register_algorithm + +# Auto-register when module is imported +register_algorithm(Argon2Algorithm()) diff --git a/tests/test_algorithms.py b/tests/test_algorithms.py index 59057c8..e8c5363 100644 --- a/tests/test_algorithms.py +++ b/tests/test_algorithms.py @@ -38,3 +38,17 @@ def test_pbkdf2_algorithm_respects_iterations(): salt2, hash2 = algo.hash("test", iterations=200000) # Different iterations should produce different hashes even with same password assert hash1 != hash2 + + +def test_argon2_algorithm_hash_round_trip(): + """Verify Argon2 algorithm can hash and verify passwords.""" + algo = get_algorithm("argon2") + salt, hashed = algo.hash("test password") + assert algo.verify("test password", salt, hashed) + assert not algo.verify("wrong password", salt, hashed) + + +def test_argon2_algorithm_identifier(): + """Verify Argon2 algorithm has correct identifier.""" + algo = get_algorithm("argon2") + assert algo.identifier == "argon2"