1 from __future__
import annotations
9 SALT_CHARS
= "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
10 DEFAULT_PBKDF2_ITERATIONS
= 600000
12 _os_alt_seps
: list[str] = list(
13 sep
for sep
in [os
.sep
, os
.path
.altsep
] if sep
is not None and sep
!= "/"
17 def gen_salt(length
: int) -> str:
18 """Generate a random string of SALT_CHARS with specified ``length``."""
20 raise ValueError("Salt length must be at least 1.")
22 return "".join(secrets
.choice(SALT_CHARS
) for _
in range(length
))
25 def _hash_internal(method
: str, salt
: str, password
: str) -> tuple[str, str]:
26 method
, *args
= method
.split(":")
27 salt
= salt
.encode("utf-8")
28 password
= password
.encode("utf-8")
30 if method
== "scrypt":
37 n
, r
, p
= map(int, args
)
39 raise ValueError("'scrypt' takes 3 arguments.") from None
41 maxmem
= 132 * n
* r
* p
# ideally 128, but some extra seems needed
43 hashlib
.scrypt(password
, salt
=salt
, n
=n
, r
=r
, p
=p
, maxmem
=maxmem
).hex(),
44 f
"scrypt:{n}:{r}:{p}",
46 elif method
== "pbkdf2":
51 iterations
= DEFAULT_PBKDF2_ITERATIONS
54 iterations
= DEFAULT_PBKDF2_ITERATIONS
57 iterations
= int(args
[1])
59 raise ValueError("'pbkdf2' takes 2 arguments.")
62 hashlib
.pbkdf2_hmac(hash_name
, password
, salt
, iterations
).hex(),
63 f
"pbkdf2:{hash_name}:{iterations}",
66 raise ValueError(f
"Invalid hash method '{method}'.")
69 def generate_password_hash(
70 password
: str, method
: str = "scrypt", salt_length
: int = 16
72 """Securely hash a password for storage. A password can be compared to a stored hash
73 using :func:`check_password_hash`.
75 The following methods are supported:
77 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
78 is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
79 - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
80 the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
82 Default parameters may be updated to reflect current guidelines, and methods may be
83 deprecated and removed if they are no longer considered secure. To migrate old
84 hashes, you may generate a new hash when checking an old hash, or you may contact
85 users with a link to reset their password.
87 :param password: The plaintext password.
88 :param method: The key derivation function and parameters.
89 :param salt_length: The number of characters to generate for the salt.
91 .. versionchanged:: 2.3
92 Scrypt support was added.
94 .. versionchanged:: 2.3
95 The default iterations for pbkdf2 was increased to 600,000.
97 .. versionchanged:: 2.3
98 All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
100 salt
= gen_salt(salt_length
)
101 h
, actual_method
= _hash_internal(method
, salt
, password
)
102 return f
"{actual_method}${salt}${h}"
105 def check_password_hash(pwhash
: str, password
: str) -> bool:
106 """Securely check that the given stored password hash, previously generated using
107 :func:`generate_password_hash`, matches the given password.
109 Methods may be deprecated and removed if they are no longer considered secure. To
110 migrate old hashes, you may generate a new hash when checking an old hash, or you
111 may contact users with a link to reset their password.
113 :param pwhash: The hashed password.
114 :param password: The plaintext password.
116 .. versionchanged:: 2.3
117 All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
120 method
, salt
, hashval
= pwhash
.split("$", 2)
124 return hmac
.compare_digest(_hash_internal(method
, salt
, password
)[0], hashval
)
127 def safe_join(directory
: str, *pathnames
: str) -> str |
None:
128 """Safely join zero or more untrusted path components to a base
129 directory to avoid escaping the base directory.
131 :param directory: The trusted base directory.
132 :param pathnames: The untrusted path components relative to the
134 :return: A safe path, otherwise ``None``.
137 # Ensure we end up with ./path if directory="" is given,
138 # otherwise the first untrusted part could become trusted.
143 for filename
in pathnames
:
145 filename
= posixpath
.normpath(filename
)
148 any(sep
in filename
for sep
in _os_alt_seps
)
149 or os
.path
.isabs(filename
)
151 or filename
.startswith("../")
155 parts
.append(filename
)
157 return posixpath
.join(*parts
)