Bowe Strickland
2018-10-26 29e4dce0a2ba187091b4644e2003297300162673
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
c272ce 41             must_close = False
be1a57 42         else:
CM 43             try:
44                 f = open(self.filename, 'r')
c272ce 45                 must_close = True
be1a57 46             except IOError:
c7e12d 47                 environ['repoze.who.logger'].warn('could not open htpasswd '
TS 48                                                   'file %s' % self.filename)
c51195 49                 return None
be1a57 50
f8ef81 51         result = None
13b95f 52         maybe_user = None
TS 53         to_check = 'ABCDEF0123456789'
54
55         # Try not to reveal how many users we have.
56         # XXX:  the max count here should be configurable ;(
57         lines = itertools.chain(f, _padding_for_file_lines())
58         for line in itertools.islice(lines, 0, 1000):
be1a57 59             try:
CM 60                 username, hashed = line.rstrip().split(':', 1)
61             except ValueError:
62                 continue
13b95f 63             if _same_string(username, login):
TS 64                 # Don't bail early:  leaks information!!
65                 maybe_user = username
66                 to_check = hashed
67
c272ce 68         if must_close:
TS 69             f.close()
70
13b95f 71         # Check *something* here, to mitigate a timing attack.
TS 72         password_ok = self.check(password, to_check)
c69f3d 73
TS 74         # Check our flags:  if both are OK, we found a match.
13b95f 75         if password_ok and maybe_user:
TS 76             result = maybe_user
77
f8ef81 78         return result
be1a57 79
97cfa2 80     def __repr__(self):
2091d4 81         return '<%s %s>' % (self.__class__.__name__,
TS 82                             id(self)) #pragma NO COVERAGE
97cfa2 83
f8ef81 84 PADDING = ' ' * 1000
TS 85
86 def _same_string(x, y):
c69f3d 87     # Attempt at isochronous string comparison.
d6ab69 88     mismatches = filter(None, [a != b for a, b, ignored
ac4267 89                                     in izip_longest(x, y, PADDING)])
5d3ddc 90     if type(mismatches) != list: #pragma NO COVER Python >= 3.0
ac4267 91         mismatches = list(mismatches)
d6ab69 92     return len(mismatches) == 0
f8ef81 93
443d47 94 def crypt_check(password, hashed):
be1a57 95     from crypt import crypt
CM 96     salt = hashed[:2]
f8ef81 97     return _same_string(hashed, crypt(password, salt))
be1a57 98
9f7532 99 def sha1_check(password, hashed):
C 100     from hashlib import sha1
4cc78d 101     from base64 import standard_b64encode
TS 102     from repoze.who._compat import must_encode
103     encrypted_string = standard_b64encode(sha1(must_encode(password)).digest())
747717 104     if hasattr(encrypted_string, "decode"):
BS 105         encrypted_string = encrypted_string.decode()
87e79d 106     return _same_string(hashed, "%s%s" % ("{SHA}", encrypted_string))
9f7532 107
db6133 108 def plain_check(password, hashed):
f8ef81 109     return _same_string(password, hashed)
TS 110
db6133 111
515c69 112 def make_plugin(filename=None, check_fn=None):
d85ba6 113     if filename is None:
CM 114         raise ValueError('filename must be specified')
115     if check_fn is None:
116         raise ValueError('check_fn must be specified')
be1a57 117     check = resolveDotted(check_fn)
d85ba6 118     return HTPasswdPlugin(filename, check)