From ab0926f916305bca30ce61de9c60fa96d58aafde Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 3 Mar 2019 00:17:27 +0100 Subject: [PATCH 1/6] add facilities to rehash on login passwords if parameters change --- modoboa/core/password_hashers/base.py | 10 ++++++++++ modoboa/core/views/auth.py | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/modoboa/core/password_hashers/base.py b/modoboa/core/password_hashers/base.py index ce05f4e72..8b354466f 100644 --- a/modoboa/core/password_hashers/base.py +++ b/modoboa/core/password_hashers/base.py @@ -86,6 +86,16 @@ class PasswordHasher(with_metaclass(MetaHasher, object)): hashed_value ) + def needs_rehash(self, hashed_value): + """Check if the provided hash needs rehasing accoring to the current + parameters + + :param str hashed_value: hased password + :rtype bool + :return: True if the password needs rehash, false otherwise + """ + return False + @classmethod def get_password_hashers(cls): """Return all the PasswordHasher supported by Modoboa""" diff --git a/modoboa/core/views/auth.py b/modoboa/core/views/auth.py index 77de63417..8945f7bf4 100644 --- a/modoboa/core/views/auth.py +++ b/modoboa/core/views/auth.py @@ -52,6 +52,14 @@ def dologin(request): ) user.set_password(form.cleaned_data["password"]) user.save() + if pwhash.needs_rehash(user.password): + logging.info( + _("Password hash parameter missmatch. " + "Updating %s password"), + user.username + ) + user.set_password(form.cleaned_data["password"]) + user.save() login(request, user) if not form.cleaned_data["rememberme"]: From 61459651546146037f15fef1eab10c6144b511fd Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 3 Mar 2019 00:22:55 +0100 Subject: [PATCH 2/6] Implement support for the argon2 password hash --- modoboa/core/password_hashers/__init__.py | 2 +- modoboa/core/password_hashers/advanced.py | 51 ++++++++++++++++++++++- requirements.txt | 1 + 3 files changed, 51 insertions(+), 3 deletions(-) diff --git a/modoboa/core/password_hashers/__init__.py b/modoboa/core/password_hashers/__init__.py index b03b5e3d0..802b9a480 100644 --- a/modoboa/core/password_hashers/__init__.py +++ b/modoboa/core/password_hashers/__init__.py @@ -9,7 +9,7 @@ from __future__ import unicode_literals from django.utils.encoding import smart_text from modoboa.core.password_hashers.advanced import ( # NOQA:F401 - BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher + BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher, ARGON2IDHasher ) from modoboa.core.password_hashers.base import ( # NOQA:F401 CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index 3024924fc..e58c5ba83 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -9,6 +9,9 @@ This module relies on `passlib` to provide more secure hashers. from __future__ import unicode_literals from passlib.hash import bcrypt, md5_crypt, sha256_crypt, sha512_crypt +from argon2 import PasswordHasher as argon2_hasher + +from django.conf import settings from modoboa.parameters import tools as param_tools from .base import PasswordHasher @@ -91,8 +94,7 @@ class SHA512CRYPTHasher(PasswordHasher): """ SHA512-CRYPT password hasher. - Supports rounds and is the strongest scheme provided by - Modoboa. Requires more resource than SHA256-CRYPT. + Supports rounds. Requires more resource than SHA256-CRYPT. """ @property @@ -108,3 +110,48 @@ class SHA512CRYPTHasher(PasswordHasher): def verify(self, clearvalue, hashed_value): return sha512_crypt.verify(clearvalue, hashed_value) + + +class ARGON2IDHasher(PasswordHasher): + """ + argon2 password hasher. + + Supports rounds, memory and number of threads. To be set in settings.py. + It is the strongest scheme provided by modoboa + but can only be used with dovecot >= 2.3 and libsodium >= 1.0.13 + """ + + def __init__(self,): + super(ARGON2IDHasher, self).__init__() + + parameters = dict() + + if hasattr(settings, "MODOBOA_ARGON2_TIME_COST"): + parameters["time_cost"] = settings.MODOBOA_ARGON2_TIME_COST + + if hasattr(settings, "MODOBOA_ARGON2_MEMORY_COST"): + parameters["memory_cost"] = settings.MODOBOA_ARGON2_MEMORY_COST + + if hasattr(settings, "MODOBOA_ARGON2_PARALLELISM"): + parameters["parallelism"] = settings.MODOBOA_ARGON2_PARALLELISM + + self.hasher = argon2_hasher(**parameters) + + @property + def scheme(self): + return "{ARGON2ID}" if self._target == "local" else "{CRYPT}" + + def _b64encode(self, pwhash): + return pwhash + + def _encrypt(self, clearvalue, salt=None): + return self.hasher.hash(clearvalue) + + def verify(self, clearvalue, hashed_value): + try: + return self.hasher.verify(hashed_value, clearvalue) + except argon2_hasher.exceptions.VerifyMismatchError: + return False + + def needs_rehash(self, hashed_value): + return self.hasher.check_needs_rehash(hashed_value.strip(self.scheme)) diff --git a/requirements.txt b/requirements.txt index eb80cd838..b96c62c70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,3 +29,4 @@ rfc6266 lxml backports.csv; python_version < '3' chardet +argon2_cffi From c4108a1544b123567598766689b75cdec9928087 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 3 Mar 2019 00:40:03 +0100 Subject: [PATCH 3/6] add basic test for argon2 hash --- modoboa/core/tests/test_authentication.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index f50fa7161..2f1806883 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -91,6 +91,12 @@ class AuthenticationTestCase(ModoTestCase): user.refresh_from_db() self.assertTrue(user.password.startswith("{SHA256}")) + self.client.logout() + self.set_global_parameter("password_scheme", "argon2id") + self.client.post(reverse("core:login"), data) + user.refresh_from_db() + self.assertTrue(user.password.startswith("{ARGON2ID}")) + self.client.logout() self.set_global_parameter("password_scheme", "fallback_scheme") self.client.post(reverse("core:login"), data) From 3e072debadbbc49551479e89e1be6f95df4ae53b Mon Sep 17 00:00:00 2001 From: Arvedui Date: Sun, 3 Mar 2019 12:42:11 +0100 Subject: [PATCH 4/6] add test for rehash on login for parameter update --- modoboa/core/tests/test_authentication.py | 37 +++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index 2f1806883..311b8b995 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -12,6 +12,8 @@ from django.core import mail from django.test import override_settings from django.urls import reverse +import argon2 + from modoboa.core.password_hashers import get_password_hasher, get_dovecot_schemes from modoboa.lib.tests import NO_SMTP, ModoTestCase from .. import factories, models @@ -110,6 +112,41 @@ class AuthenticationTestCase(ModoTestCase): user.refresh_from_db() self.assertTrue(user.password.startswith(pw_hash.scheme)) + def test_password_parameter_change(self): + """Validate hash parameter update on login works""" + username = "user@test.com" + password = "toto" + data = {"username": username, "password": password} + user = models.User.objects.get(username=username) + self.set_global_parameter("password_scheme", "argon2id") + + self.client.logout() + with self.settings( + MODOBOA_ARGON2_TIME_COST=4, + MODOBOA_ARGON2_MEMORY_COST=10000, + MODOBOA_ARGON2_PARALLELISM=4): + self.client.post(reverse("core:login"), data) + user.refresh_from_db() + print(user.password) + self.assertTrue(user.password.startswith("{ARGON2ID}")) + parameters = argon2.extract_parameters(user.password.lstrip("{ARGON2ID}")) + self.assertEqual(parameters.time_cost, 4) + self.assertEqual(parameters.memory_cost, 10000) + self.assertEqual(parameters.parallelism, 4) + + self.client.logout() + with self.settings( + MODOBOA_ARGON2_TIME_COST=3, + MODOBOA_ARGON2_MEMORY_COST=1000, + MODOBOA_ARGON2_PARALLELISM=2): + self.client.post(reverse("core:login"), data) + user.refresh_from_db() + self.assertTrue(user.password.startswith("{ARGON2ID}")) + parameters = argon2.extract_parameters(user.password.lstrip("{ARGON2ID}")) + self.assertEqual(parameters.time_cost, 3) + self.assertEqual(parameters.memory_cost, 1000) + self.assertEqual(parameters.parallelism, 2) + def test_supported_schemes(self): """Validate dovecot supported schemes.""" supported_schemes = get_dovecot_schemes() From 0a7a7a10bba0e1035ca47f725195917595fc9cb4 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Wed, 13 Mar 2019 22:19:20 +0100 Subject: [PATCH 5/6] fixup! Implement support for the argon2 password hash --- modoboa/core/password_hashers/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modoboa/core/password_hashers/__init__.py b/modoboa/core/password_hashers/__init__.py index 802b9a480..2af1b766f 100644 --- a/modoboa/core/password_hashers/__init__.py +++ b/modoboa/core/password_hashers/__init__.py @@ -9,7 +9,8 @@ from __future__ import unicode_literals from django.utils.encoding import smart_text from modoboa.core.password_hashers.advanced import ( # NOQA:F401 - BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher, ARGON2IDHasher + BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher, + ARGON2IDHasher ) from modoboa.core.password_hashers.base import ( # NOQA:F401 CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher From bd2ad0ce3f0f8c2b1ba55306423b78fe0d535fd5 Mon Sep 17 00:00:00 2001 From: Arvedui Date: Wed, 13 Mar 2019 22:20:10 +0100 Subject: [PATCH 6/6] fixup! add test for rehash on login for parameter update --- modoboa/core/tests/test_authentication.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index 311b8b995..41a3b75f0 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -122,12 +122,11 @@ class AuthenticationTestCase(ModoTestCase): self.client.logout() with self.settings( - MODOBOA_ARGON2_TIME_COST=4, - MODOBOA_ARGON2_MEMORY_COST=10000, - MODOBOA_ARGON2_PARALLELISM=4): + MODOBOA_ARGON2_TIME_COST=4, + MODOBOA_ARGON2_MEMORY_COST=10000, + MODOBOA_ARGON2_PARALLELISM=4): self.client.post(reverse("core:login"), data) user.refresh_from_db() - print(user.password) self.assertTrue(user.password.startswith("{ARGON2ID}")) parameters = argon2.extract_parameters(user.password.lstrip("{ARGON2ID}")) self.assertEqual(parameters.time_cost, 4) @@ -136,13 +135,14 @@ class AuthenticationTestCase(ModoTestCase): self.client.logout() with self.settings( - MODOBOA_ARGON2_TIME_COST=3, - MODOBOA_ARGON2_MEMORY_COST=1000, - MODOBOA_ARGON2_PARALLELISM=2): + MODOBOA_ARGON2_TIME_COST=3, + MODOBOA_ARGON2_MEMORY_COST=1000, + MODOBOA_ARGON2_PARALLELISM=2): self.client.post(reverse("core:login"), data) user.refresh_from_db() self.assertTrue(user.password.startswith("{ARGON2ID}")) - parameters = argon2.extract_parameters(user.password.lstrip("{ARGON2ID}")) + parameters = argon2.extract_parameters( + user.password.lstrip("{ARGON2ID}")) self.assertEqual(parameters.time_cost, 3) self.assertEqual(parameters.memory_cost, 1000) self.assertEqual(parameters.parallelism, 2)