X-Git-Url: https://mudpy.org/gitweb?p=mudpy.git;a=blobdiff_plain;f=lib%2Fmudpy%2Fpassword.py;fp=lib%2Fmudpy%2Fpassword.py;h=258855ac031a0de3f08daa4b6e5e57bfc6781cda;hp=0000000000000000000000000000000000000000;hb=520cbd3c71a1c3a90fc4425c400f4bb4572890a8;hpb=e79cdd1520524f3c90a3e0be5a1d5164afedbaae diff --git a/lib/mudpy/password.py b/lib/mudpy/password.py new file mode 100644 index 0000000..258855a --- /dev/null +++ b/lib/mudpy/password.py @@ -0,0 +1,185 @@ +# -*- coding: utf-8 -*- +u"""Password hashing functions and constants for the mudpy engine.""" + +# Copyright (c) 2004-2010 Jeremy Stanley . 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 +