Migrating Umbraco CMS users to Django
Nov. 26, 2014

I recently was tasked with importing an ASP.net db backup file into Django with Postgres. Besides the frustrating hassle of using Microsoft SQL Server Management Studio to export ~7000 rows of users into a CSV file so I could load it into Django, I had to figure out what to do with the passwords.

The db was used in a ASP.net CMS called Umbraco. The passwords were hashed in some way.

Wouldn't it be great if users could still login without changing their passwords? But how would that happen when the passwords are hashed with some unknown method?

Django has a very cool way to check passwords against multiple hashing methods, then update the password to the best method upon login. I spent a while digging through Umbraco's source code, as well as reading several SO answers, and came up with this. I hope it saves someone a few hours of work.

When users try to login, it will check against this new hasher, then update the password with Django's better hashing method.

I got ideas from these SO questions: Migrating ASP.NET membership users to Django without resetting passwords? and what kind of hashing does umbraco use in its membership provider

Just add this in your PASSWORD_HASHERS settings. Don't set it as the first one, or it will be the prefered method, and passwords will be changed to this method.

settings:

​PASSSWORD_HASHERS = (
    'django.contrib.auth.hashers.PBKDF2PasswordHasher',
    'path.to.the.hasher.UmbracoPasswordHasher',  # insert it somewhere in here, just not first.
    'django.contrib.auth.hashers.PBKDF2SHA1PasswordHasher',
    'django.contrib.auth.hashers.BCryptSHA256PasswordHasher',
    'django.contrib.auth.hashers.BCryptPasswordHasher',
    'django.contrib.auth.hashers.SHA1PasswordHasher',
    'django.contrib.auth.hashers.MD5PasswordHasher',
    'django.contrib.auth.hashers.CryptPasswordHasher',
)

hasher:

import hashlib
import hmac

from django.contrib.auth.hashers import (BasePasswordHasher, mask_hash)
from django.utils.datastructures import SortedDict
from django.utils.crypto import constant_time_compare
from django.utils.translation import ugettext_noop as _

def from_char_code(*args):
    return ''.join(map(unichr, args))

def str2rstr_utf16le(input):
    output = ''

    for i in range(0, len(input)):
        output += from_char_code(
            ord(input[i]) & 0xFF,
            (ord(input[i])>> 8) & 0xFF
        )

    return output

class UmbracoPasswordHasher(BasePasswordHasher):
    """
    hasher used for handeling passwords hashed by Umbraco CMS
    """
    algorithm = "umbraco_hmacsha1"

    def salt(self):
        return ''

    def encode(self, password, salt):
        assert password is not None
        assert salt == ''
        pwd = str(str2rstr_utf16le(password))
        hash = hmac.new(pwd, pwd, hashlib.sha1)
        hash = hash.digest().encode("base64").strip()
        return "%s$$%s" % (self.algorithm, 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 SortedDict([
            (_('algorithm'), algorithm),
            (_('hash'), mask_hash(hash)),
            ])

You will need to prepend 'umbraco_hmacsha1$$' to all of you passwords. You can run something like:

for user in User.objects.all():
    if not user.has_usable_password():
        # don't need to update passwords that are already hashed in a way django understands
        user.password = 'umbraco_hmacsha1$$' + user.password
        user.save()

comments powered by Disqus