Switch password handler to passlib's PBKDF2
[mudpy.git] / lib / mudpy / password.py
index bad5579..a34dab9 100644 (file)
-# -*- coding: utf-8 -*-
 """Password hashing functions and constants for the mudpy engine."""
 
-# Copyright (c) 2004-2013 Jeremy Stanley <fungi@yuggoth.org>. Permission
+# Copyright (c) 2004-2015 Jeremy Stanley <fungi@yuggoth.org>. Permission
 # to use, copy, modify, and distribute this software is granted under
 # terms provided in the LICENSE file distributed with this software.
 
-# 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
+import passlib.context
 
+_CONTEXT = passlib.context.CryptContext(
+    all__vary_rounds=0.1, default="pbkdf2_sha512",
+    pbkdf2_sha512__default_rounds=1000, schemes=["pbkdf2_sha512"])
 
-def _pack_bytes(numbers):
-    """
-    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.
-    """
-    import struct
-    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):
-    """
-    This is a wrapper around base64.b64encode with preferences
-    appropriate for encoding Unix-style passwd hash strings.
-    """
-    import base64
-    return base64.b64encode(
-        byte_sequence,
-        b"./"
-    ).decode("ascii").rstrip("=")
-
-
-def _generate_salt(salt_len=2):
-    """
-    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.
-    """
-    import math
-    import random
-    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="$"):
-    """
-    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.
-    """
-    import re
-    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="$"
-):
-    """
-    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)
-    """
-    import hashlib
-
-    # 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()
-        # TODO: remove this check after the switch to py3k
-        try:
-            hashed = "".join(format(x, "02x") for x in bytes(hashed))
-        except ValueError:
-            hashed = "".join(format(ord(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):
-    """
-    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]
-    import mudpy.misc
-    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)