Update authentication to use PhpBB version 3.
[matthijs/projects/dorestad-bookings.git] / tools / phpass / __init__.py
1 #!/usr/bin/env python
2
3 # phpass version: 0.3 / genuine.
4
5 # Placed in public domain
6
7
8 #CHECK: use pyDES instead of the native crypt module?
9
10 import os
11 import time
12 import hashlib
13 import crypt
14
15
16 try:
17     import bcrypt
18     _bcrypt_hashpw = bcrypt.hashpw
19 except ImportError:
20     _bcrypt_hashpw = None
21
22 # On App Engine, this function is not available.
23 if hasattr(os, 'getpid'):
24     _pid = os.getpid()
25 else:
26     # Fake PID
27     import random
28     _pid = random.randint(0, 100000)
29
30
31 class PasswordHash:
32     
33     def __init__(self, iteration_count_log2=8, portable_hashes=True, 
34          algorithm=''):
35         alg = algorithm.lower()
36         if (alg == 'blowfish' or alg == 'bcrypt') and _bcrypt_hashpw is None:
37             raise NotImplementedError('The bcrypt module is required')
38         self.itoa64 = \
39             './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
40         if iteration_count_log2 < 4 or iteration_count_log2 > 31:
41             iteration_count_log2 = 8
42         self.iteration_count_log2 = iteration_count_log2
43         self.portable_hashes = portable_hashes
44         self.algorithm = algorithm
45         self.random_state = '%r%r' % (time.time(), _pid)
46     
47     def get_random_bytes(self, count):
48         outp = ''
49         try:
50             outp = os.urandom(count)
51         except:
52             pass
53         if len(outp) < count:
54             outp = ''
55             rem = count
56             while rem > 0:
57                 self.random_state = hashlib.md5(str(time.time()) 
58                     + self.random_state).hexdigest()
59                 outp += hashlib.md5(self.random_state).digest()
60                 rem -= 1
61             outp = outp[:count]
62         return outp
63     
64     def encode64(self, inp, count):
65         outp = ''
66         cur = 0
67         while cur < count:
68             value = ord(inp[cur])
69             cur += 1
70             outp += self.itoa64[value & 0x3f]
71             if cur < count:
72                 value |= (ord(inp[cur]) << 8)
73             outp += self.itoa64[(value >> 6) & 0x3f]
74             if cur >= count:
75                 break
76             cur += 1
77             if cur < count:
78                 value |= (ord(inp[cur]) << 16)
79             outp += self.itoa64[(value >> 12) & 0x3f]
80             if cur >= count:
81                 break
82             cur += 1
83             outp += self.itoa64[(value >> 18) & 0x3f]
84         return outp
85     
86     def gensalt_private(self, inp):
87         outp = '$P$'
88         outp += self.itoa64[min([self.iteration_count_log2 + 5, 30])]
89         outp += self.encode64(inp, 6)
90         return outp
91     
92     def crypt_private(self, pw, setting):
93         outp = '*0'
94         if setting.startswith(outp):
95             outp = '*1'
96         if not setting.startswith('$P$') and not setting.startswith('$H$'):
97             return outp
98         count_log2 = self.itoa64.find(setting[3])
99         if count_log2 < 7 or count_log2 > 30:
100             return outp
101         count = 1 << count_log2
102         salt = setting[4:12]
103         if len(salt) != 8:
104             return outp
105         if not isinstance(pw, str):
106             pw = pw.encode('utf-8')
107         hx = hashlib.md5(salt + pw).digest()
108         while count:
109             hx = hashlib.md5(hx + pw).digest()
110             count -= 1
111         return setting[:12] + self.encode64(hx, 16)
112     
113     def gensalt_extended(self, inp):
114         count_log2 = min([self.iteration_count_log2 + 8, 24])
115         count = (1 << count_log2) - 1
116         outp = '_'
117         outp += self.itoa64[count & 0x3f]
118         outp += self.itoa64[(count >> 6) & 0x3f]
119         outp += self.itoa64[(count >> 12) & 0x3f]
120         outp += self.itoa64[(count >> 18) & 0x3f]
121         outp += self.encode64(inp, 3)
122         return outp
123     
124     def gensalt_blowfish(self, inp):
125         itoa64 = \
126             './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
127         outp = '$2a$'
128         outp += chr(ord('0') + self.iteration_count_log2 / 10)
129         outp += chr(ord('0') + self.iteration_count_log2 % 10)
130         outp += '$'
131         cur = 0
132         while True:
133             c1 = ord(inp[cur])
134             cur += 1
135             outp += itoa64[c1 >> 2]
136             c1 = (c1 & 0x03) << 4
137             if cur >= 16:
138                 outp += itoa64[c1]
139                 break
140             c2 = ord(inp[cur])
141             cur += 1
142             c1 |= c2 >> 4
143             outp += itoa64[c1]
144             c1 = (c2 & 0x0f) << 2
145             c2 = ord(inp[cur])
146             cur += 1
147             c1 |= c2 >> 6
148             outp += itoa64[c1]
149             outp += itoa64[c2 & 0x3f]
150         return outp
151     
152     def hash_password(self, pw):
153         rnd = ''
154         alg = self.algorithm.lower()
155         if (not alg or alg == 'blowfish' or alg == 'bcrypt') \
156              and not self.portable_hashes:
157             if _bcrypt_hashpw is None:
158                 if (alg == 'blowfish' or alg == 'bcrypt'):
159                     raise NotImplementedError('The bcrypt module is required')
160             else:
161                 rnd = self.get_random_bytes(16)
162                 salt = self.gensalt_blowfish(rnd)
163                 hx = _bcrypt_hashpw(pw, salt)
164                 if len(hx) == 60:
165                     return hx
166         if (not alg or alg == 'ext-des') and not self.portable_hashes:
167             if len(rnd) < 3:
168                 rnd = self.get_random_bytes(3)
169             hx = crypt.crypt(pw, self.gensalt_extended(rnd))
170             if len(hx) == 20:
171                 return hx
172         if len(rnd) < 6:
173             rnd = self.get_random_bytes(6)
174         hx = self.crypt_private(pw, self.gensalt_private(rnd))
175         if len(hx) == 34:
176             return hx
177         return '*'
178     
179     def check_password(self, pw, stored_hash):
180         # This part is different with the original PHP
181         if stored_hash.startswith('$2a$'):
182             # bcrypt
183             if _bcrypt_hashpw is None:
184                 raise NotImplementedError('The bcrypt module is required')
185             hx = _bcrypt_hashpw(pw, stored_hash)
186         elif stored_hash.startswith('_'):
187             # ext-des
188             hx = crypt.crypt(pw, stored_hash)
189         else:
190             # portable hash
191             hx = self.crypt_private(pw, stored_hash)
192         return hx == stored_hash
193     
194