diff --git a/modoboa/admin/__init__.py b/modoboa/admin/__init__.py index ca9eeb504..d331e303b 100644 --- a/modoboa/admin/__init__.py +++ b/modoboa/admin/__init__.py @@ -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, diff --git a/modoboa/admin/models.py b/modoboa/admin/models.py index b4a170e31..caf304e79 100644 --- a/modoboa/admin/models.py +++ b/modoboa/admin/models.py @@ -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) diff --git a/modoboa/lib/__init__.py b/modoboa/lib/__init__.py index e69de29bb..e823e5932 100644 --- a/modoboa/lib/__init__.py +++ b/modoboa/lib/__init__.py @@ -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): + * 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