Merge pull request #1690 from Arvedui/argon2

Argon2 support
This commit is contained in:
Antoine Nguyen
2019-03-19 09:08:20 +01:00
committed by GitHub
6 changed files with 113 additions and 3 deletions
+2 -1
View File
@@ -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
BLFCRYPTHasher, MD5CRYPTHasher, SHA256CRYPTHasher, SHA512CRYPTHasher,
ARGON2IDHasher
)
from modoboa.core.password_hashers.base import ( # NOQA:F401
CRYPTHasher, MD5Hasher, PLAINHasher, SHA256Hasher
+49 -2
View File
@@ -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))
+10
View File
@@ -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"""
+43
View File
@@ -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
@@ -91,6 +93,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)
@@ -104,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()
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()
+8
View File
@@ -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"]:
+1
View File
@@ -29,3 +29,4 @@ rfc6266
lxml
backports.csv; python_version < '3'
chardet
argon2_cffi