Login

A more complete Drupal 7 compatible password hasher

Author:
pberndt
Posted:
February 22, 2014
Language:
Python
Version:
Not specified
Score:
0 (after 0 ratings)

Use this class to authenticate against Drupal 7 password strings. When importing passwords from Drupal, the database values should be prefixed with "drupal$".

In contrast to the two present solutions, this class also works with passwords which were imported to Drupal 7 (e.g. from Drupal 6 or phpBB) and with passwords with variable iteration numbers. It is possible to use this class for encoding passwords, but due to questionable design decisions in Drupal (like truncating the non-standard base64 encoded hash at 43 characters) I'd recommend to do this this only if you really need to be able to migrate back to Drupal.

  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. Template tag - list punctuation for a list of items by shapiromatron 3 months ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 3 months, 1 week ago
  3. Serializer factory with Django Rest Framework by julio 10 months, 1 week ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 10 months, 4 weeks ago
  5. Help text hyperlinks by sa2812 11 months, 3 weeks 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')

#

skargo (on January 21, 2015):

I really need this snippet but as a newbie to django have no clue how to implement it. I appreciate if you add a brief how-to. Thanks.

#

Please login first before commenting.