You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
bazarr/libs/werkzeug/security.py

158 lines
5.2 KiB

from __future__ import annotations
5 years ago
import hashlib
import hmac
import os
import posixpath
import secrets
5 years ago
SALT_CHARS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
DEFAULT_PBKDF2_ITERATIONS = 600000
5 years ago
_os_alt_seps: list[str] = list(
sep for sep in [os.sep, os.path.altsep] if sep is not None and sep != "/"
5 years ago
)
def gen_salt(length: int) -> str:
5 years ago
"""Generate a random string of SALT_CHARS with specified ``length``."""
if length <= 0:
raise ValueError("Salt length must be at least 1.")
5 years ago
return "".join(secrets.choice(SALT_CHARS) for _ in range(length))
5 years ago
def _hash_internal(method: str, salt: str, password: str) -> tuple[str, str]:
method, *args = method.split(":")
salt = salt.encode("utf-8")
password = password.encode("utf-8")
5 years ago
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.")
5 years ago
return (
hashlib.pbkdf2_hmac(hash_name, password, salt, iterations).hex(),
f"pbkdf2:{hash_name}:{iterations}",
)
else:
raise ValueError(f"Invalid hash method '{method}'.")
5 years ago
def generate_password_hash(
password: str, method: str = "scrypt", 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:
5 years ago
- ``scrypt``, the default. The parameters are ``n``, ``r``, and ``p``, the default
is ``scrypt:32768:8:1``. See :func:`hashlib.scrypt`.
- ``pbkdf2``, less secure. The parameters are ``hash_method`` and ``iterations``,
the default is ``pbkdf2:sha256:600000``. See :func:`hashlib.pbkdf2_hmac`.
5 years ago
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.
5 years ago
: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.
5 years ago
.. versionchanged:: 2.3
Scrypt support was added.
5 years ago
.. versionchanged:: 2.3
The default iterations for pbkdf2 was increased to 600,000.
5 years ago
.. versionchanged:: 2.3
All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
5 years ago
"""
salt = gen_salt(salt_length)
5 years ago
h, actual_method = _hash_internal(method, salt, password)
return f"{actual_method}${salt}${h}"
5 years ago
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.
5 years ago
:param pwhash: The hashed password.
:param password: The plaintext password.
5 years ago
.. versionchanged:: 2.3
All plain hashes are deprecated and will not be supported in Werkzeug 3.0.
5 years ago
"""
try:
method, salt, hashval = pwhash.split("$", 2)
except ValueError:
5 years ago
return False
return hmac.compare_digest(_hash_internal(method, salt, password)[0], hashval)
5 years ago
def safe_join(directory: str, *pathnames: str) -> str | None:
5 years ago
"""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 = "."
5 years ago
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)