| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287 |
- # SPDX-License-Identifier: MIT
- 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)
- # Cache a Parameters object for check_needs_rehash.
- 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)
|