diff --git a/modoboa/core/app_settings.py b/modoboa/core/app_settings.py index 788983144..73061befa 100644 --- a/modoboa/core/app_settings.py +++ b/modoboa/core/app_settings.py @@ -7,6 +7,8 @@ from django.conf import settings from django.contrib.auth import password_validation from django.utils.translation import ugettext as _, ugettext_lazy +from modoboa.core.password_hashers import get_dovecot_schemes +from modoboa.core.password_hashers.base import PasswordHasher from modoboa.lib import fields as lib_fields from modoboa.lib.form_utils import ( HorizontalRadioSelect, SeparatorField, YesNoField @@ -45,14 +47,9 @@ class GeneralParametersForm(param_forms.AdminParametersForm): password_scheme = forms.ChoiceField( label=ugettext_lazy("Default password scheme"), - choices=[("sha512crypt", "sha512crypt"), - ("sha256crypt", "sha256crypt"), - ("blfcrypt", "bcrypt"), - ("md5crypt", ugettext_lazy("md5crypt (weak)")), - ("sha256", ugettext_lazy("sha256 (weak)")), - ("md5", ugettext_lazy("md5 (weak)")), - ("crypt", ugettext_lazy("crypt (weak)")), - ("plain", ugettext_lazy("plain (weak)"))], + choices=[(hasher.name, ugettext_lazy(hasher.label)) + for hasher in PasswordHasher.get_password_hashers() + if hasher().scheme in get_dovecot_schemes()], initial="sha512crypt", help_text=ugettext_lazy("Scheme used to crypt mailbox passwords"), widget=forms.Select(attrs={"class": "form-control"}) diff --git a/modoboa/core/password_hashers/__init__.py b/modoboa/core/password_hashers/__init__.py index 0eb35c6fa..e8463ae9a 100644 --- a/modoboa/core/password_hashers/__init__.py +++ b/modoboa/core/password_hashers/__init__.py @@ -6,12 +6,17 @@ Password hashers for Modoboa. from __future__ import unicode_literals +import os + +from django.utils.encoding import force_text + from modoboa.core.password_hashers.advanced import ( # NOQA:F401 BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher ) from modoboa.core.password_hashers.base import ( # NOQA:F401 CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher ) +from modoboa.lib.sysutils import exec_cmd def get_password_hasher(scheme): @@ -29,3 +34,25 @@ def get_password_hasher(scheme): except KeyError: hasher = PLAINHasher return hasher + +def get_dovecot_schemes(): + """Return schemes supported by dovecot""" + supported_schemes = ["{PLAIN}"] + dpath = None + code, output = exec_cmd("which doveadm") + if not code: + dpath = force_text(output).strip() + else: + known_paths = ("/usr/bin/doveadm", "/usr/local/bin/doveadm") + for fpath in known_paths: + if os.path.isfile(fpath) and os.access(fpath, os.X_OK): + dpath = fpath + if dpath: + try: + code, schemes = exec_cmd("%s pw -l" % dpath) + except OSError: + pass + else: + supported_schemes = ["{{{}}}".format(scheme) for scheme in force_text(schemes).split()] + + return supported_schemes diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index 854cfb9ad..40fa9d499 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -21,6 +21,9 @@ class BLFCRYPTHasher(PasswordHasher): Supports rounds and provides compatibility with dovecot on *BSD systems. """ + name = "blfcrypt" + label = "blfcrypt" + @property def scheme(self): return "{BLF-CRYPT}" if self._target == "local" else "{CRYPT}" @@ -49,6 +52,9 @@ class MD5CRYPTHasher(PasswordHasher): This scheme can't be considered as secure anymore. """ + name = "md5crypt" + label = "md5crypt" + @property def scheme(self): return "{MD5-CRYPT}" if self._target == "local" else "{CRYPT}" @@ -71,6 +77,9 @@ class SHA256CRYPTHasher(PasswordHasher): Supports rounds and is a good compromise between security and performance. """ + name = "sha256crypt" + label = "sha256crypt" + @property def scheme(self): return "{SHA256-CRYPT}" if self._target == "local" else "{CRYPT}" @@ -95,6 +104,9 @@ class SHA512CRYPTHasher(PasswordHasher): Supports rounds and is the strongest scheme provided by Modoboa. Requires more resource than SHA256-CRYPT. """ + name = "sha512crypt" + label = "sha512crypt" + @property def scheme(self): return "{SHA512-CRYPT}" if self._target == "local" else "{CRYPT}" diff --git a/modoboa/core/password_hashers/base.py b/modoboa/core/password_hashers/base.py index 93b316919..1f9b37653 100644 --- a/modoboa/core/password_hashers/base.py +++ b/modoboa/core/password_hashers/base.py @@ -68,12 +68,19 @@ class PasswordHasher(object): hashed_value ) + @classmethod + def get_password_hashers(cls): + return cls.__subclasses__() + class PLAINHasher(PasswordHasher): """ Plain (ie. clear) password hasher. """ + name = "plain" + label = "plain (weak)" + @property def scheme(self): return "{PLAIN}" @@ -89,6 +96,9 @@ class CRYPTHasher(PasswordHasher): Uses python `crypt` standard module. """ + name = "crypt" + label = "crypt (weak)" + @property def scheme(self): return "{CRYPT}" @@ -112,6 +122,9 @@ class MD5Hasher(PasswordHasher): Uses python `hashlib` standard module. """ + name = "md5" + label = "md5 (weak)" + @property def scheme(self): return "{MD5}" @@ -128,6 +141,9 @@ class SHA256Hasher(PasswordHasher): Uses python `hashlib` and `base64` standard modules. """ + name = "sha256" + label = "sha256 (weak)" + @property def scheme(self): return "{SHA256}"