| |
|
|
| from __future__ import annotations |
|
|
| import os |
|
|
| from typing import ClassVar, Literal |
|
|
| from ._utils import ( |
| Parameters, |
| _check_types, |
| extract_parameters, |
| validate_params_for_platform, |
| ) |
| from .exceptions import InvalidHashError |
| from .low_level import Type, hash_secret, verify_secret |
| from .profiles import get_default_parameters |
|
|
|
|
| default_params = get_default_parameters() |
|
|
| DEFAULT_RANDOM_SALT_LENGTH = default_params.salt_len |
| DEFAULT_HASH_LENGTH = default_params.hash_len |
| DEFAULT_TIME_COST = default_params.time_cost |
| DEFAULT_MEMORY_COST = default_params.memory_cost |
| DEFAULT_PARALLELISM = default_params.parallelism |
|
|
|
|
| def _ensure_bytes(s: bytes | str, encoding: str) -> bytes: |
| """ |
| Ensure *s* is a bytes string. Encode using *encoding* if it isn't. |
| """ |
| if isinstance(s, bytes): |
| return s |
| return s.encode(encoding) |
|
|
|
|
| class PasswordHasher: |
| r""" |
| High level class to hash passwords with sensible defaults. |
| |
| Uses Argon2\ **id** by default and uses a random salt_ for hashing. But it |
| can verify any type of Argon2 as long as the hash is correctly encoded. |
| |
| The reason for this being a class is both for convenience to carry |
| parameters and to verify the parameters only *once*. Any unnecessary |
| slowdown when hashing is a tangible advantage for a brute-force attacker. |
| |
| Args: |
| time_cost: |
| Defines the amount of computation realized and therefore the |
| execution time, given in number of iterations. |
| |
| memory_cost: Defines the memory usage, given in kibibytes_. |
| |
| parallelism: |
| Defines the number of parallel threads (*changes* the resulting |
| hash value). |
| |
| hash_len: Length of the hash in bytes. |
| |
| salt_len: Length of random salt to be generated for each password. |
| |
| encoding: |
| The Argon2 C library expects bytes. So if :meth:`hash` or |
| :meth:`verify` are passed a ``str``, it will be encoded using this |
| encoding. |
| |
| type: |
| Argon2 type to use. Only change for interoperability with legacy |
| systems. |
| |
| .. versionadded:: 16.0.0 |
| .. versionchanged:: 18.2.0 |
| Switch from Argon2i to Argon2id based on the recommendation by the |
| current RFC draft. See also :doc:`parameters`. |
| .. versionchanged:: 18.2.0 |
| Changed default *memory_cost* to 100 MiB and default *parallelism* to 8. |
| .. versionchanged:: 18.2.0 ``verify`` now will determine the type of hash. |
| .. versionchanged:: 18.3.0 The Argon2 type is configurable now. |
| .. versionadded:: 21.2.0 :meth:`from_parameters` |
| .. versionchanged:: 21.2.0 |
| Changed defaults to :data:`argon2.profiles.RFC_9106_LOW_MEMORY`. |
| |
| .. _salt: https://en.wikipedia.org/wiki/Salt_(cryptography) |
| .. _kibibytes: https://en.wikipedia.org/wiki/Binary_prefix#kibi |
| """ |
|
|
| __slots__ = ["_parameters", "encoding"] |
|
|
| _parameters: Parameters |
| encoding: str |
|
|
| def __init__( |
| self, |
| time_cost: int = DEFAULT_TIME_COST, |
| memory_cost: int = DEFAULT_MEMORY_COST, |
| parallelism: int = DEFAULT_PARALLELISM, |
| hash_len: int = DEFAULT_HASH_LENGTH, |
| salt_len: int = DEFAULT_RANDOM_SALT_LENGTH, |
| encoding: str = "utf-8", |
| type: Type = Type.ID, |
| ): |
| e = _check_types( |
| time_cost=(time_cost, int), |
| memory_cost=(memory_cost, int), |
| parallelism=(parallelism, int), |
| hash_len=(hash_len, int), |
| salt_len=(salt_len, int), |
| encoding=(encoding, str), |
| type=(type, Type), |
| ) |
| if e: |
| raise TypeError(e) |
|
|
| params = Parameters( |
| type=type, |
| version=19, |
| salt_len=salt_len, |
| hash_len=hash_len, |
| time_cost=time_cost, |
| memory_cost=memory_cost, |
| parallelism=parallelism, |
| ) |
|
|
| validate_params_for_platform(params) |
|
|
| |
| self._parameters = params |
| self.encoding = encoding |
|
|
| @classmethod |
| def from_parameters(cls, params: Parameters) -> PasswordHasher: |
| """ |
| Construct a `PasswordHasher` from *params*. |
| |
| Returns: |
| A `PasswordHasher` instance with the parameters from *params*. |
| |
| .. versionadded:: 21.2.0 |
| """ |
|
|
| return cls( |
| time_cost=params.time_cost, |
| memory_cost=params.memory_cost, |
| parallelism=params.parallelism, |
| hash_len=params.hash_len, |
| salt_len=params.salt_len, |
| type=params.type, |
| ) |
|
|
| @property |
| def time_cost(self) -> int: |
| return self._parameters.time_cost |
|
|
| @property |
| def memory_cost(self) -> int: |
| return self._parameters.memory_cost |
|
|
| @property |
| def parallelism(self) -> int: |
| return self._parameters.parallelism |
|
|
| @property |
| def hash_len(self) -> int: |
| return self._parameters.hash_len |
|
|
| @property |
| def salt_len(self) -> int: |
| return self._parameters.salt_len |
|
|
| @property |
| def type(self) -> Type: |
| return self._parameters.type |
|
|
| def hash(self, password: str | bytes, *, salt: bytes | None = None) -> str: |
| """ |
| Hash *password* and return an encoded hash. |
| |
| Args: |
| password: Password to hash. |
| |
| salt: |
| If None, a random salt is securely created. |
| |
| .. danger:: |
| |
| You should **not** pass a salt unless you really know what |
| you are doing. |
| |
| Raises: |
| argon2.exceptions.HashingError: If hashing fails. |
| |
| Returns: |
| Hashed *password*. |
| |
| .. versionadded:: 23.1.0 *salt* parameter |
| """ |
| return hash_secret( |
| secret=_ensure_bytes(password, self.encoding), |
| salt=salt or os.urandom(self.salt_len), |
| time_cost=self.time_cost, |
| memory_cost=self.memory_cost, |
| parallelism=self.parallelism, |
| hash_len=self.hash_len, |
| type=self.type, |
| ).decode("ascii") |
|
|
| _header_to_type: ClassVar[dict[bytes, Type]] = { |
| b"$argon2i$": Type.I, |
| b"$argon2d$": Type.D, |
| b"$argon2id": Type.ID, |
| } |
|
|
| def verify( |
| self, hash: str | bytes, password: str | bytes |
| ) -> Literal[True]: |
| """ |
| Verify that *password* matches *hash*. |
| |
| .. warning:: |
| |
| It is assumed that the caller is in full control of the hash. No |
| other parsing than the determination of the hash type is done by |
| *argon2-cffi*. |
| |
| Args: |
| hash: An encoded hash as returned from :meth:`PasswordHasher.hash`. |
| |
| password: The password to verify. |
| |
| Raises: |
| argon2.exceptions.VerifyMismatchError: |
| If verification fails because *hash* is not valid for |
| *password*. |
| |
| argon2.exceptions.VerificationError: |
| If verification fails for other reasons. |
| |
| argon2.exceptions.InvalidHashError: |
| If *hash* is so clearly invalid, that it couldn't be passed to |
| Argon2. |
| |
| Returns: |
| ``True`` on success, otherwise an exception is raised. |
| |
| .. versionchanged:: 16.1.0 |
| Raise :exc:`~argon2.exceptions.VerifyMismatchError` on mismatches |
| instead of its more generic superclass. |
| .. versionadded:: 18.2.0 Hash type agility. |
| """ |
| hash = _ensure_bytes(hash, "ascii") |
| try: |
| hash_type = self._header_to_type[hash[:9]] |
| except LookupError: |
| raise InvalidHashError from None |
|
|
| return verify_secret( |
| hash, _ensure_bytes(password, self.encoding), hash_type |
| ) |
|
|
| def check_needs_rehash(self, hash: str | bytes) -> bool: |
| """ |
| Check whether *hash* was created using the instance's parameters. |
| |
| Whenever your Argon2 parameters -- or *argon2-cffi*'s defaults! -- |
| change, you should rehash your passwords at the next opportunity. The |
| common approach is to do that whenever a user logs in, since that |
| should be the only time when you have access to the cleartext |
| password. |
| |
| Therefore it's best practice to check -- and if necessary rehash -- |
| passwords after each successful authentication. |
| |
| Args: |
| hash: An encoded Argon2 password hash. |
| |
| Returns: |
| Whether *hash* was created using the instance's parameters. |
| |
| .. versionadded:: 18.2.0 |
| .. versionchanged:: 24.1.0 Accepts bytes for *hash*. |
| """ |
| if isinstance(hash, bytes): |
| hash = hash.decode("ascii") |
|
|
| return self._parameters != extract_parameters(hash) |
|
|