Skip to content

Commit

Permalink
Let user set hashing algo and iterations, fix #112
Browse files Browse the repository at this point in the history
Cleanup tests
  • Loading branch information
Federico Ceratto committed Feb 7, 2017
1 parent 1f98401 commit 665c993
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 250 deletions.
20 changes: 17 additions & 3 deletions cork/cork.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ class BaseCork(object):

def __init__(self, directory=None, backend=None, email_sender=None,
initialize=False, session_domain=None, smtp_server=None,
smtp_url='localhost', session_key_name=None):
smtp_url='localhost', session_key_name=None,
preferred_hashing_algorithm=None, pbkdf2_iterations=None):
"""Auth/Authorization/Accounting class
:param directory: configuration directory
Expand All @@ -87,14 +88,21 @@ def __init__(self, directory=None, backend=None, email_sender=None,
:param roles_fname: roles filename (without .json), defaults to 'roles'
:type roles_fname: str.
"""
if preferred_hashing_algorithm not in ("argon2", "PBKDF2", "scrypt"):
raise Exception("preferred_hashing_algorithm must be 'argon2', 'PBKDF2' or 'scrypt'")

if preferred_hashing_algorithm == "PBKDF2" and not pbkdf2_iterations:
raise Exception("scrypt_iterations must be set")

if smtp_server:
smtp_url = smtp_server
self.mailer = Mailer(email_sender, smtp_url)
self.password_reset_timeout = 3600 * 24
self.session_domain = session_domain
self.session_key_name = session_key_name or 'beaker.session'
self.preferred_hashing_algorithm = 'PBKDF2'
self.saltlength = { 'PBKDF2':32, 'scrypt':32, 'argon2':57 }
self.preferred_hashing_algorithm = preferred_hashing_algorithm
self.pbkdf2_iterations = pbkdf2_iterations

# Setup JsonBackend by default for backward compatibility.
if backend is None:
Expand Down Expand Up @@ -648,6 +656,12 @@ def _hash_scrypt(self, username, pwd, salt=None):

assert len(salt) == self.saltlength['scrypt'], "Incorrect salt length"

username = username.encode('utf-8')
assert isinstance(username, bytes)

pwd = pwd.encode('utf-8')
assert isinstance(pwd, bytes)

cleartext = "%s\0%s" % (username, pwd)
h = scrypt.hash(cleartext, salt)

Expand Down Expand Up @@ -675,7 +689,7 @@ def _hash_pbkdf2(self, username, pwd, salt=None):
assert isinstance(pwd, bytes)

cleartext = username + b'\0' + pwd
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, self.pbkdf2_iterations, dklen=32)

# 'p' for PBKDF2
hashed = b'p' + salt + h
Expand Down
21 changes: 0 additions & 21 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,6 @@ def mytmpdir(tmpdir):
tmpdir.join('password_reset_email.tpl').write("""Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}""")
return tmpdir

# used by test_scrypt.py
@pytest.fixture
def aaa(mytmpdir):
aaa = Cork(mytmpdir, smtp_server='localhost', email_sender='test@localhost')
return aaa




# mb = self.setup_test_db()
# self.aaa = MockedUnauthenticatedCork(backend=mb,
# smtp_server='localhost', email_sender='test@localhost')
# cookie_name = None
# if hasattr(self, 'purge_test_db'):
# self.purge_test_db()
#
# del(self.aaa)
# cookie_name = None



def assert_is_redirect(e, path):
"""Check if an HTTPResponse is a redirect.
Expand Down
76 changes: 9 additions & 67 deletions tests/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
# Released under LGPLv3+ license, see LICENSE.txt
#
# Unit testing
# Test scrypt-based password hashing
#

from base64 import b64encode
Expand Down Expand Up @@ -57,15 +58,16 @@ def json_db_dir(tmpdir, templates_dir):
@pytest.fixture
def aaa(json_db_dir):
"""Setup a MockedAdminCork instance"""
aaa = MockedAdminCork(json_db_dir.strpath, smtp_server='localhost', email_sender='test@localhost')
aaa = MockedAdminCork(json_db_dir.strpath, smtp_server='localhost', email_sender='test@localhost',
preferred_hashing_algorithm='scrypt')
aaa.mailer.use_threads = False
return aaa


@pytest.fixture
def aaa_unauth(json_db_dir):
"""Setup test directory and a MockedAdminCork instance"""
aaa = MockedUnauthenticatedCork(json_db_dir.strpath)
aaa = MockedUnauthenticatedCork(json_db_dir.strpath, preferred_hashing_algorithm='scrypt')
aaa.mailer.use_threads = False
return aaa

Expand Down Expand Up @@ -101,12 +103,12 @@ def wrapper(*a, **kw):
# Tests

def test_init(json_db_dir):
Cork(json_db_dir.strpath)
Cork(json_db_dir.strpath, preferred_hashing_algorithm='scrypt')


def test_initialize_storage(json_db_dir):
jb = JsonBackend(json_db_dir.strpath, initialize=True)
Cork(backend=jb)
Cork(backend=jb, preferred_hashing_algorithm='scrypt')
assert json_db_dir.join('users.json').read() == '{}'
assert json_db_dir.join('roles.json').read() == '{}'
assert json_db_dir.join('register.json').read() == '{}'
Expand All @@ -122,7 +124,7 @@ def test_initialize_storage(json_db_dir):
def test_unable_to_save(json_db_dir):
bogus_dir = '/___inexisting_directory___'
with pytest.raises(BackendIOException):
Cork(bogus_dir, initialize=True)
Cork(bogus_dir, initialize=True, preferred_hashing_algorithm='scrypt')


def test_loadjson_missing_file(aaa):
Expand All @@ -143,68 +145,8 @@ def test_loadjson_unchanged(aaa):
assert mtimes == aaa._store._mtimes


# Test PBKDF2-based password hashing

def test_password_hashing_PBKDF2(aaa):
shash = aaa._hash(u'user_foo', u'bogus_pwd')
assert isinstance(shash, bytes)
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa._verify_password('user_foo', 'bogus_pwd', shash) == True, \
"Hashing verification should succeed"


def test_hashlib_pbk():
# Hashlib works under py2 and py3 producing the same output.
# With iterations = 10 and dklen = 32 the output is also consistent with
# beaker under py2 as in the previous versions of Cork
import hashlib
cleartext = b'hello'
salt = b'hi'
h = hashlib.pbkdf2_hmac('sha1', cleartext, salt, 10, dklen=32)
assert b64encode(h) == b'QTH8vcCFLLqLhxCTnkz6sq+Un3B4RQgWjMPpRC9hfEY='

def test_password_hashing_PBKDF2_known_hash(aaa):
assert aaa.preferred_hashing_algorithm == 'PBKDF2'
salt = b's' * 32
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzax44AxQgK6uD9q1YWxLos1ispCe1Z7T7pOFK1PwdWEs='

def test_password_hashing_PBKDF2_known_hash_2(aaa):
assert aaa.preferred_hashing_algorithm == 'PBKDF2'
salt = b'\0' * 32
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8Uh4pyEOHoRz4j0lDzAmqb7Dvmo8GpeXwiKTDsuYFw='


def test_password_hashing_PBKDF2_known_hash_3(aaa):
assert aaa.preferred_hashing_algorithm == 'PBKDF2'
salt = b'x' * 32
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
assert shash == b'cHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4MEaIU5Op97lmvwX5NpVSTBP8jg8OlrN7c2K8K8tnNks='


def test_password_hashing_PBKDF2_incorrect_hash_len(aaa):
salt = b'x' * 31 # Incorrect length
with pytest.raises(AssertionError):
shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)


def test_password_hashing_PBKDF2_incorrect_hash_value(aaa):
shash = aaa._hash(u'user_foo', u'bogus_pwd')
assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash)
assert shash.endswith(b'='), "hash should end with '='"
assert aaa._verify_password(u'user_foo', u'####', shash) == False, \
"Hashing verification should fail"
assert aaa._verify_password(u'###', u'bogus_pwd', shash) == False, \
"Hashing verification should fail"


def test_password_hashing_PBKDF2_collision(aaa):
salt = b'S' * 32
hash1 = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt)
hash2 = aaa._hash(u'user_foobogus', u'_pwd', salt=salt)
assert hash1 != hash2, "Hash collision"




# Test password hashing for inexistent algorithms
Expand Down
60 changes: 11 additions & 49 deletions tests/test_argon2.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@

# Cork - Authentication module for the Bottle web framework
# Copyright (C) 2017 Federico Ceratto and others, see AUTHORS file.
# Released under LGPLv3+ license, see LICENSE.txt
Expand All @@ -7,58 +6,21 @@
# Test argon2-based password hashing
#

from pytest import raises
from pytest import fixture, raises
from time import time
import os
import shutil

from cork import Cork, JsonBackend, AuthException
import testutils

HASHLEN = 57

testdir = None # Test directory
aaa = None # global Cork instance
cookie_name = None # global variable to track cookie status

tmproot = testutils.pick_temp_directory()


def setup_dir():
"""Setup test directory with valid JSON files"""
global testdir
tstamp = "%f" % time()
testdir = "%s/fl_%s" % (tmproot, tstamp)
os.mkdir(testdir)
os.mkdir(testdir + '/views')
with open("%s/users.json" % testdir, 'w') as f:
f.write("""{"admin": {"email_addr": null, "desc": null, "role": "admin", "hash": "69f75f38ac3bfd6ac813794f3d8c47acc867adb10b806e8979316ddbf6113999b6052efe4ba95c0fa9f6a568bddf60e8e5572d9254dbf3d533085e9153265623", "creation_date": "2012-04-09 14:22:27.075596"}}""")
with open("%s/roles.json" % testdir, 'w') as f:
f.write("""{"special": 200, "admin": 100, "user": 50}""")
with open("%s/register.json" % testdir, 'w') as f:
f.write("""{}""")
with open("%s/views/registration_email.tpl" % testdir, 'w') as f:
f.write("""Username:{{username}} Email:{{email_addr}} Code:{{registration_code}}""")
with open("%s/views/password_reset_email.tpl" % testdir, 'w') as f:
f.write("""Username:{{username}} Email:{{email_addr}} Code:{{reset_code}}""")
print("setup done in %s" % testdir)

def setUp():
global aaa
setup_dir()
aaa = Cork(testdir, smtp_server='localhost', email_sender='test@localhost')

def teardown_dir():
global cookie_name
global testdir
if testdir:
shutil.rmtree(testdir)
testdir = None
cookie_name = None

def tearDown():
global aaa
aaa = None
teardown_dir()
@fixture
def aaa(mytmpdir):
aaa = Cork(mytmpdir, smtp_server='localhost', email_sender='test@localhost',
preferred_hashing_algorithm='argon2')
return aaa


def test_password_hashing_argon2(aaa):
Expand All @@ -69,19 +31,19 @@ def test_password_hashing_argon2(aaa):


def test_password_hashing_argon2_known_hash(aaa):
salt = b's' * 57
salt = b's' * HASHLEN
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='argon2')
assert shash == b'YXNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzcwb8JjdgqJy0tZD1EAUVV3p38dw1z3UMPRq6rjIZtnlNUDnJrHQfvhj080HpkfYvK06LqpAZU2GboPNwK4C6OORgYIWuF5nNlc31rcPmIezXU44QA3usHj49cJjrqDtEQPs2uqKELTHgzO2EPSnmwhDfAEpNflfIWzRRBhncSQRV'


def test_password_hashing_argon2_known_hash_2(aaa):
salt = b'\0' * 57
salt = b'\0' * HASHLEN
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='argon2')
assert shash == b'YQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIQBJq48WAKKXtJ8qIJBvbSMsD/CS7UvkC3nPOcme/f6XMmkHrx/4ExnJtrEHPfpy+wnAd+lofstp5cwsM1mSA+LCUWxkWMIgz7nPDZEGPguft2Tq2xgj2gSzAZPVVKw4Gzdl5hIieh5gJ2SkT4zi6bqIIrO4YVucZWYeFeaYqIN'


def test_password_hashing_argon2_known_hash_3(aaa):
salt = b'x' * 57
salt = b'x' * HASHLEN
shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='argon2')
assert shash == b'YXh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eKU485dAloUtOxe9zOuxY3g4G+U+Ci+9wGrjhsQPccIw2a+DmP0r+x9I7nQSDERmx+r2xIF3QjPlAjV/AOd/8SNjxK8WzjlOTM9aDbIzYMo6KW10pLswwU2heRCspOy+cEeOzEvzlw1VHZN/iK512mRqfHUHbo7tU1PPoQEsqVTv'

Expand All @@ -101,7 +63,7 @@ def test_password_hashing_argon2_incorrect_hash_value(aaa):
"Hashing verification should fail"

def test_password_hashing_argon2_collision(aaa):
salt = b'S' * 57
salt = b'S' * HASHLEN
hash1 = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='argon2')
hash2 = aaa._hash('user_foobogus', '_pwd', salt=salt, algo='argon2')
assert hash1 != hash2, "Hash collision"
Expand Down
Loading

0 comments on commit 665c993

Please sign in to comment.