From a519ae48b14b290759968bdde9a70e0f7fc4a15a Mon Sep 17 00:00:00 2001 From: Hynek Schlawack Date: Sun, 5 Dec 2021 09:03:14 +0100 Subject: [PATCH] Implement argon2.profiles & adapt to RFC 9106 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 --- CHANGELOG.md | 12 +++++ README.rst | 8 ++-- docs/api.rst | 88 +++++++++++++++++++++++++++++----- docs/cli.rst | 20 ++++---- docs/installation.rst | 2 +- docs/parameters.rst | 26 ++++++---- src/argon2/__init__.py | 3 +- src/argon2/__main__.py | 32 +++++++++---- src/argon2/_password_hasher.py | 26 ++++++++-- src/argon2/profiles.py | 58 ++++++++++++++++++++++ tox.ini | 5 +- 11 files changed, 227 insertions(+), 53 deletions(-) create mode 100644 src/argon2/profiles.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 99461ac..9f0842e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.rst b/README.rst index 1ae3b03..3031da6 100644 --- a/README.rst +++ b/README.rst @@ -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 diff --git a/docs/api.rst b/docs/api.rst index 38eed62..24a0a9a 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -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. @@ -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 @@ -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: @@ -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 @@ -70,10 +135,9 @@ Exceptions Utilities --------- +.. autofunction:: argon2.extract_parameters -.. autofunction:: extract_parameters - -.. autoclass:: Parameters +.. autoclass:: argon2.Parameters Low Level diff --git a/docs/cli.rst b/docs/cli.rst index adc7ed3..ddbd446 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -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. diff --git a/docs/installation.rst b/docs/installation.rst index 1448cfb..c425cfe 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -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: diff --git a/docs/parameters.rst b/docs/parameters.rst index 417b742..f967892 100644 --- a/docs/parameters.rst +++ b/docs/parameters.rst @@ -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 `_, 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 `_, 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 `_ for concurent user logins is to keep it under 0.5 ms. - The RFC recommends under 500 ms. + One `recommendation `_ 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 `_. -.. _`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 diff --git a/src/argon2/__init__.py b/src/argon2/__init__.py index 74d6d10..2236fe2 100644 --- a/src/argon2/__init__.py +++ b/src/argon2/__init__.py @@ -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, @@ -46,5 +46,6 @@ "hash_password", "hash_password_raw", "low_level", + "profiles", "verify_password", ] diff --git a/src/argon2/__main__.py b/src/argon2/__main__.py index ed59d53..217c154 100644 --- a/src/argon2/__main__.py +++ b/src/argon2/__main__.py @@ -10,6 +10,7 @@ DEFAULT_PARALLELISM, DEFAULT_TIME_COST, PasswordHasher, + profiles, ) @@ -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,)) diff --git a/src/argon2/_password_hasher.py b/src/argon2/_password_hasher.py index e1434d4..095453d 100644 --- a/src/argon2/_password_hasher.py +++ b/src/argon2/_password_hasher.py @@ -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): @@ -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 @@ -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 diff --git a/src/argon2/profiles.py b/src/argon2/profiles.py new file mode 100644 index 0000000..7f61032 --- /dev/null +++ b/src/argon2/profiles.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: MIT + +""" +This module offers access to standardized parameters that you can load using +:meth:`PasswordHasher.from_parameters()`. See the `source code +`_ for +concrete values and :doc:`parameters` for more information. + +.. versionadded:: 21.2.0 +""" + +from ._utils import Parameters +from .low_level import Type + + +# FIRST RECOMMENDED option per RFC 9106. +RFC_9106_HIGH_MEMORY = Parameters( + type=Type.ID, + version=19, + salt_len=16, + hash_len=32, + time_cost=1, + memory_cost=2097152, # 2 GiB + parallelism=4, +) + +# SECOND RECOMMENDED option per RFC 9106. +RFC_9106_LOW_MEMORY = Parameters( + type=Type.ID, + version=19, + salt_len=16, + hash_len=32, + time_cost=3, + memory_cost=65536, # 64 MiB + parallelism=4, +) + +# The pre-RFC defaults in argon2-cffi 18.2.0 - 21.1.0. +PRE_21_2 = Parameters( + type=Type.ID, + version=19, + salt_len=16, + hash_len=16, + time_cost=2, + memory_cost=102400, # 100 MiB + parallelism=8, +) + +# Only for testing! +CHEAPEST = Parameters( + type=Type.ID, + version=19, + salt_len=8, + hash_len=4, + time_cost=1, + memory_cost=8, + parallelism=1, +) diff --git a/tox.ini b/tox.ini index 8c117df..4936e2f 100644 --- a/tox.ini +++ b/tox.ini @@ -68,8 +68,9 @@ commands = python -m cogapp --check -P pyproject.toml description = Run tests and measure coverage. extras = tests commands = - coverage run --parallel -m pytest {posargs} - coverage run --parallel -m argon2 -n 1 -t 1 -m 8 -p 1 + coverage run -m pytest {posargs} + coverage run -m argon2 -n 1 -t 1 -m 8 -p 1 + coverage run -m argon2 --profile CHEAPEST [testenv:coverage-report]