Ticket #15367: django-passhash-2011-09-10.diff
File django-passhash-2011-09-10.diff, 27.0 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..3c891c1 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': 2000, # 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/models.py
diff --git a/django/contrib/auth/models.py b/django/contrib/auth/models.py index 5ae4817..1abf9c0 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) -
deleted file django/contrib/auth/utils.py
diff --git a/django/contrib/auth/utils.py b/django/contrib/auth/utils.py deleted file mode 100644 index 57c693f..0000000
+ - 1 import hashlib2 from django.utils.encoding import smart_str3 from django.utils.crypto import constant_time_compare4 5 UNUSABLE_PASSWORD = '!' # This will never be a valid hash6 7 def get_hexdigest(algorithm, salt, raw_password):8 """9 Returns a string of the hexdigest of the given plaintext password and salt10 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 crypt16 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-929 for use as a salt.30 31 The default length of 12 with the a-z, A-Z, 0-9 character set returns32 a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits33 """34 import random35 try:36 random = random.SystemRandom()37 except NotImplementedError:38 pass39 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. Handles44 encryption formats behind the scenes.45 """46 parts = enc_password.split('$')47 if len(parts) != 3:48 return False49 algo, salt, hsh = parts50 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_PASSWORD54 55 def make_password(algo, raw_password):56 """57 Produce a new password string in this format: algorithm$salt$hash58 """59 if raw_password is None:60 return UNUSABLE_PASSWORD61 salt = get_random_string()62 hsh = get_hexdigest(algo, salt, raw_password)63 return '%s$%s$%s' % (algo, salt, hsh) -
django/utils/crypto.py
diff --git a/django/utils/crypto.py b/django/utils/crypto.py index 95af680..277a3af 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 7 8 from django.conf import settings 8 9 10 9 11 def salted_hmac(key_salt, value, secret=None): 10 12 """ 11 13 Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a … … def salted_hmac(key_salt, value, secret=None): 27 29 # However, we need to ensure that we *always* do this. 28 30 return hmac.new(key, msg=value, digestmod=hashlib.sha1) 29 31 32 30 33 def constant_time_compare(val1, val2): 31 34 """ 32 35 Returns True if the two strings are equal, False otherwise. … … def constant_time_compare(val1, val2): 39 42 for x, y in zip(val1, val2): 40 43 result |= ord(x) ^ ord(y) 41 44 return result == 0 45 46 47 class PBKDF2RandomSource(object): 48 """ 49 Underlying pseudorandom function (PRF) for pbkdf2() 50 51 For example:: 52 53 import hashlib 54 prf = PBKDF2RandomSource(hashlib.sha256) 55 56 """ 57 58 def __init__(self, digest): 59 self.digest = digest 60 self.digest_size = digest().digest_size 61 62 def __call__(self, key, data): 63 return hmac.new(key, data, self.digest).digest() 64 65 66 def pbkdf2(password, salt, iterations=2000, dklen=0, prf=None): 67 """ 68 Implements PBKDF2 as defined in RFC 2898, section 5.2 69 70 Based on a routine written by aaz: 71 http://stackoverflow.com/questions/5130513/pbkdf2-hmac-sha2-test-vectors 72 73 DO NOT change the default behavior of this function. Ever. 74 75 For example:: 76 77 >>> pbkdf2("password", "salt", dklen=20).encode('hex') 78 'afe6c5530785b6cc6b1c6453384731bd5ee432ee' 79 80 """ 81 assert iterations > 0 82 if not prf: 83 prf = PBKDF2RandomSource(hashlib.sha256) 84 hlen = prf.digest_size 85 if not dklen: 86 dklen = hlen 87 if dklen > (2 ** 32 - 1) * hlen: 88 raise ValueError('dklen too big') 89 l = -(-dklen // hlen) 90 r = dklen - (l - 1) * hlen 91 92 def int_to_32bit_be(i): 93 assert i > 0 94 return struct.pack('>I', i) 95 96 def xor_string(A, B): 97 return ''.join([chr(ord(a) ^ ord(b)) for a, b in zip(A, B)]) 98 99 def F(i): 100 def U(): 101 U = salt + int_to_32bit_be(i) 102 for j in range(iterations): 103 U = prf(password, U) 104 yield U 105 return reduce(xor_string, U()) 106 107 T = [F(x) for x in range(1, l + 1)] 108 dk = ''.join(T[:-1]) + T[-1][:r] 109 return dk -
new file django/utils/passhash.py
diff --git a/django/utils/passhash.py b/django/utils/passhash.py new file mode 100644 index 0000000..14202cb
- + 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.utils.crypto import pbkdf2, constant_time_compare 21 from django.contrib.auth.utils import get_random_string 22 from django.core.exceptions import ImproperlyConfigured 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 hasher = determine_hasher(encoded) 50 must_update = (hasher.algorithm != preferred.algorithm) 51 if encoded.startswith('$2a$'): 52 # migration for people who used django-bcrypt 53 encoded = 'bcrypt$' + encoded 54 must_update = True 55 is_correct = hasher.verify(password, encoded) 56 if setter and is_correct and must_update: 57 setter() 58 return is_correct 59 60 61 def make_password(password, salt=None, hasher='default'): 62 """ 63 Turn a plain-text password into a hash for database storage 64 65 Same as encode() but generates a new random salt. If 66 password is None or blank then UNUSABLE_PASSWORD will be 67 returned which disallows logins. 68 """ 69 if not password: 70 return UNUSABLE_PASSWORD 71 hasher = get_hasher(hasher) 72 if not salt: 73 salt = hasher.gensalt() 74 password = smart_str(password) 75 salt = smart_str(salt) 76 return hasher.encode(password, salt) 77 78 79 def get_hasher(algorithm='default'): 80 """ 81 Returns an instance of a loaded password hasher. 82 83 If algorithm is 'default', the default hasher will be returned. 84 This function will also lazy import hashers specified in your 85 settings file if needed. 86 """ 87 if hasattr(algorithm, 'algorithm'): 88 return algorithm 89 elif algorithm == 'default': 90 if PREFERRED_HASHER is None: 91 load_hashers() 92 return PREFERRED_HASHER 93 else: 94 if HASHERS is None: 95 load_hashers() 96 if algorithm not in HASHERS: 97 raise ValueError( 98 ('Unknown password hashing algorithm "%s". Did you specify ' 99 'it in PASSWORD_HASHERS?') % (algorithm)) 100 return HASHERS[algorithm] 101 102 103 def load_hashers(): 104 global HASHERS 105 global PREFERRED_HASHER 106 hashers = [] 107 for spec in settings.PASSWORD_HASHERS: 108 backend = spec['BACKEND'] 109 kwargs = spec.get('OPTIONS', {}) 110 try: 111 mod_path, cls_name = backend.rsplit('.', 1) 112 mod = importlib.import_module(mod_path) 113 hasher_cls = getattr(mod, cls_name) 114 except (AttributeError, ImportError, ValueError): 115 raise InvalidPasswordHasherError( 116 "hasher not found: %s" % (backend)) 117 hasher = hasher_cls(**kwargs) 118 if not getattr(hasher, 'algorithm'): 119 raise InvalidPasswordHasherError( 120 "hasher doesn't specify an algorithm name: %s" % (backend)) 121 hashers.append(hasher) 122 HASHERS = dict([(hasher.algorithm, hasher) for hasher in hashers]) 123 PREFERRED_HASHER = hashers[0] 124 125 126 def determine_hasher(encoded): 127 """ 128 Which hasher is being used for this encoded password? 129 """ 130 assert encoded 131 encoded = smart_str(encoded) 132 if len(encoded) == 32 and '$' not in encoded: 133 # migration for legacy unsalted md5 passwords 134 return get_hasher('md5') 135 elif encoded.startswith('$2a$'): 136 # migration for people who used django-bcrypt 137 return get_hasher('bcrypt') 138 else: 139 algorithm = encoded.split('$', 1)[0] 140 return get_hasher(algorithm) 141 142 143 class InvalidPasswordHasherError(ImproperlyConfigured): 144 pass 145 146 147 class BasePasswordHasher(object): 148 """ 149 Abstract base class for password hashers 150 151 When creating your own hasher, you need to override algorithm, 152 verify() and encode(). 153 154 PasswordHasher objects are immutable. 155 """ 156 algorithm = None 157 158 def gensalt(self): 159 """ 160 I should generate cryptographically secure nonce salt in ascii 161 """ 162 return get_random_string() 163 164 def verify(self, password, encoded): 165 """ 166 Abstract method to check if password is correct 167 """ 168 raise NotImplementedError() 169 170 def encode(self, password, salt): 171 """ 172 Abstract method for creating encoded database values 173 174 The result is normally formatted as "algorithm$salt$hash" and 175 must be fewer than 128 characters. 176 """ 177 raise NotImplementedError() 178 179 180 class PBKDF2PasswordHasher(BasePasswordHasher): 181 """ 182 Secure password hashing using the PBKDF2 algorithm (recommended) 183 184 I'm configured to use PBKDF2 + HMAC + SHA256 with 2000 iterations. 185 The result is a 64 byte binary string. 186 """ 187 algorithm = "pbkdf2" 188 189 def __init__(self, iterations=2000): 190 BasePasswordHasher.__init__(self) 191 self.iterations = iterations 192 193 def encode(self, password, salt, iterations=None): 194 assert password 195 assert salt and '$' not in salt 196 if not iterations: 197 iterations = self.iterations 198 hash = pbkdf2(password, salt, iterations) 199 hash = hash.encode('base64').strip() 200 return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash) 201 202 def verify(self, password, encoded): 203 algorithm, iterations, salt, hash = encoded.split('$', 3) 204 assert algorithm == self.algorithm 205 encoded_2 = self.encode(password, salt, int(iterations)) 206 return constant_time_compare(encoded, encoded_2) 207 208 209 class BCryptPasswordHasher(BasePasswordHasher): 210 """ 211 Secure password hashing using the bcrypt algorithm (recommended) 212 213 This is considered by many to be the most secure algorithm but you 214 must first install the py-crypt library. Please be warned that 215 this library depends on native C code and might cause portability 216 issues. 217 """ 218 algorithm = "bcrypt" 219 220 def __init__(self, rounds=12): 221 BasePasswordHasher.__init__(self) 222 self.rounds = rounds 223 224 def _import(self): 225 try: 226 import bcrypt 227 except ImportError: 228 raise ValueError('py-bcrypt library not installed') 229 return bcrypt 230 231 def gensalt(self): 232 bcrypt = self._import() 233 return bcrypt.gensalt(self.rounds) 234 235 def encode(self, password, salt): 236 bcrypt = self._import() 237 data = bcrypt.hashpw(password, salt) 238 return "%s$%s" % (self.algorithm, data) 239 240 def verify(self, password, encoded): 241 bcrypt = self._import() 242 algorithm, data = encoded.split('$', 1) 243 assert algorithm == self.algorithm 244 return constant_time_compare(data, bcrypt.hashpw(password, data)) 245 246 247 class SHA1PasswordHasher(BasePasswordHasher): 248 """ 249 The SHA1 password hashing algorithm (not recommended) 250 """ 251 algorithm = "sha1" 252 253 def encode(self, password, salt): 254 assert password 255 assert salt and '$' not in salt 256 hash = hashlib.sha1(salt + password).hexdigest() 257 return "%s$%s$%s" % (self.algorithm, salt, hash) 258 259 def verify(self, password, encoded): 260 algorithm, salt, hash = encoded.split('$', 2) 261 assert algorithm == self.algorithm 262 encoded_2 = self.encode(password, salt) 263 return constant_time_compare(encoded, encoded_2) 264 265 266 class MD5PasswordHasher(BasePasswordHasher): 267 """ 268 I am an incredibly insecure algorithm you should *never* use 269 270 I store unsalted MD5 hashes without the algorithm prefix. 271 272 This class is implemented because Django used to store passwords 273 this way. Some older Django installs still have these values 274 lingering around so we need to handle and upgrade them properly. 275 """ 276 algorithm = "md5" 277 278 def gensalt(self): 279 return '' 280 281 def encode(self, password, salt): 282 return hashlib.md5(password).hexdigest() 283 284 def verify(self, password, encoded): 285 encoded_2 = self.encode(password, '') 286 return constant_time_compare(encoded, encoded_2) 287 288 289 class CryptPasswordHasher(BasePasswordHasher): 290 """ 291 Password hashing using UNIX crypt (not recommended) 292 293 The crypt module is not supported on all platforms. 294 """ 295 algorithm = "crypt" 296 297 def _import(self): 298 try: 299 import crypt 300 except ImportError: 301 raise ValueError('"crypt" password algorithm not supported in ' 302 'this environment') 303 return crypt 304 305 def gensalt(self): 306 return get_random_string(2) 307 308 def encode(self, password, salt): 309 crypt = self._import() 310 assert len(salt) == 2 311 data = crypt.crypt(password, salt) 312 # we don't need to store the salt, but django used to do this 313 return "%s$%s$%s" % (self.algorithm, '', data) 314 315 def verify(self, password, encoded): 316 crypt = self._import() 317 algorithm, salt, data = encoded.split('$', 2) 318 assert algorithm == self.algorithm 319 return constant_time_compare(data, crypt.crypt(password, data)) -
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..e8b19ec
- + 1 2 import hashlib 3 4 from django.utils import unittest 5 from django.utils.crypto import pbkdf2, PBKDF2RandomSource 6 7 8 class TestUtilsCryptoPBKDF2(unittest.TestCase): 9 """ 10 Tests PBKDF2 implementation against public test vectors in: 11 http://tools.ietf.org/html/draft-josefsson-pbkdf2-test-vectors-06 12 13 Unofficial vectors are also included to make sure other people get 14 the same results I did with different hashing algorithms. 15 """ 16 17 rfc_vectors = [ 18 { 19 "args": { 20 "password": "password", 21 "salt": "salt", 22 "iterations": 1, 23 "dklen": 20, 24 "prf": PBKDF2RandomSource(hashlib.sha1), 25 }, 26 "result": "0c60c80f961f0e71f3a9b524af6012062fe037a6", 27 }, 28 { 29 "args": { 30 "password": "password", 31 "salt": "salt", 32 "iterations": 2, 33 "dklen": 20, 34 "prf": PBKDF2RandomSource(hashlib.sha1), 35 }, 36 "result": "ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957", 37 }, 38 { 39 "args": { 40 "password": "password", 41 "salt": "salt", 42 "iterations": 4096, 43 "dklen": 20, 44 "prf": PBKDF2RandomSource(hashlib.sha1), 45 }, 46 "result": "4b007901b765489abead49d926f721d065a429c1", 47 }, 48 # # this takes way too long :( 49 # { 50 # "args": { 51 # "password": "password", 52 # "salt": "salt", 53 # "iterations": 16777216, 54 # "dklen": 20, 55 # "prf": PBKDF2RandomSource(hashlib.sha1), 56 # }, 57 # "result": "eefe3d61cd4da4e4e9945b3d6ba2158c2634e984", 58 # }, 59 { 60 "args": { 61 "password": "passwordPASSWORDpassword", 62 "salt": "saltSALTsaltSALTsaltSALTsaltSALTsalt", 63 "iterations": 4096, 64 "dklen": 25, 65 "prf": PBKDF2RandomSource(hashlib.sha1), 66 }, 67 "result": "3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038", 68 }, 69 { 70 "args": { 71 "password": "pass\0word", 72 "salt": "sa\0lt", 73 "iterations": 4096, 74 "dklen": 16, 75 "prf": PBKDF2RandomSource(hashlib.sha1), 76 }, 77 "result": "56fa6aa75548099dcc37d7f03425e0c3", 78 }, 79 ] 80 81 my_vectors = [ 82 { 83 "args": { 84 "password": "password", 85 "salt": "salt", 86 "iterations": 1, 87 "dklen": 20, 88 "prf": PBKDF2RandomSource(hashlib.sha256), 89 }, 90 "result": "120fb6cffcf8b32c43e7225256c4f837a86548c9", 91 }, 92 { 93 "args": { 94 "password": "password", 95 "salt": "salt", 96 "iterations": 1, 97 "dklen": 20, 98 "prf": PBKDF2RandomSource(hashlib.sha512), 99 }, 100 "result": "867f70cf1ade02cff3752599a3a53dc4af34c7a6", 101 }, 102 { 103 "args": { 104 "password": "password", 105 "salt": "salt", 106 "iterations": 1000, 107 "dklen": 0, 108 "prf": PBKDF2RandomSource(hashlib.sha512), 109 }, 110 "result": ("afe6c5530785b6cc6b1c6453384731bd5ee432ee" 111 "549fd42fb6695779ad8a1c5bf59de69c48f774ef" 112 "c4007d5298f9033c0241d5ab69305e7b64eceeb8d" 113 "834cfec"), 114 }, 115 ] 116 117 def test_vectors(self): 118 for vector in self.rfc_vectors + self.my_vectors: 119 result = pbkdf2(**vector['args']) 120 self.assertEqual(result.encode('hex'), vector['result']) -
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..4cbca25
- + 1 2 from django.utils import unittest 3 from django.utils.passhash import * 4 5 6 class TestUtilsHashPass(unittest.TestCase): 7 8 def test_simple(self): 9 encoded = make_password('letmein') 10 self.assertTrue(encoded.startswith('pbkdf2$')) 11 self.assertTrue(check_password(u'letmein', encoded)) 12 self.assertFalse(check_password('letmeinz', encoded)) 13 14 def test_pkbdf2(self): 15 encoded = make_password('letmein', 'seasalt', 'pbkdf2') 16 self.assertEqual(encoded, 'pbkdf2$2000$seasalt$BmIZnhZ3zVdDpviQIvlBPZUHRP/UnT5uEqiSr17zLg4=') 17 self.assertTrue(check_password(u'letmein', encoded)) 18 self.assertFalse(check_password('letmeinz', encoded)) 19 20 def test_sha1(self): 21 encoded = make_password('letmein', 'seasalt', 'sha1') 22 self.assertEqual(encoded, 'sha1$seasalt$fec3530984afba6bade3347b7140d1a7da7da8c7') 23 self.assertTrue(check_password(u'letmein', encoded)) 24 self.assertFalse(check_password('letmeinz', encoded)) 25 26 def test_md5(self): 27 encoded = make_password('letmein', 'seasalt', 'md5') 28 self.assertEqual(encoded, '0d107d09f5bbe40cade3de5c71e9e9b7') 29 self.assertTrue(check_password(u'letmein', encoded)) 30 self.assertFalse(check_password('letmeinz', encoded)) 31 32 def test_crypt(self): 33 try: 34 import crypt 35 except ImportError: 36 return 37 encoded = make_password('letmein', 'ab', 'crypt') 38 self.assertEqual(encoded, 'crypt$$abN/qM.L/H8EQ') 39 self.assertTrue(check_password(u'letmein', encoded)) 40 self.assertFalse(check_password('letmeinz', encoded)) 41 42 def test_bcrypt(self): 43 try: 44 import bcrypt 45 except ImportError: 46 return 47 encoded = make_password('letmein', hasher='bcrypt') 48 self.assertTrue(encoded.startswith('bcrypt$')) 49 self.assertTrue(check_password(u'letmein', encoded)) 50 self.assertFalse(check_password('letmeinz', encoded)) 51 52 def test_unusable(self): 53 encoded = make_password(None) 54 self.assertFalse(is_password_usable(encoded)) 55 self.assertFalse(check_password(None, encoded)) 56 self.assertFalse(check_password(UNUSABLE_PASSWORD, encoded)) 57 self.assertFalse(check_password('', encoded)) 58 self.assertFalse(check_password(u'letmein', encoded)) 59 self.assertFalse(check_password('letmeinz', encoded)) 60 61 def test_bad_algorithm(self): 62 def doit(): 63 make_password('letmein', hasher='lolcat') 64 self.assertRaises(ValueError, doit) 65 66 def test_low_level_pkbdf2(self): 67 hasher = PBKDF2PasswordHasher() 68 encoded = hasher.encode('letmein', 'seasalt') 69 self.assertEqual(encoded, 'pbkdf2$2000$seasalt$BmIZnhZ3zVdDpviQIvlBPZUHRP/UnT5uEqiSr17zLg4=') 70 self.assertTrue(hasher.verify('letmein', encoded)) 71 72 def test_upgrade(self): 73 self.assertEqual('pbkdf2', get_hasher('default').algorithm) 74 for algo in ('sha1', 'md5'): 75 encoded = make_password('letmein', hasher=algo) 76 state = {'upgraded': False} 77 def setter(): 78 state['upgraded'] = True 79 self.assertTrue(check_password('letmein', encoded, setter)) 80 self.assertTrue(state['upgraded']) 81 82 def test_no_upgrade(self): 83 encoded = make_password('letmein') 84 state = {'upgraded': False} 85 def setter(): 86 state['upgraded'] = True 87 self.assertFalse(check_password('WRONG', encoded, setter)) 88 self.assertFalse(state['upgraded']) 89 90 def test_no_upgrade_on_incorrect_pass(self): 91 self.assertEqual('pbkdf2', get_hasher('default').algorithm) 92 for algo in ('sha1', 'md5'): 93 encoded = make_password('letmein', hasher=algo) 94 state = {'upgraded': False} 95 def setter(): 96 state['upgraded'] = True 97 self.assertFalse(check_password('WRONG', encoded, setter)) 98 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 *