aboutsummaryrefslogtreecommitdiffstats
path: root/deluge/security.py
diff options
context:
space:
mode:
Diffstat (limited to 'deluge/security.py')
-rw-r--r--deluge/security.py127
1 files changed, 127 insertions, 0 deletions
diff --git a/deluge/security.py b/deluge/security.py
new file mode 100644
index 0000000..85638c4
--- /dev/null
+++ b/deluge/security.py
@@ -0,0 +1,127 @@
+# This file is adapted from Werkzeug (https://github.com/pallets/werkzeug)
+# and is licensed under the BSD 3-Clause License:
+
+# Copyright 2007 Pallets
+
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+
+# 3. Neither the name of the copyright holder nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
+# PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
+# TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+from __future__ import annotations
+
+import hashlib
+import hmac
+import secrets
+
+from deluge.error import InvalidHashError
+
+SALT_CHARS = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
+
+
+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]:
+ method, *args = method.split('$')
+ salt_bytes = salt.encode()
+ password_bytes = password.encode()
+
+ 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_bytes, salt=salt_bytes, n=n, r=r, p=p, maxmem=maxmem
+ ).hex(),
+ f'scrypt${n}${r}${p}',
+ )
+
+ else:
+ raise ValueError(f"Invalid hash method '{method}'.")
+
+
+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 check_password_hash.
+
+ Args:
+ password (str): The plaintext password to hash.
+ method (str): The key derivation function and parameters. Defaults to 'scrypt'.
+ salt_length (int): The length of the salt to generate. Defaults to 16.
+
+ Returns:
+ str: The hashed password in the format '$method$salt$hash'.
+
+ """
+ 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
+ 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.
+
+ Args:
+ pwhash (str): The hashed password in the format '$method$salt$hash
+ password (str): The plaintext password to check against the hash.
+
+ Raises:
+ ValueError: If the hash format is invalid or the method is not recognized.
+
+ Returns:
+ bool: True if the password matches the hash, False otherwise.
+ """
+ method = None
+ try:
+ method, salt, stored_hash = pwhash.lstrip('$').rsplit('$', 2)
+ computed_hash = _hash_internal(method, salt, password)[0]
+ except ValueError as ve:
+ raise InvalidHashError(
+ 'Invalid password hash format. Expected format: "$method$salt$hash".',
+ method=method if method else pwhash,
+ ) from ve
+
+ return hmac.compare_digest(computed_hash, stored_hash)