1
2 u"""Password hashing functions and constants for the mudpy engine."""
3
4
5
6
7
8
9
10 MD5 = 0
11 SHA1 = 1
12 SHA224 = 2
13 SHA256 = 3
14 SHA384 = 4
15 SHA512 = 5
16
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
24 packed = ""
25 for number in numbers:
26 number = int(number)
27 assert 0 <= number <= 255
28
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
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
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
69 collapsed = ""
70 for i in xrange(16):
71
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,
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
128 if not salt:
129 salt = _generate_salt(salt_len=salt_len)
130
131
132
133 algorithm = int(algorithm)
134
135
136
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
147
148 rounds = int(rounds)
149
150
151
152 assert 0 <= rounds <= 16
153
154
155 hashed = salt+password
156
157
158
159 for i in xrange(2**rounds):
160 hashed = algorithms[algorithm](hashed).digest()
161
162
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