mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
MD5-CRYPT is now a supported password scheme. Thanks to Walter Doekes!
see #284
This commit is contained in:
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user