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. |
407443
|
82 |
mismatches = sum([a != b for a, b, ignored |
TS |
83 |
in itertools.izip_longest(x, y, PADDING)]) |
|
84 |
return 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) |