diff options
Diffstat (limited to 'md5.py')
-rwxr-xr-x | md5.py | 134 |
1 files changed, 134 insertions, 0 deletions
@@ -0,0 +1,134 @@ | |||
1 | #!/usr/bin/python | ||
2 | """ | ||
3 | Apache Portable Runtime (APR) MD5 Calculation | ||
4 | by Mike Crute on July 12, 2008 | ||
5 | for SoftGroup Interactive, Inc. | ||
6 | Released under the terms of the BSD license. | ||
7 | |||
8 | This is basically a feature-complete implementation of the MD5 | ||
9 | algorithm used by the APR, Apache and the htpasswd tool. APR | ||
10 | takes a different approach to generating MD5 sums which is a | ||
11 | little bit wierd. | ||
12 | |||
13 | This is a pythonic adaption of the C code in the APR library. | ||
14 | If there are any questions please refer directly to the C code. | ||
15 | You'll find it in apache subversion under | ||
16 | /ap/apr-util/crypto/apr_md5.c | ||
17 | """ | ||
18 | |||
19 | import string | ||
20 | from hashlib import md5 | ||
21 | from random import random | ||
22 | from math import floor | ||
23 | |||
24 | __all__ = [ "generate_md5", "generate_salt", "generate_short_salt" ] | ||
25 | |||
26 | # Defined as such for brevity | ||
27 | md5digest = lambda x: md5(x).digest() | ||
28 | |||
29 | def ap_to64(input, count=4): | ||
30 | """Weird-ass implementation of base64 conversion used by the | ||
31 | APR library. | ||
32 | """ | ||
33 | chars = "./0123456789%s%s" % (string.uppercase, string.lowercase) | ||
34 | output = "" | ||
35 | input = int(input) # Need ints to do binary math | ||
36 | |||
37 | for i in range(0, count): | ||
38 | output += chars[input & 0x3f] # Take 6 bits right | ||
39 | input >>= 6 # shift by 6 bits | ||
40 | |||
41 | return output | ||
42 | |||
43 | def generate_short_salt(): | ||
44 | """Generate a short, 2-character salt. | ||
45 | This is suitable for use in the htpasswd crypt routine. | ||
46 | """ | ||
47 | return generate_salt()[0:2] | ||
48 | |||
49 | def generate_salt(): | ||
50 | """Mine a little salt for your passwords. | ||
51 | Returns 8 random characters in base64. It is 4 + 4 because the original | ||
52 | implmentation was in C and they wanted it it fit nicely in an integer. | ||
53 | """ | ||
54 | salt = ap_to64(floor(random() * 16777215)) | ||
55 | salt += ap_to64(floor(random() * 16777215)) | ||
56 | return salt | ||
57 | |||
58 | def generate_md5(passwd, salt=None): | ||
59 | """Generate an APRfied MD5 hash. | ||
60 | This was adapted directly from the C code in the APR library. | ||
61 | It was made more pythonic where possible but please reference | ||
62 | the APR code for a better understanding of what's going on. | ||
63 | """ | ||
64 | # I know not what this means or how it may change in the future | ||
65 | # (well OK, its the APR version that generated the hash) and | ||
66 | # I don't think it is really "checked" by Apache but I'm too | ||
67 | # lazy to confirm this. | ||
68 | MAGIC_TOKEN = "$apr1$" | ||
69 | |||
70 | # Mainly just used for testing but why not leave it? | ||
71 | salt = salt if salt else generate_salt() | ||
72 | |||
73 | # Start with our password in the clear, a little magic and | ||
74 | # a pinch of salt | ||
75 | message = "%s%s%s" % (passwd, MAGIC_TOKEN, salt) | ||
76 | |||
77 | # Then just as many characters of the MD5(pw, salt, pw) | ||
78 | retval = md5digest(passwd + salt + passwd) | ||
79 | passlen = len(passwd) | ||
80 | while passlen > 0: | ||
81 | end = 16 if passlen > 16 else passlen | ||
82 | message += retval[0:end] | ||
83 | passlen -= 16 | ||
84 | |||
85 | # Then something really wierd | ||
86 | passlen = len(passwd) | ||
87 | while passlen != 0: | ||
88 | if passlen & 1: | ||
89 | message += chr(0) | ||
90 | else: | ||
91 | message += passwd[0] | ||
92 | passlen >>= 1 | ||
93 | |||
94 | retval = md5digest(message) | ||
95 | |||
96 | for i in range(0, 1000): | ||
97 | if i & 1: | ||
98 | message = passwd | ||
99 | else: | ||
100 | message = retval[0:16] | ||
101 | |||
102 | if i % 3: | ||
103 | message += salt | ||
104 | |||
105 | if i % 7: | ||
106 | message += passwd | ||
107 | |||
108 | if i & 1: | ||
109 | message += retval[0:16] | ||
110 | else: | ||
111 | message += passwd | ||
112 | |||
113 | retval = md5digest(message) | ||
114 | |||
115 | # Now make the output string | ||
116 | output = ap_to64((ord(retval[0]) << 16) | (ord(retval[6]) << 8) | ord(retval[12])) | ||
117 | output += ap_to64((ord(retval[1]) << 16) | (ord(retval[7]) << 8) | ord(retval[13])) | ||
118 | output += ap_to64((ord(retval[2]) << 16) | (ord(retval[8]) << 8) | ord(retval[14])) | ||
119 | output += ap_to64((ord(retval[3]) << 16) | (ord(retval[9]) << 8) | ord(retval[15])) | ||
120 | output += ap_to64((ord(retval[4]) << 16) | (ord(retval[10]) << 8) | ord(retval[5])) | ||
121 | output += ap_to64(ord(retval[11]), 2) | ||
122 | |||
123 | return "%s%s$%s" % (MAGIC_TOKEN, salt, output) | ||
124 | |||
125 | def test_driver(): | ||
126 | """Run this to verify that the library is functioning properly. | ||
127 | This one test case should be enough to verify the functionality. | ||
128 | """ | ||
129 | output = generate_md5("test", "yTbof...") | ||
130 | assert output == "$apr1$yTbof...$r3r2AZWwYNbWRfNmLfrEh1" | ||
131 | print "Passed the test!" | ||
132 | |||
133 | if __name__ == "__main__": | ||
134 | test_driver() | ||