Skip to content

Commit

Permalink
Make PasswordHasher use Argon2id (#34)
Browse files Browse the repository at this point in the history
* Make PasswordHasher use Argon2id

C.f. https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-4

* Add PR link

* Address review comments

* Add clarification to verification times
  • Loading branch information
hynek committed Apr 9, 2018
1 parent a2ea053 commit 406833e
Show file tree
Hide file tree
Showing 13 changed files with 115 additions and 47 deletions.
19 changes: 6 additions & 13 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,15 @@ The third digit is only for regressions.
Vendoring Argon2 @ UNRELEASED


Backward-incompatible changes:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

*none*


Deprecations:
^^^^^^^^^^^^^

*none*


Changes:
^^^^^^^^

*none*
- The hash type for ``argon2.PasswordHasher`` is Argon2\ **id** now.

This decision has been made based on the recommendations in the latest `Argon2 RFC draft <https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-4>`_.
`#33 <https://github.com/hynek/argon2_cffi/pull/33>`_
`#34 <https://github.com/hynek/argon2_cffi/pull/34>`_
- To make the change of hash type backward compatible, ``argon2.PasswordHasher.verify()`` now determines the type of the hash and verifies it accordingly.


----
Expand Down
3 changes: 2 additions & 1 deletion FAQ.rst
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
Frequently Asked Questions
==========================

I'm using ``bcrypt``/``scrypt``/``PBKDF2``, do I need to migrate?
I'm using ``bcrypt``/``PBKDF2``/``scrypt``/``yescrypt``, do I need to migrate?
Using password hashes that aren't memory hard carries a certain risk but there's **no immediate danger or need for action**.
If however you are deciding how to hash password *today*, pick Argon2 because it's a superior, future-proof choice.

But if you already use one of the hashes mentioned in the question, you should be fine for the foreseeable future.
If you're using ``scrypt`` or ``yescrypt``, you will be probably fine for good.

Why do the ``verify()`` methods raise an Exception instead of returning ``False``?
#. The Argon2 library had no concept of a "wrong password" error in the beginning.
Expand Down
2 changes: 1 addition & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ CFFI-based Argon2 Bindings for Python
>>> ph = PasswordHasher()
>>> hash = ph.hash("s3kr3tp4ssw0rd")
>>> hash # doctest: +SKIP
'$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
'$argon2id$v=19$m=512,t=2,p=2$Z0tsPw0iK7Ky2Iwp63HKBA$psMUXgYOIaAhaZ990uep8w'
>>> ph.verify(hash, "s3kr3tp4ssw0rd")
True
>>> ph.verify(hash, "t0t411ywr0ng")
Expand Down
4 changes: 3 additions & 1 deletion docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Unless you have any special needs, all you need to know is:
>>> ph = PasswordHasher()
>>> hash = ph.hash("s3kr3tp4ssw0rd")
>>> hash # doctest: +SKIP
'$argon2i$v=19$m=512,t=2,p=2$5VtWOO3cGWYQHEMaYGbsfQ$AcmqasQgW/wI6wAHAMk4aQ'
'$argon2id$v=19$m=512,t=2,p=2$Wd3CJItnL93pSLG/Mvel2g$DnsqcrS7+Enwu8EdJrUX2Q'
>>> ph.verify(hash, "s3kr3tp4ssw0rd")
True
>>> ph.verify(hash, "t0t411ywr0ng")
Expand Down Expand Up @@ -46,6 +46,8 @@ Exceptions

.. autoexception:: argon2.exceptions.HashingError

.. autoexception:: argon2.exceptions.InvalidHash


Low Level
---------
Expand Down
14 changes: 7 additions & 7 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,14 @@ You can use command line arguments to set hashing parameters:
.. code-block:: text
$ python -m argon2 -t 1 -m 512 -p 2
Running Argon2i 100 times with:
hash_len: 16
memory_cost: 512
parallelism: 2
time_cost: 1
Running Argon2id 100 times with:
hash_len: 16
memory_cost: 512
parallelism: 2
time_cost: 1
Measuring...
Measuring...
0.418ms per password verification
0.432ms per password verification
This should make it much easier to determine the right parameters for your use case and your environment.
31 changes: 21 additions & 10 deletions docs/parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,35 @@ Choosing Parameters
.. note::

You can probably just use :class:`argon2.PasswordHasher` with its default values and be fine.
Only tweak these if you've determined using :doc:`cli` that these defaults are too slow or too fast for your use case.
But it's good to double check using ``argon2_cffi``'s :doc:`cli` client, whether its defaults are too slow or too fast for your use case.

Finding the right parameters for a password hashing algorithm is a daunting task.
The authors of Argon2 specified a method in their `paper <https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf>`_ but it should be noted that they also mention that no value for ``time_cost`` or ``memory_cost`` is actually insecure (cf. section 6.4).
The authors of Argon2 specified a method in their `paper <https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf>`_, however some parts of it have been revised in the `RFC draft`_ for Argon2 that is currently being written.

#. Choose whether you want Argon2i or Argon2d (``type``).
If you don't know what that means, choose Argon2i (:attr:`argon2.Type.I`).
#. Figure out how many threads can be used on each call to Argon2 (``parallelism``).
The current recommended best practice is as follow:

#. Choose whether you want Argon2i, Argon2d, or Argon2id (``type``).
If you don't know what that means, choose Argon2id (:attr:`argon2.Type.ID`).
#. Figure out how many threads can be used on each call to Argon2 (``parallelism``, called "lanes" in the RFC).
They recommend twice as many as the number of cores dedicated to hashing passwords.
:class:`~argon2.PasswordHasher` will *not* determine this for you and use a default value that you can find in the linked API docs.
#. Figure out how much memory each call can afford (``memory_cost``).
#. Choose a salt length.
16 Bytes are fine.
#. Choose a hash length (``hash_len``).
16 Bytes are fine.
The RFC recommends 4 GB for backend authentication and 1 GB for frontend authentication.
#. Select the salt length.
16 bytes is sufficient for all applications, but can be reduced to 8 bytes in the case of space constraints.
#. Choose a hash length (``hash_len``, called "tag length" in the documentation).
16 bytes is sufficient for password verification.
#. Figure out how long each call can take.
One `recommendation <https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2015/march/enough-with-the-salts-updates-on-secure-password-schemes/>`_ for concurent user logins is to keep it under 0.5ms.
One `recommendation <https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2015/march/enough-with-the-salts-updates-on-secure-password-schemes/>`_ for concurent user logins is to keep it under 0.5 ms.
The RFC recommends under 500 ms.
The truth is somwhere between those two values: more is more secure, less is a better user experience.

Please note though, that even a verification time of 1 second won't protect you against bad passwords from the "top 10,000 passwords" lists that you can find online.
#. Measure the time for hashing using your chosen parameters.
Find a ``time_cost`` that is within your accounted time.
If ``time_cost=1`` takes too long, lower ``memory_cost``.

``argon2_cffi``'s :doc:`cli` will help you with this process.


.. _`RFC draft`: https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-4
2 changes: 1 addition & 1 deletion src/argon2/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def main(argv):
"hash_len": args.l,
}

print("Running Argon2i %d times with:" % (args.n,))
print("Running Argon2id %d times with:" % (args.n,))

for k, v in sorted(six.iteritems(params)):
print("%s: %d" % (k, v))
Expand Down
44 changes: 37 additions & 7 deletions src/argon2/_password_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os

from ._utils import _check_types
from .exceptions import InvalidHash
from .low_level import Type, hash_secret, verify_secret


Expand All @@ -26,10 +27,11 @@ class PasswordHasher(object):
r"""
High level class to hash passwords with sensible defaults.
Uses *always* Argon2\ **i** and a random salt_.
*Always* uses Argon2\ **id** and a random salt_ for hashing, but it can
verify any type of Argon2 as long as the hash is correctly encoded.
The reason for this being a class is both for convenience to carry
parameters and to verify the parameters only *once*. Any unnecessary
parameters and to verify the parameters only *once*. Any unnecessary
slowdown when hashing is a tangible advantage for a brute force attacker.
:param int time_cost: Defines the amount of computation realized and
Expand All @@ -45,9 +47,14 @@ class PasswordHasher(object):
encoded using this encoding.
.. versionadded:: 16.0.0
.. versionchanged:: 18.2.0
Switch from Argon2i to Argon2id based on the recommendation by the
current RFC_ draft.
.. versionchanged:: 18.2.0 ``verify`` now will determine the type of hash.
.. _salt: https://en.wikipedia.org/wiki/Salt_(cryptography)
.. _kibibytes: https://en.wikipedia.org/wiki/Binary_prefix#kibi
.. _RFC: https://tools.ietf.org/html/draft-irtf-cfrg-argon2-03#section-4
"""
__slots__ = [
"time_cost", "memory_cost", "parallelism", "hash_len", "salt_len",
Expand Down Expand Up @@ -98,22 +105,38 @@ def hash(self, password):
memory_cost=self.memory_cost,
parallelism=self.parallelism,
hash_len=self.hash_len,
type=Type.I,
type=Type.ID,
).decode("ascii")

_header_to_type = {
b"$argon2i$": Type.I,
b"$argon2d$": Type.D,
b"$argon2id": Type.ID,
}

def verify(self, hash, password):
"""
Verify that *password* matches *hash*.
:param unicode hash: An encoded hash as returned from
.. warning::
It is assumed that the caller is in full control of the hash. No
other parsing than the determination of the hash type is done by
``argon2_cffi``.
:param hash: An encoded hash as returned from
:meth:`PasswordHasher.hash`.
:type hash: ``bytes`` or ``unicode``
:param password: The password to verify.
:type password: ``bytes`` or ``unicode``
:raises argon2.exceptions.VerifyMismatchError: If verification fails
because *hash* is not valid for *secret* of *type*.
because *hash* is not valid for *password*.
:raises argon2.exceptions.VerificationError: If verification fails for
other reasons.
:raises argon2.exceptions.InvalidHash: If *hash* is so clearly
invalid, that it couldn't be passed to Argon2.
:return: ``True`` on success, raise
:exc:`~argon2.exceptions.VerificationError` otherwise.
Expand All @@ -122,9 +145,16 @@ def verify(self, hash, password):
.. versionchanged:: 16.1.0
Raise :exc:`~argon2.exceptions.VerifyMismatchError` on mismatches
instead of its more generic superclass.
.. versionadded:: 18.2.0 Hash type agility.
"""
hash = _ensure_bytes(hash, "ascii")
try:
hash_type = self._header_to_type[hash[:9]]
except (IndexError, KeyError, LookupError):
raise InvalidHash()

return verify_secret(
_ensure_bytes(hash, "ascii"),
hash,
_ensure_bytes(password, self.encoding),
Type.I,
hash_type
)
8 changes: 8 additions & 0 deletions src/argon2/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,11 @@ class HashingError(Argon2Error):
You can find the original error message from Argon2 in ``args[0]``.
"""


class InvalidHash(ValueError):
"""
Raised if the hash is invalid before passing it to Argon2.
.. versionadded:: 18.2.0
"""
8 changes: 6 additions & 2 deletions src/argon2/low_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
class Type(Enum):
"""
Enum of Argon2 variants.
Please see :doc:`parameters` on how to pick one.
"""
D = lib.Argon2_d
r"""
Expand All @@ -48,8 +50,7 @@ class Type(Enum):
"""
I = lib.Argon2_i
r"""
Argon2\ **i** uses data-independent memory access, which is preferred for
password hashing and password-based key derivation. Argon2i is slower as
Argon2\ **i** uses data-independent memory access. Argon2i is slower as
it makes more passes over the memory to protect from tradeoff attacks.
"""
ID = lib.Argon2_id
Expand All @@ -59,6 +60,9 @@ class Type(Enum):
Argon2i's resistance to side-channel cache timing attacks and much of
Argon2d's resistance to GPU cracking attacks.
That makes it the preferred type for password hashing and password-based
key derivation.
.. versionadded:: 16.3.0
"""

Expand Down
2 changes: 1 addition & 1 deletion tests/test_low_level.py
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ def test_old_hash(self):


@given(
password=st.binary(lib.ARGON2_MIN_PWD_LENGTH, 65),
password=st.binary(min_size=lib.ARGON2_MIN_PWD_LENGTH, max_size=65),
time_cost=st.integers(lib.ARGON2_MIN_TIME, 3),
parallelism=st.integers(lib.ARGON2_MIN_LANES, 5),
memory_cost=st.integers(0, 1025),
Expand Down
24 changes: 21 additions & 3 deletions tests/test_password_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from argon2 import PasswordHasher
from argon2._password_hasher import _ensure_bytes
from argon2.exceptions import InvalidHash


class TestEnsureBytes(object):
Expand Down Expand Up @@ -49,15 +50,16 @@ def test_hash(self, password):

h = ph.hash(password)

prefix = u"$argon2i$v=19$m=8,t=1,p=1$"
prefix = u"$argon2id$v=19$m=8,t=1,p=1$"

assert isinstance(h, six.text_type)
assert h[:len(prefix)] == prefix

@bytes_and_unicode_password
def test_verify(self, password):
def test_verify_agility(self, password):
"""
Verification works with unicode and bytes.
Verification works with unicode and bytes and variant is correctly
detected.
"""
ph = PasswordHasher(1, 8, 1, 16, 16, "latin1")
hash = ( # handrolled artisanal test vector
Expand All @@ -67,6 +69,15 @@ def test_verify(self, password):

assert ph.verify(hash, password)

@bytes_and_unicode_password
def test_hash_verify(self, password):
"""
Hashes are valid and can be verified.
"""
ph = PasswordHasher()

assert ph.verify(ph.hash(password), password) is True

def test_check(self):
"""
Raises a helpful TypeError on wrong arguments.
Expand All @@ -75,3 +86,10 @@ def test_check(self):
PasswordHasher("1")

assert "'time_cost' must be a int (got str)." == e.value.args[0]

def test_verify_invalid_hash(self):
"""
If the hash can't be parsed, InvalidHash is raised.
"""
with pytest.raises(InvalidHash):
PasswordHasher().verify("tiger", "does not matter")
1 change: 1 addition & 0 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ commands =
basepython = python3.6
extras = docs
commands =
python -m doctest README.rst
sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html
sphinx-build -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html

Expand Down

0 comments on commit 406833e

Please sign in to comment.