File size: 8,213 Bytes
d1ceb73 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# SPDX-License-Identifier: MIT
from __future__ import annotations
import os
from typing import ClassVar
from ._typing import Literal
from ._utils import Parameters, _check_types, extract_parameters
from .exceptions import InvalidHashError
from .low_level import Type, hash_secret, verify_secret
from .profiles import RFC_9106_LOW_MEMORY
DEFAULT_RANDOM_SALT_LENGTH = RFC_9106_LOW_MEMORY.salt_len
DEFAULT_HASH_LENGTH = RFC_9106_LOW_MEMORY.hash_len
DEFAULT_TIME_COST = RFC_9106_LOW_MEMORY.time_cost
DEFAULT_MEMORY_COST = RFC_9106_LOW_MEMORY.memory_cost
DEFAULT_PARALLELISM = RFC_9106_LOW_MEMORY.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 always 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.
:param int time_cost: Defines the amount of computation realized and
therefore the execution time, given in number of iterations.
:param int memory_cost: Defines the memory usage, given in kibibytes_.
:param int parallelism: Defines the number of parallel threads (*changes*
the resulting hash value).
:param int hash_len: Length of the hash in bytes.
:param int salt_len: Length of random salt to be generated for each
password.
:param str 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.
:param Type 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)
# Cache a Parameters object for check_needs_rehash.
self._parameters = Parameters(
type=type,
version=19,
salt_len=salt_len,
hash_len=hash_len,
time_cost=time_cost,
memory_cost=memory_cost,
parallelism=parallelism,
)
self.encoding = encoding
@classmethod
def from_parameters(cls, params: Parameters) -> PasswordHasher:
"""
Construct a `PasswordHasher` from *params*.
.. versionadded:: 21.2.0
"""
ph = cls()
ph._parameters = params
return ph
@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.
Parameters:
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*.
:param hash: An encoded hash as returned from
:meth:`PasswordHasher.hash`.
:type hash: ``bytes`` or ``str``
:param password: The password to verify.
:type password: ``bytes`` or ``str``
:raises argon2.exceptions.VerifyMismatchError: If verification fails
because *hash* is not valid for *password*.
:raises argon2.exceptions.VerificationError: If verification fails for
other reasons.
:raises argon2.exceptions.InvalidHashError: If *hash* is so clearly
invalid, that it couldn't be passed to Argon2.
:return: ``True`` on success, raise
:exc:`~argon2.exceptions.VerificationError` otherwise.
:rtype: bool
.. 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) -> 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.
:rtype: bool
.. versionadded:: 18.2.0
"""
return self._parameters != extract_parameters(hash)
|