def handler_checking_password(user):
u"""Handle the login account password."""
- import md5
+ import password
# get the next waiting line of input
input_data = user.input_queue.pop(0)
# does the hashed input equal the stored hash?
- if unicode(
- md5.new(
- ( user.account.get(u"name") + input_data ).encode(u"utf-8")
- ).hexdigest()
- ) == user.account.get(u"passhash"):
+ if password.verify( input_data, user.account.get(u"passhash") ):
# if so, set the username and load from cold storage
if not user.replace_old_connections():
def handler_entering_new_password(user):
u"""Handle a new password entry."""
- import md5
+ import password
# get the next waiting line of input
input_data = user.input_queue.pop(0)
):
# hash and store it, then move on to verification
- user.account.set(
- u"passhash",
- unicode(
- md5.new(
- ( user.account.get(u"name") + input_data ).encode(u"utf-8")
- ).hexdigest()
- )
- )
+ user.account.set( u"passhash", password.create(input_data) )
user.state = u"verifying_new_password"
# the password was weak, try again if you haven't tried too many times
def handler_verifying_new_password(user):
u"""Handle the re-entered new password for verification."""
- import md5
+ import password
# get the next waiting line of input
input_data = user.input_queue.pop(0)
# hash the input and match it to storage
- if unicode(
- md5.new(
- ( user.account.get(u"name") + input_data ).encode(u"utf-8")
- ).hexdigest()
- ) == user.account.get(u"passhash"):
+ if password.verify( input_data, user.account.get(u"passhash") ):
user.authenticate()
# the hashes matched, so go active
--- /dev/null
+# -*- coding: utf-8 -*-
+u"""Password hashing functions and constants for the mudpy engine."""
+
+# Copyright (c) 2004-2010 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
+
+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
+
+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 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 denoting 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 for
+ 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
+