|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637 |
- import base64
- import binascii
- import functools
- import hashlib
- import importlib
- import warnings
- from collections import OrderedDict
-
- from django.conf import settings
- from django.core.exceptions import ImproperlyConfigured
- from django.core.signals import setting_changed
- from django.dispatch import receiver
- from django.utils.crypto import (
- constant_time_compare, get_random_string, pbkdf2,
- )
- from django.utils.module_loading import import_string
- from django.utils.translation import gettext_noop as _
-
- UNUSABLE_PASSWORD_PREFIX = '!' # This will never be a valid encoded hash
- UNUSABLE_PASSWORD_SUFFIX_LENGTH = 40 # number of random chars to add after UNUSABLE_PASSWORD_PREFIX
-
-
- def is_password_usable(encoded):
- """
- Return True if this password wasn't generated by
- User.set_unusable_password(), i.e. make_password(None).
- """
- return encoded is None or not encoded.startswith(UNUSABLE_PASSWORD_PREFIX)
-
-
- def check_password(password, encoded, setter=None, preferred='default'):
- """
- Return a boolean of whether the raw password matches the three
- part encoded digest.
-
- If setter is specified, it'll be called when you need to
- regenerate the password.
- """
- if password is None or not is_password_usable(encoded):
- return False
-
- preferred = get_hasher(preferred)
- try:
- hasher = identify_hasher(encoded)
- except ValueError:
- # encoded is gibberish or uses a hasher that's no longer installed.
- return False
-
- hasher_changed = hasher.algorithm != preferred.algorithm
- must_update = hasher_changed or preferred.must_update(encoded)
- is_correct = hasher.verify(password, encoded)
-
- # If the hasher didn't change (we don't protect against enumeration if it
- # does) and the password should get updated, try to close the timing gap
- # between the work factor of the current encoded password and the default
- # work factor.
- if not is_correct and not hasher_changed and must_update:
- hasher.harden_runtime(password, encoded)
-
- if setter and is_correct and must_update:
- setter(password)
- return is_correct
-
-
- def make_password(password, salt=None, hasher='default'):
- """
- Turn a plain-text password into a hash for database storage
-
- Same as encode() but generate a new random salt. If password is None then
- return a concatenation of UNUSABLE_PASSWORD_PREFIX and a random string,
- which disallows logins. Additional random string reduces chances of gaining
- access to staff or superuser accounts. See ticket #20079 for more info.
- """
- if password is None:
- return UNUSABLE_PASSWORD_PREFIX + get_random_string(UNUSABLE_PASSWORD_SUFFIX_LENGTH)
- hasher = get_hasher(hasher)
- salt = salt or hasher.salt()
- return hasher.encode(password, salt)
-
-
- @functools.lru_cache()
- def get_hashers():
- hashers = []
- for hasher_path in settings.PASSWORD_HASHERS:
- hasher_cls = import_string(hasher_path)
- hasher = hasher_cls()
- if not getattr(hasher, 'algorithm'):
- raise ImproperlyConfigured("hasher doesn't specify an "
- "algorithm name: %s" % hasher_path)
- hashers.append(hasher)
- return hashers
-
-
- @functools.lru_cache()
- def get_hashers_by_algorithm():
- return {hasher.algorithm: hasher for hasher in get_hashers()}
-
-
- @receiver(setting_changed)
- def reset_hashers(**kwargs):
- if kwargs['setting'] == 'PASSWORD_HASHERS':
- get_hashers.cache_clear()
- get_hashers_by_algorithm.cache_clear()
-
-
- def get_hasher(algorithm='default'):
- """
- Return an instance of a loaded password hasher.
-
- If algorithm is 'default', return the default hasher. Lazily import hashers
- specified in the project's settings file if needed.
- """
- if hasattr(algorithm, 'algorithm'):
- return algorithm
-
- elif algorithm == 'default':
- return get_hashers()[0]
-
- else:
- hashers = get_hashers_by_algorithm()
- try:
- return hashers[algorithm]
- except KeyError:
- raise ValueError("Unknown password hashing algorithm '%s'. "
- "Did you specify it in the PASSWORD_HASHERS "
- "setting?" % algorithm)
-
-
- def identify_hasher(encoded):
- """
- Return an instance of a loaded password hasher.
-
- Identify hasher algorithm by examining encoded hash, and call
- get_hasher() to return hasher. Raise ValueError if
- algorithm cannot be identified, or if hasher is not loaded.
- """
- # Ancient versions of Django created plain MD5 passwords and accepted
- # MD5 passwords with an empty salt.
- if ((len(encoded) == 32 and '$' not in encoded) or
- (len(encoded) == 37 and encoded.startswith('md5$$'))):
- algorithm = 'unsalted_md5'
- # Ancient versions of Django accepted SHA1 passwords with an empty salt.
- elif len(encoded) == 46 and encoded.startswith('sha1$$'):
- algorithm = 'unsalted_sha1'
- else:
- algorithm = encoded.split('$', 1)[0]
- return get_hasher(algorithm)
-
-
- def mask_hash(hash, show=6, char="*"):
- """
- Return the given hash, with only the first ``show`` number shown. The
- rest are masked with ``char`` for security reasons.
- """
- masked = hash[:show]
- masked += char * len(hash[show:])
- return masked
-
-
- class BasePasswordHasher:
- """
- Abstract base class for password hashers
-
- When creating your own hasher, you need to override algorithm,
- verify(), encode() and safe_summary().
-
- PasswordHasher objects are immutable.
- """
- algorithm = None
- library = None
-
- def _load_library(self):
- if self.library is not None:
- if isinstance(self.library, (tuple, list)):
- name, mod_path = self.library
- else:
- mod_path = self.library
- try:
- module = importlib.import_module(mod_path)
- except ImportError as e:
- raise ValueError("Couldn't load %r algorithm library: %s" %
- (self.__class__.__name__, e))
- return module
- raise ValueError("Hasher %r doesn't specify a library attribute" %
- self.__class__.__name__)
-
- def salt(self):
- """Generate a cryptographically secure nonce salt in ASCII."""
- return get_random_string()
-
- def verify(self, password, encoded):
- """Check if the given password is correct."""
- raise NotImplementedError('subclasses of BasePasswordHasher must provide a verify() method')
-
- def encode(self, password, salt):
- """
- Create an encoded database value.
-
- The result is normally formatted as "algorithm$salt$hash" and
- must be fewer than 128 characters.
- """
- raise NotImplementedError('subclasses of BasePasswordHasher must provide an encode() method')
-
- def safe_summary(self, encoded):
- """
- Return a summary of safe values.
-
- The result is a dictionary and will be used where the password field
- must be displayed to construct a safe representation of the password.
- """
- raise NotImplementedError('subclasses of BasePasswordHasher must provide a safe_summary() method')
-
- def must_update(self, encoded):
- return False
-
- def harden_runtime(self, password, encoded):
- """
- Bridge the runtime gap between the work factor supplied in `encoded`
- and the work factor suggested by this hasher.
-
- Taking PBKDF2 as an example, if `encoded` contains 20000 iterations and
- `self.iterations` is 30000, this method should run password through
- another 10000 iterations of PBKDF2. Similar approaches should exist
- for any hasher that has a work factor. If not, this method should be
- defined as a no-op to silence the warning.
- """
- warnings.warn('subclasses of BasePasswordHasher should provide a harden_runtime() method')
-
-
- class PBKDF2PasswordHasher(BasePasswordHasher):
- """
- Secure password hashing using the PBKDF2 algorithm (recommended)
-
- Configured to use PBKDF2 + HMAC + SHA256.
- The result is a 64 byte binary string. Iterations may be changed
- safely but you must rename the algorithm if you change SHA256.
- """
- algorithm = "pbkdf2_sha256"
- iterations = 150000
- digest = hashlib.sha256
-
- def encode(self, password, salt, iterations=None):
- assert password is not None
- assert salt and '$' not in salt
- iterations = iterations or self.iterations
- hash = pbkdf2(password, salt, iterations, digest=self.digest)
- hash = base64.b64encode(hash).decode('ascii').strip()
- return "%s$%d$%s$%s" % (self.algorithm, iterations, salt, hash)
-
- def verify(self, password, encoded):
- algorithm, iterations, salt, hash = encoded.split('$', 3)
- assert algorithm == self.algorithm
- encoded_2 = self.encode(password, salt, int(iterations))
- return constant_time_compare(encoded, encoded_2)
-
- def safe_summary(self, encoded):
- algorithm, iterations, salt, hash = encoded.split('$', 3)
- assert algorithm == self.algorithm
- return OrderedDict([
- (_('algorithm'), algorithm),
- (_('iterations'), iterations),
- (_('salt'), mask_hash(salt)),
- (_('hash'), mask_hash(hash)),
- ])
-
- def must_update(self, encoded):
- algorithm, iterations, salt, hash = encoded.split('$', 3)
- return int(iterations) != self.iterations
-
- def harden_runtime(self, password, encoded):
- algorithm, iterations, salt, hash = encoded.split('$', 3)
- extra_iterations = self.iterations - int(iterations)
- if extra_iterations > 0:
- self.encode(password, salt, extra_iterations)
-
-
- class PBKDF2SHA1PasswordHasher(PBKDF2PasswordHasher):
- """
- Alternate PBKDF2 hasher which uses SHA1, the default PRF
- recommended by PKCS #5. This is compatible with other
- implementations of PBKDF2, such as openssl's
- PKCS5_PBKDF2_HMAC_SHA1().
- """
- algorithm = "pbkdf2_sha1"
- digest = hashlib.sha1
-
-
- class Argon2PasswordHasher(BasePasswordHasher):
- """
- Secure password hashing using the argon2 algorithm.
-
- This is the winner of the Password Hashing Competition 2013-2015
- (https://password-hashing.net). It requires the argon2-cffi library which
- depends on native C code and might cause portability issues.
- """
- algorithm = 'argon2'
- library = 'argon2'
-
- time_cost = 2
- memory_cost = 512
- parallelism = 2
-
- def encode(self, password, salt):
- argon2 = self._load_library()
- data = argon2.low_level.hash_secret(
- password.encode(),
- salt.encode(),
- time_cost=self.time_cost,
- memory_cost=self.memory_cost,
- parallelism=self.parallelism,
- hash_len=argon2.DEFAULT_HASH_LENGTH,
- type=argon2.low_level.Type.I,
- )
- return self.algorithm + data.decode('ascii')
-
- def verify(self, password, encoded):
- argon2 = self._load_library()
- algorithm, rest = encoded.split('$', 1)
- assert algorithm == self.algorithm
- try:
- return argon2.low_level.verify_secret(
- ('$' + rest).encode('ascii'),
- password.encode(),
- type=argon2.low_level.Type.I,
- )
- except argon2.exceptions.VerificationError:
- return False
-
- def safe_summary(self, encoded):
- (algorithm, variety, version, time_cost, memory_cost, parallelism,
- salt, data) = self._decode(encoded)
- assert algorithm == self.algorithm
- return OrderedDict([
- (_('algorithm'), algorithm),
- (_('variety'), variety),
- (_('version'), version),
- (_('memory cost'), memory_cost),
- (_('time cost'), time_cost),
- (_('parallelism'), parallelism),
- (_('salt'), mask_hash(salt)),
- (_('hash'), mask_hash(data)),
- ])
-
- def must_update(self, encoded):
- (algorithm, variety, version, time_cost, memory_cost, parallelism,
- salt, data) = self._decode(encoded)
- assert algorithm == self.algorithm
- argon2 = self._load_library()
- return (
- argon2.low_level.ARGON2_VERSION != version or
- self.time_cost != time_cost or
- self.memory_cost != memory_cost or
- self.parallelism != parallelism
- )
-
- def harden_runtime(self, password, encoded):
- # The runtime for Argon2 is too complicated to implement a sensible
- # hardening algorithm.
- pass
-
- def _decode(self, encoded):
- """
- Split an encoded hash and return: (
- algorithm, variety, version, time_cost, memory_cost,
- parallelism, salt, data,
- ).
- """
- bits = encoded.split('$')
- if len(bits) == 5:
- # Argon2 < 1.3
- algorithm, variety, raw_params, salt, data = bits
- version = 0x10
- else:
- assert len(bits) == 6
- algorithm, variety, raw_version, raw_params, salt, data = bits
- assert raw_version.startswith('v=')
- version = int(raw_version[len('v='):])
- params = dict(bit.split('=', 1) for bit in raw_params.split(','))
- assert len(params) == 3 and all(x in params for x in ('t', 'm', 'p'))
- time_cost = int(params['t'])
- memory_cost = int(params['m'])
- parallelism = int(params['p'])
- return (
- algorithm, variety, version, time_cost, memory_cost, parallelism,
- salt, data,
- )
-
-
- class BCryptSHA256PasswordHasher(BasePasswordHasher):
- """
- Secure password hashing using the bcrypt algorithm (recommended)
-
- This is considered by many to be the most secure algorithm but you
- must first install the bcrypt library. Please be warned that
- this library depends on native C code and might cause portability
- issues.
- """
- algorithm = "bcrypt_sha256"
- digest = hashlib.sha256
- library = ("bcrypt", "bcrypt")
- rounds = 12
-
- def salt(self):
- bcrypt = self._load_library()
- return bcrypt.gensalt(self.rounds)
-
- def encode(self, password, salt):
- bcrypt = self._load_library()
- password = password.encode()
- # Hash the password prior to using bcrypt to prevent password
- # truncation as described in #20138.
- if self.digest is not None:
- # Use binascii.hexlify() because a hex encoded bytestring is str.
- password = binascii.hexlify(self.digest(password).digest())
-
- data = bcrypt.hashpw(password, salt)
- return "%s$%s" % (self.algorithm, data.decode('ascii'))
-
- def verify(self, password, encoded):
- algorithm, data = encoded.split('$', 1)
- assert algorithm == self.algorithm
- encoded_2 = self.encode(password, data.encode('ascii'))
- return constant_time_compare(encoded, encoded_2)
-
- def safe_summary(self, encoded):
- algorithm, empty, algostr, work_factor, data = encoded.split('$', 4)
- assert algorithm == self.algorithm
- salt, checksum = data[:22], data[22:]
- return OrderedDict([
- (_('algorithm'), algorithm),
- (_('work factor'), work_factor),
- (_('salt'), mask_hash(salt)),
- (_('checksum'), mask_hash(checksum)),
- ])
-
- def must_update(self, encoded):
- algorithm, empty, algostr, rounds, data = encoded.split('$', 4)
- return int(rounds) != self.rounds
-
- def harden_runtime(self, password, encoded):
- _, data = encoded.split('$', 1)
- salt = data[:29] # Length of the salt in bcrypt.
- rounds = data.split('$')[2]
- # work factor is logarithmic, adding one doubles the load.
- diff = 2**(self.rounds - int(rounds)) - 1
- while diff > 0:
- self.encode(password, salt.encode('ascii'))
- diff -= 1
-
-
- class BCryptPasswordHasher(BCryptSHA256PasswordHasher):
- """
- Secure password hashing using the bcrypt algorithm
-
- This is considered by many to be the most secure algorithm but you
- must first install the bcrypt library. Please be warned that
- this library depends on native C code and might cause portability
- issues.
-
- This hasher does not first hash the password which means it is subject to
- bcrypt's 72 bytes password truncation. Most use cases should prefer the
- BCryptSHA256PasswordHasher.
- """
- algorithm = "bcrypt"
- digest = None
-
-
- class SHA1PasswordHasher(BasePasswordHasher):
- """
- The SHA1 password hashing algorithm (not recommended)
- """
- algorithm = "sha1"
-
- def encode(self, password, salt):
- assert password is not None
- assert salt and '$' not in salt
- hash = hashlib.sha1((salt + password).encode()).hexdigest()
- return "%s$%s$%s" % (self.algorithm, salt, hash)
-
- def verify(self, password, encoded):
- algorithm, salt, hash = encoded.split('$', 2)
- assert algorithm == self.algorithm
- encoded_2 = self.encode(password, salt)
- return constant_time_compare(encoded, encoded_2)
-
- def safe_summary(self, encoded):
- algorithm, salt, hash = encoded.split('$', 2)
- assert algorithm == self.algorithm
- return OrderedDict([
- (_('algorithm'), algorithm),
- (_('salt'), mask_hash(salt, show=2)),
- (_('hash'), mask_hash(hash)),
- ])
-
- def harden_runtime(self, password, encoded):
- pass
-
-
- class MD5PasswordHasher(BasePasswordHasher):
- """
- The Salted MD5 password hashing algorithm (not recommended)
- """
- algorithm = "md5"
-
- def encode(self, password, salt):
- assert password is not None
- assert salt and '$' not in salt
- hash = hashlib.md5((salt + password).encode()).hexdigest()
- return "%s$%s$%s" % (self.algorithm, salt, hash)
-
- def verify(self, password, encoded):
- algorithm, salt, hash = encoded.split('$', 2)
- assert algorithm == self.algorithm
- encoded_2 = self.encode(password, salt)
- return constant_time_compare(encoded, encoded_2)
-
- def safe_summary(self, encoded):
- algorithm, salt, hash = encoded.split('$', 2)
- assert algorithm == self.algorithm
- return OrderedDict([
- (_('algorithm'), algorithm),
- (_('salt'), mask_hash(salt, show=2)),
- (_('hash'), mask_hash(hash)),
- ])
-
- def harden_runtime(self, password, encoded):
- pass
-
-
- class UnsaltedSHA1PasswordHasher(BasePasswordHasher):
- """
- Very insecure algorithm that you should *never* use; store SHA1 hashes
- with an empty salt.
-
- This class is implemented because Django used to accept such password
- hashes. Some older Django installs still have these values lingering
- around so we need to handle and upgrade them properly.
- """
- algorithm = "unsalted_sha1"
-
- def salt(self):
- return ''
-
- def encode(self, password, salt):
- assert salt == ''
- hash = hashlib.sha1(password.encode()).hexdigest()
- return 'sha1$$%s' % hash
-
- def verify(self, password, encoded):
- encoded_2 = self.encode(password, '')
- return constant_time_compare(encoded, encoded_2)
-
- def safe_summary(self, encoded):
- assert encoded.startswith('sha1$$')
- hash = encoded[6:]
- return OrderedDict([
- (_('algorithm'), self.algorithm),
- (_('hash'), mask_hash(hash)),
- ])
-
- def harden_runtime(self, password, encoded):
- pass
-
-
- class UnsaltedMD5PasswordHasher(BasePasswordHasher):
- """
- Incredibly insecure algorithm that you should *never* use; stores unsalted
- MD5 hashes without the algorithm prefix, also accepts MD5 hashes with an
- empty salt.
-
- This class is implemented because Django used to store passwords this way
- and to accept such password hashes. Some older Django installs still have
- these values lingering around so we need to handle and upgrade them
- properly.
- """
- algorithm = "unsalted_md5"
-
- def salt(self):
- return ''
-
- def encode(self, password, salt):
- assert salt == ''
- return hashlib.md5(password.encode()).hexdigest()
-
- def verify(self, password, encoded):
- if len(encoded) == 37 and encoded.startswith('md5$$'):
- encoded = encoded[5:]
- encoded_2 = self.encode(password, '')
- return constant_time_compare(encoded, encoded_2)
-
- def safe_summary(self, encoded):
- return OrderedDict([
- (_('algorithm'), self.algorithm),
- (_('hash'), mask_hash(encoded, show=3)),
- ])
-
- def harden_runtime(self, password, encoded):
- pass
-
-
- class CryptPasswordHasher(BasePasswordHasher):
- """
- Password hashing using UNIX crypt (not recommended)
-
- The crypt module is not supported on all platforms.
- """
- algorithm = "crypt"
- library = "crypt"
-
- def salt(self):
- return get_random_string(2)
-
- def encode(self, password, salt):
- crypt = self._load_library()
- assert len(salt) == 2
- data = crypt.crypt(password, salt)
- assert data is not None # A platform like OpenBSD with a dummy crypt module.
- # we don't need to store the salt, but Django used to do this
- return "%s$%s$%s" % (self.algorithm, '', data)
-
- def verify(self, password, encoded):
- crypt = self._load_library()
- algorithm, salt, data = encoded.split('$', 2)
- assert algorithm == self.algorithm
- return constant_time_compare(data, crypt.crypt(password, data))
-
- def safe_summary(self, encoded):
- algorithm, salt, data = encoded.split('$', 2)
- assert algorithm == self.algorithm
- return OrderedDict([
- (_('algorithm'), algorithm),
- (_('salt'), salt),
- (_('hash'), mask_hash(data, show=3)),
- ])
-
- def harden_runtime(self, password, encoded):
- pass
|