Skip to content
Snippets Groups Projects
Commit ac003909 authored by Julian's avatar Julian
Browse files

Argon2 for user password hashing

Argon2 is a modern password hashing algorithm. It is significantly more secure
than the previous algorithm (salted SHA512). User logins with Argon2 are
relativly slow and cause significant spikes in CPU and memory (100MB) usage.

Existing passwords are gradually migrated to Argon2 on login.
parent 117e257c
No related branches found
No related tags found
No related merge requests found
...@@ -16,6 +16,7 @@ Please note that we refer to Debian packages here and **not** pip packages. ...@@ -16,6 +16,7 @@ Please note that we refer to Debian packages here and **not** pip packages.
- python3-fido2 (version 0.5.0 or 0.9.1, optional) - python3-fido2 (version 0.5.0 or 0.9.1, optional)
- python3-oauthlib - python3-oauthlib
- python3-flask-babel - python3-flask-babel
- python3-argon2
- python3-mysqldb or python3-pymysql for MySQL/MariaDB support - python3-mysqldb or python3-pymysql for MySQL/MariaDB support
Some of the dependencies (especially fido2) changed their API in recent versions, so make sure to install the versions from Debian Buster or Bullseye. Some of the dependencies (especially fido2) changed their API in recent versions, so make sure to install the versions from Debian Buster or Bullseye.
......
...@@ -25,6 +25,7 @@ Depends: ...@@ -25,6 +25,7 @@ Depends:
python3-fido2, python3-fido2,
python3-oauthlib, python3-oauthlib,
python3-flask-babel, python3-flask-babel,
python3-argon2,
uwsgi, uwsgi,
uwsgi-plugin-python3, uwsgi-plugin-python3,
Recommends: Recommends:
......
...@@ -40,6 +40,7 @@ setup( ...@@ -40,6 +40,7 @@ setup(
'Flask-Migrate==2.1.1', 'Flask-Migrate==2.1.1',
'Flask-Babel==0.11.2', 'Flask-Babel==0.11.2',
'alembic==1.0.0', 'alembic==1.0.0',
'argon2-cffi==18.3.0',
# The main dependencies on their own lead to version collisions and pip is # The main dependencies on their own lead to version collisions and pip is
# not very good at resolving them, so we pin the versions from Debian Buster # not very good at resolving them, so we pin the versions from Debian Buster
......
...@@ -130,6 +130,31 @@ class TestCryptPasswordHash(unittest.TestCase): ...@@ -130,6 +130,31 @@ class TestCryptPasswordHash(unittest.TestCase):
self.assertTrue(obj.verify('password')) self.assertTrue(obj.verify('password'))
self.assertFalse(obj.verify('notpassword')) self.assertFalse(obj.verify('notpassword'))
class TestArgon2PasswordHash(unittest.TestCase):
def test_verify(self):
obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=2,p=8$Jc8LpCgPLjwlN/7efHLvwQ$ZqSg3CFb2/hBb3X8hOq4aw')
self.assertTrue(obj.verify('password'))
self.assertFalse(obj.verify('notpassword'))
obj = Argon2PasswordHash('{argon2}$invalid$')
self.assertFalse(obj.verify('password'))
def test_from_password(self):
obj = Argon2PasswordHash.from_password('password')
self.assertIsNotNone(obj.value)
self.assertTrue(obj.value.startswith('{argon2}'))
self.assertTrue(obj.verify('password'))
self.assertFalse(obj.verify('notpassword'))
def test_needs_rehash(self):
obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=2,p=8$Jc8LpCgPLjwlN/7efHLvwQ$ZqSg3CFb2/hBb3X8hOq4aw')
self.assertFalse(obj.needs_rehash)
obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=2,p=8$Jc8LpCgPLjwlN/7efHLvwQ$ZqSg3CFb2/hBb3X8hOq4aw', target_cls=PlaintextPasswordHash)
self.assertTrue(obj.needs_rehash)
obj = Argon2PasswordHash('{argon2}$argon2d$v=19$m=102400,t=2,p=8$kshPgLU1+h72l/Z8QWh8Ig$tYerKCe/5I2BCPKu8hCl2w')
self.assertTrue(obj.needs_rehash)
obj = Argon2PasswordHash('{argon2}$argon2id$v=19$m=102400,t=1,p=8$aa6i4vg/szKX5xHVGFaAeQ$v6j0ltuVqQaZlmuepaVJ1A')
self.assertTrue(obj.needs_rehash)
class TestInvalidPasswordHash(unittest.TestCase): class TestInvalidPasswordHash(unittest.TestCase):
def test(self): def test(self):
obj = InvalidPasswordHash('test') obj = InvalidPasswordHash('test')
......
...@@ -2,6 +2,7 @@ import secrets ...@@ -2,6 +2,7 @@ import secrets
import hashlib import hashlib
import base64 import base64
from crypt import crypt from crypt import crypt
import argon2
def build_value(method_name, data): def build_value(method_name, data):
return '{' + method_name + '}' + data return '{' + method_name + '}' + data
...@@ -179,6 +180,28 @@ class CryptPasswordHash(PasswordHash): ...@@ -179,6 +180,28 @@ class CryptPasswordHash(PasswordHash):
def verify(self, password): def verify(self, password):
return secrets.compare_digest(crypt(password, self.data), self.data) return secrets.compare_digest(crypt(password, self.data), self.data)
@registry.register
class Argon2PasswordHash(PasswordHash):
METHOD_NAME = 'argon2'
hasher = argon2.PasswordHasher()
@classmethod
def from_password(cls, password):
return cls(build_value(cls.METHOD_NAME, cls.hasher.hash(password)))
def verify(self, password):
try:
return self.hasher.verify(self.data, password)
except argon2.exceptions.Argon2Error:
return False
except argon2.exceptions.InvalidHash:
return False
@property
def needs_rehash(self):
return super().needs_rehash or self.hasher.check_needs_rehash(self.data)
class InvalidPasswordHash: class InvalidPasswordHash:
def __init__(self, value=None): def __init__(self, value=None):
self.value = value self.value = value
...@@ -262,9 +285,8 @@ class PasswordHashAttribute: ...@@ -262,9 +285,8 @@ class PasswordHashAttribute:
setattr(obj, self.attribute_name, value.value) setattr(obj, self.attribute_name, value.value)
# Hashing method for (potentially) low entropy secrets like user passwords. Is # Hashing method for (potentially) low entropy secrets like user passwords. Is
# usually slow and uses salting to make dictionary attacks difficult. Note # usually slow and uses salting to make dictionary attacks difficult.
# that SSHA512 is not slow and should be replaced with a modern alternative. LowEntropyPasswordHash = Argon2PasswordHash
LowEntropyPasswordHash = SaltedSHA512PasswordHash
# Hashing method for high entropy secrets like API keys. The secrets are # Hashing method for high entropy secrets like API keys. The secrets are
# generated instead of user-selected to ensure a high level of entropy. Is # generated instead of user-selected to ensure a high level of entropy. Is
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment