codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import hashlib4import hmac5import os6import posixpath7import secrets8 9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"10DEFAULT_PBKDF2_ITERATIONS = 1_000_00011 12_os_alt_seps: list[str] = list(13 sep for sep in [os.sep, os.altsep] if sep is not None and sep != "/"14)15# https://chrisdenton.github.io/omnipath/Special%20Dos%20Device%20Names.html16_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}26 27 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.")32 33 return "".join(secrets.choice(SALT_CHARS) for _ in range(length))34 35 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()40 41 if method == "scrypt":42 if not args:43 n = 2**1544 r = 845 p = 146 else:47 try:48 n, r, p = map(int, args)49 except ValueError:50 raise ValueError("'scrypt' takes 3 arguments.") from None51 52 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed53 return (54 hashlib.scrypt(55 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem56 ).hex(),57 f"scrypt:{n}:{r}:{p}",58 )59 elif method == "pbkdf2":60 len_args = len(args)61 62 if len_args == 0:63 hash_name = "sha256"64 iterations = DEFAULT_PBKDF2_ITERATIONS65 elif len_args == 1:66 hash_name = args[0]67 iterations = DEFAULT_PBKDF2_ITERATIONS68 elif len_args == 2:69 hash_name = args[0]70 iterations = int(args[1])71 else:72 raise ValueError("'pbkdf2' takes 2 arguments.")73 74 return (75 hashlib.pbkdf2_hmac(76 hash_name, password_bytes, salt_bytes, iterations77 ).hex(),78 f"pbkdf2:{hash_name}:{iterations}",79 )80 else:81 raise ValueError(f"Invalid hash method '{method}'.")82 83 84def generate_password_hash(85 password: str, method: str = "scrypt", salt_length: int = 1686) -> str:87 """Securely hash a password for storage. A password can be compared to a stored hash88 using :func:`check_password_hash`.89 90 The following methods are supported:91 92 - ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default93 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`.96 97 Default parameters may be updated to reflect current guidelines, and methods may be98 deprecated and removed if they are no longer considered secure. To migrate old99 hashes, you may generate a new hash when checking an old hash, or you may contact100 users with a link to reset their password.101 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.105 106 .. versionchanged:: 3.1107 The default iterations for pbkdf2 was increased to 1,000,000.108 109 .. versionchanged:: 2.3110 Scrypt support was added.111 112 .. versionchanged:: 2.3113 The default iterations for pbkdf2 was increased to 600,000.114 115 .. versionchanged:: 2.3116 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}"121 122 123def check_password_hash(pwhash: str, password: str) -> bool:124 """Securely check that the given stored password hash, previously generated using125 :func:`generate_password_hash`, matches the given password.126 127 Methods may be deprecated and removed if they are no longer considered secure. To128 migrate old hashes, you may generate a new hash when checking an old hash, or you129 may contact users with a link to reset their password.130 131 :param pwhash: The hashed password.132 :param password: The plaintext password.133 134 .. versionchanged:: 2.3135 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 False141 142 return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)143 144 145def safe_join(directory: str, *untrusted: str) -> str | None:146 """Safely join zero or more untrusted path components to a trusted base147 directory to avoid escaping the base directory.148 149 The untrusted path is assumed to be from/for a URL, such as for serving150 files. Therefore, it should only use the forward slash ``/`` path separator,151 and will be joined using that separator. On Windows, the backslash ``\\``152 separator is not allowed.153 154 :param directory: The trusted base directory.155 :param untrusted: The untrusted path components relative to the156 base directory.157 :return: A safe path, otherwise ``None``.158 159 .. versionchanged:: 3.1.6160 Special device names in multi-segment paths are not allowed on Windows.161 162 .. versionchanged:: 3.1.5163 More special device names, regardless of extension or trailing spaces,164 are not allowed on Windows.165 166 .. versionchanged:: 3.1.4167 Special device names are not allowed on Windows.168 """169 if not directory:170 # Ensure we end up with ./path if directory="" is given,171 # otherwise the first untrusted part could become trusted.172 directory = "."173 174 parts = [directory]175 176 for part in untrusted:177 if not part:178 continue179 180 part = posixpath.normpath(part)181 182 if (183 os.path.isabs(part)184 # ntpath.isabs doesn't catch this185 or part.startswith("/")186 or part == ".."187 or part.startswith("../")188 or any(sep in part for sep in _os_alt_seps)189 or (190 os.name == "nt"191 and any(192 p.partition(".")[0].strip().upper() in _windows_device_files193 for p in part.split("/")194 )195 )196 ):197 return None198 199 parts.append(part)200 201 return posixpath.join(*parts)202 