Initial work

This commit is contained in:
Hynek Schlawack 2015-12-06 20:21:05 +01:00
commit 2132ab8c3d
20 changed files with 1114 additions and 0 deletions

12
.coveragerc Normal file
View File

@ -0,0 +1,12 @@
[run]
branch = True
source = argon2
[paths]
source =
src/argon2
.tox/*/lib/python*/site-packages/argon2
.tox/pypy/site-packages/argon2
[report]
show_missing = True

13
.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
dist
.tox
.eggs
.coverage
.coverage.*
.cache
src/argon2/_ffi.py
__pycache__
*.pyc
*.egg-info
*.dylib
*.so
.hypothesis

11
CHANGELOG.rst Normal file
View File

@ -0,0 +1,11 @@
Changelog
=========
Versions are year-based with a strict backward compatibility policy.
The third digit is only for regressions.
15.0.0 (UNRELEASED)
-------------------
Initial work.

25
CODE_OF_CONDUCT.rst Normal file
View File

@ -0,0 +1,25 @@
Contributor Code of Conduct
===========================
As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, religion, or nationality.
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery
* Personal attacks
* Trolling or insulting/derogatory comments
* Public or private harassment
* Publishing other's private information, such as physical or electronic addresses, without explicit permission
* Other unethical or unprofessional conduct.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct.
By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project.
Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team.
This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community.
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
This Code of Conduct is adapted from the `Contributor Covenant <http://contributor-covenant.org>`_, version 1.2.0, available at http://contributor-covenant.org/version/1/2/0/.

39
CONTRIBUTING.rst Normal file
View File

@ -0,0 +1,39 @@
How To Contribute
=================
Every open source project lives from the generous help by contributors that sacrifice their time and ``argon2_cffi`` is no different.
Here are a few guidelines to get you started:
- To run the test suite, all you need is a recent tox_.
It will ensure the test suite runs with all dependencies against all Python versions just as it will on `Travis CI`_.
If you lack some Python version, you can can always limit the environments like ``tox -e py27,py35`` (in that case you may want to look into pyenv_ that makes it very easy to install many different Python versions in parallel).
- Make sure your changes pass our CI.
You won't get any feedback until it's green unless you ask for it.
- If your change is noteworthy, add an entry to the changelog_.
- No contribution is too small; please submit as many fixes for typos and grammar bloopers as you can!
- Dont break `backward compatibility`_.
- *Always* add tests and docs for your code.
This is a hard rule; patches with missing tests or documentation wont be merged.
- Write `good test docstrings`_.
- Obey `PEP 8`_ and `PEP 257`_.
- If you address review feedback, make sure to bump the pull request.
Maintainers dont receive notifications if you push new commits.
Please note that this project is released with a Contributor `Code of Conduct`_.
By participating in this project you agree to abide by its terms.
Please report any harm to `Hynek Schlawack <me>`_ in any way you find appropriate.
Thank you for considering to contribute to ``argon2_cffi``!
.. _me: https://hynek.me/about/
.. _`PEP 8`: https://www.python.org/dev/peps/pep-0008/
.. _`PEP 257`: https://www.python.org/dev/peps/pep-0257/
.. _`good test docstrings`: https://jml.io/pages/test-docstrings.html
.. _`Code of Conduct`: https://github.com/hynek/argon2_cffi/blob/master/CODE_OF_CONDUCT.rst
.. _changelog: https://github.com/hynek/argon2_cffi/blob/master/CHANGELOG.rst
.. _`backward compatibility`: https://argon2_cffi.readthedocs.org/en/latest/backward-compatibility.html
.. _`tox`: https://testrun.org/tox/
.. _`Travis CI`: https://travis-ci.org/
.. _pyenv: https://github.com/yyuu/pyenv

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015 Hynek Schlawack
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include *.rst *.txt
include .coveragerc
include LICENSE
include tox.ini
recursive-include tests *.py
exclude src/argon2/_ffi.py

98
README.rst Normal file
View File

@ -0,0 +1,98 @@
=====================================
CFFI-based Argon2 Bindings for Python
=====================================
`Argon2 <https://github.com/p-h-c/phc-winner-argon2>`_ won the `Password Hashing Competition <https://password-hashing.net/>`_ in 2015.
``argon2_cffi`` is the simplest way to use it in Python and PyPy:
.. code-block:: pycon
>>> import argon2
>>> encoded_hash = argon2.hash_password(b"secret", b"somesalt")
>>> encoded_hash
b'$argon2i$m=4096,t=3,p=2$c29tZXNhbHQ$FNqxwHC2l1liWu3JTgGn6w'
>>> argon2.verify_password(encoded_hash, b"secret")
True
>>> argon2.verify_password(encoded_hash, b"wrong")
Traceback (most recent call last):
...
argon2.exceptions.VerificationError: Decoding failed
You can omit the ``salt`` argument for a secure random salt of length ``argon2.DEFAULT_RANDOM_SALT_LENGTH``:
.. code-block:: pycon
>>> argon2.hash_password(b"secret") # doctest: +SKIP
b'$argon2i$m=4096,t=3,p=2$GIESi4asMZaP051OPlH/zw$s5bQHIupLB1Fep/U5NXIVQ'
Hands-on
========
``argon2_cffi`` comes with hopefully reasonable defaults for Argon2 parameters.
But of course, you can set them yourself if you wish:
.. code-block:: pycon
>>> argon2.hash_password(
... b"secret", b"somesalt",
... time_cost=1, # number of iterations
... memory_cost=8, # used memory in KiB
... parallelism=8, # number of threads used
... hash_len=64, # length of resulting raw hash
... type=argon2.Type.D, # choose Argon2i or Argon2d
... )
b'$argon2d$m=8,t=1,p=8$c29tZXNhbHQ$RGhaBn2bZoWmKwBrWsLZT4Y950n1efoRde77Af3OKWUomJoFz7nEYVQbM6bAk/rqAi0hDP0y6XO5qJ0y8cqwUA'
The raw hash can also be computed.
The function takes the same parameters as ``hash_password()``:
.. code-block:: pycon
>>> argon2.hash_password_raw(b"secret", b"somesalt")
b'\x14\xda\xb1\xc0p\xb6\x97YbZ\xed\xc9N\x01\xa7\xeb'
Choosing Parameters
-------------------
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).
#. Choose whether you want Argon2i or Argon2d (``type``).
If you don't know what that means, choose Argon2i (``Type.I``).
#. Figure out how many threads can be used on each call to Argon2 (``parallelism``).
They recommend twice as many as the number of cores dedicated to hashing passwords.
#. 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.
#. 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.
#. 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``.
CLI
^^^
To aid you with finding the parameters, ``argon2_cffi`` can be run using ``python -m argon2`` which will benchmark Argon2 in the current environment..
You can use command line arguments to set hashing parameters:
.. code-block:: text
$ python -m argon2 -t 1 -m 800
Running Argon2i with:
hash_len: 16
memory_cost: 800
parallelism: 2
time_cost: 1
Measuring...
0.515ms per password
This should make it much easier to determine the right parameters for your use case and your environment.

17
setup.cfg Normal file
View File

@ -0,0 +1,17 @@
# Packaging
[wheel]
# we're pure-python
universal = 1
# Testing
[pytest]
minversion = 2.8
strict = true
addopts = -ra
testpaths = tests
[flake8]
exclude = src/argon2/_ffi.py

92
setup.py Normal file
View File

@ -0,0 +1,92 @@
from setuptools import setup, find_packages
import sys
import codecs
import os
import re
HERE = os.path.abspath(os.path.dirname(__file__))
###############################################################################
NAME = "argon2_cffi"
PACKAGES = find_packages(where="src")
CFFI_MODULES = [os.path.join(HERE, "src", "argon2", "_ffi_build.py:ffi")]
META_PATH = ("src", "argon2", "__init__.py")
KEYWORDS = ["password", "hash", "hashing", "security"]
CLASSIFIERS = [
"Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python :: 2",
"Programming Language :: Python :: 2.6",
"Programming Language :: Python :: 2.7",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.3",
"Programming Language :: Python :: 3.4",
"Programming Language :: Python :: 3.5",
"Programming Language :: Python",
"Topic :: Software Development :: Libraries :: Python Modules",
]
SETUP_REQUIRES = ["cffi"]
INSTALL_REQUIRES = ["six", "cffi>=1.0.0"]
EXTRAS_REQUIRES = {
':python_version<"3.4"': ["enum34"], # for wheels
}
if sys.version_info[0:2] < (3, 4): # for sdist
INSTALL_REQUIRES += ["enum34"]
###############################################################################
def read(*parts):
"""
Build an absolute path from *parts* and and return the contents of the
resulting file. Assume UTF-8 encoding.
"""
with codecs.open(os.path.join(HERE, *parts), "rb", "utf-8") as f:
return f.read()
META_FILE = read(*META_PATH)
def find_meta(meta):
"""
Extract __*meta*__ from META_FILE.
"""
meta_match = re.search(
r"^__{meta}__ = ['\"]([^'\"]*)['\"]".format(meta=meta),
META_FILE, re.M
)
if meta_match:
return meta_match.group(1)
raise RuntimeError("Unable to find __{meta}__ string.".format(meta=meta))
if __name__ == "__main__":
setup(
name=NAME,
description=find_meta("description"),
license=find_meta("license"),
url=find_meta("uri"),
version=find_meta("version"),
author=find_meta("author"),
author_email=find_meta("email"),
maintainer=find_meta("author"),
maintainer_email=find_meta("email"),
long_description=read("README.rst") + "\n\n" + read("CHANGELOG.rst"),
keywords=KEYWORDS,
packages=PACKAGES,
package_dir={"": "src"},
cffi_modules=CFFI_MODULES,
classifiers=CLASSIFIERS,
setup_requires=SETUP_REQUIRES,
install_requires=INSTALL_REQUIRES,
extras_requires=EXTRAS_REQUIRES,
zip_safe=False,
)

43
src/argon2/__init__.py Normal file
View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
from . import exceptions
from ._util import Type
from ._api import (
DEFAULT_HASH_LENGTH,
DEFAULT_MEMORY_COST,
DEFAULT_PARALLELISM,
DEFAULT_RANDOM_SALT_LENGTH,
DEFAULT_TIME_COST,
hash_password,
hash_password_raw,
verify_password,
)
__version__ = "15.0.0.dev0"
__title__ = "argon2_cffi"
__description__ = "argon2 password hashing algorithm."
__uri__ = ""
__author__ = "Hynek Schlawack"
__email__ = "hs@ox.cx"
__license__ = "MIT"
__copyright__ = "Copyright (c) 2015 {author}".format(author=__author__)
__all__ = [
"DEFAULT_HASH_LENGTH",
"DEFAULT_MEMORY_COST",
"DEFAULT_PARALLELISM",
"DEFAULT_RANDOM_SALT_LENGTH",
"DEFAULT_TIME_COST",
"Type",
"exceptions",
"hash_password",
"hash_password_raw",
"verify_password",
]

80
src/argon2/__main__.py Normal file
View File

@ -0,0 +1,80 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import argparse
import os
import sys
import timeit
import six
from . import (
hash_password,
DEFAULT_RANDOM_SALT_LENGTH,
DEFAULT_TIME_COST,
DEFAULT_MEMORY_COST,
DEFAULT_PARALLELISM,
DEFAULT_HASH_LENGTH,
Type,
)
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`",
default=DEFAULT_MEMORY_COST)
parser.add_argument("-p", type=int, help="`parallellism`",
default=DEFAULT_PARALLELISM)
parser.add_argument("-l", type=int, help="`hash_length`",
default=DEFAULT_HASH_LENGTH)
args = parser.parse_args(argv[1:])
password = b"secret"
salt = os.urandom(DEFAULT_RANDOM_SALT_LENGTH)
hash = hash_password(
password, salt,
time_cost=args.t,
memory_cost=args.m,
parallelism=args.p,
type=args.d,
hash_len=args.l,
)
params = {
"time_cost": args.t,
"memory_cost": args.m,
"parallelism": args.p,
"hash_len": args.l,
}
print("Running Argon2{0} {1} times with:".format(
Type(args.d).name.lower(),
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})".format(
hash=hash, password=password
),
setup="from argon2 import verify_password; gc.enable()",
number=args.n,
)
print("\n{0:.3}ms per password".format(duration / args.n * 1000))
if __name__ == "__main__":
main(sys.argv)

118
src/argon2/_api.py Normal file
View File

@ -0,0 +1,118 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import os
from ._util import (
check_types, error_to_str, guess_type, ffi, lib, get_encoded_len, Type,
NoneType,
)
from .exceptions import VerificationError, HashingError
__all__ = [
"DEFAULT_RANDOM_SALT_LENGTH",
"hash_password",
"hash_password_raw",
"verify_password",
]
DEFAULT_RANDOM_SALT_LENGTH = 16
DEFAULT_HASH_LENGTH = 16
DEFAULT_TIME_COST = 3
DEFAULT_MEMORY_COST = 2**12
DEFAULT_PARALLELISM = 2
def hash_password(password, salt=None,
time_cost=DEFAULT_TIME_COST,
memory_cost=DEFAULT_MEMORY_COST,
parallelism=DEFAULT_PARALLELISM,
hash_len=DEFAULT_HASH_LENGTH,
type=Type.I):
return _hash(password, salt, time_cost, memory_cost, parallelism, hash_len,
type, True)
def hash_password_raw(password, salt=None,
time_cost=DEFAULT_TIME_COST,
memory_cost=DEFAULT_MEMORY_COST,
parallelism=DEFAULT_PARALLELISM,
hash_len=DEFAULT_HASH_LENGTH,
type=Type.I):
return _hash(password, salt, time_cost, memory_cost, parallelism, hash_len,
type, False)
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_buf = ffi.new("char[]", encoded_len)
else:
raw_len = hash_len
raw_buf = ffi.new("char[]", raw_len)
rv = lib.argon2_hash(
time_cost, memory_cost, parallelism,
ffi.new("char[]", password), len(password),
ffi.new("char[]", salt), len(salt),
raw_buf, hash_len,
encoded_buf, encoded_len,
type.value,
)
if rv != lib.ARGON2_OK:
raise HashingError(error_to_str(rv))
return (
ffi.string(encoded_buf) if encoded_len != 0
else bytes(ffi.buffer(raw_buf))
)
def verify_password(hash, password, type=None):
"""
Verify whether *password* is correct for *hash* of *type*.
:return: ``True`` on success, throw exception otherwise.
:rtype: bool
"""
e = check_types(
password=(password, bytes),
hash=(hash, bytes),
type=(type, (Type, NoneType)),
)
if e:
raise TypeError(e)
if type is None:
type = guess_type(hash)
rv = lib.argon2_verify(
ffi.new("char[]", hash),
ffi.new("char[]", password),
len(password),
type.value,
)
if rv == lib.ARGON2_OK:
return True
else:
raise VerificationError(error_to_str(rv))

86
src/argon2/_ffi_build.py Normal file
View File

@ -0,0 +1,86 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
from cffi import FFI
ffi = FFI()
ffi.set_source(
"argon2._ffi", None,
libraries=["argon2"],
)
ffi.cdef("""\
typedef enum Argon2_type { Argon2_d = 0, Argon2_i = 1 } argon2_type;
int argon2_hash(const uint32_t t_cost, const uint32_t m_cost,
const uint32_t parallelism, const void *pwd,
const size_t pwdlen, const void *salt, const size_t saltlen,
void *hash, const size_t hashlen, char *encoded,
const size_t encodedlen, argon2_type type);
int argon2_verify(const char *encoded, const void *pwd, const size_t pwdlen,
argon2_type type);
const char *error_message(int error_code);
/* Error codes */
typedef enum Argon2_ErrorCodes {
ARGON2_OK = 0,
ARGON2_OUTPUT_PTR_NULL = 1,
ARGON2_OUTPUT_TOO_SHORT = 2,
ARGON2_OUTPUT_TOO_LONG = 3,
ARGON2_PWD_TOO_SHORT = 4,
ARGON2_PWD_TOO_LONG = 5,
ARGON2_SALT_TOO_SHORT = 6,
ARGON2_SALT_TOO_LONG = 7,
ARGON2_AD_TOO_SHORT = 8,
ARGON2_AD_TOO_LONG = 9,
ARGON2_SECRET_TOO_SHORT = 10,
ARGON2_SECRET_TOO_LONG = 11,
ARGON2_TIME_TOO_SMALL = 12,
ARGON2_TIME_TOO_LARGE = 13,
ARGON2_MEMORY_TOO_LITTLE = 14,
ARGON2_MEMORY_TOO_MUCH = 15,
ARGON2_LANES_TOO_FEW = 16,
ARGON2_LANES_TOO_MANY = 17,
ARGON2_PWD_PTR_MISMATCH = 18, /* NULL ptr with non-zero length */
ARGON2_SALT_PTR_MISMATCH = 19, /* NULL ptr with non-zero length */
ARGON2_SECRET_PTR_MISMATCH = 20, /* NULL ptr with non-zero length */
ARGON2_AD_PTR_MISMATCH = 21, /* NULL ptr with non-zero length */
ARGON2_MEMORY_ALLOCATION_ERROR = 22,
ARGON2_FREE_MEMORY_CBK_NULL = 23,
ARGON2_ALLOCATE_MEMORY_CBK_NULL = 24,
ARGON2_INCORRECT_PARAMETER = 25,
ARGON2_INCORRECT_TYPE = 26,
ARGON2_OUT_PTR_MISMATCH = 27,
ARGON2_THREADS_TOO_FEW = 28,
ARGON2_THREADS_TOO_MANY = 29,
ARGON2_MISSING_ARGS = 30,
ARGON2_ENCODING_FAIL = 31,
ARGON2_DECODING_FAIL = 32,
ARGON2_ERROR_CODES_LENGTH /* Do NOT remove; Do NOT add error codes after
this
error code */
} argon2_error_codes;
""")

103
src/argon2/_util.py Normal file
View File

@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import ctypes.util
from enum import Enum
from six import iteritems, PY3
from ._ffi import ffi
from .exceptions import InvalidHash
lib = ffi.dlopen(ctypes.util.find_library("argon2"))
class Type(Enum):
D = lib.Argon2_d
I = lib.Argon2_i
NoneType = type(None)
def check_types(**kw):
"""
Check each ``name: (value, types)`` in *kw*.
Returns a human-readable string of all violations or `None``.
"""
errors = []
for name, (value, types) in iteritems(kw):
if not isinstance(value, types):
if isinstance(types, tuple):
types = ", or ".join(t.__name__ for t in types)
else:
types = types.__name__
errors.append("'{name}' must be a {type} (got {actual})".format(
name=name,
type=types,
actual=type(value).__name__,
))
if errors != []:
return ", ".join(errors) + "."
def error_to_str(error):
"""
Convert an Argon2 error code into a native string.
"""
msg = ffi.string(lib.error_message(error))
if PY3:
msg = msg.decode("ascii")
return msg
def encoded_str_len(l):
"""
Compute how long a byte string of length *l* becomes if encoded to hex.
"""
return (l << 2) / 3 + 2
def get_encoded_len(hash_len, salt_len):
"""
Compute the size of the required buffer for an encoded hash with *hash_len*
and *salt_len*.
"""
# From https://github.com/P-H-C/phc-winner-argon2/blob/master/src/run.c:
#
# Sample encode: $argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9H
# kL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY
# Maximumum lengths are defined as:
# strlen $argon2i$ = 9
# m=65536 with strlen (uint32_t)-1 = 10, so this total is 12
# ,t=2,p=4 If we consider each number to potentially reach four digits
# in future, this = 14
# $c29tZXNhbHQAAAAAAAAAAA Formula for this is
# (SALT_LEN * 4 + 3) / 3 + 1 = 23
# $QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY per above formula, = 44
# + NULL byte
# 9 + 12 + 14 + 23 + 44 + 1 = 103
# Rounded to 4 byte boundary: 104
l = 36 + int(encoded_str_len(hash_len) + encoded_str_len(salt_len))
while l % 4: # round up to 4 byte boundary.
l += 1
return l
def guess_type(s):
"""
Guesses what type of encoded Argon2 hash *s* is or raises InvalidHash.
"""
prefix = s[:8]
if prefix == b"$argon2i":
return Type.I
elif prefix == b"$argon2d":
return Type.D
else:
raise InvalidHash(s)

30
src/argon2/exceptions.py Normal file
View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
class Argon2Error(Exception):
"""
Superclass of all argon2 exceptions.
Never thrown directly.
"""
class VerificationError(Argon2Error):
"""
Raised if verification failed.
"""
class InvalidHash(VerificationError):
"""
Raised if :func:`argon2.verify_password` should guess the type of a hash
but the hash is invalid.
"""
class HashingError(Argon2Error):
"""
Raised if hasing failed.
"""

0
tests/__init__.py Normal file
View File

211
tests/test_api.py Normal file
View File

@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import binascii
import pytest
from hypothesis import given
from hypothesis import strategies as st
from argon2 import (
hash_password, hash_password_raw, verify_password, Type,
DEFAULT_RANDOM_SALT_LENGTH,
)
from argon2.exceptions import VerificationError, HashingError
from argon2._util import encoded_str_len
# Example data obtained using the official Argon2 CLI client:
#
# $ echo -n "password" | ./argon2 somesalt -t 2 -m 16 -p 4
# Type: Argon2i
# Iterations: 2
# Memory: 65536 KiB
# Parallelism: 4
# Hash: 4162f32384d8f4790bd994cb73c83a4a29f076165ec18af3cfdcf10a8d1b9066
# Encoded: $argon2i$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$QWLzI4TY9HkL2ZTLc
# 8g6SinwdhZewYrzz9zxCo0bkGY
# 0.176 seconds
# Verification ok
#
# $ echo -n "password" | ./argon2 somesalt -t 2 -m 16 -p 4 -d
# Type: Argon2d
# Iterations: 2
# Memory: 65536 KiB
# Parallelism: 4
# Hash: 9ca3b9fc007d09daf489dcf854e9a785ff5a32c62ec50acf26477977add23225
# Encoded: $argon2d$m=65536,t=2,p=4$c29tZXNhbHQAAAAAAAAAAA$nKO5/AB9Cdr0idz4V
# Omnhf9aMsYuxQrPJkd5d63SMiU
# 0.189 seconds
# Verification ok
TEST_HASH_I = (
b"$argon2i$m=65536,t=2,p=4"
b"$c29tZXNhbHQAAAAAAAAAAA"
b"$QWLzI4TY9HkL2ZTLc8g6SinwdhZewYrzz9zxCo0bkGY"
)
TEST_HASH_D = (
b"$argon2d$m=65536,t=2,p=4"
b"$c29tZXNhbHQAAAAAAAAAAA$"
b"nKO5/AB9Cdr0idz4VOmnhf9aMsYuxQrPJkd5d63SMiU"
)
TEST_RAW_I = binascii.unhexlify(
b"4162f32384d8f4790bd994cb73c83a4a29f076165ec18af3cfdcf10a8d1b9066"
)
TEST_RAW_D = binascii.unhexlify( # N.B. includes NUL byte!
b"9ca3b9fc007d09daf489dcf854e9a785ff5a32c62ec50acf26477977add23225"
)
TEST_HASH_FAST = (
b"$argon2i$m=8,t=1,p=1$c29tZXNhbHQAAAAAAAAAAA$owd7NH5aC7mrx3sIc0zMF+R8RkPH"
b"S23ZuFM0IO3uck8"
) # same password/salt, but much cheaper.
TEST_PASSWORD = b"password"
TEST_SALT_LEN = 16
TEST_SALT = b"somesalt"
TEST_SALT += b"\x00" * (TEST_SALT_LEN - len(TEST_SALT))
TEST_TIME = 2
TEST_MEMORY = 65536
TEST_PARALLELISM = 4
TEST_HASH_LEN = 32
i_and_d_encoded = pytest.mark.parametrize("type,hash", [
(Type.I, TEST_HASH_I,),
(Type.D, TEST_HASH_D,),
])
i_and_d_raw = pytest.mark.parametrize("type,hash", [
(Type.I, TEST_RAW_I,),
(Type.D, TEST_RAW_D,),
])
class TestHash(object):
@i_and_d_encoded
def test_hash_password(self, type, hash):
"""
Creates the same encoded hash as the Argon2 CLI client.
"""
rv = hash_password(
TEST_PASSWORD,
TEST_SALT,
TEST_TIME,
TEST_MEMORY,
TEST_PARALLELISM,
TEST_HASH_LEN,
type,
)
assert hash == rv
assert isinstance(rv, bytes)
@i_and_d_raw
def test_hash_password_raw(self, type, hash):
"""
Creates the same raw hash as the Argon2 CLI client.
"""
rv = hash_password_raw(
TEST_PASSWORD,
TEST_SALT,
TEST_TIME,
TEST_MEMORY,
TEST_PARALLELISM,
TEST_HASH_LEN,
type,
)
assert hash == rv
assert isinstance(rv, bytes)
def test_hash_nul_bytes(self):
"""
Hashing passwords with NUL bytes works as expected.
"""
rv = hash_password_raw(b"abc\x00", TEST_SALT)
assert rv != hash_password_raw(b"abc", TEST_SALT)
def test_random_salt(self):
"""
Omitting a salt, creates a random one.
"""
rv = hash_password(b"secret")
salt = rv.split(b",")[-1].split(b"$")[1]
assert (
# -1 for not NUL byte
int(encoded_str_len(DEFAULT_RANDOM_SALT_LENGTH)) - 1 == len(salt)
)
def test_hash_wrong_arg_type(self):
"""
Passing an argument of wrong type raises TypeError.
"""
with pytest.raises(TypeError):
hash_password(u"oh no, unicode!")
def test_illegal_argon2_parameter(self):
"""
Raises HashingError if hashing fails.
"""
with pytest.raises(HashingError):
hash_password(TEST_PASSWORD, memory_cost=1)
@given(st.binary(max_size=128))
def test_hash_fast(self, password):
"""
Hash various passwords as cheaply as possible.
"""
hash_password(
password,
salt=b"12345678",
time_cost=1,
memory_cost=8,
parallelism=1,
hash_len=8,
)
class TestVerify(object):
@i_and_d_encoded
def test_auto_success(self, type, hash):
"""
Given a valid hash and password, we figure out the type ourself and
succeed.
"""
assert True is verify_password(hash, TEST_PASSWORD)
@i_and_d_encoded
def test_explicit_success(self, type, hash):
"""
Given a valid hash and password and correct type, we succeed.
"""
assert True is verify_password(hash, TEST_PASSWORD, type)
def test_explicit_fail(self):
"""
Given a valid hash and password and wrong type, we fail.
"""
with pytest.raises(VerificationError):
verify_password(TEST_HASH_I, TEST_PASSWORD, Type.D)
def test_wrong_arg_type(self):
"""
Passing an argument of wrong type raises TypeError.
"""
with pytest.raises(TypeError):
verify_password(TEST_HASH_I, TEST_PASSWORD.decode("ascii"))
def test_auto_fast(self):
"""
Just a test ensure that test_auto_fail works on correct data.
"""
assert verify_password(TEST_HASH_FAST, b"password")
@given(st.binary(max_size=128))
def test_auto_fail(self, wrong_password):
"""
Given a valid but wrong hash and password, we figure out the type
ourself and fail.
"""
with pytest.raises(VerificationError):
verify_password(TEST_HASH_FAST, wrong_password)

68
tests/test_util.py Normal file
View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
from six import PY3
import pytest
from hypothesis import given
from hypothesis import strategies as st
from argon2.exceptions import InvalidHash
from argon2._util import get_encoded_len, check_types, NoneType, guess_type
from .test_api import i_and_d_encoded
def test_get_encoded_len():
"""
Verify we get the same result as the official example.
"""
assert 104 == get_encoded_len(32, 16)
class TestCheckTypes(object):
def test_success(self):
"""
Returns None if all types are okay.
"""
assert None is check_types(
bytes=(b"bytes", bytes),
tuple=((1, 2), tuple),
str_or_None=(None, (str, NoneType)),
)
def test_fail(self):
"""
Returns summary of failures.
"""
rv = check_types(
bytes=(u"not bytes", bytes),
str_or_None=(42, (str, NoneType))
)
assert "." == rv[-1] # proper grammar FTW
assert "'str_or_None' must be a str, or NoneType (got int)" in rv
if PY3:
assert "'bytes' must be a bytes (got str)" in rv
else:
assert "'bytes' must be a str (got unicode)" in rv
class TestGuessType(object):
@i_and_d_encoded
def test_success(self, type, hash):
"""
Returns Type.I on likely Argon2i hashes.
"""
assert type is guess_type(hash)
@given(st.binary(max_size=128))
def test_fail(self, wrong_hash):
"""
If the hash can't be neither i nor d, raise InvalidHash.
"""
with pytest.raises(InvalidHash):
guess_type(wrong_hash)

41
tox.ini Normal file
View File

@ -0,0 +1,41 @@
[tox]
envlist = coverage-clean,py26,py27,py33,py34,py35,pypy,docs,flake8,manifest,coverage-report
[testenv]
passenv = TERM # ensure colors
deps =
coverage
hypothesis
hypothesis-pytest
pytest
commands =
coverage run --parallel -m pytest {posargs}
coverage run --parallel -m argon2 -n 1
[testenv:docs]
basepython = python3.5
deps =
commands = python -m doctest README.rst
[testenv:flake8]
basepython = python3.5
deps = flake8
commands = flake8 src tests setup.py
[testenv:manifest]
deps = check-manifest
commands = check-manifest
[testenv:coverage-clean]
deps = coverage
skip_install = true
commands = coverage erase
[testenv:coverage-report]
deps = coverage
skip_install = true
commands =
coverage combine
coverage report