diff --git a/modoboa/core/password_hashers/advanced.py b/modoboa/core/password_hashers/advanced.py index e58c5ba83..7080cdc28 100644 --- a/modoboa/core/password_hashers/advanced.py +++ b/modoboa/core/password_hashers/advanced.py @@ -9,7 +9,11 @@ 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 + +try: + from argon2 import PasswordHasher as argon2_hasher +except ImportError: + argon2_hasher = None from django.conf import settings @@ -112,46 +116,52 @@ class SHA512CRYPTHasher(PasswordHasher): return sha512_crypt.verify(clearvalue, hashed_value) -class ARGON2IDHasher(PasswordHasher): - """ - argon2 password hasher. +if argon2_hasher is not None: + 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 - """ + 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__() + def __init__(self,): + super(ARGON2IDHasher, self).__init__() - parameters = dict() + parameters = dict() - if hasattr(settings, "MODOBOA_ARGON2_TIME_COST"): - parameters["time_cost"] = settings.MODOBOA_ARGON2_TIME_COST + 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_MEMORY_COST"): + parameters["memory_cost"] = settings.MODOBOA_ARGON2_MEMORY_COST - if hasattr(settings, "MODOBOA_ARGON2_PARALLELISM"): - parameters["parallelism"] = settings.MODOBOA_ARGON2_PARALLELISM + if hasattr(settings, "MODOBOA_ARGON2_PARALLELISM"): + parameters["parallelism"] = settings.MODOBOA_ARGON2_PARALLELISM - self.hasher = argon2_hasher(**parameters) + self.hasher = argon2_hasher(**parameters) - @property - def scheme(self): - return "{ARGON2ID}" if self._target == "local" else "{CRYPT}" + @property + def scheme(self): + return "{ARGON2ID}" if self._target == "local" else "{CRYPT}" - def _b64encode(self, pwhash): - return pwhash + def _b64encode(self, pwhash): + return pwhash - def _encrypt(self, clearvalue, salt=None): - return self.hasher.hash(clearvalue) + 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 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)) + def needs_rehash(self, hashed_value): + return self.hasher.check_needs_rehash( + hashed_value.strip(self.scheme) + ) +else: + class ARGON2IDHasher: + pass diff --git a/modoboa/core/tests/test_authentication.py b/modoboa/core/tests/test_authentication.py index 41a3b75f0..912855f4c 100644 --- a/modoboa/core/tests/test_authentication.py +++ b/modoboa/core/tests/test_authentication.py @@ -6,15 +6,20 @@ from __future__ import unicode_literals import os import smtplib -from unittest import skipIf +from unittest import skipIf, skipUnless from django.core import mail from django.test import override_settings from django.urls import reverse -import argon2 +try: + import argon2 +except ImportError: + argon2 = None -from modoboa.core.password_hashers import get_password_hasher, get_dovecot_schemes +from modoboa.core.password_hashers import ( + get_password_hasher, get_dovecot_schemes +) from modoboa.lib.tests import NO_SMTP, ModoTestCase from .. import factories, models @@ -93,11 +98,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}")) + if argon2 is not None: + 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") @@ -112,8 +118,9 @@ 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""" + @skipUnless(argon2, "argon2-cffi not installed") + def test_password_argon2_parameter_change(self): + """Validate hash parameter update on login works with argon2.""" username = "user@test.com" password = "toto" data = {"username": username, "password": password} @@ -124,11 +131,14 @@ class AuthenticationTestCase(ModoTestCase): with self.settings( MODOBOA_ARGON2_TIME_COST=4, MODOBOA_ARGON2_MEMORY_COST=10000, - MODOBOA_ARGON2_PARALLELISM=4): + MODOBOA_ARGON2_PARALLELISM=4, + ): 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, 4) self.assertEqual(parameters.memory_cost, 10000) self.assertEqual(parameters.parallelism, 4) @@ -137,7 +147,8 @@ class AuthenticationTestCase(ModoTestCase): with self.settings( MODOBOA_ARGON2_TIME_COST=3, MODOBOA_ARGON2_MEMORY_COST=1000, - MODOBOA_ARGON2_PARALLELISM=2): + MODOBOA_ARGON2_PARALLELISM=2, + ): self.client.post(reverse("core:login"), data) user.refresh_from_db() self.assertTrue(user.password.startswith("{ARGON2ID}")) diff --git a/requirements.txt b/requirements.txt index b96c62c70..eb80cd838 100644 --- a/requirements.txt +++ b/requirements.txt @@ -29,4 +29,3 @@ rfc6266 lxml backports.csv; python_version < '3' chardet -argon2_cffi diff --git a/setup.py b/setup.py index a9984a9db..6acb51e3b 100644 --- a/setup.py +++ b/setup.py @@ -81,5 +81,6 @@ if __name__ == "__main__": "ldap": LDAP_REQUIRES, "mysql": MYSQL_REQUIRES, "postgresql": POSTGRESQL_REQUIRES, + "argon2": ["argon2-cffi >= 16.1.0"], }, ) diff --git a/test-requirements.txt b/test-requirements.txt index 00fae81e6..2b7bf52d0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,3 +1,4 @@ +argon2-cffi>=16.1.0 factory-boy>=2.4 mock==2.0.0; python_version < '3.3' httmock==1.2.5