Skip to content

Commit

Permalink
Add argon2 as allowable password hash. (#209)
Browse files Browse the repository at this point in the history
Simple really - and add a comment in the docs that you probably have to install an additional package to use it.

Argon2 implementation doesn't honor the deprecated passlib model of passing in options at hash time.

Added a new config variable SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS that is used a CryptContext creation time
that is the new passlib model for how to set hash options.

Kept the SECURITY_PASSWORD_HASH_OPTIONS for backwards compat.

Argon2 doesn't need double hash - so add it to 'SINGLE' hash list.

Convert example to use argon2.

closes: #205
closes: #133
  • Loading branch information
jwag956 committed Nov 14, 2019
1 parent 75c21e8 commit d3a9e51
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 10 deletions.
13 changes: 9 additions & 4 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ Core
``SECURITY_PASSWORD_HASH`` Specifies the password hash algorithm to
use when hashing passwords. Recommended
values for production systems are
``bcrypt``, ``sha512_crypt``, or
``pbkdf2_sha512``. Defaults to
``bcrypt``, ``argon2``, ``sha512_crypt``, or
``pbkdf2_sha512``. Some algorithms require the installation
of a backend package (e.g. `bcrypt`_, `argon2`_). Defaults to
``bcrypt``.
``SECURITY_PASSWORD_SCHEMES`` List of support password hash algorithms.
``SECURITY_PASSWORD_HASH`` must be from this list.
Expand Down Expand Up @@ -70,7 +71,10 @@ Core
creating and validating tokens.
Defaults to ``hex_md5``.
``SECURITY_PASSWORD_HASH_OPTIONS`` Specifies additional options to be passed
to the hashing method.
to the hashing method. This is deprecated as of passlib 1.7.
``SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS`` Pass additional options to the various hashing methods. This is a
dict of the form ``{<scheme>__<option>: <value>, ..}``
e.g. {"argon2__rounds": 4}.
``SECURITY_EMAIL_SENDER`` Specifies the email address to send
emails as. Defaults to value set
to ``MAIL_DEFAULT_SENDER`` if
Expand Down Expand Up @@ -155,7 +159,8 @@ Core
.. _set_cookie: https://flask.palletsprojects.com/en/1.1.x/api/?highlight=set_cookie#flask.Response.set_cookie
.. _axios: https://github.com/axios/axios
.. _cachetools: https://pypi.org/project/cachetools/

.. _bcrypt: https://pypi.org/project/bcrypt/
.. _argon2: https://pypi.org/project/argon2-cffi/

URLs and Views
--------------
Expand Down
6 changes: 3 additions & 3 deletions examples/fsqlalchemy1.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,13 @@
# Create app
app = Flask(__name__)
app.config["DEBUG"] = True
app.config["SECRET_KEY"] = "super-secret"
# generated using: secrets.token_urlsafe()
app.config["SECRET_KEY"] = "pf9Wkove4IKEAXvy-cQkeDPhv9Cb3Ag-wyJILbq_dFw"
app.config["SECURITY_PASSWORD_HASH"] = "argon2"
app.config["SQLALCHEMY_DATABASE_URI"] = os.environ.get(
"SQLALCHEMY_DATABASE_URI", "sqlite://"
)
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
# Bcrypt is set as default SECURITY_PASSWORD_HASH, which requires a salt
app.config["SECURITY_PASSWORD_SALT"] = "super-secret-random-salt"

# As of Flask-SQLAlchemy 2.4.0 it is easy to pass in options directly to the
# underlying engine. This option makes sure that DB connections from the pool
Expand Down
12 changes: 11 additions & 1 deletion flask_security/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,12 @@
"django_salted_md5",
"django_salted_sha1",
"django_des_crypt",
"argon2",
"plaintext",
},
"PASSWORD_SCHEMES": [
"bcrypt",
"argon2",
"des_crypt",
"pbkdf2_sha256",
"pbkdf2_sha512",
Expand All @@ -104,6 +106,8 @@
# And always last one...
"plaintext",
],
"PASSWORD_HASH_OPTIONS": {}, # Deprecated at passlib 1.7
"PASSWORD_HASH_PASSLIB_OPTIONS": {}, # >= 1.7.1 method to pass options.
"DEPRECATED_PASSWORD_SCHEMES": ["auto"],
"LOGIN_URL": "/login",
"LOGOUT_URL": "/logout",
Expand Down Expand Up @@ -451,7 +455,13 @@ def _get_pwd_context(app):
"Invalid password hashing scheme %r. Allowed values are %s"
% (pw_hash, allowed)
)
return CryptContext(schemes=schemes, default=pw_hash, deprecated=deprecated)
cc = CryptContext(
schemes=schemes,
default=pw_hash,
deprecated=deprecated,
**cv("PASSWORD_HASH_PASSLIB_OPTIONS", app=app)
)
return cc


def _get_i18n_domain(app):
Expand Down
4 changes: 2 additions & 2 deletions flask_security/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,15 +229,15 @@ def encrypt_password(password):
def hash_password(password):
"""Hash the specified plaintext password.
It uses the configured hashing options.
.. versionadded:: 2.0.2
:param password: The plaintext password to hash
"""
if use_double_hash():
password = get_hmac(password).decode("ascii")

# Passing in options as part of hash is deprecated in passlib 1.7
# and new algorithms like argon2 don't even support it.
return _pwd_context.hash(
password,
**config_value("PASSWORD_HASH_OPTIONS", default={}).get(
Expand Down
2 changes: 2 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"Flask-Mongoengine>=0.9.5",
"peewee>=3.11.2",
"Flask-SQLAlchemy>=2.3",
"argon2_cffi>=19.1.0",
"bcrypt>=3.1.5",
"cachetools>=3.1.0",
"check-manifest>=0.25",
Expand Down Expand Up @@ -108,6 +109,7 @@
"Programming Language :: Python :: 3.5",
"Programming Language :: Python :: 3.6",
"Programming Language :: Python :: 3.7",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Development Status :: 4 - Beta",
Expand Down
39 changes: 39 additions & 0 deletions tests/test_hashing.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
hashing tests
"""

import timeit

import pytest
from pytest import raises
from utils import authenticate, init_app_with_options
from passlib.hash import pbkdf2_sha256, django_pbkdf2_sha256, plaintext
Expand Down Expand Up @@ -122,3 +125,39 @@ def test_missing_hash_salt_option(app, sqlalchemy_datastore):
"SECURITY_PASSWORD_SINGLE_HASH": False,
}
)


def test_verify_password_argon2(app, sqlalchemy_datastore):
init_app_with_options(
app, sqlalchemy_datastore, **{"SECURITY_PASSWORD_HASH": "argon2"}
)
with app.app_context():
assert verify_password("pass", hash_password("pass"))


def test_verify_password_argon2_opts(app, sqlalchemy_datastore):
init_app_with_options(
app,
sqlalchemy_datastore,
**{
"SECURITY_PASSWORD_HASH": "argon2",
"SECURITY_PASSWORD_HASH_PASSLIB_OPTIONS": {
"argon2__rounds": 4,
"argon2__salt_size": 16,
"argon2__hash_len": 16,
},
}
)
with app.app_context():
hashed_pwd = hash_password("pass")
assert "t=4" in hashed_pwd
assert verify_password("pass", hashed_pwd)


@pytest.mark.skip
def test_argon2_speed(app, sqlalchemy_datastore):
init_app_with_options(
app, sqlalchemy_datastore, **{"SECURITY_PASSWORD_HASH": "argon2"}
)
with app.app_context():
timeit.timeit(lambda: hash_password("pass"), number=100)

0 comments on commit d3a9e51

Please sign in to comment.