import hashlib from django.utils.encoding import smart_str from django.utils.crypto import constant_time_compare UNUSABLE_PASSWORD = '!' # This will never be a valid hash def get_hexdigest(algorithm, salt, raw_password): """ Returns a string of the hexdigest of the given plaintext password and salt using the given algorithm ('md5', 'sha1' or 'crypt'). """ raw_password, salt = smart_str(raw_password), smart_str(salt) if algorithm == 'crypt': try: import crypt except ImportError: raise ValueError('"crypt" password algorithm not supported in this environment') return crypt.crypt(raw_password, salt) if algorithm == 'md5': return hashlib.md5(salt + raw_password).hexdigest() elif algorithm == 'sha1': return hashlib.sha1(salt + raw_password).hexdigest() raise ValueError("Got unknown password algorithm type in password.") def get_random_string(length=12, allowed_chars='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'): """ Returns a random string of length characters from the set of a-z, A-Z, 0-9 for use as a salt. The default length of 12 with the a-z, A-Z, 0-9 character set returns a 71-bit salt. log_2((26+26+10)^12) =~ 71 bits """ import random try: random = random.SystemRandom() except NotImplementedError: pass return ''.join([random.choice(allowed_chars) for i in range(length)]) def check_password(raw_password, enc_password): """ Returns a boolean of whether the raw_password was correct. Handles encryption formats behind the scenes. """ parts = enc_password.split('$') if len(parts) != 3: return False algo, salt, hsh = parts return constant_time_compare(hsh, get_hexdigest(algo, salt, raw_password)) def is_password_usable(encoded_password): return encoded_password is not None and encoded_password != UNUSABLE_PASSWORD def make_password(algo, raw_password): """ Produce a new password string in this format: algorithm$salt$hash """ if raw_password is None: return UNUSABLE_PASSWORD salt = get_random_string() hsh = get_hexdigest(algo, salt, raw_password) return '%s$%s$%s' % (algo, salt, hsh)