Switch password handler to passlib's PBKDF2
[mudpy.git] / lib / mudpy / password.py
index 29fc5d9..a34dab9 100644 (file)
-# -*- coding: utf-8 -*-
-u"""Password hashing functions and constants for the mudpy engine."""
+"""Password hashing functions and constants for the mudpy engine."""
 
-# Copyright (c) 2004-2010 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
 
-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
-   # this will need to be declared as b"" during 2to3 migration
-   packed = ""
-   for number in numbers:
-      number = int(number)
-      assert 0 <= number <= 255
-      # need to use b"B" during 2to3 migration
-      packed += struct.pack("B", number)
-   return packed
+_CONTEXT = passlib.context.CryptContext(
+    all__vary_rounds=0.1, default="pbkdf2_sha512",
+    pbkdf2_sha512__default_rounds=1000, schemes=["pbkdf2_sha512"])
 
-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,
-      u"./".encode(u"ascii")
-   ).rstrip(u"=")
 
-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, random
-   salt = []
-   for i in xrange(int(math.ceil(salt_len*0.75))):
-      salt.append( random.randint(0,255) )
-   return _bytes_to_text( _pack_bytes(salt) )[:salt_len]
+def create(password):
+    return _CONTEXT.encrypt(password)
 
-def upgrade_legacy_hash(legacy_hash, salt, sep=u"$"):
-   """
-   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(u"^[0-9a-f]{32}$", legacy_hash), "Not a valid MD5 hexdigest"
-   # this needs to be declared as b"" in 2to3
-   collapsed = ""
-   for i in xrange(16):
-      # this needs to become a byte() call in 2to3
-      collapsed += chr( int(legacy_hash[2*i:2*i+2], 16) )
-   return u"%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=u"$"
-):
-   """
-   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 xrange(2**rounds):
-      hashed = algorithms[algorithm](hashed).digest()
-
-   # concatenate the output fields, coercing into text form as needed
-   return u"%s%s%s%s%s%s%s%s" % (
-      sep, algorithm, sep, rounds, sep, salt, sep, _bytes_to_text(hashed)
-   )
 
 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]
-   algorithm, rounds, salt, hashed = encoded_hash[1:].split(sep)
-   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)