# to use, copy, modify, and distribute this software is granted under
# terms provided in the LICENSE file distributed with this software.
-import base64
-import hashlib
-import math
-import random
-import re
-import struct
+import passlib.context
-# convenience constants for indexing the supported hashing algorithms,
-# guaranteed a stable part of the interface
-MD5 = 0 # hashlib.md5
-SHA1 = 1 # hashlib.sha1
-SHA224 = 2 # hashlib.sha224
-SHA256 = 3 # hashlib.sha256
-SHA384 = 4 # hashlib.sha385
-SHA512 = 5 # hashlib.sha512
+_CONTEXT = passlib.context.CryptContext(
+ all__vary_rounds=0.1, default="pbkdf2_sha512",
+ pbkdf2_sha512__default_rounds=1000, schemes=["pbkdf2_sha512"])
-def _pack_bytes(numbers):
- """Make a packed byte sequence:
-
- This is a wrapper around struct.pack, used to turn a list of integers
- between 0 and 255 into a packed sequence akin to a C-style string.
- """
- packed = b""
- for number in numbers:
- number = int(number)
- assert 0 <= number <= 255
- packed += struct.pack("B", number)
- return packed
-
-
-def _bytes_to_text(byte_sequence):
- """Generate printable representation of 8-bit data:
-
- This is a wrapper around base64.b64encode with preferences
- appropriate for encoding Unix-style passwd hash strings.
- """
- return base64.b64encode(
- byte_sequence,
- b"./"
- ).decode("ascii").rstrip("=")
-
-
-def _generate_salt(salt_len=2):
- """Generate salt for a password hash:
-
- This simply generates a sequence of pseudo-random characters (with
- 6-bits of effective entropy per character). Since it relies on base64
- encoding (which operates on 6-bit chunks of data), we only generate
- 0.75 times as many bytes (rounded up) as the number of characters we
- need and discard any excess characters over the specified length.
- This ensures full distribution over each character of the salt.
- """
- salt = []
- for i in range(int(math.ceil(salt_len * 0.75))):
- salt.append(random.randint(0, 255))
- return _bytes_to_text(_pack_bytes(salt))[:salt_len]
-
-
-def upgrade_legacy_hash(legacy_hash, salt, sep="$"):
- """Upgrade an older password hash:
-
- This utility function is meant to provide a migration path for users
- of mudpy's legacy account-name-salted MD5 hexdigest password hashes.
- By passing the old passhash (as legacy_hash) and name (as salt)
- facets to this function, a conforming new-style password hash will be
- returned.
- """
- assert re.match("^[0-9a-f]{32}$",
- legacy_hash), "Not a valid MD5 hexdigest"
- collapsed = b""
- for i in range(16):
- # this needs to become a byte() call in 2to3
- collapsed += bytes(legacy_hash[2 * i:2 * i + 2].decode("ascii"))
- return "%s%s%s%s%s%s%s%s" % (
- sep,
- MD5,
- sep,
- 0, # 2**0 provides one round of hashing
- sep,
- salt,
- sep,
- _bytes_to_text(collapsed)
- )
-
-
-def create(
- password,
- salt=None,
- algorithm=SHA1,
- rounds=4,
- salt_len=2,
- sep="$"
-):
- """Generate a password hash:
-
- The meat of the module, this function takes a provided password and
- generates a Unix-like passwd hash suitable for storage in portable,
- text-based data files. The password is prepended with a salt (which
- can also be specified explicitly, if the output needs to be
- repeatable) and then hashed with the requested algorithm iterated as
- many times as 2 raised to the power of the rounds parameter.
-
- The first character of the text returned by this function denotes the
- separator character used to identify subsequent fields. The fields in
- order are:
-
- 1. the decimal index number indicating which algorithm was used,
- also mapped as convenience constants at the beginning of this
- module
-
- 2. the number of times (as an exponent of 2) which the algorithm
- was iterated, represented by a decimal value between 0 and 16
- inclusive (0 results in one round, 16 results in 65536 rounds,
- and anything higher than that is a potential resource
- consumption denial of service on the application anyway)
-
- 3. the plain-text salt with which the password was prepended
- before hashing
-
- 4. the resulting password hash itself, base64-encoded using . and
- / as the two non-alpha-numeric characters required to reach 64
-
- The defaults provided should be safe for everyday use, but something
- more heavy-duty may be in order for admin users, such as::
-
- create(password, algorithm=SHA256, rounds=12, salt_len=16)
- """
-
- # if a specific salt wasn't specified, we need to generate one
- if not salt:
- salt = _generate_salt(salt_len=salt_len)
-
- # make sure the algorithm index number is coerced into integer form,
- # since it could also be passed as text (in decimal) for convenience
- algorithm = int(algorithm)
-
- # the list of algorithms supported by this function corresponds to
- # the convenience constants defined at the beginning of the module
- algorithms = {
- MD5: hashlib.md5,
- SHA1: hashlib.sha1,
- SHA224: hashlib.sha224,
- SHA256: hashlib.sha256,
- SHA384: hashlib.sha384,
- SHA512: hashlib.sha512,
- }
-
- # make sure the rounds exponent is coerced into integer form, since
- # it could also be passed as text (in decimal) for convenience
- rounds = int(rounds)
-
- # to avoid a potential resource consumption denial of service attack,
- # only consider values in the range of 0-16
- assert 0 <= rounds <= 16
-
- # here is where the salt is prepended to the provided password text
- hashed = salt + password
-
- # iterate the hashing algorithm over its own digest the specified
- # number of times
- for i in range(2 ** rounds):
- hashed = algorithms[algorithm](hashed.encode("utf-8")).digest()
- hashed = "".join(format(x, "02x") for x in bytes(hashed))
-
- # concatenate the output fields, coercing into text form as needed
- return "%s%s%s%s%s%s%s%s" % (
- sep, algorithm, sep, rounds, sep, salt, sep,
- _bytes_to_text(hashed.encode("ascii"))
- )
+def create(password):
+ return _CONTEXT.encrypt(password)
def verify(password, encoded_hash):
- """Verify a password:
-
- This simple function requires a text password and a mudpy-format
- password hash (as generated by the create function). It returns True
- if the password, hashed with the parameters from the encoded_hash,
- comes out the same as the encoded_hash.
- """
- sep = encoded_hash[0]
- algorithm, rounds, salt, hashed = encoded_hash.split(sep)[1:]
- if encoded_hash == create(
- password=password,
- salt=salt,
- sep=sep,
- algorithm=algorithm,
- rounds=rounds
- ):
- return True
- else:
- return False
+ return _CONTEXT.verify(password, encoded_hash)