I am in process of migrating from Wordpress 2.8 to Django 1.8. As I found out Wordpress 2.8 (and probably future versions as well) stores password in MD5 crypto format (phpass library). I tried passlib extension for Django 1.8 but it didn't work for me. So I ended up writing custom hasher with MD5 crypto algorithm.
NOTE: During migration add "md5_crypt" to password hash (user_pass field)
I added MD5CryptPasswordHasher to the top of the list to make it default (in order not to mix up different hashing algorithms, what if I will migrate once again to another platform?) but it can be added to the bottom of the list if one just want to add support for the algorithm for existing users but force new users to migrate to PBKDF2PasswordHasher hasher or other.
settings.py
PASSWORD_HASHERS = (
'your_project_name.hashers.MD5CryptPasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
'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.UnsaltedSHA1PasswordHasher',
'django.contrib.auth.hashers.UnsaltedMD5PasswordHasher',
'django.contrib.auth.hashers.CryptPasswordHasher',
)
hashers.py
import math
import hashlib
from django.contrib.auth.hashers import BasePasswordHasher
from django.utils.crypto import get_random_string
from django.contrib.auth.hashers import mask_hash
from collections import OrderedDict
from django.utils.translation import ugettext, ugettext_lazy as _
itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
def encode64(inp, count):
outp = ''
cur = 0
while cur < count:
value = inp[cur]
cur += 1
outp += itoa64[value & 0x3f]
if cur < count:
value |= (inp[cur] << 8)
outp += itoa64[(value >> 6) & 0x3f]
if cur >= count:
break
cur += 1
if cur < count:
value |= (inp[cur] << 16)
outp += itoa64[(value >> 12) & 0x3f]
if cur >= count:
break
cur += 1
outp += itoa64[(value >> 18) & 0x3f]
return outp.encode()
def crypt_private(pw, algorithm, code, salt, iterations):
header = "%s$%s$%s%s" % (algorithm, code, itoa64[int(math.log(iterations, 2))], salt)
pw = pw.encode()
salt = salt.encode()
hx = hashlib.md5(salt + pw).digest()
while iterations:
hx = hashlib.md5(hx + pw).digest()
iterations -= 1
return header + encode64(hx, 16).decode()
def get_md5_crypto_hash_params(encoded):
algorithm, code, rest = encoded.split('$', 2)
count_log2 = itoa64.find(rest[0])
iterations = 1 << count_log2
salt = rest[1:9]
return (algorithm, salt, iterations)
class MD5CryptPasswordHasher(BasePasswordHasher):
"""
The Salted MD5 Crypt password hashing algorithm that is used by Wordpress 2.8
WARNING!
The algorithm is not robust enough to handle any kind of MD5 crypt variations
It was stripped and refactored based on passlib implementations especially for Wordpress 2.8 format
"""
algorithm = "md5_crypt"
iterations = 8192
code = "P"
salt_len = 8
def salt(self):
return get_random_string(salt_len)
def encode(self, password, salt):
assert password is not None
assert salt != ''
return crypt_private(password, self.algorithm, self.code, salt, self.iterations)
pass
def verify(self, password, encoded):
algorithm, salt, iterations = get_md5_crypto_hash_params(encoded)
assert algorithm == self.algorithm
return crypt_private(password, algorithm, self.code, salt, iterations) == encoded
def safe_summary(self, encoded):
algorithm, code, rest = encoded.split('$', 2)
salt = rest[1:9]
hash = rest[9:]
assert algorithm == self.algorithm
return OrderedDict([
(_('algorithm'), algorithm),
(_('salt'), mask_hash(salt, show=2)),
(_('hash'), mask_hash(hash)),
])
crypt()
. Perhaps give it a try to see if you can mangle the salt and encoding to get them to match. But I would be wary of any framework like that which enforces what you do like that (so that you can't use requirements driven methods)... – ircmaxellcrypt
is the equivalent to PHP'scrypt()
in DES mode, not MD5. – David Eykmd5$$1$f1KtBi.v$$nWwBN8CP3igfC3Emo0OB8/
and testing it against a known password? Or is that that the salt and hash are in hexadecimal in the Django format? – bcoughlan$1$
needed which is what upsets that. So Python supports crypt in multiple modes, but Django does not (which is a huge issue IMHO). – ircmaxell$
, and therefore results in the unusability of other crypto methods... – ircmaxell