├── 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 |
--------------------------------------------------------------------------------