Ticket #15367: django-passhash-2011-09-12.diff
File django-passhash-2011-09-12.diff, 32.8 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..47c4986 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 'django.utils.passhash.PBKDF2PasswordHasher', 480 'django.utils.passhash.BCryptPasswordHasher', 481 'django.utils.passhash.SHA1PasswordHasher', 482 'django.utils.passhash.MD5PasswordHasher', 483 'django.utils.passhash.CryptPasswordHasher', 484 ) 485 475 486 ########### 476 487 # SIGNING # 477 488 ########### -
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..d8f4545
- + 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 backend in settings.PASSWORD_HASHERS: 109 try: 110 mod_path, cls_name = backend.rsplit('.', 1) 111 mod = importlib.import_module(mod_path) 112 hasher_cls = getattr(mod, cls_name) 113 except (AttributeError, ImportError, ValueError): 114 raise InvalidPasswordHasherError( 115 "hasher not found: %s" % (backend)) 116 hasher = hasher_cls() 117 if not getattr(hasher, 'algorithm'): 118 raise InvalidPasswordHasherError( 119 "hasher doesn't specify an algorithm name: %s" % (backend)) 120 hashers.append(hasher) 121 HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers]) 122 PREFERRED_HASHER = hashers[0] 123 124 125 def determine_hasher(encoded): 126 """ 127 Which hasher is being used for this encoded password? 128 """ 129 if len(encoded) == 32 and '$' not in encoded: 130 # migration for legacy unsalted md5 passwords 131 return get_hasher('md5') 132 else: 133 algorithm = encoded.split('$', 1)[0] 134 return get_hasher(algorithm) 135 136 137 class InvalidPasswordHasherError(ImproperlyConfigured): 138 pass 139 140 141 class BasePasswordHasher(object): 142 """ 143 Abstract base class for password hashers 144 145 When creating your own hasher, you need to override algorithm, 146 verify() and encode(). 147 148 PasswordHasher objects are immutable. 149 """ 150 algorithm = None 151 152 def gensalt(self): 153 """ 154 I should generate cryptographically secure nonce salt in ascii 155 """ 156 return get_random_string() 157 158 def verify(self, password, encoded): 159 """ 160 Abstract method to check if password is correct 161 """ 162 raise NotImplementedError() 163 164 def encode(self, password, salt): 165 """ 166 Abstract method for creating encoded database values 167 168 The result is normally formatted as "algorithm$salt$hash" and 169 must be fewer than 128 characters. 170 """ 171 raise NotImplementedError() 172 173 174 class PBKDF2PasswordHasher(BasePasswordHasher): 175 """ 176 Secure password hashing using the PBKDF2 algorithm (recommended) 177 178 I'm configured to use PBKDF2 + HMAC + SHA256 with 10000 179 iterations. The result is a 64 byte binary string. Iterations 180 may be changed safely but you must rename the algorithm if you 181 change SHA256. 182 """ 183 algorithm = "pbkdf2" 184 iterations = 10000 185 186 def encode(self, password, salt, iterations=None): 187 assert password 188 assert salt and '$' not in salt 189 if not iterations: 190 iterations = self.iterations 191 hash = pbkdf2(password, salt, iterations) 192 hash = hash.encode('base64').strip() 193 return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 194 195 def verify(self, password, encoded): 196 algorithm, iterations, salt, hash = encoded.split('$', 3) 197 assert algorithm == self.algorithm 198 encoded_2 = self.encode(password, salt, int(iterations)) 199 return constant_time_compare(encoded, encoded_2) 200 201 202 class BCryptPasswordHasher(BasePasswordHasher): 203 """ 204 Secure password hashing using the bcrypt algorithm (recommended) 205 206 This is considered by many to be the most secure algorithm but you 207 must first install the py-crypt library. Please be warned that 208 this library depends on native C code and might cause portability 209 issues. 210 """ 211 algorithm = "bcrypt" 212 rounds = 12 213 214 def _import(self): 215 try: 216 import bcrypt 217 except ImportError: 218 raise ValueError('py-bcrypt library not installed') 219 return bcrypt 220 221 def gensalt(self): 222 bcrypt = self._import() 223 return bcrypt.gensalt(self.rounds) 224 225 def encode(self, password, salt): 226 bcrypt = self._import() 227 data = bcrypt.hashpw(password, salt) 228 return "%s$%s" % (self.algorithm, data) 229 230 def verify(self, password, encoded): 231 bcrypt = self._import() 232 algorithm, data = encoded.split('$', 1) 233 assert algorithm == self.algorithm 234 return constant_time_compare(data, bcrypt.hashpw(password, data)) 235 236 237 class SHA1PasswordHasher(BasePasswordHasher): 238 """ 239 The SHA1 password hashing algorithm (not recommended) 240 """ 241 algorithm = "sha1" 242 243 def encode(self, password, salt): 244 assert password 245 assert salt and '$' not in salt 246 hash = hashlib.sha1(salt + password).hexdigest() 247 return "%s$%s$%s" % (self.algorithm, salt, hash) 248 249 def verify(self, password, encoded): 250 algorithm, salt, hash = encoded.split('$', 2) 251 assert algorithm == self.algorithm 252 encoded_2 = self.encode(password, salt) 253 return constant_time_compare(encoded, encoded_2) 254 255 256 class MD5PasswordHasher(BasePasswordHasher): 257 """ 258 I am an incredibly insecure algorithm you should *never* use 259 260 I store unsalted MD5 hashes without the algorithm prefix. 261 262 This class is implemented because Django used to store passwords 263 this way. Some older Django installs still have these values 264 lingering around so we need to handle and upgrade them properly. 265 """ 266 algorithm = "md5" 267 268 def gensalt(self): 269 return '' 270 271 def encode(self, password, salt): 272 return hashlib.md5(password).hexdigest() 273 274 def verify(self, password, encoded): 275 encoded_2 = self.encode(password, '') 276 return constant_time_compare(encoded, encoded_2) 277 278 279 class CryptPasswordHasher(BasePasswordHasher): 280 """ 281 Password hashing using UNIX crypt (not recommended) 282 283 The crypt module is not supported on all platforms. 284 """ 285 algorithm = "crypt" 286 287 def _import(self): 288 try: 289 import crypt 290 except ImportError: 291 raise ValueError('"crypt" password algorithm not supported in ' 292 'this environment') 293 return crypt 294 295 def gensalt(self): 296 return get_random_string(2) 297 298 def encode(self, password, salt): 299 crypt = self._import() 300 assert len(salt) == 2 301 data = crypt.crypt(password, salt) 302 # we don't need to store the salt, but django used to do this 303 return "%s$%s$%s" % (self.algorithm, '', data) 304 305 def verify(self, password, encoded): 306 crypt = self._import() 307 algorithm, salt, data = encoded.split('$', 2) 308 assert algorithm == self.algorithm 309 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 *