Skip to content

Commit

Permalink
Add high-level abstraction in the form of PasswordHasher
Browse files Browse the repository at this point in the history
  • Loading branch information
hynek committed Dec 22, 2015
1 parent df21a44 commit 90d244e
Show file tree
Hide file tree
Showing 15 changed files with 264 additions and 98 deletions.
3 changes: 2 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ Vendoring ``argon2`` @ 4fe0d8cda37691228dd5a96a310be57369403a4b_.
Changes:
^^^^^^^^

None
- Add ``argon2.PasswordHasher``.
A higher-level class specifically for hashing passwords that also works on unicode.


15.0.1 (2015-12-18)
Expand Down
24 changes: 1 addition & 23 deletions FAQ.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,6 @@ Frequently Asked Questions

I'm using ``bcrypt``/``scrypt``/``PBKDF2``, 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.
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.


Why does ``argon2_cffi`` work on bytes and not on Unicode strings?
Argon2 (as virtually any C library) works on bytes.
Trying to guess what encoding your password should be hashed as, is a road to security problems:
It's not unthinkable that someone hashes their password in Python using UTF-8 and checks it in C using latin-9.

The *encoded* hash *is* always ASCII bytes though.
So we *could* encode/decode it on demand.

We've decided against it for two reasons:

#. **Simplicity/symmetry**: In ``argon2_cffi`` *every* string is bytes.
No need to think about it.
#. **Performance**: The C functions of Argon2 *always* take bytes.
If the user *wants* to use bytes, accepting Unicode would lead to overhead: either to check types or they would have to do an superfluous encode/decode dance.

This is a problem, because when you're choosing your parameters, your hashing times shouldn't be significantly slower than those of your potential attackers.
And since ``argon2_cffi`` is mainly a low-level library, we want to give you the best performance possible.

All that said, it's possible that some kind of higher-level helper functions will make it into ``argon2_cffi`` eventually.
For now, we encourage you to write your own that perfectly fit your use-case though.
11 changes: 6 additions & 5 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ CFFI-based Argon2 Bindings for Python

.. code-block:: pycon
>>> import argon2
>>> hash = argon2.hash_password(b"secret")
>>> from argon2 import PasswordHasher
>>> ph = PasswordHasher()
>>> hash = ph.hash("secret")
>>> hash # doctest: +SKIP
b'$argon2i$m=512,t=2,p=2$c29tZXNhbHQ$2IdoNVglVTxb9w4YVJqW8w'
>>> argon2.verify_password(hash, b"secret")
'$argon2i$m=512,t=2,p=2$c29tZXNhbHQ$2IdoNVglVTxb9w4YVJqW8w'
>>> ph.verify(hash, "secret")
True
>>> argon2.verify_password(hash, b"wrong")
>>> ph.verify(hash, "wrong")
Traceback (most recent call last):
...
argon2.exceptions.VerificationError: Decoding failed
Expand Down
27 changes: 20 additions & 7 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,43 @@ API Reference

.. module:: argon2

``argon2_cffi`` comes with hopefully reasonable defaults for Argon2 parameters that result in a verification time of between 0.5ms and 1ms on recent-ish hardware.
``argon2_cffi`` comes with an high-level API and hopefully reasonable defaults for Argon2 parameters that result in a verification time of between 0.5ms and 1ms on recent-ish hardware.

So unless you have any special needs, all you need to know is:
Unless you have any special needs, all you need to know is:

.. doctest::

>>> import argon2
>>> hash = argon2.hash_password(b"s3kr3tp4ssw0rd")
>>> from argon2 import PasswordHasher
>>> ph = PasswordHasher()
>>> hash = ph.hash("s3kr3tp4ssw0rd")
>>> hash # doctest: +SKIP
b'$argon2i$m=512,t=2,p=2$0FFfEeL6JmUnpxwgwcSC8g$98BmZUa5A/3t5wb3ZxFLBg'
>>> argon2.verify_password(hash, b"s3kr3tp4ssw0rd")
>>> ph.verify(hash, "s3kr3tp4ssw0rd")
True
>>> argon2.verify_password(hash, b"t0t411ywr0ng")
>>> ph.verify(hash, b"t0t411ywr0ng")
Traceback (most recent call last):
...
argon2.exceptions.VerificationError: Decoding failed

But of course, ``argon2_cffi`` gives you more control should you need it:
But of course the :class:`PasswordHasher` class has all the parametrization you'll need:

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


.. autoexception:: argon2.exceptions.VerificationError


Low Level
---------

Use these functions if you want to build your own high-level abstraction.

.. autofunction:: hash_password

.. doctest::

>>> import argon2
>>> argon2.hash_password(
... b"secret", b"somesalt",
... time_cost=1, # number of iterations
Expand Down
14 changes: 7 additions & 7 deletions docs/argon2.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ Argon2

.. note::

**TL;DR**: Use Argon2\ **i** to securely hash your passwords.
**TL;DR**: Use :class:`argon2.PasswordHasher` with its default parameters to securely hash your passwords.

You do **not** need to read or understand anything below this box.

Expand All @@ -22,24 +22,24 @@ Argon2i
Argon2i is slower as it makes more passes over the memory to protect from tradeoff attacks.


Why “just use bcrypt” Is Not the Answer
---------------------------------------
Why “just use bcrypt” Is Not the Best Answer (Anymore)
------------------------------------------------------

There's an unfortunate meme to respond to questions of storage of secrets like passwords to “just use bcrypt_”.
The problem is, neither bcrypt nor its closest NIST-approved competitor PBKDF2_ are fit for hashing passwords in the days of ASIC_ password breakers.
In a nutshell, password crackers are able to create highly parallelized hardware specifically tailored to crack computationally expensive password hashes.
The current workhorses of password hashing are unquestionably bcrypt_ and PBKDF2_.
And while they're still fine to use, the password cracking community embraced new technologies like GPU_\ s and ASIC_\ s to crack password in a highly parallel fashion.

An effective measure against extreme parallelism proved making computation of password hashes also *memory* hard.
The best known implementation of that approach is to date scrypt_.
However according to the `Argon2 paper`_, page 2:

[…] the existence of a trivial time-memory tradeoff allows compact implementations with the same energy cost.


Therefore a new algorithm was needed.
This time future-proof and with committee-vetting instead of single implementors.

.. _bcrypt: https://en.wikipedia.org/wiki/Bcrypt
.. _PBKDF2: https://en.wikipedia.org/wiki/PBKDF2
.. _GPU: http://hashcat.net/oclhashcat/
.. _ASIC: https://en.wikipedia.org/wiki/Application-specific_integrated_circuit
.. _scrypt: https://en.wikipedia.org/wiki/Scrypt
.. _Argon2 paper: https://password-hashing.net/argon2-specs.pdf
Expand Down
2 changes: 1 addition & 1 deletion docs/backward-compatibility.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ If breaking changes are needed do be done, they are:
#. …the old behavior raises a :exc:`DeprecationWarning` for a year.
#. …are done with another announcement in the changelog_.

What explicitly *may* change over time are the default hashing parameters.
What explicitly *may* change over time are the default hashing parameters and the behavior of the :doc:`cli`.

.. _changelog: https://argon2-cffi.readthedocs.org/en/stable/changelog.html
5 changes: 5 additions & 0 deletions docs/parameters.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
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.

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).

Expand Down
2 changes: 2 additions & 0 deletions src/argon2/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
hash_password_raw,
verify_password,
)
from ._password_hasher import PasswordHasher


__version__ = "16.0.0.dev0"
Expand All @@ -35,6 +36,7 @@
"DEFAULT_PARALLELISM",
"DEFAULT_RANDOM_SALT_LENGTH",
"DEFAULT_TIME_COST",
"PasswordHasher",
"Type",
"exceptions",
"hash_password",
Expand Down
31 changes: 17 additions & 14 deletions src/argon2/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import six

from . import (
hash_password,
PasswordHasher,
DEFAULT_TIME_COST,
DEFAULT_MEMORY_COST,
DEFAULT_PARALLELISM,
Expand All @@ -22,9 +22,6 @@ def main(argv):
parser = argparse.ArgumentParser(description="Benchmark Argon2.")
parser.add_argument("-n", type=int, default=100,
help="Number of iterations to measure.")
parser.add_argument("-d", action="store_const",
const=Type.D, default=Type.I,
help="Use Argon2d instead of the default Argon2i.")
parser.add_argument("-t", type=int, help="`time_cost`",
default=DEFAULT_TIME_COST)
parser.add_argument("-m", type=int, help="`memory_cost`",
Expand All @@ -37,14 +34,13 @@ def main(argv):
args = parser.parse_args(argv[1:])

password = b"secret"
hash = hash_password(
password,
ph = PasswordHasher(
time_cost=args.t,
memory_cost=args.m,
parallelism=args.p,
type=args.d,
hash_len=args.l,
)
hash = ph.hash(password)

params = {
"time_cost": args.t,
Expand All @@ -53,20 +49,27 @@ def main(argv):
"hash_len": args.l,
}

print("Running Argon2{0} {1} times with:".format(
Type(args.d).name.lower(),
args.n,
))
print("Running Argon2i {0} times with:".format(args.n))

for k, v in sorted(six.iteritems(params)):
print("{0}: {1}".format(k, v))

print("\nMeasuring...")
duration = timeit.timeit(
"verify_password({hash!r}, {password!r}, {type})".format(
hash=hash, password=password, type=args.d,
"ph.verify({hash!r}, {password!r})".format(
hash=hash, password=password,
),
setup="from argon2 import verify_password, Type; gc.enable()",
setup="""\
from argon2 import PasswordHasher, Type
ph = PasswordHasher(
time_cost={time_cost!r},
memory_cost={memory_cost!r},
parallelism={parallelism!r},
hash_len={hash_len!r},
)
gc.enable()""".format(time_cost=args.t, memory_cost=args.m, parallelism=args.p,
hash_len=args.l),
number=args.n,
)
print("\n{0:.3}ms per password verification"
Expand Down
41 changes: 11 additions & 30 deletions src/argon2/_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import os

from ._util import (
check_types, error_to_str, ffi, lib, get_encoded_len, Type, NoneType,
_error_to_str, ffi, lib, _get_encoded_len, Type
)
from .exceptions import VerificationError, HashingError

Expand Down Expand Up @@ -44,11 +44,11 @@ def hash_password(password, salt=None,
:param bytes salt: A salt_. Should be random and different for each
password. Will generate a random salt for you if left ``None``
(recommended).
:param int time_cost: Defines the amount of computation realized and
therefore the execution time, given in number of iterations.
:param int memory_cost: Defines the memory usage, given in kibibytes_.
:param int parallelism: Defines the number of parallel threads (*changes*
the resulting hash value).
:param int time_cost: Amount of computation realized and therefore the
execution time, given in number of iterations.
:param int memory_cost: Memory usage, given in kibibytes_.
:param int parallelism: Number of parallel threads (*changes* the resulting
hash value).
:param int hash_len: Length of the hash in bytes.
:param Type type: Which Argon2 variant to use. In doubt use the default
:attr:`Type.I` which is better suited for passwords.
Expand Down Expand Up @@ -79,25 +79,13 @@ def hash_password_raw(password, salt=None,

def _hash(password, salt, time_cost, memory_cost, parallelism, hash_len, type,
encoded):
e = check_types(
password=(password, bytes),
salt=(salt, (bytes, NoneType)),
time_cost=(time_cost, int),
memory_cost=(memory_cost, int),
parallelism=(parallelism, int),
hash_len=(hash_len, int),
type=(type, Type),
encoded=(encoded, bool),
)
if e:
raise TypeError(e)
if salt is None:
salt = os.urandom(DEFAULT_RANDOM_SALT_LENGTH)

raw_buf = encoded_buf = ffi.NULL
raw_len = encoded_len = 0
if encoded:
encoded_len = get_encoded_len(hash_len, len(salt))
encoded_len = _get_encoded_len(hash_len, len(salt))
encoded_buf = ffi.new("char[]", encoded_len)
else:
raw_len = hash_len
Expand All @@ -112,7 +100,7 @@ def _hash(password, salt, time_cost, memory_cost, parallelism, hash_len, type,
type.value,
)
if rv != lib.ARGON2_OK:
raise HashingError(error_to_str(rv))
raise HashingError(_error_to_str(rv))

return (
ffi.string(encoded_buf) if encoded_len != 0
Expand All @@ -130,17 +118,10 @@ def verify_password(hash, password, type=Type.I):
in *hash*.
:param Type type: Type for *hash*.
:return: ``True`` on success, throw exception otherwise.
:return: ``True`` on success, raise
:exc:`~argon2.exceptions.VerificationError` otherwise.
:rtype: bool
"""
e = check_types(
password=(password, bytes),
hash=(hash, bytes),
type=(type, Type),
)
if e:
raise TypeError(e)

rv = lib.argon2_verify(
ffi.new("char[]", hash),
ffi.new("char[]", password),
Expand All @@ -150,4 +131,4 @@ def verify_password(hash, password, type=Type.I):
if rv == lib.ARGON2_OK:
return True
else:
raise VerificationError(error_to_str(rv))
raise VerificationError(_error_to_str(rv))

0 comments on commit 90d244e

Please sign in to comment.