Ticket #15367: django-passhash-2011-09-10.diff

File django-passhash-2011-09-10.diff, 27.0 KB (added by Justine Tunney, 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/'  
    472472# The number of days a password reset link is valid for
    473473PASSWORD_RESET_TIMEOUT_DAYS = 3
    474474
     475# the first hasher in this list is the preferred algorithm.  any
     476# password using different algorithms will be converted automatically
     477# upon login
     478PASSWORD_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
    475496###########
    476497# SIGNING #
    477498###########
  • 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 _  
    1111from django.contrib import auth
    1212from django.contrib.auth.signals import user_logged_in
    1313# 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)
     14from django.utils.passhash import \
     15    check_password, make_password, is_password_usable, UNUSABLE_PASSWORD
    1716from django.contrib.contenttypes.models import ContentType
    1817
    1918def update_last_login(sender, user, **kwargs):
    class User(models.Model):  
    228227        return full_name.strip()
    229228
    230229    def set_password(self, raw_password):
    231         self.password = make_password('sha1', raw_password)
     230        self.password = make_password(raw_password)
    232231
    233232    def check_password(self, raw_password):
    234233        """
    235234        Returns a boolean of whether the raw_password was correct. Handles
    236235        encryption formats behind the scenes.
    237236        """
    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)
    248241
    249242    def set_unusable_password(self):
    250243        # Sets a value that will never be a valid hash
    251         self.password = make_password('sha1', None)
     244        self.password = make_password(None)
    252245
    253246    def has_usable_password(self):
    254247        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 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)
  • django/utils/crypto.py

    diff --git a/django/utils/crypto.py b/django/utils/crypto.py
    index 95af680..277a3af 100644
    a b  
    22Django's standard crypto functions and utilities.
    33"""
    44
    5 import hashlib
    65import hmac
     6import struct
     7import hashlib
    78from django.conf import settings
    89
     10
    911def salted_hmac(key_salt, value, secret=None):
    1012    """
    1113    Returns the HMAC-SHA1 of 'value', using a key generated from key_salt and a
    def salted_hmac(key_salt, value, secret=None):  
    2729    # However, we need to ensure that we *always* do this.
    2830    return hmac.new(key, msg=value, digestmod=hashlib.sha1)
    2931
     32
    3033def constant_time_compare(val1, val2):
    3134    """
    3235    Returns True if the two strings are equal, False otherwise.
    def constant_time_compare(val1, val2):  
    3942    for x, y in zip(val1, val2):
    4043        result |= ord(x) ^ ord(y)
    4144    return result == 0
     45
     46
     47class 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
     66def 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
     15import hashlib
     16
     17from django.conf import settings
     18from django.utils import importlib
     19from django.utils.encoding import smart_str
     20from django.utils.crypto import pbkdf2, constant_time_compare
     21from django.contrib.auth.utils import get_random_string
     22from django.core.exceptions import ImproperlyConfigured
     23
     24
     25UNUSABLE_PASSWORD = '!'  # This will never be a valid encoded hash
     26HASHERS = None  # lazily loaded from PASSWORD_HASHERS
     27PREFERRED_HASHER = None  # defaults to first item in PASSWORD_HASHERS
     28
     29
     30def is_password_usable(encoded):
     31    return (encoded is not None and encoded != UNUSABLE_PASSWORD)
     32
     33
     34def 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
     61def 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
     79def 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
     103def 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
     126def 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
     143class InvalidPasswordHasherError(ImproperlyConfigured):
     144    pass
     145
     146
     147class 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
     180class 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
     209class 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
     247class 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
     266class 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
     289class 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
     2import hashlib
     3
     4from django.utils import unittest
     5from django.utils.crypto import pbkdf2, PBKDF2RandomSource
     6
     7
     8class 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
     2from django.utils import unittest
     3from django.utils.passhash import *
     4
     5
     6class 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 *  
    2020from baseconv import *
    2121from jslex import *
    2222from ipv6 import *
     23from crypto import *
     24from passhash import *
Back to Top