Skip to content

Commit

Permalink
Implement argon2.profiles & adapt to RFC 9106
Browse files Browse the repository at this point in the history
Now that Argon2 is a proper RFC, we need to support it.

To stay more agile, this introduces the concept of profiles, based on the
existing `Parameters` class.

Fixes #101

Co-authored-by: Brendan McDonnell <brendanm149@gmail.com>
  • Loading branch information
hynek and B-McDonnell committed Dec 5, 2021
1 parent 54d3d96 commit a519ae4
Show file tree
Hide file tree
Showing 11 changed files with 227 additions and 53 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@ What explicitly *may* change over time are the default hashing parameters and th

We hope to provide wheels for Windows on `arm64` soon, but are waiting for GitHub Actions to support that.

- `argon2.Parameters.from_parameters()` together with the `argon2.profiles` module that offers easy access to the RFC-recommended configuration parameters and then some.
[#101](https://github.com/hynek/argon2-cffi/pull/101)
[#110](https://github.com/hynek/argon2-cffi/pull/110)

- The CLI interface now has a `--profile` option that takes any name from `argon2.profiles`.


## Changed

- `argon2.PasswordHasher` now uses the RFC 9106 low-memory profile by default.
The old defaults are available as `argon2.profiles.PRE_21_2`.


## [21.1.0](https://github.com/hynek/argon2-cffi/compare/20.1.0...21.1.0) - 2021-08-29

Expand Down
8 changes: 4 additions & 4 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,14 @@
>>> from argon2 import PasswordHasher
>>> ph = PasswordHasher()
>>> hash = ph.hash("s3kr3tp4ssw0rd")
>>> hash = ph.hash("correct horse battery staple")
>>> hash # doctest: +SKIP
'$argon2id$v=19$m=102400,t=2,p=8$tSm+JOWigOgPZx/g44K5fQ$WDyus6py50bVFIPkjA28lQ'
>>> ph.verify(hash, "s3kr3tp4ssw0rd")
'$argon2id$v=19$m=65536,t=3,p=4$MIIRqgvgQbgj220jfp0MPA$YfwJSVjtjSU0zzV/P3S9nnQ/USre2wvJMjfCIjrTQbg'
>>> ph.verify(hash, "correct horse battery staple")
True
>>> ph.check_needs_rehash(hash)
False
>>> ph.verify(hash, "t0t411ywr0ng")
>>> ph.verify(hash, "Tr0ub4dor&3")
Traceback (most recent call last):
...
argon2.exceptions.VerifyMismatchError: The password does not match the supplied hash
Expand Down
88 changes: 76 additions & 12 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ API Reference

.. module:: argon2

*argon2-cffi* comes with an high-level API and hopefully reasonable defaults for *Argon2* parameters that result in a verification time of 40--50ms on recent-ish hardware.
*argon2-cffi* comes with an high-level API and uses the officially recommended low-memory *Argon2* parameters that result in a verification time of 40--50ms on recent-ish hardware.

.. warning::

The current memory requirement is set to rather conservative 100 MB.
However, in memory constrained environments like Docker containers that can lead to problems.
The current memory requirement is set to rather conservative 64 MB.
However, in memory constrained environments such as *Docker* containers that can lead to problems.
One possible non-obvious symptom are apparent freezes that are caused by swapping.

Please check :doc:`parameters` for more details.
Expand All @@ -19,14 +19,14 @@ Unless you have any special needs, all you need to know is:

>>> from argon2 import PasswordHasher
>>> ph = PasswordHasher()
>>> hash = ph.hash("s3kr3tp4ssw0rd")
>>> hash = ph.hash("correct horse battery staple")
>>> hash # doctest: +SKIP
'$argon2id$v=19$m=102400,t=2,p=8$tSm+JOWigOgPZx/g44K5fQ$WDyus6py50bVFIPkjA28lQ'
>>> ph.verify(hash, "s3kr3tp4ssw0rd")
'$argon2id$v=19$m=65536,t=3,p=4$MIIRqgvgQbgj220jfp0MPA$YfwJSVjtjSU0zzV/P3S9nnQ/USre2wvJMjfCIjrTQbg'
>>> ph.verify(hash, "correct horse battery staple")
True
>>> ph.check_needs_rehash(hash)
False
>>> ph.verify(hash, "t0t411ywr0ng")
>>> ph.verify(hash, "Tr0ub4dor&3")
Traceback (most recent call last):
...
argon2.exceptions.VerifyMismatchError: The password does not match the supplied hash
Expand All @@ -42,7 +42,7 @@ A login function could thus look like this:
While the :class:`PasswordHasher` class has the aspiration to be good to use out of the box, it has all the parametrization you'll need:

.. autoclass:: PasswordHasher
:members: hash, verify, check_needs_rehash
:members: from_parameters, hash, verify, check_needs_rehash

If you don't specify any parameters, the following constants are used:

Expand All @@ -52,7 +52,72 @@ If you don't specify any parameters, the following constants are used:
.. data:: DEFAULT_MEMORY_COST
.. data:: DEFAULT_PARALLELISM

You can see their values in :class:`PasswordHasher`.
They are taken from :data:`argon2.profiles.RFC_9106_LOW_MEMORY`.


Profiles
--------

.. automodule:: argon2.profiles


You can try them out using the :doc:`cli` interface.
For example:

.. code-block:: console
$ python -m argon2 --profile RFC_9106_HIGH_MEMORY
Running Argon2id 100 times with:
hash_len: 32 bytes
memory_cost: 2097152 KiB
parallelism: 4 threads
time_cost: 1 iterations
Measuring...
866.5ms per password verification
That should give you a feeling on how they perform in *your* environment.

.. data:: RFC_9106_HIGH_MEMORY

Called "FIRST RECOMMENDED option" by `RFC 9106`_.

Requires beefy 2 GiB, so be careful in memory-contrained systems.

.. versionadded:: 21.2.0

.. data:: RFC_9106_LOW_MEMORY

Called "SECOND RECOMMENDED option" by `RFC 9106`_.

The main difference is that it only takes 64 MiB of RAM.

The values from this profile are the default parameters used by :class:`argon2.PasswordHasher`.

.. versionadded:: 21.2.0

.. data:: PRE_21_2

The default values that *argon2-cffi* used from 18.2.0 until 21.2.0.

Needs 100 MiB of RAM.

.. versionadded:: 21.2.0

.. data:: CHEAPEST

This is the cheapest-possible profile.

.. warning::

This is only for testing purposes!
Do **not** use in production!

.. versionadded:: 21.2.0


.. _`RFC 9106`: https://www.rfc-editor.org/rfc/rfc9106.html


Exceptions
Expand All @@ -70,10 +135,9 @@ Exceptions
Utilities
---------

.. autofunction:: argon2.extract_parameters

.. autofunction:: extract_parameters

.. autoclass:: Parameters
.. autoclass:: argon2.Parameters


Low Level
Expand Down
20 changes: 12 additions & 8 deletions docs/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,24 @@ CLI
===

To aid you with finding the parameters, *argon2-cffi* offers a CLI interface that can be accessed using ``python -m argon2``.
It will benchmark *Argon2*’s password *verification* in the current environment.
You can use command line arguments to set hashing parameters:
It will benchmark *Argon2*’s password *verification* in the current environment:

.. code-block:: text
.. code-block:: console
$ python -m argon2
Running Argon2id 100 times with:
hash_len: 16 bytes
memory_cost: 102400 KiB
parallelism: 8 threads
time_cost: 2 iterations
hash_len: 32 bytes
memory_cost: 65536 KiB
parallelism: 4 threads
time_cost: 3 iterations
Measuring...
45.3ms per password verification
45.7ms per password verification
You can use command line arguments to set hashing parameters.
Either by setting them one by one (``-t`` for time, ``-m`` for memory, ``-p`` for parallelism, ``-l`` for hash length), or by passing ``--profile`` followed by one of the names from :mod:`argon2.profiles`.
In that case, the other parameters are ignored.
If you don't pass any arguments as above, it runs with :class:`argon2.PasswordHasher`'s default values.

This should make it much easier to determine the right parameters for your use case and your environment.
2 changes: 1 addition & 1 deletion docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ Override Automatic SSE2 Detection
---------------------------------

Usually the build process tries to guess whether or not it should use SSE2_-optimized code.
This can go wrong and is problematic for cross-compiling.
Despite our best efforts, this can go wrong.

Therefore you can use the ``ARGON2_CFFI_USE_SSE2`` environment variable to control the process:

Expand Down
26 changes: 16 additions & 10 deletions docs/parameters.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,45 @@ Choosing Parameters
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>`_, however some parts of it have been revised in the `RFC draft`_ for *Argon2* that is currently being written.
As of September 2021, we have the official Internet standard `RFC 9106`_ to help use with it.

The current recommended best practice is as follow:
It comes with two recommendations in `section 4 <https://www.rfc-editor.org/rfc/rfc9106.html#section-4>`_, that (as of *argon2-cffi* 21.2.0) you can load directly from the :mod:`argon2.profiles` module: :data:`argon2.profiles.RFC_9106_HIGH_MEMORY` (called "FIRST RECOMMENDED") and :data:`argon2.profiles.RFC_9106_LOW_MEMORY` ("SECOND RECOMMENDED") into :meth:`argon2.PasswordHasher.from_parameters()`.

Please use the :doc:`cli` interface together with its `\-\-profile` argument to see if they work for you.

----

If you need finer tuning, 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.
They recommend 4 threads.
#. Figure out how much memory each call can afford (``memory_cost``).
The RFC recommends 4 GB for backend authentication and 1 GB for frontend authentication.
The APIs use Kibibytes_ (1024 bytes) as base unit.
#. 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://web.archive.org/web/20160304024620/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.
One `recommendation <https://web.archive.org/web/20160304024620/https://www.nccgroup.trust/us/about-us/newsroom-and-events/blog/2015/march/enough-with-the-salts-updates-on-secure-password-schemes/>`_ for concurrent user logins is to keep it under 0.5 ms.
The RFC used to recommend under 500 ms.
The truth is somewhere between those two values: more is more secure, less is a better user experience.
*argon2-cffi*'s defaults try to land somewhere in the middle and aim for ~50ms, but the actual time depends on your hardware.
*argon2-cffi*'s current defaults land with ~50ms somewhere in the middle, but the actual time depends on your hardware.

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.
Start with ``time_cost=1`` and measure the time it takes.
Raise ``time_cost`` until it 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.


.. note::

Alternatively, you can also refer to the `OWASP cheatsheet <https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id>`_.


.. _`RFC draft`: https://datatracker.ietf.org/doc/html/draft-irtf-cfrg-argon2-13#section-4
.. _`RFC 9106`: https://www.rfc-editor.org/rfc/rfc9106.html
.. _kibibytes: https://en.wikipedia.org/wiki/Kibibyte
3 changes: 2 additions & 1 deletion src/argon2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
The secure Argon2 password hashing algorithm.
"""

from . import exceptions, low_level
from . import exceptions, low_level, profiles
from ._legacy import hash_password, hash_password_raw, verify_password
from ._password_hasher import (
DEFAULT_HASH_LENGTH,
Expand Down Expand Up @@ -46,5 +46,6 @@
"hash_password",
"hash_password_raw",
"low_level",
"profiles",
"verify_password",
]
32 changes: 22 additions & 10 deletions src/argon2/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
DEFAULT_PARALLELISM,
DEFAULT_TIME_COST,
PasswordHasher,
profiles,
)


Expand All @@ -30,23 +31,34 @@ def main(argv):
parser.add_argument(
"-l", type=int, help="`hash_length`", default=DEFAULT_HASH_LENGTH
)
parser.add_argument(
"--profile",
type=str,
help="A profile from `argon2.profiles. Takes precendence.",
default=None,
)

args = parser.parse_args(argv[1:])

password = b"secret"
ph = PasswordHasher(
time_cost=args.t,
memory_cost=args.m,
parallelism=args.p,
hash_len=args.l,
)
if args.profile:
ph = PasswordHasher.from_parameters(
getattr(profiles, args.profile.upper())
)
else:
ph = PasswordHasher(
time_cost=args.t,
memory_cost=args.m,
parallelism=args.p,
hash_len=args.l,
)
hash = ph.hash(password)

params = {
"time_cost": (args.t, "iterations"),
"memory_cost": (args.m, "KiB"),
"parallelism": (args.p, "threads"),
"hash_len": (args.l, "bytes"),
"time_cost": (ph.time_cost, "iterations"),
"memory_cost": (ph.memory_cost, "KiB"),
"parallelism": (ph.parallelism, "threads"),
"hash_len": (ph.hash_len, "bytes"),
}

print("Running Argon2id %d times with:" % (args.n,))
Expand Down
26 changes: 21 additions & 5 deletions src/argon2/_password_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@
from ._utils import Parameters, _check_types, extract_parameters
from .exceptions import InvalidHash
from .low_level import Type, hash_secret, verify_secret
from .profiles import RFC_9106_LOW_MEMORY


DEFAULT_RANDOM_SALT_LENGTH = 16
DEFAULT_HASH_LENGTH = 16
DEFAULT_TIME_COST = 2
DEFAULT_MEMORY_COST = 102400
DEFAULT_PARALLELISM = 8
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


def _ensure_bytes(s, encoding):
Expand Down Expand Up @@ -57,6 +58,9 @@ class PasswordHasher:
Changed default *memory_cost* to 100 MiB and default *parallelism* to 8.
.. versionchanged:: 18.2.0 ``verify`` now will determine the type of hash.
.. versionchanged:: 18.3.0 The *Argon2* type is configurable now.
.. versionadded:: 21.2.0 :meth:`from_parameters`
.. versionchanged:: 21.2.0
Changed defaults to :data:`argon2.profiles.RFC_9106_LOW_MEMORY`.
.. _salt: https://en.wikipedia.org/wiki/Salt_(cryptography)
.. _kibibytes: https://en.wikipedia.org/wiki/Binary_prefix#kibi
Expand Down Expand Up @@ -97,6 +101,18 @@ def __init__(
)
self.encoding = encoding

@classmethod
def from_parameters(cls, params: Parameters) -> "PasswordHasher":
"""
Construct a `PasswordHasher` from *params*.
.. versionadded:: 21.2.0
"""
ph = cls()
ph._parameters = params

return ph

@property
def time_cost(self):
return self._parameters.time_cost
Expand Down

0 comments on commit a519ae4

Please sign in to comment.