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
168
169
170
171
172
from __future__ import annotations
 
import hashlib
import hmac
import os
import posixpath
import secrets
import warnings
 
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]:
    if method == "plain":
        warnings.warn(
            "The 'plain' password method is deprecated and will be removed in"
            " Werkzeug 3.0. Migrate to the 'scrypt' method.",
            stacklevel=3,
        )
        return password, method
 
    method, *args = method.split(":")
    salt = salt.encode("utf-8")
    password = password.encode("utf-8")
 
    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, salt=salt, 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, salt, iterations).hex(),
            f"pbkdf2:{hash_name}:{iterations}",
        )
    else:
        warnings.warn(
            f"The '{method}' password method is deprecated and will be removed in"
            " Werkzeug 3.0. Migrate to the 'scrypt' method.",
            stacklevel=3,
        )
        return hmac.new(salt, password, method).hexdigest(), method
 
 
def generate_password_hash(
    password: str, method: str = "pbkdf2", 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``, more secure but not available on PyPy. The parameters are ``n``,
        ``r``, and ``p``, the default is ``scrypt:32768:8:1``. See
        :func:`hashlib.scrypt`.
    -   ``pbkdf2``, the default. 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)