Target Python 2.6. Make the test suite pass without deprecations.
[mailman.git] / mailman / passwords.py
blob1e46cd42efd4946ee47160a6ce873acbf9e8f032
1 # Copyright (C) 2007-2008 by the Free Software Foundation, Inc.
3 # This file is part of GNU Mailman.
5 # GNU Mailman is free software: you can redistribute it and/or modify it under
6 # the terms of the GNU General Public License as published by the Free
7 # Software Foundation, either version 3 of the License, or (at your option)
8 # any later version.
10 # GNU Mailman is distributed in the hope that it will be useful, but WITHOUT
11 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 # more details.
15 # You should have received a copy of the GNU General Public License along with
16 # GNU Mailman. If not, see <http://www.gnu.org/licenses/>.
18 """Password hashing and verification schemes.
20 Represents passwords using RFC 2307 syntax (as best we can tell).
21 """
23 import os
24 import re
25 import hmac
26 import hashlib
28 from array import array
29 from base64 import urlsafe_b64decode as decode
30 from base64 import urlsafe_b64encode as encode
31 from munepy import Enum
33 from mailman.core import errors
35 SALT_LENGTH = 20 # bytes
36 ITERATIONS = 2000
38 __all__ = [
39 'Schemes',
40 'make_secret',
41 'check_response',
46 class PasswordScheme(object):
47 TAG = ''
49 @staticmethod
50 def make_secret(password):
51 """Return the hashed password"""
52 raise NotImplementedError
54 @staticmethod
55 def check_response(challenge, response):
56 """Return True if response matches challenge.
58 It is expected that the scheme specifier prefix is already stripped
59 from the response string.
60 """
61 raise NotImplementedError
65 class NoPasswordScheme(PasswordScheme):
66 TAG = 'NONE'
68 @staticmethod
69 def make_secret(password):
70 return ''
72 @staticmethod
73 def check_response(challenge, response):
74 return False
78 class ClearTextPasswordScheme(PasswordScheme):
79 TAG = 'CLEARTEXT'
81 @staticmethod
82 def make_secret(password):
83 return password
85 @staticmethod
86 def check_response(challenge, response):
87 return challenge == response
91 class SHAPasswordScheme(PasswordScheme):
92 TAG = 'SHA'
94 @staticmethod
95 def make_secret(password):
96 h = hashlib.sha1(password)
97 return encode(h.digest())
99 @staticmethod
100 def check_response(challenge, response):
101 h = hashlib.sha1(response)
102 return challenge == encode(h.digest())
106 class SSHAPasswordScheme(PasswordScheme):
107 TAG = 'SSHA'
109 @staticmethod
110 def make_secret(password):
111 salt = os.urandom(SALT_LENGTH)
112 h = hashlib.sha1(password)
113 h.update(salt)
114 return encode(h.digest() + salt)
116 @staticmethod
117 def check_response(challenge, response):
118 # Get the salt from the challenge
119 challenge_bytes = decode(challenge)
120 digest = challenge_bytes[:20]
121 salt = challenge_bytes[20:]
122 h = hashlib.sha1(response)
123 h.update(salt)
124 return digest == h.digest()
128 # Basic algorithm given by Bob Fleck
129 class PBKDF2PasswordScheme(PasswordScheme):
130 # This is a bit nasty if we wanted a different prf or iterations. OTOH,
131 # we really have no clue what the standard LDAP-ish specification for
132 # those options is.
133 TAG = 'PBKDF2 SHA %d' % ITERATIONS
135 @staticmethod
136 def _pbkdf2(password, salt, iterations):
137 """From RFC2898 sec. 5.2. Simplified to handle only 20 byte output
138 case. Output of 20 bytes means always exactly one block to handle,
139 and a constant block counter appended to the salt in the initial hmac
140 update.
142 h = hmac.new(password, None, hashlib.sha1)
143 prf = h.copy()
144 prf.update(salt + '\x00\x00\x00\x01')
145 T = U = array('l', prf.digest())
146 while iterations:
147 prf = h.copy()
148 prf.update(U.tostring())
149 U = array('l', prf.digest())
150 T = array('l', (t ^ u for t, u in zip(T, U)))
151 iterations -= 1
152 return T.tostring()
154 @staticmethod
155 def make_secret(password):
156 """From RFC2898 sec. 5.2. Simplified to handle only 20 byte output
157 case. Output of 20 bytes means always exactly one block to handle,
158 and a constant block counter appended to the salt in the initial hmac
159 update.
161 salt = os.urandom(SALT_LENGTH)
162 digest = PBKDF2PasswordScheme._pbkdf2(password, salt, ITERATIONS)
163 derived_key = encode(digest + salt)
164 return derived_key
166 @staticmethod
167 def check_response(challenge, response, prf, iterations):
168 # Decode the challenge to get the number of iterations and salt
169 # XXX we don't support anything but sha prf
170 if prf.lower() <> 'sha':
171 return False
172 try:
173 iterations = int(iterations)
174 except (ValueError, TypeError):
175 return False
176 challenge_bytes = decode(challenge)
177 digest = challenge_bytes[:20]
178 salt = challenge_bytes[20:]
179 key = PBKDF2PasswordScheme._pbkdf2(response, salt, iterations)
180 return digest == key
184 class Schemes(Enum):
185 # no_scheme is deliberately ugly because no one should be using it. Yes,
186 # this makes cleartext inconsistent, but that's a common enough
187 # terminology to justify the missing underscore.
188 no_scheme = 1
189 cleartext = 2
190 sha = 3
191 ssha = 4
192 pbkdf2 = 5
195 _SCHEMES_BY_ENUM = {
196 Schemes.no_scheme : NoPasswordScheme,
197 Schemes.cleartext : ClearTextPasswordScheme,
198 Schemes.sha : SHAPasswordScheme,
199 Schemes.ssha : SSHAPasswordScheme,
200 Schemes.pbkdf2 : PBKDF2PasswordScheme,
204 # Some scheme tags have arguments, but the key for this dictionary should just
205 # be the lowercased scheme name.
206 _SCHEMES_BY_TAG = dict((_SCHEMES_BY_ENUM[e].TAG.split(' ')[0].lower(), e)
207 for e in _SCHEMES_BY_ENUM)
209 _DEFAULT_SCHEME = NoPasswordScheme
213 def make_secret(password, scheme=None):
214 # The hash algorithms operate on bytes not strings. The password argument
215 # as provided here by the client will be a string (in Python 2 either
216 # unicode or 8-bit, in Python 3 always unicode). We need to encode this
217 # string into a byte array, and the way to spell that in Python 2 is to
218 # encode the string to utf-8. The returned secret is a string, so it must
219 # be a unicode.
220 if isinstance(password, unicode):
221 password = password.encode('utf-8')
222 scheme_class = _SCHEMES_BY_ENUM.get(scheme)
223 if not scheme_class:
224 raise errors.BadPasswordSchemeError(scheme)
225 secret = scheme_class.make_secret(password)
226 return '{%s}%s' % (scheme_class.TAG, secret)
229 def check_response(challenge, response):
230 mo = re.match(r'{(?P<scheme>[^}]+?)}(?P<rest>.*)',
231 challenge, re.IGNORECASE)
232 if not mo:
233 return False
234 # See above for why we convert here. However because we should have
235 # generated the challenge, we assume that it is already a byte string.
236 if isinstance(response, unicode):
237 response = response.encode('utf-8')
238 scheme_group, rest_group = mo.group('scheme', 'rest')
239 scheme_parts = scheme_group.split()
240 scheme = scheme_parts[0].lower()
241 scheme_enum = _SCHEMES_BY_TAG.get(scheme, _DEFAULT_SCHEME)
242 scheme_class = _SCHEMES_BY_ENUM[scheme_enum]
243 if isinstance(rest_group, unicode):
244 rest_group = rest_group.encode('utf-8')
245 return scheme_class.check_response(rest_group, response, *scheme_parts[1:])
248 def lookup_scheme(scheme_name):
249 return _SCHEMES_BY_TAG.get(scheme_name.lower())