Switch password handler to passlib's PBKDF2
authorJeremy Stanley <fungi@yuggoth.org>
Wed, 18 Feb 2015 03:35:59 +0000 (03:35 +0000)
committerJeremy Stanley <fungi@yuggoth.org>
Wed, 18 Feb 2015 03:35:59 +0000 (03:35 +0000)
The passlib implementation of PBKDF2 is strong, portable and more
heavily audited. Use that instead of implementing our own custom
handler. Also simplify password use by dropping optional parameters
from the create and verify functions, and don't bother carrying the
old upgrade_legacy_hash public function forward; it can be reworked
to provide in-place hash upgrades later if desired.

lib/mudpy/password.py
requirements.txt

index 2e37b91..a34dab9 100644 (file)
 # 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)
index c3726e8..90396eb 100644 (file)
@@ -1 +1,2 @@
+passlib
 pyyaml