Repository: red-coracle/flask-argon2

Issue:
default settings
Hello,

Seems it's not possible to change default argon2 settings, at least I didn't figure out the way to set them via typical app.config:

```
# example code (custom flask script)

from flask_argon2 import Argon2
from flask import Flask
app = Flask(__name__)

# example trying to change only memory cost value
app.config['ARGON2_MEMORY_COST'] = 131072
app.config['DEFAULT_MEMORY_COST'] = 131072
crypt_argon2 = Argon2(app)
```

looks like the Argon2 module is taking pre-set default values from argon2 module (password_hasher.py), and ignores any values passed in via app.config. And unless changing the argon2 module code, I can't find other way to set custom values. 


```
# argon2/password_hasher.py
from .profiles import RFC_9106_LOW_MEMORY
DEFAULT_RANDOM_SALT_LENGTH = RFC_9106_LOW_MEMORY.salt_len
DEFAULT_HASH_LENGTH = RFC_9106_LOW_MEMORY.hash_len
DEFAULT_TIME_COST = RFC_9106_LOW_MEMORY.time_cost
DEFAULT_MEMORY_COST = RFC_9106_LOW_MEMORY.memory_cost
DEFAULT_PARALLELISM = RFC_9106_LOW_MEMORY.parallelism
```

Values passed to argon2 hash function:
```
# argon2/profiles.py
# SECOND RECOMMENDED option per RFC 9106.
RFC_9106_LOW_MEMORY = Parameters(
    type=Type.ID,
    version=19,
    salt_len=16,
    hash_len=32,
    time_cost=3,
    memory_cost=65536,  # 64 MiB
    parallelism=4,
)

```

Is this intended, or I am doing something wrong?

Discussion:

Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
new file mode 100644
index 0000000..2fa020d
--- /dev/null
+++ b/.github/workflows/main.yml
@@ -0,0 +1,27 @@
+name: pytest
+
+on: [push, pull_request]
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    strategy:
+      matrix:
+        python-version: ['3.7', '3.8', '3.9', '3.10']
+
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        fetch-depth: 1
+    - name: Set up Python ${{ matrix.python-version }}
+      uses: actions/setup-python@v3
+      with:
+        python-version: ${{ matrix.python-version }}
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install -r requirements.txt
+    - name: Test with pytest
+      run: |
+        pip install pytest
+        pytest test_flask_argon2.py
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index f70bf5c..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,28 +0,0 @@
-sudo: required
-language: python
-dist: focal
-python:
-  - 2.7
-  - 3.5
-  - 3.6
-  - 3.7
-  - 3.8
-  - "nightly"
-  - "pypy"
-  - "pypy3"
-
-matrix:
-  allow_failures:
-    - python: "nightly"
-    - python: pypy
-    - python: pypy3
-
-install:
-  - pip install .
-  - pip install flask
-
-script:
-  - python setup.py test
-
-notifications:
-  email: false
diff --git a/flask_argon2.py b/flask_argon2.py
index 2f75d97..0177963 100644
--- a/flask_argon2.py
+++ b/flask_argon2.py
@@ -4,13 +4,10 @@
 
     A Flask extension providing argon2 hashing and comparison.
 
-    :copyright: (c) 2019 by DominusTemporis.
+    :copyright: (c) 2022 by red-coracle.
     :license: MIT, see LICENSE for more details.
 """
 
-from __future__ import absolute_import
-from __future__ import print_function
-# from sys import version_info
 
 __version_info__ = ('0', '2', '0', '0')
 __version__ = '.'.join(__version_info__)
@@ -26,8 +23,6 @@ except ImportError as e:
     print('argon2_cffi is required to use Flask-Argon2')
     raise e
 
-# PY3 = version_info[0] >= 3
-
 
 def generate_password_hash(password):
     '''
@@ -110,35 +105,41 @@ class Argon2(object):
 
     def __init__(self,
                  app=None,
-                 time_cost=_time_cost,
-                 memory_cost=_memory_cost,
-                 parallelism=_parallelism,
-                 hash_len=_hash_len,
-                 salt_len=_salt_len,
-                 encoding=_encoding):
+                 time_cost: int = None,
+                 memory_cost: int = None,
+                 parallelism: int = None,
+                 hash_len: int = None,
+                 salt_len: int = None,
+                 encoding: str = None):
+        # Keep for backwards compatibility
         self.time_cost = time_cost
         self.memory_cost = memory_cost
         self.parallelism = parallelism
         self.hash_len = hash_len
         self.salt_len = salt_len
         self.encoding = encoding
-        self.ph = argon2.PasswordHasher(self.time_cost,
-                                        self.memory_cost,
-                                        self.parallelism,
-                                        self.hash_len,
-                                        self.salt_len,
-                                        self.encoding)
-    
+        self.init_app(app)
+
     def init_app(self, app):
         '''Initalizes the application with the extension.
         :param app: The Flask application object.
         '''
-        self.time_cost = app.config.get('ARGON2_TIME_COST', self._time_cost)
-        self.memory_cost = app.config.get('ARGON2_MEMORY_COST', self._memory_cost)
-        self.parallelism = app.config.get('ARGON2_PARALLELISM', self._parallelism)
-        self.hash_len = app.config.get('ARGON2_HASH_LENGTH', self._hash_len)
-        self.salt_len = app.config.get('ARGON2_SALT_LENGTH', self._salt_len)
-        self.encoding = app.config.get('ARGON2_ENCODING', self._encoding)
+
+        self.time_cost = self.time_cost or self._time_cost
+        self.memory_cost = self.memory_cost or self._memory_cost
+        self.parallelism = self.parallelism or self._parallelism
+        self.hash_len = self.hash_len or self._hash_len
+        self.salt_len = self.salt_len or self._salt_len
+        self.encoding = self.encoding or self._encoding
+
+        if app is not None:
+            self.time_cost = app.config.get('ARGON2_TIME_COST', self.time_cost)
+            self.memory_cost = app.config.get('ARGON2_MEMORY_COST', self.memory_cost)
+            self.parallelism = app.config.get('ARGON2_PARALLELISM', self.parallelism)
+            self.hash_len = app.config.get('ARGON2_HASH_LENGTH', self.hash_len)
+            self.salt_len = app.config.get('ARGON2_SALT_LENGTH', self.salt_len)
+            self.encoding = app.config.get('ARGON2_ENCODING', self.encoding)
+
         self.ph = argon2.PasswordHasher(self.time_cost,
                                         self.memory_cost,
                                         self.parallelism,
@@ -146,7 +147,6 @@ class Argon2(object):
                                         self.salt_len,
                                         self.encoding)
 
-
     def generate_password_hash(self, password):
         '''Generates a password hash using argon2.
         Example usage of :class:`generate_password_hash` might look something
@@ -158,13 +158,6 @@ class Argon2(object):
         if not password:
             raise ValueError('Password must be non-empty.')
 
-        # if PY3:
-        #     if isinstance(password, str):
-        #         password = bytes(password, 'utf-8')
-        # else:
-        #     if isinstance(password, unicode):
-        #         password = password.encode('utf-8')
-
         return self.ph.hash(password)
 
     def check_password_hash(self, pw_hash, password):
diff --git a/requirements.txt b/requirements.txt
index ca76291..8dce1a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-argon2_cffi>=20.1.0
-Flask>=1.*
+argon2_cffi>=21.0.0
+Flask>=2.*


Test patch:
diff --git a/test_flask_argon2.py b/test_flask_argon2.py
index c3547d4..e47a314 100644
--- a/test_flask_argon2.py
+++ b/test_flask_argon2.py
@@ -1,48 +1,67 @@
-# coding:utf-8
-
-import unittest
 import flask
-from flask_argon2 import (Argon2, check_password_hash, generate_password_hash)
+import pytest
+from argon2.profiles import CHEAPEST
+
+from flask_argon2 import Argon2, check_password_hash, generate_password_hash
+
+
+@pytest.fixture(scope='module')
+def _argon2():
+    app = flask.Flask(__name__)
+    return Argon2(app)
+
+
+def test_check_hash(_argon2):
+    pw_hash = _argon2.generate_password_hash('secret')
+    assert check_password_hash(pw_hash, 'secret') is True
+    pw_hash = _argon2.generate_password_hash(u'\u2603')
+    assert _argon2.check_password_hash(pw_hash, u'\u2603') is True
+    pw_hash = generate_password_hash('hunter2')
+    assert check_password_hash(pw_hash, 'hunter2') is True
+
+
+def test_unicode_hash(_argon2):
+    password = u'東京'
+    pw_hash = _argon2.generate_password_hash(password)
+    assert _argon2.check_password_hash(pw_hash, password) is True
+
 
+def test_hash_error(_argon2):
+    pw_hash = _argon2.generate_password_hash('secret')
+    assert _argon2.check_password_hash(pw_hash, 'hunter2') is False
 
-class BasicTestCase(unittest.TestCase):
 
-    def setUp(self):
-        app = flask.Flask(__name__)
-        self.argon2 = Argon2(app)
+def test_invalid_hash(_argon2):
+    assert _argon2.check_password_hash('secret', 'hunter2') is False
 
-    def test_check_hash(self):
-        pw_hash = self.argon2.generate_password_hash('secret')
-        self.assertTrue(self.argon2.check_password_hash(pw_hash, 'secret'))
-        pw_hash = self.argon2.generate_password_hash(u'\u2603')
-        self.assertTrue(self.argon2.check_password_hash(pw_hash, u'\u2603'))
-        pw_hash = generate_password_hash('hunter2')
-        self.assertTrue(check_password_hash(pw_hash, 'hunter2'))
 
-    def test_unicode_hash(self):
-        password = u'東京'
-        pw_hash = generate_password_hash(password)
-        self.assertTrue(check_password_hash(pw_hash, password))
+def test_init_override():
+    app = flask.Flask(__name__)
+    _argon2 = Argon2(app, parallelism=3)
+    pw_hash = _argon2.generate_password_hash('secret')
+    assert _argon2.parallelism == 3
+    assert _argon2.check_password_hash(pw_hash, 'secret') is True
 
-    def test_hash_error(self):
-        pw_hash = self.argon2.generate_password_hash('secret')
-        self.assertFalse(self.argon2.check_password_hash(pw_hash, 'hunter2'))
-    
-    def test_hash_invalid_hash(self):
-        """ Check that an InvalidHash is raised when an unhased password is used. """
-        self.assertFalse(self.argon2.check_password_hash('secret', 'hunter2'))
 
+def test_app_config_override():
+    app = flask.Flask(__name__)
+    app.config['ARGON2_MEMORY_COST'] = CHEAPEST.memory_cost
+    _argon2 = Argon2(app)
+    assert _argon2.memory_cost == CHEAPEST.memory_cost
+    assert _argon2.ph.memory_cost == CHEAPEST.memory_cost
 
-class OverridesTestCase(unittest.TestCase):
 
-    def setUp(self):
-        app = flask.Flask(__name__)
-        self.argon2 = Argon2(app, parallelism=3)
+def test_multiple_overrides():
+    app = flask.Flask(__name__)
+    app.config['ARGON2_PARALLELISM'] = CHEAPEST.parallelism
+    _argon2 = Argon2(app, hash_len=16)
+    assert _argon2.parallelism == CHEAPEST.parallelism
+    assert _argon2.hash_len == 16
 
-    def test_changed_parallelism(self):
-        pw_hash = self.argon2.generate_password_hash('secret')
-        self.assertTrue(self.argon2.parallelism == 3)
-        self.assertTrue(self.argon2.check_password_hash(pw_hash, 'secret'))
 
-if __name__ == '__main__':
-    unittest.main()
+def test_multiple_override_same_parameter():
+    app = flask.Flask(__name__)
+    app.config['ARGON2_PARALLELISM'] = CHEAPEST.parallelism
+    _argon2 = Argon2(app, parallelism=8)
+    assert _argon2.parallelism == CHEAPEST.parallelism
+    assert _argon2.ph.parallelism == CHEAPEST.parallelism