MD5-CRYPT is now a supported password scheme. Thanks to Walter Doekes!

see #284
This commit is contained in:
Antoine Nguyen
2012-07-02 10:37:10 +02:00
parent bd0706e025
commit 937a6f158b
3 changed files with 108 additions and 9 deletions
+6 -2
View File
@@ -23,8 +23,12 @@ parameters.register_admin("MAILBOX_TYPE", type="list", deflt="maildir",
parameters.register_admin("MAILDIR_ROOT", type="string", deflt=".maildir",
help=ugettext_lazy("Sub-directory (inside the mailbox) where messages are stored when using the maildir format"))
parameters.register_admin(
"PASSWORD_SCHEME", type="list", deflt="crypt",
values=[("crypt", "crypt"), ("md5", "md5"), ("sha256", "sha256"), ("plain", "plain")],
"PASSWORD_SCHEME", type="list", deflt="md5crypt",
values=[("crypt", "crypt"),
("md5", "md5"),
("md5crypt", "md5crypt"),
("sha256", "sha256"),
("plain", "plain")],
help=ugettext_lazy("Scheme used to crypt mailbox passwords")
)
parameters.register_admin("ITEMS_PER_PAGE", type="int", deflt=30,
+23 -7
View File
@@ -6,7 +6,7 @@ from django.contrib.auth.models import User as DUser, UserManager, Group
from django.utils.translation import ugettext as _, ugettext_lazy
from django.utils.crypto import constant_time_compare
from django.conf import settings
from modoboa.lib import parameters, events
from modoboa.lib import md5crypt, parameters, events
from modoboa.lib.exceptions import PermDeniedException
from modoboa.lib.sysutils import exec_cmd, exec_as_vuser
from modoboa.lib.emailutils import split_mailbox
@@ -37,7 +37,7 @@ class User(DUser):
proxy = True
ordering = ["username"]
password_expr = re.compile(r'\{(\w+)\}(.+)')
password_expr = re.compile(r'(\{(\w+)\}|(\$1\$))(.+)')
def __init__(self, *args, **kwargs):
"""Constructor
@@ -106,18 +106,30 @@ class User(DUser):
def _crypt_password(self, raw_value):
scheme = parameters.get_admin("PASSWORD_SCHEME")
if scheme == "crypt":
salt = ''.join(Random().sample(string.letters + string.digits, 2))
salt = "".join(Random().sample(string.letters + string.digits, 2))
result = crypt.crypt(raw_value, salt)
prefix = "{CRYPT}"
elif scheme == "md5":
obj = hashlib.md5(raw_value)
result = obj.hexdigest()
prefix = "{MD5}"
# The md5crypt scheme is the only supported method that has both:
# (a) a salt ("crypt" has this too),
# (b) supports passwords lengths of more than 8 characters (all except
# "crypt").
elif scheme == "md5crypt":
salt = "".join(Random().sample(string.letters + string.digits, 2))
result = md5crypt(raw_value, salt)
prefix = "" # the result already has $1$ prepended to it to signify what this is
elif scheme == "sha256":
obj = hashlib.sha256(raw_value)
result = base64.b64encode(obj.digest())
prefix = "{SHA256}"
else:
scheme = "plain"
result = raw_value
return "{%s}%s" % (scheme.upper(), result)
prefix = "{PLAIN}"
return "%s%s" % (prefix, result)
def set_password(self, raw_value, curvalue=None):
"""Password update
@@ -146,14 +158,18 @@ class User(DUser):
m = self.password_expr.match(self.password)
if m is None:
return False
scheme = m.group(1).lower()
val2 = m.group(2)
scheme = (m.group(2) or m.group(3)).lower()
val2 = m.group(4)
if scheme == "crypt":
val1 = crypt.crypt(raw_value, m.group(2))
val1 = crypt.crypt(raw_value, val2)
elif scheme == "md5":
val1 = hashlib.md5(raw_value).hexdigest()
elif scheme == "sha256":
val1 = base64.b64encode(hashlib.sha256(raw_value).digest())
elif scheme == "$1$": # md5crypt
salt, hashed = val2.split('$')
val1 = md5crypt(raw_value, salt)
val2 = self.password # re-add scheme for comparison below
else:
val1 = raw_value
return constant_time_compare(val1, val2)
+79
View File
@@ -0,0 +1,79 @@
# coding: utf-8
import hashlib
def md5crypt(password, salt, magic='$1$'):
"""Taken from:
http://code.activestate.com/recipes/325204-passwd-file-compatible-1-md5-crypt/
Based on FreeBSD src/lib/libcrypt/crypt.c 1.2
http://www.freebsd.org/cgi/cvsweb.cgi/~checkout~/src/lib/libcrypt/crypt.c?rev=1.2&content-type=text/plain
Original license:
* "THE BEER-WARE LICENSE" (Revision 42):
* <phk@login.dknet.dk> wrote this file. As long as you retain this notice you
* can do whatever you want with this stuff. If we meet some day, and you think
* this stuff is worth it, you can buy me a beer in return. Poul-Henning Kamp
"A port of Poul-Henning Kamp's MD5 password hash routine, as initially
found in FreeBSD 2. It is also used in Cisco routers, Apache htpasswd files,
and other places that you find "$1$" at the beginning of password hashes."
by Mark Johnston
"""
# The password first, since that is what is most unknown
# Then our magic string. Then the raw salt.
m = hashlib.md5()
m.update(password + magic + salt)
# /* Then just as many characters of the MD5(pw,salt,pw) */
mixin = hashlib.md5(password + salt + password).digest()
for i in range(0, len(password)):
m.update(mixin[i % 16])
# Then something really weird... Also really broken, as far as I
# can tell. -m
i = len(password)
while i:
if i & 1:
m.update('\x00')
else:
m.update(password[0])
i >>= 1
final = m.digest()
# And now, just to make sure things don't run too fast
for i in range(1000):
m2 = hashlib.md5()
if i & 1:
m2.update(password)
else:
m2.update(final)
if i % 3:
m2.update(salt)
if i % 7:
m2.update(password)
if i & 1:
m2.update(final)
else:
m2.update(password)
final = m2.digest()
# This is the bit that uses to64() in the original code.
itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
rearranged = ''
for a, b, c in ((0, 6, 12), (1, 7, 13), (2, 8, 14), (3, 9, 15), (4, 10, 5)):
v = ord(final[a]) << 16 | ord(final[b]) << 8 | ord(final[c])
for i in range(4):
rearranged += itoa64[v & 0x3f]; v >>= 6
v = ord(final[11])
for i in range(2):
rearranged += itoa64[v & 0x3f]; v >>= 6
return magic + salt + '$' + rearranged