1from __future__ import annotations
2
3import hashlib
4import hmac
5import os
6import posixpath
7import secrets
8
9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
10DEFAULT_PBKDF2_ITERATIONS = 600000
11
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
16
17def gen_salt(length: int) -> str:
18 """Generate a random string of SALT_CHARS with specified ``length``."""
19 if length <= 0:
20 raise ValueError("Salt length must be at least 1.")
21
22 return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
23
24
25def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
26 method, *args = method.split(":")
27 salt_bytes = salt.encode()
28 password_bytes = password.encode()
29
30 if method == "scrypt":
31 if not args:
32 n = 2**15
33 r = 8
34 p = 1
35 else:
36 try:
37 n, r, p = map(int, args)
38 except ValueError:
39 raise ValueError("'scrypt' takes 3 arguments.") from None
40
41 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed
42 return (
43 hashlib.scrypt(
44 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
45 ).hex(),
46 f"scrypt:{n}:{r}:{p}",
47 )
48 elif method == "pbkdf2":
49 len_args = len(args)
50
51 if len_args == 0:
52 hash_name = "sha256"
53 iterations = DEFAULT_PBKDF2_ITERATIONS
54 elif len_args == 1:
55 hash_name = args[0]
56 iterations = DEFAULT_PBKDF2_ITERATIONS
57 elif len_args == 2:
58 hash_name = args[0]
59 iterations = int(args[1])
60 else:
61 raise ValueError("'pbkdf2' takes 2 arguments.")
62
63 return (
64 hashlib.pbkdf2_hmac(
65 hash_name, password_bytes, salt_bytes, iterations
66 ).hex(),
67 f"pbkdf2:{hash_name}:{iterations}",
68 )
69 else:
70 raise ValueError(f"Invalid hash method '{method}'.")
71
72
73def generate_password_hash(
74 password: str, method: str = "scrypt", salt_length: int = 16
75) -> str:
76 """Securely hash a password for storage. A password can be compared to a stored hash
77 using :func:`check_password_hash`.
78
79 The following methods are supported:
80
81 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
82 is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
83 - ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
84 the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
85
86 Default parameters may be updated to reflect current guidelines, and methods may be
87 deprecated and removed if they are no longer considered secure. To migrate old
88 hashes, you may generate a new hash when checking an old hash, or you may contact
89 users with a link to reset their password.
90
91 :param password: The plaintext password.
92 :param method: The key derivation function and parameters.
93 :param salt_length: The number of characters to generate for the salt.
94
95 .. versionchanged:: 2.3
96 Scrypt support was added.
97
98 .. versionchanged:: 2.3
99 The default iterations for pbkdf2 was increased to 600,000.
100
101 .. versionchanged:: 2.3
102 All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
103 """
104 salt = gen_salt(salt_length)
105 h, actual_method = _hash_internal(method, salt, password)
106 return f"{actual_method}${salt}${h}"
107
108
109def check_password_hash(pwhash: str, password: str) -> bool:
110 """Securely check that the given stored password hash, previously generated using
111 :func:`generate_password_hash`, matches the given password.
112
113 Methods may be deprecated and removed if they are no longer considered secure. To
114 migrate old hashes, you may generate a new hash when checking an old hash, or you
115 may contact users with a link to reset their password.
116
117 :param pwhash: The hashed password.
118 :param password: The plaintext password.
119
120 .. versionchanged:: 2.3
121 All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
122 """
123 try:
124 method, salt, hashval = pwhash.split("$", 2)
125 except ValueError:
126 return False
127
128 return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
129
130
131def safe_join(directory: str, *pathnames: str) -> str | None:
132 """Safely join zero or more untrusted path components to a base
133 directory to avoid escaping the base directory.
134
135 :param directory: The trusted base directory.
136 :param pathnames: The untrusted path components relative to the
137 base directory.
138 :return: A safe path, otherwise ``None``.
139 """
140 if not directory:
141 # Ensure we end up with ./path if directory="" is given,
142 # otherwise the first untrusted part could become trusted.
143 directory = "."
144
145 parts = [directory]
146
147 for filename in pathnames:
148 if filename != "":
149 filename = posixpath.normpath(filename)
150
151 if (
152 any(sep in filename for sep in _os_alt_seps)
153 or os.path.isabs(filename)
154 or filename == ".."
155 or filename.startswith("../")
156 ):
157 return None
158
159 parts.append(filename)
160
161 return posixpath.join(*parts)