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)
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
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).
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
46 class PasswordScheme(object):
50 def make_secret(password
):
51 """Return the hashed password"""
52 raise NotImplementedError
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.
61 raise NotImplementedError
65 class NoPasswordScheme(PasswordScheme
):
69 def make_secret(password
):
73 def check_response(challenge
, response
):
78 class ClearTextPasswordScheme(PasswordScheme
):
82 def make_secret(password
):
86 def check_response(challenge
, response
):
87 return challenge
== response
91 class SHAPasswordScheme(PasswordScheme
):
95 def make_secret(password
):
96 h
= hashlib
.sha1(password
)
97 return encode(h
.digest())
100 def check_response(challenge
, response
):
101 h
= hashlib
.sha1(response
)
102 return challenge
== encode(h
.digest())
106 class SSHAPasswordScheme(PasswordScheme
):
110 def make_secret(password
):
111 salt
= os
.urandom(SALT_LENGTH
)
112 h
= hashlib
.sha1(password
)
114 return encode(h
.digest() + salt
)
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
)
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
133 TAG
= 'PBKDF2 SHA %d' % ITERATIONS
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
142 h
= hmac
.new(password
, None, hashlib
.sha1
)
144 prf
.update(salt
+ '\x00\x00\x00\x01')
145 T
= U
= array('l', prf
.digest())
148 prf
.update(U
.tostring())
149 U
= array('l', prf
.digest())
150 T
= array('l', (t ^ u
for t
, u
in zip(T
, U
)))
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
161 salt
= os
.urandom(SALT_LENGTH
)
162 digest
= PBKDF2PasswordScheme
._pbkdf
2(password
, salt
, ITERATIONS
)
163 derived_key
= encode(digest
+ salt
)
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':
173 iterations
= int(iterations
)
174 except (ValueError, TypeError):
176 challenge_bytes
= decode(challenge
)
177 digest
= challenge_bytes
[:20]
178 salt
= challenge_bytes
[20:]
179 key
= PBKDF2PasswordScheme
._pbkdf
2(response
, salt
, iterations
)
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.
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
220 if isinstance(password
, unicode):
221 password
= password
.encode('utf-8')
222 scheme_class
= _SCHEMES_BY_ENUM
.get(scheme
)
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
)
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())