A more complete Drupal 7 compatible password hasher

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
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)

More like this

  1. Instructions and code to use drupal 7 passwords by grillermo 10 months, 2 weeks ago
  2. Plaintext password by yetty 5 months, 2 weeks ago
  3. Bitwise operator queryset filter by hgeerts@osso.nl 3 years, 11 months ago
  4. Password Validation - Require Letters and Numbers - no regex by watchedman 2 years, 6 months ago
  5. MySQL django password function by mcosta 4 years, 10 months ago

Comments

dogstick (on March 3, 2014):

Thanks a lot for posting this - massive help to everyone migrating from Drupal :)

Just one issue, in for loop in _apply_hash function I had to add the following:

password = password.decode().encode('utf-8')

#

(Forgotten your password?)