Package mudpy :: Module password
[frames] | no frames]

Source Code for Module mudpy.password

  1  # -*- coding: utf-8 -*- 
  2  u"""Password hashing functions and constants for the mudpy engine.""" 
  3   
  4  # Copyright (c) 2004-2011 Jeremy Stanley <fungi@yuggoth.org>. Permission 
  5  # to use, copy, modify, and distribute this software is granted under 
  6  # terms provided in the LICENSE file distributed with this software. 
  7   
  8  # convenience constants for indexing the supported hashing algorithms, 
  9  # guaranteed a stable part of the interface 
 10  MD5 = 0 # hashlib.md5 
 11  SHA1 = 1 # hashlib.sha1 
 12  SHA224 = 2 # hashlib.sha224 
 13  SHA256 = 3 # hashlib.sha256 
 14  SHA384 = 4 # hashlib.sha385 
 15  SHA512 = 5 # hashlib.sha512 
 16   
17 -def _pack_bytes(numbers):
18 """ 19 This is a wrapper around struct.pack, used to turn a list of integers 20 between 0 and 255 into a packed sequence akin to a C-style string. 21 """ 22 import struct 23 # this will need to be declared as b"" during 2to3 migration 24 packed = "" 25 for number in numbers: 26 number = int(number) 27 assert 0 <= number <= 255 28 # need to use b"B" during 2to3 migration 29 packed += struct.pack("B", number) 30 return packed
31
32 -def _bytes_to_text(byte_sequence):
33 """ 34 This is a wrapper around base64.b64encode with preferences 35 appropriate for encoding Unix-style passwd hash strings. 36 """ 37 import base64 38 return base64.b64encode( 39 byte_sequence, 40 u"./".encode(u"ascii") 41 ).rstrip(u"=")
42
43 -def _generate_salt(salt_len=2):
44 """ 45 This simply generates a sequence of pseudo-random characters (with 46 6-bits of effective entropy per character). Since it relies on base64 47 encoding (which operates on 6-bit chunks of data), we only generate 48 0.75 times as many bytes (rounded up) as the number of characters we 49 need and discard any excess characters over the specified length. 50 This ensures full distribution over each character of the salt. 51 """ 52 import math, random 53 salt = [] 54 for i in xrange(int(math.ceil(salt_len*0.75))): 55 salt.append( random.randint(0,255) ) 56 return _bytes_to_text( _pack_bytes(salt) )[:salt_len]
57
58 -def upgrade_legacy_hash(legacy_hash, salt, sep=u"$"):
59 """ 60 This utility function is meant to provide a migration path for users 61 of mudpy's legacy account-name-salted MD5 hexdigest password hashes. 62 By passing the old passhash (as legacy_hash) and name (as salt) 63 facets to this function, a conforming new-style password hash will be 64 returned. 65 """ 66 import re 67 assert re.match(u"^[0-9a-f]{32}$", legacy_hash), "Not a valid MD5 hexdigest" 68 # this needs to be declared as b"" in 2to3 69 collapsed = "" 70 for i in xrange(16): 71 # this needs to become a byte() call in 2to3 72 collapsed += chr( int(legacy_hash[2*i:2*i+2], 16) ) 73 return u"%s%s%s%s%s%s%s%s" % ( 74 sep, 75 MD5, 76 sep, 77 0, # 2**0 provides one round of hashing 78 sep, 79 salt, 80 sep, 81 _bytes_to_text(collapsed) 82 )
83
84 -def create( 85 password, 86 salt=None, 87 algorithm=SHA1, 88 rounds=4, 89 salt_len=2, 90 sep=u"$" 91 ):
92 """ 93 The meat of the module, this function takes a provided password and 94 generates a Unix-like passwd hash suitable for storage in portable, 95 text-based data files. The password is prepended with a salt (which 96 can also be specified explicitly, if the output needs to be 97 repeatable) and then hashed with the requested algorithm iterated as 98 many times as 2 raised to the power of the rounds parameter. 99 100 The first character of the text returned by this function denotes the 101 separator character used to identify subsequent fields. The fields in 102 order are: 103 104 1. the decimal index number indicating which algorithm was used, 105 also mapped as convenience constants at the beginning of this 106 module 107 108 2. the number of times (as an exponent of 2) which the algorithm 109 was iterated, represented by a decimal value between 0 and 16 110 inclusive (0 results in one round, 16 results in 65536 rounds, 111 and anything higher than that is a potential resource 112 consumption denial of service on the application anyway) 113 114 3. the plain-text salt with which the password was prepended 115 before hashing 116 117 4. the resulting password hash itself, base64-encoded using . and 118 / as the two non-alpha-numeric characters required to reach 64 119 120 The defaults provided should be safe for everyday use, but something 121 more heavy-duty may be in order for admin users, such as:: 122 123 create(password, algorithm=SHA256, rounds=12, salt_len=16) 124 """ 125 import hashlib 126 127 # if a specific salt wasn't specified, we need to generate one 128 if not salt: 129 salt = _generate_salt(salt_len=salt_len) 130 131 # make sure the algorithm index number is coerced into integer form, 132 # since it could also be passed as text (in decimal) for convenience 133 algorithm = int(algorithm) 134 135 # the list of algorithms supported by this function corresponds to 136 # the convenience constants defined at the beginning of the module 137 algorithms = { 138 MD5: hashlib.md5, 139 SHA1: hashlib.sha1, 140 SHA224: hashlib.sha224, 141 SHA256: hashlib.sha256, 142 SHA384: hashlib.sha384, 143 SHA512: hashlib.sha512, 144 } 145 146 # make sure the rounds exponent is coerced into integer form, since 147 # it could also be passed as text (in decimal) for convenience 148 rounds = int(rounds) 149 150 # to avoid a potential resource consumption denial of service attack, 151 # only consider values in the range of 0-16 152 assert 0 <= rounds <= 16 153 154 # here is where the salt is prepended to the provided password text 155 hashed = salt+password 156 157 # iterate the hashing algorithm over its own digest the specified 158 # number of times 159 for i in xrange(2**rounds): 160 hashed = algorithms[algorithm](hashed).digest() 161 162 # concatenate the output fields, coercing into text form as needed 163 return u"%s%s%s%s%s%s%s%s" % ( 164 sep, algorithm, sep, rounds, sep, salt, sep, _bytes_to_text(hashed) 165 )
166
167 -def verify(password, encoded_hash):
168 """ 169 This simple function requires a text password and a mudpy-format 170 password hash (as generated by the create function). It returns True 171 if the password, hashed with the parameters from the encoded_hash, 172 comes out the same as the encoded_hash. 173 """ 174 sep = encoded_hash[0] 175 algorithm, rounds, salt, hashed = encoded_hash[1:].split(sep) 176 if encoded_hash == create( 177 password=password, 178 salt=salt, 179 sep=sep, 180 algorithm=algorithm, 181 rounds=rounds 182 ): 183 return True 184 else: 185 return False
186