|
1 # -*- coding: UTF-8 -*- |
|
2 # Copyright (c) 2010, Pascal Volk |
|
3 # See COPYING for distribution information. |
|
4 |
|
5 """ |
|
6 VirtualMailManager.password |
|
7 |
|
8 VirtualMailManager's password module to generate password hashes from |
|
9 passwords or random passwords. There are two functions: |
|
10 |
|
11 hashed_password = pwhash(password[, scheme][, user]) |
|
12 random_password = randompw() |
|
13 """ |
|
14 |
|
15 from crypt import crypt |
|
16 from random import choice, shuffle |
|
17 from subprocess import Popen, PIPE |
|
18 |
|
19 try: |
|
20 import hashlib |
|
21 except ImportError: |
|
22 from VirtualMailManager.pycompat import hashlib |
|
23 |
|
24 from VirtualMailManager import ENCODING, Configuration |
|
25 from VirtualMailManager.EmailAddress import EmailAddress |
|
26 from VirtualMailManager.common import get_unicode, version_str |
|
27 from VirtualMailManager.constants.ERROR import VMM_ERROR |
|
28 from VirtualMailManager.errors import VMMError |
|
29 |
|
30 COMPAT = hasattr(hashlib, 'compat') |
|
31 SALTCHARS = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' |
|
32 PASSWDCHARS = '._-+#*23456789abcdefghikmnopqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ' |
|
33 DEFAULT_B64 = (None, 'B64', 'BASE64') |
|
34 DEFAULT_HEX = (None, 'HEX') |
|
35 |
|
36 _ = lambda msg: msg |
|
37 _get_salt = lambda s_len: ''.join(choice(SALTCHARS) for x in xrange(s_len)) |
|
38 |
|
39 |
|
40 def _dovecotpw(password, scheme, encoding): |
|
41 """Communicates with dovecotpw (Dovecot 2.0: `doveadm pw`) and returns |
|
42 the hashed password: {scheme[.encoding]}hash |
|
43 """ |
|
44 if encoding: |
|
45 scheme = '.'.join((scheme, encoding)) |
|
46 cmd_args = [Configuration.dget('bin.dovecotpw'), '-s', scheme, '-p', |
|
47 get_unicode(password)] |
|
48 if Configuration.dget('misc.dovecot_version') >= 0x20000a01: |
|
49 cmd_args.insert(1, 'pw') |
|
50 process = Popen(cmd_args, stdout=PIPE, stderr=PIPE) |
|
51 stdout, stderr = process.communicate() |
|
52 if process.returncode: |
|
53 raise VMMError(stderr.strip(), VMM_ERROR) |
|
54 return stdout.strip() |
|
55 |
|
56 |
|
57 def _md4_new(): |
|
58 """Returns an new MD4-hash object if supported by the hashlib or |
|
59 provided by PyCrypto - other `None`. |
|
60 """ |
|
61 try: |
|
62 return hashlib.new('md4') |
|
63 except ValueError, err: |
|
64 if str(err) == 'unsupported hash type': |
|
65 if not COMPAT: |
|
66 try: |
|
67 from Crypto.Hash import MD4 |
|
68 return MD4.new() |
|
69 except ImportError: |
|
70 return None |
|
71 else: |
|
72 raise |
|
73 |
|
74 |
|
75 def _sha256_new(data=''): |
|
76 """Returns a new sha256 object from the hashlib. |
|
77 |
|
78 Returns `None` if the PyCrypto in pycompat.hashlib is too old.""" |
|
79 if not COMPAT: |
|
80 return hashlib.sha256(data) |
|
81 try: |
|
82 return hashlib.new('sha256', data) |
|
83 except ValueError, err: |
|
84 if str(err) == 'unsupported hash type': |
|
85 return None |
|
86 else: |
|
87 raise |
|
88 |
|
89 |
|
90 def _format_digest(digest, scheme, encoding): |
|
91 """Formats the arguments to a string: {scheme[.encoding]}digest.""" |
|
92 if not encoding: |
|
93 return '{%s}%s' % (scheme, digest) |
|
94 return '{%s.%s}%s' % (scheme, encoding, digest) |
|
95 |
|
96 |
|
97 def _clear_hash(password, scheme, encoding): |
|
98 """Generates a (encoded) CLEARTEXT/PLAIN 'hash'.""" |
|
99 if encoding: |
|
100 if encoding == 'HEX': |
|
101 password = password.encode('hex') |
|
102 else: |
|
103 password = password.encode('base64').replace('\n', '') |
|
104 return _format_digest(password, scheme, encoding) |
|
105 return get_unicode('{%s}%s' % (scheme, password)) |
|
106 |
|
107 |
|
108 def _crypt_hash(password, scheme, encoding): |
|
109 """Generates (encoded) CRYPT/MD5/MD5-CRYPT hashes.""" |
|
110 if scheme == 'CRYPT': |
|
111 salt = _get_salt(2) |
|
112 else: |
|
113 salt = '$1$%s$' % _get_salt(8) |
|
114 encrypted = crypt(password, salt) |
|
115 if encoding: |
|
116 if encoding == 'HEX': |
|
117 encrypted = encrypted.encode('hex') |
|
118 else: |
|
119 encrypted = encrypted.encode('base64').rstrip() |
|
120 return _format_digest(encrypted, scheme, encoding) |
|
121 |
|
122 |
|
123 def _md4_hash(password, scheme, encoding): |
|
124 """Generates encoded PLAIN-MD4 hashes.""" |
|
125 md4 = _md4_new() |
|
126 if md4: |
|
127 md4.update(password) |
|
128 if encoding in DEFAULT_HEX: |
|
129 digest = md4.hexdigest() |
|
130 else: |
|
131 digest = md4.digest().encode('base64').rstrip() |
|
132 return _format_digest(digest, scheme, encoding) |
|
133 return _dovecotpw(password, scheme, encoding) |
|
134 |
|
135 |
|
136 def _md5_hash(password, scheme, encoding, user=None): |
|
137 """Generates DIGEST-MD5 aka PLAIN-MD5 and LDAP-MD5 hashes.""" |
|
138 md5 = hashlib.md5() |
|
139 if scheme == 'DIGEST-MD5': |
|
140 # Prior to Dovecot v1.1.12/v1.2.beta2 there was a problem with a |
|
141 # empty auth_realms setting in dovecot.conf and user@domain.tld |
|
142 # usernames. So we have to generate different hashes for different |
|
143 # versions. See also: |
|
144 # http://dovecot.org/list/dovecot-news/2009-March/000103.html |
|
145 # http://hg.dovecot.org/dovecot-1.1/rev/2b0043ba89ae |
|
146 if Configuration.dget('misc.dovecot_version') >= 0x1010cf00: |
|
147 md5.update('%s:%s:' % (user.localpart, user.domainname)) |
|
148 else: |
|
149 md5.update('%s::' % user) |
|
150 md5.update(password) |
|
151 if (scheme in ('PLAIN-MD5', 'DIGEST-MD5') and encoding in DEFAULT_HEX) \ |
|
152 or (scheme == 'LDAP-MD5' and encoding == 'HEX'): |
|
153 digest = md5.hexdigest() |
|
154 else: |
|
155 digest = md5.digest().encode('base64').rstrip() |
|
156 return _format_digest(digest, scheme, encoding) |
|
157 |
|
158 |
|
159 def _ntlm_hash(password, scheme, encoding): |
|
160 """Generates NTLM hashes.""" |
|
161 md4 = _md4_new() |
|
162 if md4: |
|
163 password = ''.join('%s\x00' % c for c in password) |
|
164 md4.update(password) |
|
165 if encoding in DEFAULT_HEX: |
|
166 digest = md4.hexdigest() |
|
167 else: |
|
168 digest = md4.digest().encode('base64').rstrip() |
|
169 return _format_digest(digest, scheme, encoding) |
|
170 return _dovecotpw(password, scheme, encoding) |
|
171 |
|
172 |
|
173 def _sha1_hash(password, scheme, encoding): |
|
174 """Generates SHA1 aka SHA hashes.""" |
|
175 sha1 = hashlib.sha1(password) |
|
176 if encoding in DEFAULT_B64: |
|
177 digest = sha1.digest().encode('base64').rstrip() |
|
178 else: |
|
179 digest = sha1.hexdigest() |
|
180 return _format_digest(digest, scheme, encoding) |
|
181 |
|
182 |
|
183 def _sha256_hash(password, scheme, encoding): |
|
184 """Generates SHA256 hashes.""" |
|
185 sha256 = _sha256_new(password) |
|
186 if sha256: |
|
187 if encoding in DEFAULT_B64: |
|
188 digest = sha256.digest().encode('base64').rstrip() |
|
189 else: |
|
190 digest = sha256.hexdigest() |
|
191 return _format_digest(digest, scheme, encoding) |
|
192 return _dovecotpw(password, scheme, encoding) |
|
193 |
|
194 |
|
195 def _sha512_hash(password, scheme, encoding): |
|
196 """Generates SHA512 hashes.""" |
|
197 if not COMPAT: |
|
198 sha512 = hashlib.sha512(password) |
|
199 if encoding in DEFAULT_B64: |
|
200 digest = sha512.digest().encode('base64').replace('\n', '') |
|
201 else: |
|
202 digest = sha512.hexdigest() |
|
203 return _format_digest(digest, scheme, encoding) |
|
204 return _dovecotpw(password, scheme, encoding) |
|
205 |
|
206 |
|
207 def _smd5_hash(password, scheme, encoding): |
|
208 """Generates SMD5 (salted PLAIN-MD5) hashes.""" |
|
209 md5 = hashlib.md5(password) |
|
210 salt = _get_salt(4) |
|
211 md5.update(salt) |
|
212 if encoding in DEFAULT_B64: |
|
213 digest = (md5.digest() + salt).encode('base64').rstrip() |
|
214 else: |
|
215 digest = md5.hexdigest() + salt.encode('hex') |
|
216 return _format_digest(digest, scheme, encoding) |
|
217 |
|
218 |
|
219 def _ssha1_hash(password, scheme, encoding): |
|
220 """Generates SSHA (salted SHA/SHA1) hashes.""" |
|
221 sha1 = hashlib.sha1(password) |
|
222 salt = _get_salt(4) |
|
223 sha1.update(salt) |
|
224 if encoding in DEFAULT_B64: |
|
225 digest = (sha1.digest() + salt).encode('base64').rstrip() |
|
226 else: |
|
227 digest = sha1.hexdigest() + salt.encode('hex') |
|
228 return _format_digest(digest, scheme, encoding) |
|
229 |
|
230 |
|
231 def _ssha256_hash(password, scheme, encoding): |
|
232 """Generates SSHA256 (salted SHA256) hashes.""" |
|
233 sha256 = _sha256_new(password) |
|
234 if sha256: |
|
235 salt = _get_salt(4) |
|
236 sha256.update(salt) |
|
237 if encoding in DEFAULT_B64: |
|
238 digest = (sha256.digest() + salt).encode('base64').rstrip() |
|
239 else: |
|
240 digest = sha256.hexdigest() + salt.encode('hex') |
|
241 return _format_digest(digest, scheme, encoding) |
|
242 return _dovecotpw(password, scheme, encoding) |
|
243 |
|
244 |
|
245 def _ssha512_hash(password, scheme, encoding): |
|
246 """Generates SSHA512 (salted SHA512) hashes.""" |
|
247 if not COMPAT: |
|
248 salt = _get_salt(4) |
|
249 sha512 = hashlib.sha512(password + salt) |
|
250 if encoding in DEFAULT_B64: |
|
251 digest = (sha512.digest() + salt).encode('base64').replace('\n', |
|
252 '') |
|
253 else: |
|
254 digest = sha512.hexdigest() + salt.encode('hex') |
|
255 return _format_digest(digest, scheme, encoding) |
|
256 return _dovecotpw(password, scheme, encoding) |
|
257 |
|
258 _scheme_info = { |
|
259 'CLEARTEXT': (_clear_hash, 0x10000f00), |
|
260 'CRAM-MD5': (_dovecotpw, 0x10000f00), |
|
261 'CRYPT': (_crypt_hash, 0x10000f00), |
|
262 'DIGEST-MD5': (_md5_hash, 0x10000f00), |
|
263 'HMAC-MD5': (_dovecotpw, 0x10000f00), |
|
264 'LANMAN': (_dovecotpw, 0x10000f00), |
|
265 'LDAP-MD5': (_md5_hash, 0x10000f00), |
|
266 'MD5': (_crypt_hash, 0x10000f00), |
|
267 'MD5-CRYPT': (_crypt_hash, 0x10000f00), |
|
268 'NTLM': (_ntlm_hash, 0x10000f00), |
|
269 'OTP': (_dovecotpw, 0x10100a01), |
|
270 'PLAIN': (_clear_hash, 0x10000f00), |
|
271 'PLAIN-MD4': (_md4_hash, 0x10000f00), |
|
272 'PLAIN-MD5': (_md5_hash, 0x10000f00), |
|
273 'RPA': (_dovecotpw, 0x10000f00), |
|
274 'SHA': (_sha1_hash, 0x10000f00), |
|
275 'SHA1': (_sha1_hash, 0x10000f00), |
|
276 'SHA256': (_sha256_hash, 0x10100a01), |
|
277 'SHA512': (_sha512_hash, 0x20000b03), |
|
278 'SKEY': (_dovecotpw, 0x10100a01), |
|
279 'SMD5': (_smd5_hash, 0x10000f00), |
|
280 'SSHA': (_ssha1_hash, 0x10000f00), |
|
281 'SSHA256': (_ssha256_hash, 0x10200a04), |
|
282 'SSHA512': (_ssha512_hash, 0x20000b03), |
|
283 } |
|
284 |
|
285 |
|
286 def pwhash(password, scheme=None, user=None): |
|
287 """Generates a password hash from the plain text *password* string. |
|
288 |
|
289 If no *scheme* is given the password scheme from the configuration will |
|
290 be used for the hash generation. When 'DIGEST-MD5' is used as scheme, |
|
291 also an EmailAddress instance must be given as *user* argument. |
|
292 """ |
|
293 assert Configuration is not None |
|
294 if not isinstance(password, basestring): |
|
295 raise TypeError('Password is not a string: %r' % password) |
|
296 if isinstance(password, unicode): |
|
297 password = password.encode(ENCODING) |
|
298 password = password.strip() |
|
299 if not password: |
|
300 raise ValueError("Couldn't accept empty password.") |
|
301 if scheme is None: |
|
302 scheme = Configuration.dget('misc.password_scheme') |
|
303 scheme_encoding = scheme.split('.') |
|
304 scheme = scheme_encoding[0].upper() |
|
305 if not scheme in _scheme_info: |
|
306 raise VMMError(_(u"Unsupported password scheme: '%s'") % scheme, |
|
307 VMM_ERROR) |
|
308 if Configuration.dget('misc.dovecot_version') < _scheme_info[scheme][1]: |
|
309 raise VMMError(_(u"The scheme '%s' requires Dovecot >= v%s") % |
|
310 (scheme, version_str(_scheme_info[scheme][1])), |
|
311 VMM_ERROR) |
|
312 if len(scheme_encoding) > 1: |
|
313 if Configuration.dget('misc.dovecot_version') < 0x10100a01: |
|
314 raise VMMError(_(u'Encoding suffixes for password schemes require \ |
|
315 Dovecot >= v1.1.alpha1'), |
|
316 VMM_ERROR) |
|
317 if scheme_encoding[1].upper() not in ('B64', 'BASE64', 'HEX'): |
|
318 raise ValueError('Unsupported encoding: %r' % scheme_encoding[1]) |
|
319 encoding = scheme_encoding[1].upper() |
|
320 else: |
|
321 encoding = None |
|
322 if scheme == 'DIGEST-MD5': |
|
323 assert isinstance(user, EmailAddress) |
|
324 return _md5_hash(password, scheme, encoding, user) |
|
325 return _scheme_info[scheme][0](password, scheme, encoding) |
|
326 |
|
327 |
|
328 def randompw(): |
|
329 """Generates a plain text random password. |
|
330 |
|
331 The length of the password can be configured in the ``vmm.cfg`` |
|
332 (account.password_length). |
|
333 """ |
|
334 assert Configuration is not None |
|
335 pw_chars = list(PASSWDCHARS) |
|
336 shuffle(pw_chars) |
|
337 pw_len = Configuration.dget('account.password_length') |
|
338 if pw_len < 8: |
|
339 pw_len = 8 |
|
340 return ''.join(choice(pw_chars) for x in xrange(pw_len)) |
|
341 |
|
342 del _ |