Tres Seaver
2011-03-16 d6ab696f0edee85949d92151b03c6507dc793444
commit | author | age
f8ef81 1 import itertools
TS 2
be1a57 3 from zope.interface import implements
CM 4
cb5426 5 from repoze.who.interfaces import IAuthenticator
CM 6 from repoze.who.utils import resolveDotted
be1a57 7
13b95f 8
TS 9 def _padding_for_file_lines():
10     yield 'aaaaaa:bbbbbb'
11
12
d85ba6 13 class HTPasswdPlugin(object):
be1a57 14
c51195 15     implements(IAuthenticator)
be1a57 16
CM 17     def __init__(self, filename, check):
18         self.filename = filename
19         self.check = check
20
21     # IAuthenticatorPlugin
c51195 22     def authenticate(self, environ, identity):
c69f3d 23         # NOW HEAR THIS!!!
TS 24         #
25         # This method is *intentionally* slower than would be ideal because
26         # it is trying to avoid leaking information via timing attacks
27         # (number of users, length of user IDs, length of passwords, etc.).
28         #
29         # Do *not* try to optimize anything away here.
be1a57 30         try:
c51195 31             login = identity['login']
CM 32             password = identity['password']
be1a57 33         except KeyError:
c51195 34             return None
be1a57 35
CM 36         if hasattr(self.filename, 'seek'):
37             # assumed to have a readline
38             self.filename.seek(0)
39             f = self.filename
40         else:
41             try:
42                 f = open(self.filename, 'r')
43             except IOError:
c7e12d 44                 environ['repoze.who.logger'].warn('could not open htpasswd '
TS 45                                                   'file %s' % self.filename)
c51195 46                 return None
be1a57 47
f8ef81 48         result = None
13b95f 49         maybe_user = None
TS 50         to_check = 'ABCDEF0123456789'
51
52         # Try not to reveal how many users we have.
53         # XXX:  the max count here should be configurable ;(
54         lines = itertools.chain(f, _padding_for_file_lines())
55         for line in itertools.islice(lines, 0, 1000):
be1a57 56             try:
CM 57                 username, hashed = line.rstrip().split(':', 1)
58             except ValueError:
59                 continue
13b95f 60             if _same_string(username, login):
TS 61                 # Don't bail early:  leaks information!!
62                 maybe_user = username
63                 to_check = hashed
64
65         # Check *something* here, to mitigate a timing attack.
66         password_ok = self.check(password, to_check)
c69f3d 67
TS 68         # Check our flags:  if both are OK, we found a match.
13b95f 69         if password_ok and maybe_user:
TS 70             result = maybe_user
71
f8ef81 72         return result
be1a57 73
97cfa2 74     def __repr__(self):
2091d4 75         return '<%s %s>' % (self.__class__.__name__,
TS 76                             id(self)) #pragma NO COVERAGE
97cfa2 77
f8ef81 78 PADDING = ' ' * 1000
TS 79
80 def _same_string(x, y):
c69f3d 81     # Attempt at isochronous string comparison.
d6ab69 82     mismatches = filter(None, [a != b for a, b, ignored
TS 83                                     in itertools.izip_longest(x, y, PADDING)])
84     return len(mismatches) == 0
f8ef81 85
443d47 86 def crypt_check(password, hashed):
be1a57 87     from crypt import crypt
CM 88     salt = hashed[:2]
f8ef81 89     return _same_string(hashed, crypt(password, salt))
be1a57 90
db6133 91 def plain_check(password, hashed):
f8ef81 92     return _same_string(password, hashed)
TS 93
db6133 94
515c69 95 def make_plugin(filename=None, check_fn=None):
d85ba6 96     if filename is None:
CM 97         raise ValueError('filename must be specified')
98     if check_fn is None:
99         raise ValueError('check_fn must be specified')
be1a57 100     check = resolveDotted(check_fn)
d85ba6 101     return HTPasswdPlugin(filename, check)