Merge pull request #1587 from Toniob/check_scheme

check for dovecot supported password schemes
This commit is contained in:
Antoine Nguyen
2018-10-15 09:11:50 +02:00
committed by GitHub
8 changed files with 117 additions and 17 deletions
+5 -8
View File
@@ -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"})
+18
View File
@@ -12,6 +12,7 @@ from modoboa.core.password_hashers.advanced import ( # NOQA:F401
from modoboa.core.password_hashers.base import ( # NOQA:F401
CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher
)
from modoboa.lib.sysutils import doveadm_cmd
def get_password_hasher(scheme):
@@ -29,3 +30,20 @@ def get_password_hasher(scheme):
except KeyError:
hasher = PLAINHasher
return hasher
def get_dovecot_schemes():
"""Return schemes supported by dovecot"""
supported_schemes = None
try:
_, schemes = doveadm_cmd("pw -l")
except OSError:
pass
else:
supported_schemes = ["{{{}}}".format(scheme)
for scheme in schemes.split()]
if not supported_schemes:
supported_schemes = ['{PLAIN}']
return supported_schemes
+5 -3
View File
@@ -21,6 +21,7 @@ class BLFCRYPTHasher(PasswordHasher):
Supports rounds and provides compatibility with dovecot on
*BSD systems.
"""
@property
def scheme(self):
return "{BLF-CRYPT}" if self._target == "local" else "{CRYPT}"
@@ -43,12 +44,13 @@ class BLFCRYPTHasher(PasswordHasher):
class MD5CRYPTHasher(PasswordHasher):
"""
MD5-CRYPT password hasher.
This scheme can't be considered as secure anymore.
"""
_weak = True
@property
def scheme(self):
return "{MD5-CRYPT}" if self._target == "local" else "{CRYPT}"
@@ -64,13 +66,13 @@ class MD5CRYPTHasher(PasswordHasher):
class SHA256CRYPTHasher(PasswordHasher):
"""
SHA256-CRYPT password hasher.
Supports rounds and is a good compromise between security and
performance.
"""
@property
def scheme(self):
return "{SHA256-CRYPT}" if self._target == "local" else "{CRYPT}"
@@ -88,13 +90,13 @@ class SHA256CRYPTHasher(PasswordHasher):
class SHA512CRYPTHasher(PasswordHasher):
"""
SHA512-CRYPT password hasher.
Supports rounds and is the strongest scheme provided by
Modoboa. Requires more resource than SHA256-CRYPT.
"""
@property
def scheme(self):
return "{SHA512-CRYPT}" if self._target == "local" else "{CRYPT}"
+32 -5
View File
@@ -17,12 +17,30 @@ from random import Random
from django.utils.crypto import constant_time_compare
from django.utils.encoding import force_bytes, force_text
from six import with_metaclass
class PasswordHasher(object):
class MetaHasher(type):
"""
PasswordHasher Metaclass
Allow classmethod to be properties
"""
@property
def name(cls):
"""Returns the name of the hasher"""
return cls.__name__.rstrip('Hasher').lower()
@property
def label(cls):
"""Returns the label of the hasher"""
return cls.name if not cls._weak else "{} (weak)".format(cls.name)
class PasswordHasher(with_metaclass(MetaHasher, object)):
"""
Base class of all hashers.
"""
_weak = False
def __init__(self, target="local"):
self._target = target
@@ -68,12 +86,18 @@ class PasswordHasher(object):
hashed_value
)
@classmethod
def get_password_hashers(cls):
"""Return all the PasswordHasher supported by Modoboa"""
return cls.__subclasses__()
class PLAINHasher(PasswordHasher):
"""
Plain (ie. clear) password hasher.
"""
_weak = True
@property
def scheme(self):
return "{PLAIN}"
@@ -83,12 +107,13 @@ class PLAINHasher(PasswordHasher):
class CRYPTHasher(PasswordHasher):
"""
crypt password hasher.
Uses python `crypt` standard module.
"""
_weak = True
@property
def scheme(self):
return "{CRYPT}"
@@ -106,12 +131,13 @@ class CRYPTHasher(PasswordHasher):
class MD5Hasher(PasswordHasher):
"""
MD5 password hasher.
Uses python `hashlib` standard module.
"""
_weak = True
@property
def scheme(self):
return "{MD5}"
@@ -122,12 +148,13 @@ class MD5Hasher(PasswordHasher):
class SHA256Hasher(PasswordHasher):
"""
SHA256 password hasher.
Uses python `hashlib` and `base64` standard modules.
"""
_weak = True
@property
def scheme(self):
return "{SHA256}"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
echo -n "MD5 MD5-CRYPT SHA SHA1 SHA256 SHA512 SMD5 SSHA SSHA256 SSHA512 PLAIN CLEAR CLEARTEXT PLAIN-TRUNC CRAM-MD5 SCRAM-SHA-1 HMAC-MD5 DIGEST-MD5 PLAIN-MD4 PLAIN-MD5 LDAP-MD5 LANMAN NTLM OTP SKEY RPA PBKDF2 CRYPT SHA256-CRYPT SHA512-CRYPT"
exit 0
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import os
import smtplib
from unittest import skipIf
@@ -23,6 +24,8 @@ except ImportError:
import mock
@override_settings(
DOVEADM_LOOKUP_PATH=["{}/doveadm".format(os.path.dirname(__file__))])
class AuthenticationTestCase(ModoTestCase):
"""Validate authentication scenarios."""
+10 -1
View File
@@ -35,7 +35,7 @@ SETTINGS_SAMPLE = {
"limits-enable_domain_limits": "False",
"csrfmiddlewaretoken": "SGgMVZsA4TPqoiV786TMST6xgOlhAf4F",
"limits-deflt_user_mailboxes_limit": "0",
"core-password_scheme": "sha512crypt",
"core-password_scheme": "plain",
"core-update_scheme": True,
"core-items_per_page": "30",
"limits-deflt_user_mailbox_aliases_limit": "0",
@@ -175,6 +175,15 @@ class SettingsTestCase(ModoTestCase):
settings = SETTINGS_SAMPLE.copy()
response = self.client.post(url, settings, format="json")
self.assertEqual(response.status_code, 200)
settings["core-password_scheme"] = "sha512crypt"
response = self.client.post(url, settings, format="json")
self.assertEqual(response.status_code, 400)
compare(response.json(), {
"form_errors": {"password_scheme": ["Select a valid choice. \
sha512crypt is not one of the available choices."]},
"prefix": "core"
})
settings["core-password_scheme"] = "plain"
settings["core-rounds_number"] = ""
response = self.client.post(url, settings, format="json")
self.assertEqual(response.status_code, 400)
+41
View File
@@ -8,9 +8,13 @@ This module extra functions/shortcuts to communicate with the system
from __future__ import unicode_literals
import inspect
import os
import re
import subprocess
from django.conf import settings
from django.utils.encoding import force_text
def exec_cmd(cmd, sudo_user=None, pinput=None, capture_output=True, **kwargs):
"""Execute a shell command.
@@ -42,6 +46,43 @@ def exec_cmd(cmd, sudo_user=None, pinput=None, capture_output=True, **kwargs):
return process.returncode, output
def doveadm_cmd(params, sudo_user=None, pinput=None,
capture_output=True, **kwargs):
"""Execute doveadm command.
Run doveadm command using the current user. Set :keyword:`sudo_user` if
you need different privileges.
:param str params: the parameters to give to doveadm
:param str sudo_user: a valid system username
:param str pinput: data to send to process's stdin
:param bool capture_output: capture process output or not
:rtype: tuple
:return: return code, command output
"""
dpath = None
code, output = exec_cmd("which doveadm")
if not code:
dpath = force_text(output).strip()
else:
known_paths = getattr(
settings, "DOVEADM_LOOKUP_PATH",
("/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
break
if dpath:
return exec_cmd("{} {}".format(dpath, params),
sudo_user=sudo_user,
pinput=pinput,
capture_output=capture_output,
**kwargs)
else:
raise OSError("doveadm command not found")
def guess_extension_name():
"""Tries to guess the application's name by inspecting the stack.