1
1
from __future__ import unicode_literals
2
2
3
3
import base64
4
+ import functools
4
5
import hashlib
5
6
6
7
from django .dispatch import receiver
16
17
17
18
18
19
UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash
20
+ MAXIMUM_PASSWORD_LENGTH = 4096 # The maximum length a password can be to prevent DoS
19
21
HASHERS = None # lazily loaded from PASSWORD_HASHERS
20
22
PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS
21
23
@@ -27,6 +29,18 @@ def reset_hashers(**kwargs):
27
29
PREFERRED_HASHER = None
28
30
29
31
32
+ def password_max_length (max_length ):
33
+ def inner (fn ):
34
+ @functools .wraps (fn )
35
+ def wrapper (self , password , * args , ** kwargs ):
36
+ if len (password ) > max_length :
37
+ raise ValueError ("Invalid password; Must be less than or equal"
38
+ " to %d bytes" % max_length )
39
+ return fn (self , password , * args , ** kwargs )
40
+ return wrapper
41
+ return inner
42
+
43
+
30
44
def is_password_usable (encoded ):
31
45
if encoded is None or encoded == UNUSABLE_PASSWORD :
32
46
return False
@@ -225,6 +239,7 @@ class PBKDF2PasswordHasher(BasePasswordHasher):
225
239
iterations = 10000
226
240
digest = hashlib .sha256
227
241
242
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
228
243
def encode (self , password , salt , iterations = None ):
229
244
assert password
230
245
assert salt and '$' not in salt
@@ -234,6 +249,7 @@ def encode(self, password, salt, iterations=None):
234
249
hash = base64 .b64encode (hash ).decode ('ascii' ).strip ()
235
250
return "%s$%d$%s$%s" % (self .algorithm , iterations , salt , hash )
236
251
252
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
237
253
def verify (self , password , encoded ):
238
254
algorithm , iterations , salt , hash = encoded .split ('$' , 3 )
239
255
assert algorithm == self .algorithm
@@ -279,13 +295,15 @@ def salt(self):
279
295
bcrypt = self ._load_library ()
280
296
return bcrypt .gensalt (self .rounds )
281
297
298
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
282
299
def encode (self , password , salt ):
283
300
bcrypt = self ._load_library ()
284
301
# Need to reevaluate the force_bytes call once bcrypt is supported on
285
302
# Python 3
286
303
data = bcrypt .hashpw (force_bytes (password ), salt )
287
304
return "%s$%s" % (self .algorithm , data )
288
305
306
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
289
307
def verify (self , password , encoded ):
290
308
algorithm , data = encoded .split ('$' , 1 )
291
309
assert algorithm == self .algorithm
@@ -310,12 +328,14 @@ class SHA1PasswordHasher(BasePasswordHasher):
310
328
"""
311
329
algorithm = "sha1"
312
330
331
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
313
332
def encode (self , password , salt ):
314
333
assert password
315
334
assert salt and '$' not in salt
316
335
hash = hashlib .sha1 (force_bytes (salt + password )).hexdigest ()
317
336
return "%s$%s$%s" % (self .algorithm , salt , hash )
318
337
338
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
319
339
def verify (self , password , encoded ):
320
340
algorithm , salt , hash = encoded .split ('$' , 2 )
321
341
assert algorithm == self .algorithm
@@ -338,12 +358,14 @@ class MD5PasswordHasher(BasePasswordHasher):
338
358
"""
339
359
algorithm = "md5"
340
360
361
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
341
362
def encode (self , password , salt ):
342
363
assert password
343
364
assert salt and '$' not in salt
344
365
hash = hashlib .md5 (force_bytes (salt + password )).hexdigest ()
345
366
return "%s$%s$%s" % (self .algorithm , salt , hash )
346
367
368
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
347
369
def verify (self , password , encoded ):
348
370
algorithm , salt , hash = encoded .split ('$' , 2 )
349
371
assert algorithm == self .algorithm
@@ -374,11 +396,13 @@ class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
374
396
def salt (self ):
375
397
return ''
376
398
399
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
377
400
def encode (self , password , salt ):
378
401
assert salt == ''
379
402
hash = hashlib .sha1 (force_bytes (password )).hexdigest ()
380
403
return 'sha1$$%s' % hash
381
404
405
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
382
406
def verify (self , password , encoded ):
383
407
encoded_2 = self .encode (password , '' )
384
408
return constant_time_compare (encoded , encoded_2 )
@@ -408,10 +432,12 @@ class UnsaltedMD5PasswordHasher(BasePasswordHasher):
408
432
def salt (self ):
409
433
return ''
410
434
435
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
411
436
def encode (self , password , salt ):
412
437
assert salt == ''
413
438
return hashlib .md5 (force_bytes (password )).hexdigest ()
414
439
440
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
415
441
def verify (self , password , encoded ):
416
442
if len (encoded ) == 37 and encoded .startswith ('md5$$' ):
417
443
encoded = encoded [5 :]
@@ -437,13 +463,15 @@ class CryptPasswordHasher(BasePasswordHasher):
437
463
def salt (self ):
438
464
return get_random_string (2 )
439
465
466
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
440
467
def encode (self , password , salt ):
441
468
crypt = self ._load_library ()
442
469
assert len (salt ) == 2
443
470
data = crypt .crypt (force_str (password ), salt )
444
471
# we don't need to store the salt, but Django used to do this
445
472
return "%s$%s$%s" % (self .algorithm , '' , data )
446
473
474
+ @password_max_length (MAXIMUM_PASSWORD_LENGTH )
447
475
def verify (self , password , encoded ):
448
476
crypt = self ._load_library ()
449
477
algorithm , salt , data = encoded .split ('$' , 2 )
@@ -458,4 +486,3 @@ def safe_summary(self, encoded):
458
486
(_ ('salt' ), salt ),
459
487
(_ ('hash' ), mask_hash (data , show = 3 )),
460
488
])
461
-
0 commit comments