Spaces:
Running
Running
from __future__ import annotations | |
import hashlib | |
import hmac | |
import os | |
import posixpath | |
import secrets | |
SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" | |
DEFAULT_PBKDF2_ITERATIONS = 600000 | |
_os_alt_seps: list[str] = list( | |
sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/" | |
) | |
def gen_salt(length: int) -> str: | |
"""Generate a random string of SALT_CHARS with specified ``length``.""" | |
if length <= 0: | |
raise ValueError("Salt length must be at least 1.") | |
return "".join(secrets.choice(SALT_CHARS) for _ in range(length)) | |
def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]: | |
method, *args = method.split(":") | |
salt_bytes = salt.encode() | |
password_bytes = password.encode() | |
if method == "scrypt": | |
if not args: | |
n = 2**15 | |
r = 8 | |
p = 1 | |
else: | |
try: | |
n, r, p = map(int, args) | |
except ValueError: | |
raise ValueError("'scrypt' takes 3 arguments.") from None | |
maxmem = 132 * n * r * p # ideally 128, but some extra seems needed | |
return ( | |
hashlib.scrypt( | |
password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem | |
).hex(), | |
f"scrypt:{n}:{r}:{p}", | |
) | |
elif method == "pbkdf2": | |
len_args = len(args) | |
if len_args == 0: | |
hash_name = "sha256" | |
iterations = DEFAULT_PBKDF2_ITERATIONS | |
elif len_args == 1: | |
hash_name = args[0] | |
iterations = DEFAULT_PBKDF2_ITERATIONS | |
elif len_args == 2: | |
hash_name = args[0] | |
iterations = int(args[1]) | |
else: | |
raise ValueError("'pbkdf2' takes 2 arguments.") | |
return ( | |
hashlib.pbkdf2_hmac( | |
hash_name, password_bytes, salt_bytes, iterations | |
).hex(), | |
f"pbkdf2:{hash_name}:{iterations}", | |
) | |
else: | |
raise ValueError(f"Invalid hash method '{method}'.") | |
def generate_password_hash( | |
password: str, method: str = "scrypt", salt_length: int = 16 | |
) -> str: | |
"""Securely hash a password for storage. A password can be compared to a stored hash | |
using :func:`check_password_hash`. | |
The following methods are supported: | |
- ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default | |
is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`. | |
- ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``, | |
the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`. | |
Default parameters may be updated to reflect current guidelines, and methods may be | |
deprecated and removed if they are no longer considered secure. To migrate old | |
hashes, you may generate a new hash when checking an old hash, or you may contact | |
users with a link to reset their password. | |
:param password: The plaintext password. | |
:param method: The key derivation function and parameters. | |
:param salt_length: The number of characters to generate for the salt. | |
.. versionchanged:: 2.3 | |
Scrypt support was added. | |
.. versionchanged:: 2.3 | |
The default iterations for pbkdf2 was increased to 600,000. | |
.. versionchanged:: 2.3 | |
All plain hashes are deprecated and will not be supported in Werkzeug 3.0. | |
""" | |
salt = gen_salt(salt_length) | |
h, actual_method = _hash_internal(method, salt, password) | |
return f"{actual_method}${salt}${h}" | |
def check_password_hash(pwhash: str, password: str) -> bool: | |
"""Securely check that the given stored password hash, previously generated using | |
:func:`generate_password_hash`, matches the given password. | |
Methods may be deprecated and removed if they are no longer considered secure. To | |
migrate old hashes, you may generate a new hash when checking an old hash, or you | |
may contact users with a link to reset their password. | |
:param pwhash: The hashed password. | |
:param password: The plaintext password. | |
.. versionchanged:: 2.3 | |
All plain hashes are deprecated and will not be supported in Werkzeug 3.0. | |
""" | |
try: | |
method, salt, hashval = pwhash.split("$", 2) | |
except ValueError: | |
return False | |
return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval) | |
def safe_join(directory: str, *pathnames: str) -> str | None: | |
"""Safely join zero or more untrusted path components to a base | |
directory to avoid escaping the base directory. | |
:param directory: The trusted base directory. | |
:param pathnames: The untrusted path components relative to the | |
base directory. | |
:return: A safe path, otherwise ``None``. | |
""" | |
if not directory: | |
# Ensure we end up with ./path if directory="" is given, | |
# otherwise the first untrusted part could become trusted. | |
directory = "." | |
parts = [directory] | |
for filename in pathnames: | |
if filename != "": | |
filename = posixpath.normpath(filename) | |
if ( | |
any(sep in filename for sep in _os_alt_seps) | |
or os.path.isabs(filename) | |
or filename == ".." | |
or filename.startswith("../") | |
): | |
return None | |
parts.append(filename) | |
return posixpath.join(*parts) | |