Spaces:
Running
Running
File size: 5,581 Bytes
47b2311 |
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 |
from __future__ import annotations
import hashlib
import hmac
import os
import posixpath
import secrets
SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
DEFAULT_PBKDF2_ITERATIONS = 1_000_000
_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:: 3.1
The default iterations for pbkdf2 was increased to 1,000,000.
.. 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)
# ntpath.isabs doesn't catch this on Python < 3.11
or filename.startswith("/")
or filename == ".."
or filename.startswith("../")
):
return None
parts.append(filename)
return posixpath.join(*parts)
|