Ticket #15367: django-passhash-2011-09-12.patch
File django-passhash-2011-09-12.patch, 33.3 KB (added by , 13 years ago) |
---|
-
django/conf/global_settings.py
diff --git a/django/conf/global_settings.py b/django/conf/global_settings.py index 10d6192..d88f3b5 100644
a b LOGIN_REDIRECT_URL = '/accounts/profile/' 472 472 # The number of days a password reset link is valid for 473 473 PASSWORD_RESET_TIMEOUT_DAYS = 3 474 474 475 # the first hasher in this list is the preferred algorithm. any 476 # password using different algorithms will be converted automatically 477 # upon login 478 PASSWORD_HASHERS = ( 479 { 480 'BACKEND': 'django.utils.passhash.PBKDF2PasswordHasher', 481 'OPTIONS': { 482 'iterations': 10000, # may be omitted 483 }, 484 }, 485 { 486 'BACKEND': 'django.utils.passhash.BCryptPasswordHasher', 487 'OPTIONS': { 488 'rounds': 12, # may be omitted 489 }, 490 }, 491 {'BACKEND': 'django.utils.passhash.SHA1PasswordHasher'}, 492 {'BACKEND': 'django.utils.passhash.MD5PasswordHasher'}, 493 {'BACKEND': 'django.utils.passhash.CryptPasswordHasher'}, 494 ) 495 475 496 ########### 476 497 # SIGNING # 477 498 ########### -
django/contrib/auth/forms.py
diff --git a/django/contrib/auth/forms.py b/django/contrib/auth/forms.py index 9602d55..ccf2801 100644
a b from django.utils.itercompat import any 5 5 from django.utils.translation import ugettext_lazy as _ 6 6 7 7 from django.contrib.auth.models import User 8 from django. contrib.auth.utilsimport UNUSABLE_PASSWORD8 from django.utils.passhash import UNUSABLE_PASSWORD 9 9 from django.contrib.auth import authenticate 10 10 from django.contrib.auth.tokens import default_token_generator 11 11 from django.contrib.sites.models import get_current_site -
django/contrib/auth/models.py
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 5ae4817..76426d0 100644
a b from django.utils.translation import ugettext_lazy as _ 11 11 from django.contrib import auth 12 12 from django.contrib.auth.signals import user_logged_in 13 13 # UNUSABLE_PASSWORD is still imported here for backwards compatibility 14 from django.contrib.auth.utils import (get_hexdigest, make_password, 15 check_password, is_password_usable, get_random_string, 16 UNUSABLE_PASSWORD) 14 from django.utils.passhash import ( 15 check_password, make_password, is_password_usable, UNUSABLE_PASSWORD) 17 16 from django.contrib.contenttypes.models import ContentType 18 17 19 18 def update_last_login(sender, user, **kwargs): … … class User(models.Model): 228 227 return full_name.strip() 229 228 230 229 def set_password(self, raw_password): 231 self.password = make_password( 'sha1',raw_password)230 self.password = make_password(raw_password) 232 231 233 232 def check_password(self, raw_password): 234 233 """ 235 234 Returns a boolean of whether the raw_password was correct. Handles 236 235 encryption formats behind the scenes. 237 236 """ 238 # Backwards-compatibility check. Older passwords won't include the 239 # algorithm or salt. 240 if '$' not in self.password: 241 is_correct = (self.password == get_hexdigest('md5', '', raw_password)) 242 if is_correct: 243 # Convert the password to the new, more secure format. 244 self.set_password(raw_password) 245 self.save() 246 return is_correct 247 return check_password(raw_password, self.password) 237 def setter(): 238 self.set_password(raw_password) 239 self.save() 240 return check_password(raw_password, self.password, setter) 248 241 249 242 def set_unusable_password(self): 250 243 # Sets a value that will never be a valid hash 251 self.password = make_password( 'sha1',None)244 self.password = make_password(None) 252 245 253 246 def has_usable_password(self): 254 247 return is_password_usable(self.password) -
django/contrib/auth/tests/__init__.py
diff --git a/django/contrib/auth/tests/__init__.py b/django/contrib/auth/tests/__init__.py index 143b19c..350d5e6 100644
a b 1 1 from django.contrib.auth.tests.auth_backends import (BackendTest, 2 2 RowlevelBackendTest, AnonymousUserBackendTest, NoAnonymousUserBackendTest, 3 3 NoBackendsTest, InActiveUserBackendTest, NoInActiveUserBackendTest) 4 from django.contrib.auth.tests.basic import BasicTestCase , PasswordUtilsTestCase4 from django.contrib.auth.tests.basic import BasicTestCase 5 5 from django.contrib.auth.tests.context_processors import AuthContextProcessorTests 6 6 from django.contrib.auth.tests.decorators import LoginRequiredTestCase 7 7 from django.contrib.auth.tests.forms import (UserCreationFormTest, -
django/contrib/auth/tests/basic.py
diff --git a/django/contrib/auth/tests/basic.py b/django/contrib/auth/tests/basic.py index 9f94c2a..2d02623 100644
a b class BasicTestCase(TestCase): 111 111 u = User.objects.get(username="joe+admin@somewhere.org") 112 112 self.assertEqual(u.email, 'joe@somewhere.org') 113 113 self.assertFalse(u.has_usable_password()) 114 115 116 class PasswordUtilsTestCase(TestCase):117 118 def _test_make_password(self, algo):119 password = utils.make_password(algo, "foobar")120 self.assertTrue(utils.is_password_usable(password))121 self.assertTrue(utils.check_password("foobar", password))122 123 def test_make_unusable(self):124 "Check that you can create an unusable password."125 password = utils.make_password("any", None)126 self.assertFalse(utils.is_password_usable(password))127 self.assertFalse(utils.check_password("foobar", password))128 129 def test_make_password_sha1(self):130 "Check creating passwords with SHA1 algorithm."131 self._test_make_password("sha1")132 133 def test_make_password_md5(self):134 "Check creating passwords with MD5 algorithm."135 self._test_make_password("md5")136 137 @skipUnless(crypt_module, "no crypt module to generate password.")138 def test_make_password_crypt(self):139 "Check creating passwords with CRYPT algorithm."140 self._test_make_password("crypt") -
django/contrib/auth/utils.py
diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py index 57c693f..5ee11fa 100644
a b 1 import hashlib 2 from django.utils.encoding import smart_str 3 from django.utils.crypto import constant_time_compare 4 5 UNUSABLE_PASSWORD = '!' # This will never be a valid hash 6 7 def get_hexdigest(algorithm, salt, raw_password): 8 """ 9 Returns a string of the hexdigest of the given plaintext password and salt 10 using the given algorithm ('md5', 'sha1' or 'crypt'). 11 """ 12 raw_password, salt = smart_str(raw_password), smart_str(salt) 13 if algorithm == 'crypt': 14 try: 15 import crypt 16 except ImportError: 17 raise ValueError('"crypt" password algorithm not supported in this environment') 18 return crypt.crypt(raw_password, salt) 19 20 if algorithm == 'md5': 21 return hashlib.md5(salt + raw_password).hexdigest() 22 elif algorithm == 'sha1': 23 return hashlib.sha1(salt + raw_password).hexdigest() 24 raise ValueError("Got unknown password algorithm type in password.") 25 26 def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): 27 """ 28 Returns a random string of length characters from the set of a-z, A-Z, 0-9 29 for use as a salt. 30 31 The default length of 12 with the a-z, A-Z, 0-9 character set returns 32 a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits 33 """ 34 import random 35 try: 36 random = random.SystemRandom() 37 except NotImplementedError: 38 pass 39 return ''.join([random.choice(allowed_chars) for i in range(length)]) 40 41 def check_password(raw_password, enc_password): 42 """ 43 Returns a boolean of whether the raw_password was correct. Handles 44 encryption formats behind the scenes. 45 """ 46 parts = enc_password.split('$') 47 if len(parts) != 3: 48 return False 49 algo, salt, hsh = parts 50 return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password)) 51 52 def is_password_usable(encoded_password): 53 return encoded_password is not None and encoded_password != UNUSABLE_PASSWORD 54 55 def make_password(algo, raw_password): 56 """ 57 Produce a new password string in this format: algorithm$salt$hash 58 """ 59 if raw_password is None: 60 return UNUSABLE_PASSWORD 61 salt = get_random_string() 62 hsh = get_hexdigest(algo, salt, raw_password) 63 return '%s$%s$%s' % (algo, salt, hsh) 1 # this file is deprecated 2 from django.utils.crypto import * 3 from django.utils.passhash import * -
django/utils/crypto.py
diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 95af680..8e25d33 100644
a b 2 2 Django's standard crypto functions and utilities. 3 3 """ 4 4 5 import hashlib6 5 import hmac 6 import struct 7 import hashlib 8 import binascii 9 import operator 7 10 from django.conf import settings 8 11 12 13 trans_5c = "".join([chr(x ^ 0x5C) for x in xrange(256)]) 14 trans_36 = "".join([chr(x ^ 0x36) for x in xrange(256)]) 15 16 9 17 def salted_hmac(key_salt, value, secret=None): 10 18 """ 11 19 Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a … … def salted_hmac(key_salt, value, secret=None): 27 35 # However, we need to ensure that we *always* do this. 28 36 return hmac.new(key, msg=value, digestmod=hashlib.sha1) 29 37 38 39 def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): 40 """ 41 Returns a random string of length characters from the set of a-z, A-Z, 0-9 42 for use as a salt. 43 44 The default length of 12 with the a-z, A-Z, 0-9 character set returns 45 a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits 46 """ 47 import random 48 try: 49 random = random.SystemRandom() 50 except NotImplementedError: 51 pass 52 return ''.join([random.choice(allowed_chars) for i in range(length)]) 53 54 30 55 def constant_time_compare(val1, val2): 31 56 """ 32 57 Returns True if the two strings are equal, False otherwise. … … def constant_time_compare(val1, val2): 39 64 for x, y in zip(val1, val2): 40 65 result |= ord(x) ^ ord(y) 41 66 return result == 0 67 68 69 def bin_to_long(x): 70 """ 71 Convert a binary string into a long integer 72 73 This is a clever optimization for fast xor vector math 74 """ 75 return long(x.encode('hex'), 16) 76 77 78 def long_to_bin(x): 79 """ 80 Convert a long integer into a binary string 81 """ 82 hex = "%x" % (x) 83 if len(hex) % 2 == 1: 84 hex = '0' + hex 85 return binascii.unhexlify(hex) 86 87 88 def fast_hmac(key, msg, digest): 89 """ 90 A trimmed down version of Python's HMAC implementation 91 """ 92 dig1, dig2 = digest(), digest() 93 if len(key) > dig1.block_size: 94 key = digest(key).digest() 95 key += chr(0) * (dig1.block_size - len(key)) 96 dig1.update(key.translate(trans_36)) 97 dig1.update(msg) 98 dig2.update(key.translate(trans_5c)) 99 dig2.update(dig1.digest()) 100 return dig2 101 102 103 def pbkdf2(password, salt, iterations, dklen=0, digest=None): 104 """ 105 Implements PBKDF2 as defined in RFC 2898, section 5.2 106 107 HMAC+SHA256 is used as the pseudo random function. 108 109 Right now 10,000 iterations is the recommended default which takes 110 160ms on a black MacBook. This is what iOs uses and is probably 111 the bare minimum for security considering 1000 iterations was 112 recommended ten years ago. This code is very well optimized for 113 CPython and is only four times slower than a C implementation I 114 hacked together. 115 """ 116 assert iterations > 0 117 if not digest: 118 digest = hashlib.sha256 119 hlen = digest().digest_size 120 if not dklen: 121 dklen = hlen 122 if dklen > (2 ** 32 - 1) * hlen: 123 raise OverflowError('dklen too big') 124 l = -(-dklen // hlen) 125 r = dklen - (l - 1) * hlen 126 127 def F(i): 128 def U(): 129 u = salt + struct.pack('>I', i) 130 for j in xrange(int(iterations)): 131 u = fast_hmac(password, u, digest).digest() 132 yield bin_to_long(u) 133 return long_to_bin(reduce(operator.xor, U())) 134 135 T = [F(x) for x in range(1, l + 1)] 136 return ''.join(T[:-1]) + T[-1][:r] -
new file django/utils/passhash.py
diff --git a/django/utils/passhash.py b/django/utils/passhash.py new file mode 100644 index 0000000..b0f1f3f
- + 1 """ 2 3 django.utils.passhash 4 ~~~~~~~~~~~~~~~~~~~~~ 5 6 Secure password hashing utilities. 7 8 I implement a variety of hashing algorithms you can use for 9 *securely* storing passwords in a database. The purpose of this 10 code is to ensure no one can ever turn a password hash stored in 11 your database back into the original password. 12 13 """ 14 15 import hashlib 16 17 from django.conf import settings 18 from django.utils import importlib 19 from django.utils.encoding import smart_str 20 from django.core.exceptions import ImproperlyConfigured 21 from django.utils.crypto import ( 22 pbkdf2, constant_time_compare, get_random_string) 23 24 25 UNUSABLE_PASSWORD = '!' # This will never be a valid encoded hash 26 HASHERS = None # lazily loaded from PASSWORD_HASHERS 27 PREFERRED_HASHER = None # defaults to first item in PASSWORD_HASHERS 28 29 30 def is_password_usable(encoded): 31 return (encoded is not None and encoded != UNUSABLE_PASSWORD) 32 33 34 def check_password(password, encoded, setter=None, preferred='default'): 35 """ 36 Returns a boolean of whether the raw password matches the three 37 part encoded digest. 38 39 If setter is specified, it'll be called when you need to 40 regenerate the password. 41 """ 42 if not password: 43 return False 44 if not is_password_usable(encoded): 45 return False 46 preferred = get_hasher(preferred) 47 password = smart_str(password) 48 encoded = smart_str(encoded) 49 must_update = False 50 if encoded.startswith('$2a$'): 51 # migration for people who used django-bcrypt 52 encoded = 'bcrypt$' + encoded 53 must_update = True 54 hasher = determine_hasher(encoded) 55 must_update = must_update or (hasher.algorithm != preferred.algorithm) 56 is_correct = hasher.verify(password, encoded) 57 if setter and is_correct and must_update: 58 setter() 59 return is_correct 60 61 62 def make_password(password, salt=None, hasher='default'): 63 """ 64 Turn a plain-text password into a hash for database storage 65 66 Same as encode() but generates a new random salt. If 67 password is None or blank then UNUSABLE_PASSWORD will be 68 returned which disallows logins. 69 """ 70 if not password: 71 return UNUSABLE_PASSWORD 72 hasher = get_hasher(hasher) 73 if not salt: 74 salt = hasher.gensalt() 75 password = smart_str(password) 76 salt = smart_str(salt) 77 return hasher.encode(password, salt) 78 79 80 def get_hasher(algorithm='default'): 81 """ 82 Returns an instance of a loaded password hasher. 83 84 If algorithm is 'default', the default hasher will be returned. 85 This function will also lazy import hashers specified in your 86 settings file if needed. 87 """ 88 if hasattr(algorithm, 'algorithm'): 89 return algorithm 90 elif algorithm == 'default': 91 if PREFERRED_HASHER is None: 92 load_hashers() 93 return PREFERRED_HASHER 94 else: 95 if HASHERS is None: 96 load_hashers() 97 if algorithm not in HASHERS: 98 raise ValueError( 99 ('Unknown password hashing algorithm "%s". Did you specify ' 100 'it in PASSWORD_HASHERS?') % (algorithm)) 101 return HASHERS[algorithm] 102 103 104 def load_hashers(): 105 global HASHERS 106 global PREFERRED_HASHER 107 hashers = [] 108 for spec in settings.PASSWORD_HASHERS: 109 backend = spec['BACKEND'] 110 kwargs = spec.get('OPTIONS', {}) 111 try: 112 mod_path, cls_name = backend.rsplit('.', 1) 113 mod = importlib.import_module(mod_path) 114 hasher_cls = getattr(mod, cls_name) 115 except (AttributeError, ImportError, ValueError): 116 raise InvalidPasswordHasherError( 117 "hasher not found: %s" % (backend)) 118 hasher = hasher_cls(**kwargs) 119 if not getattr(hasher, 'algorithm'): 120 raise InvalidPasswordHasherError( 121 "hasher doesn't specify an algorithm name: %s" % (backend)) 122 hashers.append(hasher) 123 HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers]) 124 PREFERRED_HASHER = hashers[0] 125 126 127 def determine_hasher(encoded): 128 """ 129 Which hasher is being used for this encoded password? 130 """ 131 if len(encoded) == 32 and '$' not in encoded: 132 # migration for legacy unsalted md5 passwords 133 return get_hasher('md5') 134 else: 135 algorithm = encoded.split('$', 1)[0] 136 return get_hasher(algorithm) 137 138 139 class InvalidPasswordHasherError(ImproperlyConfigured): 140 pass 141 142 143 class BasePasswordHasher(object): 144 """ 145 Abstract base class for password hashers 146 147 When creating your own hasher, you need to override algorithm, 148 verify() and encode(). 149 150 PasswordHasher objects are immutable. 151 """ 152 algorithm = None 153 154 def gensalt(self): 155 """ 156 I should generate cryptographically secure nonce salt in ascii 157 """ 158 return get_random_string() 159 160 def verify(self, password, encoded): 161 """ 162 Abstract method to check if password is correct 163 """ 164 raise NotImplementedError() 165 166 def encode(self, password, salt): 167 """ 168 Abstract method for creating encoded database values 169 170 The result is normally formatted as "algorithm$salt$hash" and 171 must be fewer than 128 characters. 172 """ 173 raise NotImplementedError() 174 175 176 class PBKDF2PasswordHasher(BasePasswordHasher): 177 """ 178 Secure password hashing using the PBKDF2 algorithm (recommended) 179 180 I'm configured to use PBKDF2 + HMAC + SHA256 with 10000 181 iterations. The result is a 64 byte binary string. Iterations 182 may be changed safely but you must rename the algorithm if you 183 change SHA256. 184 """ 185 algorithm = "pbkdf2" 186 187 def __init__(self, iterations=10000): 188 BasePasswordHasher.__init__(self) 189 self.iterations = iterations 190 191 def encode(self, password, salt, iterations=None): 192 assert password 193 assert salt and '$' not in salt 194 if not iterations: 195 iterations = self.iterations 196 hash = pbkdf2(password, salt, iterations) 197 hash = hash.encode('base64').strip() 198 return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 199 200 def verify(self, password, encoded): 201 algorithm, iterations, salt, hash = encoded.split('$', 3) 202 assert algorithm == self.algorithm 203 encoded_2 = self.encode(password, salt, int(iterations)) 204 return constant_time_compare(encoded, encoded_2) 205 206 207 class BCryptPasswordHasher(BasePasswordHasher): 208 """ 209 Secure password hashing using the bcrypt algorithm (recommended) 210 211 This is considered by many to be the most secure algorithm but you 212 must first install the py-crypt library. Please be warned that 213 this library depends on native C code and might cause portability 214 issues. 215 """ 216 algorithm = "bcrypt" 217 218 def __init__(self, rounds=12): 219 BasePasswordHasher.__init__(self) 220 self.rounds = rounds 221 222 def _import(self): 223 try: 224 import bcrypt 225 except ImportError: 226 raise ValueError('py-bcrypt library not installed') 227 return bcrypt 228 229 def gensalt(self): 230 bcrypt = self._import() 231 return bcrypt.gensalt(self.rounds) 232 233 def encode(self, password, salt): 234 bcrypt = self._import() 235 data = bcrypt.hashpw(password, salt) 236 return "%s$%s" % (self.algorithm, data) 237 238 def verify(self, password, encoded): 239 bcrypt = self._import() 240 algorithm, data = encoded.split('$', 1) 241 assert algorithm == self.algorithm 242 return constant_time_compare(data, bcrypt.hashpw(password, data)) 243 244 245 class SHA1PasswordHasher(BasePasswordHasher): 246 """ 247 The SHA1 password hashing algorithm (not recommended) 248 """ 249 algorithm = "sha1" 250 251 def encode(self, password, salt): 252 assert password 253 assert salt and '$' not in salt 254 hash = hashlib.sha1(salt + password).hexdigest() 255 return "%s$%s$%s" % (self.algorithm, salt, hash) 256 257 def verify(self, password, encoded): 258 algorithm, salt, hash = encoded.split('$', 2) 259 assert algorithm == self.algorithm 260 encoded_2 = self.encode(password, salt) 261 return constant_time_compare(encoded, encoded_2) 262 263 264 class MD5PasswordHasher(BasePasswordHasher): 265 """ 266 I am an incredibly insecure algorithm you should *never* use 267 268 I store unsalted MD5 hashes without the algorithm prefix. 269 270 This class is implemented because Django used to store passwords 271 this way. Some older Django installs still have these values 272 lingering around so we need to handle and upgrade them properly. 273 """ 274 algorithm = "md5" 275 276 def gensalt(self): 277 return '' 278 279 def encode(self, password, salt): 280 return hashlib.md5(password).hexdigest() 281 282 def verify(self, password, encoded): 283 encoded_2 = self.encode(password, '') 284 return constant_time_compare(encoded, encoded_2) 285 286 287 class CryptPasswordHasher(BasePasswordHasher): 288 """ 289 Password hashing using UNIX crypt (not recommended) 290 291 The crypt module is not supported on all platforms. 292 """ 293 algorithm = "crypt" 294 295 def _import(self): 296 try: 297 import crypt 298 except ImportError: 299 raise ValueError('"crypt" password algorithm not supported in ' 300 'this environment') 301 return crypt 302 303 def gensalt(self): 304 return get_random_string(2) 305 306 def encode(self, password, salt): 307 crypt = self._import() 308 assert len(salt) == 2 309 data = crypt.crypt(password, salt) 310 # we don't need to store the salt, but django used to do this 311 return "%s$%s$%s" % (self.algorithm, '', data) 312 313 def verify(self, password, encoded): 314 crypt = self._import() 315 algorithm, salt, data = encoded.split('$', 2) 316 assert algorithm == self.algorithm 317 return constant_time_compare(data, crypt.crypt(password, data)) -
docs/topics/auth.txt
diff --git a/docs/topics/auth.txt b/docs/topics/auth.txt index 69f6fd7..917ca7f 100644
a b Django provides two functions in :mod:`django.contrib.auth`: 633 633 Manually managing a user's password 634 634 ----------------------------------- 635 635 636 .. currentmodule:: django. contrib.auth.utils636 .. currentmodule:: django.utils.passhash 637 637 638 638 .. versionadded:: 1.4 639 639 640 The :mod:`django. contrib.auth.utils` module provides a set of functions640 The :mod:`django.utils.passhash` module provides a set of functions 641 641 to create and validate hashed password. You can use them independently 642 from the ``User`` model. 642 from the ``User`` model. The following algorithms are supported: 643 643 644 .. function:: check_password() 644 * ``pbkdf2``: This is the default algorithm. 645 646 .. function:: check_password(password, password_hash, setter=None) 645 647 646 648 If you'd like to manually authenticate a user by comparing a plain-text 647 649 password to the hashed password in the database, use the convenience … … Manually managing a user's password 650 652 user's ``password`` field in the database to check against, and returns 651 653 ``True`` if they match, ``False`` otherwise. 652 654 653 .. function:: make_password() 655 .. versionadded:: 1.4 656 657 .. function:: make_password(password) 654 658 655 659 .. versionadded:: 1.4 656 660 … … Manually managing a user's password 661 665 ``None``, an unusable password is returned (a one that will be never 662 666 accepted by :func:`django.contrib.auth.utils.check_password`). 663 667 668 :setting:`settings.LOGIN_URL <LOGIN_URL>` 669 664 670 .. function:: is_password_usable() 665 671 666 672 .. versionadded:: 1.4 -
new file tests/regressiontests/utils/crypto.py
diff --git a/tests/regressiontests/utils/crypto.py b/tests/regressiontests/utils/crypto.py new file mode 100644 index 0000000..f025ffa
- + 1 2 import math 3 import timeit 4 import hashlib 5 6 from django.utils import unittest 7 from django.utils.crypto import pbkdf2 8 9 10 class TestUtilsCryptoPBKDF2(unittest.TestCase): 11 12 # http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06 13 rfc_vectors = [ 14 { 15 "args": { 16 "password": "password", 17 "salt": "salt", 18 "iterations": 1, 19 "dklen": 20, 20 "digest": hashlib.sha1, 21 }, 22 "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6", 23 }, 24 { 25 "args": { 26 "password": "password", 27 "salt": "salt", 28 "iterations": 2, 29 "dklen": 20, 30 "digest": hashlib.sha1, 31 }, 32 "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957", 33 }, 34 { 35 "args": { 36 "password": "password", 37 "salt": "salt", 38 "iterations": 4096, 39 "dklen": 20, 40 "digest": hashlib.sha1, 41 }, 42 "result": "4b007901b765489abead49d926f721d065a429c1", 43 }, 44 # # this takes way too long :( 45 # { 46 # "args": { 47 # "password": "password", 48 # "salt": "salt", 49 # "iterations": 16777216, 50 # "dklen": 20, 51 # "digest": hashlib.sha1, 52 # }, 53 # "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984", 54 # }, 55 { 56 "args": { 57 "password": "passwordPASSWORDpassword", 58 "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt", 59 "iterations": 4096, 60 "dklen": 25, 61 "digest": hashlib.sha1, 62 }, 63 "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038", 64 }, 65 { 66 "args": { 67 "password": "pass\0word", 68 "salt": "sa\0lt", 69 "iterations": 4096, 70 "dklen": 16, 71 "digest": hashlib.sha1, 72 }, 73 "result": "56fa6aa75548099dcc37d7f03425e0c3", 74 }, 75 ] 76 77 regression_vectors = [ 78 { 79 "args": { 80 "password": "password", 81 "salt": "salt", 82 "iterations": 1, 83 "dklen": 20, 84 "digest": hashlib.sha256, 85 }, 86 "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9", 87 }, 88 { 89 "args": { 90 "password": "password", 91 "salt": "salt", 92 "iterations": 1, 93 "dklen": 20, 94 "digest": hashlib.sha512, 95 }, 96 "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6", 97 }, 98 { 99 "args": { 100 "password": "password", 101 "salt": "salt", 102 "iterations": 1000, 103 "dklen": 0, 104 "digest": hashlib.sha512, 105 }, 106 "result": ("afe6c5530785b6cc6b1c6453384731bd5ee432ee" 107 "549fd42fb6695779ad8a1c5bf59de69c48f774ef" 108 "c4007d5298f9033c0241d5ab69305e7b64eceeb8d" 109 "834cfec"), 110 }, 111 ] 112 113 def test_public_vectors(self): 114 for vector in self.rfc_vectors: 115 result = pbkdf2(**vector['args']) 116 self.assertEqual(result.encode('hex'), vector['result']) 117 118 def test_regression_vectors(self): 119 for vector in self.regression_vectors: 120 result = pbkdf2(**vector['args']) 121 self.assertEqual(result.encode('hex'), vector['result']) 122 123 def test_performance_scalability(self): 124 """ 125 Theory: If you run with 100 iterations, it should take 100 126 times as long as running with 1 iteration. 127 """ 128 n1, n2 = 100, 10000 129 elapsed = lambda f: timeit.timeit(f, number=1) 130 t1 = elapsed(lambda: pbkdf2("password", "salt", iterations=n1)) 131 t2 = elapsed(lambda: pbkdf2("password", "salt", iterations=n2)) 132 measured_scale_exponent = math.log(t2 / t1, n2 / n1) 133 self.assertLess(measured_scale_exponent, 1.1) -
new file tests/regressiontests/utils/passhash.py
diff --git a/tests/regressiontests/utils/passhash.py b/tests/regressiontests/utils/passhash.py new file mode 100644 index 0000000..9e51244
- + 1 2 from django.utils import unittest 3 from django.utils.unittest import skipUnless 4 from django.utils.passhash import * 5 6 try: 7 import crypt 8 except ImportError: 9 crypt = None 10 11 try: 12 import bcrypt 13 except ImportError: 14 bcrypt = None 15 16 17 class TestUtilsHashPass(unittest.TestCase): 18 19 def test_simple(self): 20 encoded = make_password('letmein') 21 self.assertTrue(encoded.startswith('pbkdf2$')) 22 self.assertTrue(is_password_usable(encoded)) 23 self.assertTrue(check_password(u'letmein', encoded)) 24 self.assertFalse(check_password('letmeinz', encoded)) 25 26 def test_pkbdf2(self): 27 encoded = make_password('letmein', 'seasalt', 'pbkdf2') 28 self.assertEqual(encoded, 'pbkdf2$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') 29 self.assertTrue(is_password_usable(encoded)) 30 self.assertTrue(check_password(u'letmein', encoded)) 31 self.assertFalse(check_password('letmeinz', encoded)) 32 33 def test_sha1(self): 34 encoded = make_password('letmein', 'seasalt', 'sha1') 35 self.assertEqual(encoded, 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7') 36 self.assertTrue(is_password_usable(encoded)) 37 self.assertTrue(check_password(u'letmein', encoded)) 38 self.assertFalse(check_password('letmeinz', encoded)) 39 40 def test_md5(self): 41 encoded = make_password('letmein', 'seasalt', 'md5') 42 self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') 43 self.assertTrue(is_password_usable(encoded)) 44 self.assertTrue(check_password(u'letmein', encoded)) 45 self.assertFalse(check_password('letmeinz', encoded)) 46 47 @skipUnless(crypt, "no crypt module to generate password.") 48 def test_crypt(self): 49 encoded = make_password('letmein', 'ab', 'crypt') 50 self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ') 51 self.assertTrue(is_password_usable(encoded)) 52 self.assertTrue(check_password(u'letmein', encoded)) 53 self.assertFalse(check_password('letmeinz', encoded)) 54 55 @skipUnless(bcrypt, "py-bcrypt not installed") 56 def test_bcrypt(self): 57 encoded = make_password('letmein', hasher='bcrypt') 58 self.assertTrue(is_password_usable(encoded)) 59 self.assertTrue(encoded.startswith('bcrypt$')) 60 self.assertTrue(check_password(u'letmein', encoded)) 61 self.assertFalse(check_password('letmeinz', encoded)) 62 63 def test_unusable(self): 64 encoded = make_password(None) 65 self.assertFalse(is_password_usable(encoded)) 66 self.assertFalse(check_password(None, encoded)) 67 self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded)) 68 self.assertFalse(check_password('', encoded)) 69 self.assertFalse(check_password(u'letmein', encoded)) 70 self.assertFalse(check_password('letmeinz', encoded)) 71 72 def test_bad_algorithm(self): 73 def doit(): 74 make_password('letmein', hasher='lolcat') 75 self.assertRaises(ValueError, doit) 76 77 def test_low_level_pkbdf2(self): 78 hasher = PBKDF2PasswordHasher() 79 encoded = hasher.encode('letmein', 'seasalt') 80 self.assertEqual(encoded, 'pbkdf2$10000$seasalt$FQCNpiZpTb0zub+HBsH6TOwyRxJ19FwvjbweatNmK/Y=') 81 self.assertTrue(hasher.verify('letmein', encoded)) 82 83 def test_upgrade(self): 84 self.assertEqual('pbkdf2', get_hasher('default').algorithm) 85 for algo in ('sha1', 'md5'): 86 encoded = make_password('letmein', hasher=algo) 87 state = {'upgraded': False} 88 def setter(): 89 state['upgraded'] = True 90 self.assertTrue(check_password('letmein', encoded, setter)) 91 self.assertTrue(state['upgraded']) 92 93 def test_no_upgrade(self): 94 encoded = make_password('letmein') 95 state = {'upgraded': False} 96 def setter(): 97 state['upgraded'] = True 98 self.assertFalse(check_password('WRONG', encoded, setter)) 99 self.assertFalse(state['upgraded']) 100 101 def test_no_upgrade_on_incorrect_pass(self): 102 self.assertEqual('pbkdf2', get_hasher('default').algorithm) 103 for algo in ('sha1', 'md5'): 104 encoded = make_password('letmein', hasher=algo) 105 state = {'upgraded': False} 106 def setter(): 107 state['upgraded'] = True 108 self.assertFalse(check_password('WRONG', encoded, setter)) 109 self.assertFalse(state['upgraded']) -
tests/regressiontests/utils/tests.py
diff --git a/tests/regressiontests/utils/tests.py b/tests/regressiontests/utils/tests.py index e91adc9..9c301bc 100644
a b from datetime_safe import * 20 20 from baseconv import * 21 21 from jslex import * 22 22 from ipv6 import * 23 from crypto import * 24 from passhash import *