Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/werkzeug/security.py: 23%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from __future__ import annotations
3import hashlib
4import hmac
5import os
6import posixpath
7import secrets
9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
10DEFAULT_PBKDF2_ITERATIONS = 1_000_000
12_os_alt_seps: list[str] = list(
13 sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/"
14)
15# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html
16_windows_device_files = {
17 "AUX",
18 "CON",
19 "CONIN$",
20 "CONOUT$",
21 *(f"COM{c}" for c in "123456789¹²³"),
22 *(f"LPT{c}" for c in "123456789¹²³"),
23 "NUL",
24 "PRN",
25}
28def gen_salt(length: int) -> str:
29 """Generate a random string of SALT_CHARS with specified ``length``."""
30 if length <= 0:
31 raise ValueError("Salt length must be at least 1.")
33 return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
36def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
37 method, *args = method.split(":")
38 salt_bytes = salt.encode()
39 password_bytes = password.encode()
41 if method == "scrypt":
42 if not args:
43 n = 2**15
44 r = 8
45 p = 1
46 else:
47 try:
48 n, r, p = map(int, args)
49 except ValueError:
50 raise ValueError("'scrypt' takes 3 arguments.") from None
52 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
53 return (
54 hashlib.scrypt(
55 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
56 ).hex(),
57 f"scrypt:{n}:{r}:{p}",
58 )
59 elif method == "pbkdf2":
60 len_args = len(args)
62 if len_args == 0:
63 hash_name = "sha256"
64 iterations = DEFAULT_PBKDF2_ITERATIONS
65 elif len_args == 1:
66 hash_name = args[0]
67 iterations = DEFAULT_PBKDF2_ITERATIONS
68 elif len_args == 2:
69 hash_name = args[0]
70 iterations = int(args[1])
71 else:
72 raise ValueError("'pbkdf2' takes 2 arguments.")
74 return (
75 hashlib.pbkdf2_hmac(
76 hash_name, password_bytes, salt_bytes, iterations
77 ).hex(),
78 f"pbkdf2:{hash_name}:{iterations}",
79 )
80 else:
81 raise ValueError(f"Invalid hash method '{method}'.")
84def generate_password_hash(
85 password: str, method: str = "scrypt", salt_length: int = 16
86) -> str:
87 """Securely hash a password for storage. A password can be compared to a stored hash
88 using :func:`check_password_hash`.
90 The following methods are supported:
92 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
93 is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
94 - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
95 the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
97 Default parameters may be updated to reflect current guidelines, and methods may be
98 deprecated and removed if they are no longer considered secure. To migrate old
99 hashes, you may generate a new hash when checking an old hash, or you may contact
100 users with a link to reset their password.
102 :param password: The plaintext password.
103 :param method: The key derivation function and parameters.
104 :param salt_length: The number of characters to generate for the salt.
106 .. versionchanged:: 3.1
107 The default iterations for pbkdf2 was increased to 1,000,000.
109 .. versionchanged:: 2.3
110 Scrypt support was added.
112 .. versionchanged:: 2.3
113 The default iterations for pbkdf2 was increased to 600,000.
115 .. versionchanged:: 2.3
116 All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
117 """
118 salt = gen_salt(salt_length)
119 h, actual_method = _hash_internal(method, salt, password)
120 return f"{actual_method}${salt}${h}"
123def check_password_hash(pwhash: str, password: str) -> bool:
124 """Securely check that the given stored password hash, previously generated using
125 :func:`generate_password_hash`, matches the given password.
127 Methods may be deprecated and removed if they are no longer considered secure. To
128 migrate old hashes, you may generate a new hash when checking an old hash, or you
129 may contact users with a link to reset their password.
131 :param pwhash: The hashed password.
132 :param password: The plaintext password.
134 .. versionchanged:: 2.3
135 All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
136 """
137 try:
138 method, salt, hashval = pwhash.split("$", 2)
139 except ValueError:
140 return False
142 return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
145def safe_join(directory: str, *pathnames: str) -> str | None:
146 """Safely join zero or more untrusted path components to a base
147 directory to avoid escaping the base directory.
149 :param directory: The trusted base directory.
150 :param pathnames: The untrusted path components relative to the
151 base directory.
152 :return: A safe path, otherwise ``None``.
154 .. versionchanged:: 3.1.5
155 More special device names, regardless of extension or trailing spaces,
156 are not allowed on Windows.
158 .. versionchanged:: 3.1.4
159 Special device names are not allowed on Windows.
160 """
161 if not directory:
162 # Ensure we end up with ./path if directory="" is given,
163 # otherwise the first untrusted part could become trusted.
164 directory = "."
166 parts = [directory]
168 for filename in pathnames:
169 if filename != "":
170 filename = posixpath.normpath(filename)
172 if (
173 any(sep in filename for sep in _os_alt_seps)
174 or (
175 os.name == "nt"
176 and filename.partition(".")[0].strip().upper() in _windows_device_files
177 )
178 or os.path.isabs(filename)
179 # ntpath.isabs doesn't catch this on Python < 3.11
180 or filename.startswith("/")
181 or filename == ".."
182 or filename.startswith("../")
183 ):
184 return None
186 parts.append(filename)
188 return posixpath.join(*parts)