4
4
import binascii
5
5
import hashlib
6
6
import importlib
7
+
import warnings
7
8
from collections import OrderedDict
8
9
9
10
from django.conf import settings
@@ -46,10 +47,17 @@ def check_password(password, encoded, setter=None, preferred='default'):
46
47
preferred = get_hasher(preferred)
47
48
hasher = identify_hasher(encoded)
48
49
49
-
must_update = hasher.algorithm != preferred.algorithm
50
-
if not must_update:
51
-
must_update = preferred.must_update(encoded)
50
+
hasher_changed = hasher.algorithm != preferred.algorithm
51
+
must_update = hasher_changed or preferred.must_update(encoded)
52
52
is_correct = hasher.verify(password, encoded)
53
+
54
+
# If the hasher didn't change (we don't protect against enumeration if it
55
+
# does) and the password should get updated, try to close the timing gap
56
+
# between the work factor of the current encoded password and the default
57
+
# work factor.
58
+
if not is_correct and not hasher_changed and must_update:
59
+
hasher.harden_runtime(password, encoded)
60
+
53
61
if setter and is_correct and must_update:
54
62
setter(password)
55
63
return is_correct
@@ -216,6 +224,19 @@ def safe_summary(self, encoded):
216
224
def must_update(self, encoded):
217
225
return False
218
226
227
+
def harden_runtime(self, password, encoded):
228
+
"""
229
+
Bridge the runtime gap between the work factor supplied in `encoded`
230
+
and the work factor suggested by this hasher.
231
+
232
+
Taking PBKDF2 as an example, if `encoded` contains 20000 iterations and
233
+
`self.iterations` is 30000, this method should run password through
234
+
another 10000 iterations of PBKDF2. Similar approaches should exist
235
+
for any hasher that has a work factor. If not, this method should be
236
+
defined as a no-op to silence the warning.
237
+
"""
238
+
warnings.warn('subclasses of BasePasswordHasher should provide a harden_runtime() method')
239
+
219
240
220
241
class PBKDF2PasswordHasher(BasePasswordHasher):
221
242
"""
@@ -258,6 +279,12 @@ def must_update(self, encoded):
258
279
algorithm, iterations, salt, hash = encoded.split('$', 3)
259
280
return int(iterations) != self.iterations
260
281
282
+
def harden_runtime(self, password, encoded):
283
+
algorithm, iterations, salt, hash = encoded.split('$', 3)
284
+
extra_iterations = self.iterations - int(iterations)
285
+
if extra_iterations > 0:
286
+
self.encode(password, salt, extra_iterations)
287
+
261
288
262
289
class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
263
290
"""
@@ -308,23 +335,8 @@ def encode(self, password, salt):
308
335
def verify(self, password, encoded):
309
336
algorithm, data = encoded.split('$', 1)
310
337
assert algorithm == self.algorithm
311
-
bcrypt = self._load_library()
312
-
313
-
# Hash the password prior to using bcrypt to prevent password truncation
314
-
# See: https://code.djangoproject.com/ticket/20138
315
-
if self.digest is not None:
316
-
# We use binascii.hexlify here because Python3 decided that a hex encoded
317
-
# bytestring is somehow a unicode.
318
-
password = binascii.hexlify(self.digest(force_bytes(password)).digest())
319
-
else:
320
-
password = force_bytes(password)
321
-
322
-
# Ensure that our data is a bytestring
323
-
data = force_bytes(data)
324
-
# force_bytes() necessary for py-bcrypt compatibility
325
-
hashpw = force_bytes(bcrypt.hashpw(password, data))
326
-
327
-
return constant_time_compare(data, hashpw)
338
+
encoded_2 = self.encode(password, force_bytes(data))
339
+
return constant_time_compare(encoded, encoded_2)
328
340
329
341
def safe_summary(self, encoded):
330
342
algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
@@ -341,6 +353,16 @@ def must_update(self, encoded):
341
353
algorithm, empty, algostr, rounds, data = encoded.split('$', 4)
342
354
return int(rounds) != self.rounds
343
355
356
+
def harden_runtime(self, password, encoded):
357
+
_, data = encoded.split('$', 1)
358
+
salt = data[:29] # Length of the salt in bcrypt.
359
+
rounds = data.split('$')[2]
360
+
# work factor is logarithmic, adding one doubles the load.
361
+
diff = 2**(self.rounds - int(rounds)) - 1
362
+
while diff > 0:
363
+
self.encode(password, force_bytes(salt))
364
+
diff -= 1
365
+
344
366
345
367
class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
346
368
"""
@@ -388,6 +410,9 @@ def safe_summary(self, encoded):
388
410
(_('hash'), mask_hash(hash)),
389
411
])
390
412
413
+
def harden_runtime(self, password, encoded):
414
+
pass
415
+
391
416
392
417
class MD5PasswordHasher(BasePasswordHasher):
393
418
"""
@@ -416,6 +441,9 @@ def safe_summary(self, encoded):
416
441
(_('hash'), mask_hash(hash)),
417
442
])
418
443
444
+
def harden_runtime(self, password, encoded):
445
+
pass
446
+
419
447
420
448
class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
421
449
"""
@@ -448,6 +476,9 @@ def safe_summary(self, encoded):
448
476
(_('hash'), mask_hash(hash)),
449
477
])
450
478
479
+
def harden_runtime(self, password, encoded):
480
+
pass
481
+
451
482
452
483
class UnsaltedMD5PasswordHasher(BasePasswordHasher):
453
484
"""
@@ -481,6 +512,9 @@ def safe_summary(self, encoded):
481
512
(_('hash'), mask_hash(encoded, show=3)),
482
513
])
483
514
515
+
def harden_runtime(self, password, encoded):
516
+
pass
517
+
484
518
485
519
class CryptPasswordHasher(BasePasswordHasher):
486
520
"""
@@ -515,3 +549,6 @@ def safe_summary(self, encoded):
515
549
(_('salt'), salt),
516
550
(_('hash'), mask_hash(data, show=3)),
517
551
])
552
+
553
+
def harden_runtime(self, password, encoded):
554
+
pass
RetroSearch is an open source project built by @garambo | Open a GitHub Issue
Search and Browse the WWW like it's 1997 | Search results from DuckDuckGo
HTML:
3.2
| Encoding:
UTF-8
| Version:
0.7.4