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