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) |