from django.contrib.auth.hashers import BasePasswordHasher from django.utils.crypto import get_random_string from collections import OrderedDict from django.utils.translation import ugettext_noop as _ import hashlib class DrupalPasswordHasherInvalidHashException(Exception): pass class DrupalPasswordHasher(BasePasswordHasher): """ Authenticate against Drupal 7 passwords. The passwords should be prefixed with drupal$ upon importing, such that Django recognizes them correctly. Drupal's method does some funny stuff (like truncating the hashed password), so you might not want to use this hasher for storing new passwords. """ algorithm = "drupal" _DRUPAL_HASH_LENGTH = 55 _DRUPAL_HASH_COUNT = 15 _digests = { '$S$': hashlib.sha512, '$H$': hashlib.md5, '$P$': hashlib.md5, } _itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' def _get_settings(self, encoded): settings_bin = encoded[:12] count_log2 = self._itoa64.index(settings_bin[3]) count = 1 << count_log2 salt = settings_bin[4:12] return { 'count': count, 'salt': salt } def _drupal_b64(self, input): output = "" count = len(input) i = 0 while True: value = ord(input[i]) i += 1 output += self._itoa64[value & 0x3f] if i < count: value |= ord(input[i]) << 8 output += self._itoa64[(value >> 6) & 0x3f] i += 1 if i >= count: break if i < count: value |= ord(input[i]) << 16 output += self._itoa64[(value >> 12) & 0x3f] i += 1 if i >= count: break output += self._itoa64[(value >> 18) & 0x3f] return output def _apply_hash(self, password, digest, settings): password_hash = digest(settings["salt"] + password).digest() for i in range(settings["count"]): password_hash = digest(password_hash + password).digest() return self._drupal_b64(password_hash)[:self._DRUPAL_HASH_LENGTH - 12] def salt(self): return get_random_string(8) def encode(self, password, salt): assert len(salt) == 8 digest = '$S$' settings = { 'count': 1 << self._DRUPAL_HASH_COUNT, 'salt': salt } encoded_hash = self._apply_hash(password, self._digests[digest], settings) return self.algorithm + "$" + digest + self._itoa64[self._DRUPAL_HASH_COUNT] + salt + encoded_hash def verify(self, password, encoded): encoded = encoded.split("$", 1)[1] if encoded[0] == 'U': # Imported passwords from old Drupal versions, see user_update_7000() encoded = encoded[1:] password = hashlib.md5(password).hexdigest() digest = encoded[:3] if digest not in self._digests: raise DrupalPasswordHasherInvalidHashException() digest = self._digests[digest] settings = self._get_settings(encoded) encoded_hash = encoded[12:] password_hash = self._apply_hash(password, digest, settings) return password_hash == encoded_hash def safe_summary(self, encoded): encoded = encoded.split("$", 1)[1] settings = self._get_settings(encoded) return OrderedDict([ (_('algorithm'), self.algorithm), (_('iterations'), settings["count"]), (_('salt'), settings["salt"]), (_('hash'), encoded[12:]), ]) def must_update(self, encoded): encoded = encoded.split("$", 1)[1] if encoded[0] == 'U': return True settings = self._get_settings(encoded) return settings["count"] < (1 << self._DRUPAL_HASH_COUNT)