codekingpro/portable-devtools
114k
1from __future__ import annotations2 3import hashlib4import hmac5import os6import posixpath7import secrets8 9SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"10DEFAULT_PBKDF2_ITERATIONS = 60000011 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**1533 r = 834 p = 135 else:36 try:37 n, r, p = map(int, args)38 except ValueError:39 raise ValueError("'scrypt' takes 3 arguments.") from None40 41 maxmem = 132 * n * r * p # ideally 128, but some extra seems needed42 return (43 hashlib.scrypt(44 password_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem45 ).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_ITERATIONS54 elif len_args == 1:55 hash_name = args[0]56 iterations = DEFAULT_PBKDF2_ITERATIONS57 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, iterations66 ).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 = 1675) -> str:76 """Securely hash a password for storage. A password can be compared to a stored hash77 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 default82 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 be87 deprecated and removed if they are no longer considered secure. To migrate old88 hashes, you may generate a new hash when checking an old hash, or you may contact89 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.396 Scrypt support was added.97 98 .. versionchanged:: 2.399 The default iterations for pbkdf2 was increased to 600,000.100 101 .. versionchanged:: 2.3102 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 using111 :func:`generate_password_hash`, matches the given password.112 113 Methods may be deprecated and removed if they are no longer considered secure. To114 migrate old hashes, you may generate a new hash when checking an old hash, or you115 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.3121 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 False127 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 base133 directory to avoid escaping the base directory.134 135 :param directory: The trusted base directory.136 :param pathnames: The untrusted path components relative to the137 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 None158 159 parts.append(filename)160 161 return posixpath.join(*parts)162 