"""Argon2 Hashing Backend using argon2-cffi."""
from typing import TYPE_CHECKING, Any, Union
from advanced_alchemy.exceptions import MissingDependencyError
from advanced_alchemy.types.password_hash.base import HashingBackend
from advanced_alchemy.typing import ARGON2_INSTALLED
if TYPE_CHECKING:
from sqlalchemy import BinaryExpression, ColumnElement
__all__ = ("Argon2Hasher",)
[docs]
class Argon2Hasher(HashingBackend):
"""Hashing backend using Argon2 via the argon2-cffi library."""
[docs]
def __init__(self, **kwargs: Any) -> None:
"""Initialize Argon2Backend.
Args:
**kwargs: Optional keyword arguments to pass to the argon2.PasswordHasher constructor.
See argon2-cffi documentation for available parameters (e.g., time_cost,
memory_cost, parallelism, hash_len, salt_len, type).
Raises:
MissingDependencyError: If the ``argon2-cffi`` package is not installed.
"""
if not ARGON2_INSTALLED:
raise MissingDependencyError(package="argon2-cffi", install_package="argon2")
from argon2 import PasswordHasher
self.hasher = PasswordHasher(**kwargs)
[docs]
def hash(self, value: "Union[str, bytes]") -> str:
"""Hash the password using Argon2.
Args:
value: The plain text password (will be encoded to UTF-8 if string).
Returns:
The Argon2 hash string.
"""
return self.hasher.hash(self._ensure_bytes(value))
[docs]
def verify(self, plain: Any, hashed: str) -> bool:
"""Verify a plain text password against an Argon2 hash.
Args:
plain: The plain text password (will be encoded to UTF-8 if string). Any other
type returns ``False`` rather than raising.
hashed: The Argon2 hash string to verify against.
Returns:
True if the password matches the hash, False otherwise.
"""
from argon2.exceptions import InvalidHash, VerificationError, VerifyMismatchError
if not isinstance(plain, (str, bytes)):
return False
try:
self.hasher.verify(hashed, self._ensure_bytes(plain))
except (VerifyMismatchError, InvalidHash, VerificationError):
return False
return True
[docs]
def needs_rehash(self, hashed: str) -> bool:
"""Return True if the hash was produced with weaker parameters than the current config.
Args:
hashed: The stored Argon2 hash string.
Returns:
True if the hash should be regenerated; False for an unparsable or foreign hash.
"""
try:
return self.hasher.check_needs_rehash(hashed)
except Exception: # noqa: BLE001
return False
[docs]
def compare_expression(self, column: "ColumnElement[str]", plain: "Union[str, bytes]") -> "BinaryExpression[bool]":
"""Direct SQL comparison is not supported for Argon2.
Raises:
NotImplementedError: Always raised.
"""
msg = "Argon2Hasher does not support direct SQL comparison."
raise NotImplementedError(msg)