├── setup.cfg ├── requirements.txt ├── .gitignore ├── .github └── workflows │ └── main.yml ├── LICENSE ├── README.markdown ├── setup.py ├── test_flask_argon2.py └── flask_argon2.py /setup.cfg: -------------------------------------------------------------------------------- 1 | [easy_install] 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argon2_cffi>=21.0.0 2 | Flask>=2.* 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .*.swp 3 | .tox 4 | tox.ini 5 | docs/_build/* 6 | build/* 7 | dist/ 8 | *.pyc 9 | __pycache__ 10 | Flask_Argon2.egg-info/* 11 | .env/* 12 | .envp3/* 13 | env/ 14 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: pytest 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ['3.7', '3.8', '3.9', '3.10'] 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | with: 15 | fetch-depth: 1 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v3 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | python -m pip install --upgrade pip 23 | pip install -r requirements.txt 24 | - name: Test with pytest 25 | run: | 26 | pip install pytest 27 | pytest test_flask_argon2.py 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 DominusTemporis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | Status API Training Shop Blog About 22 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | # Flask-Argon2 2 | 3 | Flask-Argon2 is a Flask extension that provides Argon2 hashing utilities for 4 | your Flask app. 5 | 6 | ## Installation 7 | 8 | Install the extension with the following command: 9 | 10 | $ pip install flask-argon2 11 | 12 | ## Usage 13 | 14 | To use the extension simply import the class wrapper and pass the Flask app 15 | object back to here. Do so like this: 16 | 17 | from flask import Flask 18 | from flask_argon2 import Argon2 19 | 20 | app = Flask(__name__) 21 | argon2 = Argon2(app) 22 | 23 | When using an application factory this extension can be initialized by the `init_app()` function. 24 | 25 | The following Flask Configs are automatically used by `init_app()`: 26 | 27 | |Variable Name | Description | 28 | |---|---| 29 | |`ARGON2_TIME_COST`|Defines the amount of computation realized and therefore the execution time, given in number of iterations.| 30 | |`ARGON2_MEMORY_COST`|Defines the memory usage, given in kibibytes.| 31 | |`ARGON2_PARALLELISM`|Defines the number of parallel threads (changes the resulting hash value).| 32 | |`ARGON2_HASH_LENGTH`|Length of the hash in bytes.| 33 | |`ARGON2_SALT_LENGTH`|Length of random salt to be generated for each password.| 34 | |`ARGON2_ENCODING`|The Argon2 C library expects bytes. So if hash() or verify() are passed an unicode string, it will be encoded using this encoding.| 35 | The default values are the same as the [argon2_cffi library](https://argon2-cffi.readthedocs.io/en/stable/api.html#argon2.PasswordHasher). 36 | 37 | Two primary methods are now exposed by way of the argon2 object. Use 38 | them like so: 39 | 40 | pw_hash = argon2.generate_password_hash('secret_password') 41 | argon2.check_password_hash(pw_hash, 'secret_password') 42 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | ''' 5 | Flask-Argon2 6 | ------------ 7 | 8 | The Flask-Argon2 extension provides Argon2i password hashing with 9 | sensible defaults and a random salt for Flask. 10 | 11 | Links 12 | ````` 13 | * `Documentation `_ 14 | * `Development version `_ 15 | * `argon2_cffi `_ 16 | ''' 17 | 18 | import os 19 | from setuptools import setup 20 | 21 | MODULE_PATH = os.path.join(os.path.dirname(__file__), 'flask_argon2.py') 22 | VERSION_LINE = tuple(f for f in open(MODULE_PATH).readlines() 23 | if '__version_info__' in f)[0] 24 | 25 | __version__ = '.'.join(eval(VERSION_LINE.split('__version_info__ = ')[-1])) 26 | 27 | setup( 28 | name='Flask-Argon2', 29 | version=__version__, 30 | url='https://github.com/red-coracle/flask-argon2', 31 | license='MIT', 32 | author='DominusTemporis', 33 | author_email='python@dominustemporis.com', 34 | description='Flask-Argon2 provides convenient wrappers for Argon2 password hashing', 35 | long_description=__doc__, 36 | py_modules=['flask_argon2'], 37 | zip_safe=False, 38 | platforms='any', 39 | include_package_data=True, 40 | install_requires=['Flask', 'argon2_cffi'], 41 | classifiers=[ 42 | 'Development Status :: 4 - Beta', 43 | 'Environment :: Web Environment', 44 | 'Intended Audience :: Developers', 45 | 'License :: OSI Approved :: MIT License', 46 | 'Operating System :: OS Independent', 47 | 'Programming Language :: Python', 48 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 49 | 'Topic :: Software Development :: Libraries :: Python Modules' 50 | ], 51 | test_suite='test_flask_argon2' 52 | ) 53 | -------------------------------------------------------------------------------- /test_flask_argon2.py: -------------------------------------------------------------------------------- 1 | import flask 2 | import pytest 3 | from argon2.profiles import CHEAPEST 4 | 5 | from flask_argon2 import Argon2, check_password_hash, generate_password_hash 6 | 7 | 8 | @pytest.fixture(scope='module') 9 | def _argon2(): 10 | app = flask.Flask(__name__) 11 | return Argon2(app) 12 | 13 | 14 | def test_check_hash(_argon2): 15 | pw_hash = _argon2.generate_password_hash('secret') 16 | assert check_password_hash(pw_hash, 'secret') is True 17 | pw_hash = _argon2.generate_password_hash(u'\u2603') 18 | assert _argon2.check_password_hash(pw_hash, u'\u2603') is True 19 | pw_hash = generate_password_hash('hunter2') 20 | assert check_password_hash(pw_hash, 'hunter2') is True 21 | 22 | 23 | def test_unicode_hash(_argon2): 24 | password = u'東京' 25 | pw_hash = _argon2.generate_password_hash(password) 26 | assert _argon2.check_password_hash(pw_hash, password) is True 27 | 28 | 29 | def test_hash_error(_argon2): 30 | pw_hash = _argon2.generate_password_hash('secret') 31 | assert _argon2.check_password_hash(pw_hash, 'hunter2') is False 32 | 33 | 34 | def test_invalid_hash(_argon2): 35 | assert _argon2.check_password_hash('secret', 'hunter2') is False 36 | 37 | 38 | def test_init_override(): 39 | app = flask.Flask(__name__) 40 | _argon2 = Argon2(app, parallelism=3) 41 | pw_hash = _argon2.generate_password_hash('secret') 42 | assert _argon2.parallelism == 3 43 | assert _argon2.check_password_hash(pw_hash, 'secret') is True 44 | 45 | 46 | def test_app_config_override(): 47 | app = flask.Flask(__name__) 48 | app.config['ARGON2_MEMORY_COST'] = CHEAPEST.memory_cost 49 | _argon2 = Argon2(app) 50 | assert _argon2.memory_cost == CHEAPEST.memory_cost 51 | assert _argon2.ph.memory_cost == CHEAPEST.memory_cost 52 | 53 | 54 | def test_multiple_overrides(): 55 | app = flask.Flask(__name__) 56 | app.config['ARGON2_PARALLELISM'] = CHEAPEST.parallelism 57 | _argon2 = Argon2(app, hash_len=16) 58 | assert _argon2.parallelism == CHEAPEST.parallelism 59 | assert _argon2.hash_len == 16 60 | 61 | 62 | def test_multiple_override_same_parameter(): 63 | app = flask.Flask(__name__) 64 | app.config['ARGON2_PARALLELISM'] = CHEAPEST.parallelism 65 | _argon2 = Argon2(app, parallelism=8) 66 | assert _argon2.parallelism == CHEAPEST.parallelism 67 | assert _argon2.ph.parallelism == CHEAPEST.parallelism 68 | -------------------------------------------------------------------------------- /flask_argon2.py: -------------------------------------------------------------------------------- 1 | """ 2 | flaskext.argon2 3 | --------------- 4 | 5 | A Flask extension providing argon2 hashing and comparison. 6 | 7 | :copyright: (c) 2022 by red-coracle. 8 | :license: MIT, see LICENSE for more details. 9 | """ 10 | 11 | 12 | __version_info__ = ('0', '3', '0', '0') 13 | __version__ = '.'.join(__version_info__) 14 | __author__ = 'red-coracle' 15 | __license__ = 'MIT' 16 | __copyright__ = 'Copyright (c) 2020 red-coracle' 17 | __all__ = ['Argon2', 'generate_password_hash', 'check_password_hash'] 18 | 19 | try: 20 | import argon2 21 | from argon2.exceptions import VerifyMismatchError, VerificationError, InvalidHash 22 | except ImportError as e: 23 | print('argon2_cffi is required to use Flask-Argon2') 24 | raise e 25 | 26 | 27 | def generate_password_hash(password): 28 | ''' 29 | This helper function wraps the identical method of :class:`Argon2`. It 30 | is intended to be used as a helper function at the expense of the 31 | configuration variable provided when passing back the app object. In other 32 | words, this shortcut does not make use of the app object at all. 33 | To use this function, simply import it from the module and use it in a 34 | similar fashion as using the method. Here is a quick example:: 35 | from flask_argon2 import generate_password_hash 36 | pw_hash = generate_password_hash('hunter2') 37 | 38 | :param password: The password to be hashed. 39 | ''' 40 | return Argon2().generate_password_hash(password) 41 | 42 | 43 | def check_password_hash(pw_hash, password): 44 | ''' 45 | This helper function wraps the similarly names method of :class:`Argon2.` It 46 | is intended to be used as a helper function at the expense of the 47 | configuration variable provided when passing back the app object. In other 48 | words, this shortcut does not make use of the app object at all. 49 | 50 | To use this function, simply import it from the module and use it in a 51 | similar fashion as using the method. Here is a quick example:: 52 | 53 | from flask_argon2 import check_password_hash 54 | check_password_hash(pw_hash, 'hunter2') # returns True 55 | 56 | :param pw_hash: The hash to be compared against. 57 | :param password: The password to compare. 58 | ''' 59 | return Argon2().check_password_hash(pw_hash, password) 60 | 61 | 62 | class Argon2(object): 63 | ''' 64 | Argon2 class container for password hashing and checking logic using 65 | argon2, of course. This class may be used to intialize your Flask app 66 | object. The purpose is to provide a simple interface for overriding 67 | Werkzeug's built-in password hashing utilities. 68 | Although such methods are not actually overriden, the API is intentionally 69 | made similar so that existing applications which make use of the previous 70 | hashing functions might be easily adapted to the stronger facility of 71 | argon2. 72 | To get started you will wrap your application's app object something like 73 | this:: 74 | app = Flask(__name__) 75 | argon2 = Argon2(app) 76 | Now the two primary utility methods are exposed via this object, `argon2`. 77 | So in the context of the application, important data, such as passwords, 78 | could be hashed using this syntax:: 79 | password = 'hunter2' 80 | pw_hash = argon2.generate_password_hash(password) 81 | Once hashed, the value is irreversible. However in the case of validating 82 | logins a simple hashing of candidate password and subsequent comparison. 83 | Importantly a comparison should be done in constant time. This helps 84 | prevent timing attacks. A simple utility method is provided for this:: 85 | candidate = 'secret' 86 | argon2.check_password_hash(pw_hash, candidate) 87 | If both the candidate and the existing password hash are a match 88 | `check_password_hash` returns True. Otherwise, it returns False. 89 | .. admonition:: Namespacing Issues 90 | It's worth noting that if you use the format, `argon2 = Argon2(app)` 91 | you are effectively overriding the argon2 module. Though it's unlikely 92 | you would need to access the module outside of the scope of the 93 | extension be aware that it's overriden. 94 | Alternatively consider using a different name, such as `flask_argon2 95 | = Argon2(app)` to prevent naming collisions. 96 | :param app: The Flask application object. Defaults to None. 97 | ''' 98 | 99 | _time_cost = argon2.DEFAULT_TIME_COST 100 | _memory_cost = argon2.DEFAULT_MEMORY_COST 101 | _parallelism = argon2.DEFAULT_PARALLELISM 102 | _hash_len = argon2.DEFAULT_HASH_LENGTH 103 | _salt_len = argon2.DEFAULT_RANDOM_SALT_LENGTH 104 | _encoding = 'utf-8' 105 | 106 | def __init__(self, 107 | app=None, 108 | time_cost: int = None, 109 | memory_cost: int = None, 110 | parallelism: int = None, 111 | hash_len: int = None, 112 | salt_len: int = None, 113 | encoding: str = None): 114 | # Keep for backwards compatibility 115 | self.time_cost = time_cost 116 | self.memory_cost = memory_cost 117 | self.parallelism = parallelism 118 | self.hash_len = hash_len 119 | self.salt_len = salt_len 120 | self.encoding = encoding 121 | self.init_app(app) 122 | 123 | def init_app(self, app): 124 | '''Initalizes the application with the extension. 125 | :param app: The Flask application object. 126 | ''' 127 | 128 | self.time_cost = self.time_cost or self._time_cost 129 | self.memory_cost = self.memory_cost or self._memory_cost 130 | self.parallelism = self.parallelism or self._parallelism 131 | self.hash_len = self.hash_len or self._hash_len 132 | self.salt_len = self.salt_len or self._salt_len 133 | self.encoding = self.encoding or self._encoding 134 | 135 | if app is not None: 136 | self.time_cost = app.config.get('ARGON2_TIME_COST', self.time_cost) 137 | self.memory_cost = app.config.get('ARGON2_MEMORY_COST', self.memory_cost) 138 | self.parallelism = app.config.get('ARGON2_PARALLELISM', self.parallelism) 139 | self.hash_len = app.config.get('ARGON2_HASH_LENGTH', self.hash_len) 140 | self.salt_len = app.config.get('ARGON2_SALT_LENGTH', self.salt_len) 141 | self.encoding = app.config.get('ARGON2_ENCODING', self.encoding) 142 | 143 | self.ph = argon2.PasswordHasher(self.time_cost, 144 | self.memory_cost, 145 | self.parallelism, 146 | self.hash_len, 147 | self.salt_len, 148 | self.encoding) 149 | 150 | def generate_password_hash(self, password): 151 | '''Generates a password hash using argon2. 152 | Example usage of :class:`generate_password_hash` might look something 153 | like this:: 154 | pw_hash = argon2.generate_password_hash('secret') 155 | :param password: The password to be hashed. 156 | ''' 157 | 158 | if not password: 159 | raise ValueError('Password must be non-empty.') 160 | 161 | return self.ph.hash(password) 162 | 163 | def check_password_hash(self, pw_hash, password): 164 | '''Tests a password hash against a candidate password. The candidate 165 | password is first hashed and then subsequently compared in constant 166 | time to the existing hash. This will either return `True` or `False`. 167 | Example usage of :class:`check_password_hash` would look something 168 | like this:: 169 | pw_hash = argon2.generate_password_hash('secret') 170 | argon2.check_password_hash(pw_hash, 'secret') # returns True 171 | :param pw_hash: The hash to be compared against. 172 | :param password: The password to compare. 173 | ''' 174 | 175 | try: 176 | return self.ph.verify(pw_hash, password) 177 | except VerifyMismatchError: 178 | return False 179 | except VerificationError: 180 | return False 181 | except InvalidHash: 182 | return False 183 | --------------------------------------------------------------------------------