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
0 commit comments