diff --git a/cork/cork.py b/cork/cork.py index ca3bbdf..9869384 100644 --- a/cork/cork.py +++ b/cork/cork.py @@ -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 @@ -87,14 +88,29 @@ 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", "PBKDF2sha1", + "PBKDF2sha256", "scrypt"): + raise Exception("preferred_hashing_algorithm must be 'argon2'," + " 'PBKDF2sha1', 'PBKDF2sha256' or 'scrypt'") + elif preferred_hashing_algorithm.startswith("PBKDF2") \ + and not pbkdf2_iterations: + raise Exception("pbkdf2_iterations must be set") + elif preferred_hashing_algorithm == 'scrypt' and not scrypt_available: + raise Exception("scrypt.hash required." + " Please install the scrypt library.") + elif preferred_hashing_algorithm == 'argon2' and not argon2_available: + raise Exception("argon2 required." + " Please install the argon2 library.") + 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: @@ -621,18 +637,20 @@ def _hash(self, username, pwd, salt=None, algo=None): if algo is None: algo = self.preferred_hashing_algorithm - if algo == 'PBKDF2': - return self._hash_pbkdf2(self, username, pwd, salt=salt) + if algo == 'PBKDF2sha1': + return self._hash_pbkdf2_sha1(username, pwd, salt=salt) + + if algo == 'PBKDF2sha256': + return self._hash_pbkdf2_sha256(username, pwd, salt=salt) if algo == 'scrypt': - return self._hash_scrypt(self, username, pwd, salt=salt) + return self._hash_scrypt(username, pwd, salt=salt) if algo == 'argon2': - return self._hash_argon2(self, username, pwd, salt=salt) + return self._hash_argon2(username, pwd, salt=salt) raise RuntimeError("Unknown hashing algorithm requested: %s" % algo) - @staticmethod def _hash_scrypt(self, username, pwd, salt=None): """Hash username and password, generating salt value if required Use scrypt. @@ -648,6 +666,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) @@ -655,10 +679,9 @@ def _hash_scrypt(self, username, pwd, salt=None): hashed = b's' + salt + h return b64encode(hashed) - @staticmethod - def _hash_pbkdf2(self, username, pwd, salt=None): + def _hash_pbkdf2_sha1(self, username, pwd, salt=None): """Hash username and password, generating salt value if required - Use PBKDF2 from Beaker + Use PBKDF2 with sha1 :returns: base-64 encoded str. """ @@ -675,13 +698,39 @@ 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 + # 'p' for PBKDF2 with sha1 hashed = b'p' + salt + h return b64encode(hashed) - @staticmethod + def _hash_pbkdf2_sha256(self, username, pwd, salt=None): + """Hash username and password, generating salt value if required + Use PBKDF2 with sha256 + + :returns: base-64 encoded str. + """ + if salt is None: + salt = os.urandom(self.saltlength['PBKDF2']) + + assert isinstance(salt, bytes) + assert len(salt) == self.saltlength['PBKDF2'], "Incorrect salt length" + + username = username.encode('utf-8') + assert isinstance(username, bytes) + + pwd = pwd.encode('utf-8') + assert isinstance(pwd, bytes) + + cleartext = username + b'\0' + pwd + h = hashlib.pbkdf2_hmac('sha256', cleartext, salt, + self.pbkdf2_iterations, dklen=32) + + # 'k' for PBKDF2 with sha256 + hashed = b'k' + salt + h + return b64encode(hashed) + def _hash_argon2(self, username, pwd, salt=None): """Hash username and password, generating salt value if required Use argon2 @@ -720,22 +769,28 @@ def _verify_password(self, username, pwd, salted_hash): if isinstance(hash_type, int): hash_type = chr(hash_type) - if hash_type == 'p': # PBKDF2 + if hash_type == 'p': # PBKDF2 with sha1 + saltend = self.saltlength['PBKDF2']+1 + salt = decoded[1:saltend] + h = self._hash_pbkdf2_sha1(username, pwd, salt) + return salted_hash == h + + if hash_type == 'k': # PBKDF2 with sha256 saltend = self.saltlength['PBKDF2']+1 salt = decoded[1:saltend] - h = self._hash_pbkdf2(self, username, pwd, salt) + h = self._hash_pbkdf2_sha256(username, pwd, salt) return salted_hash == h if hash_type == 's': # scrypt saltend = self.saltlength['scrypt']+1 salt = decoded[1:saltend] - h = self._hash_scrypt(self, username, pwd, salt) + h = self._hash_scrypt(username, pwd, salt) return salted_hash == h if hash_type == 'a': # argon2 saltend = self.saltlength['argon2']+1 salt = decoded[1:saltend] - h = self._hash_argon2(self, username, pwd, salt) + h = self._hash_argon2(username, pwd, salt) return salted_hash == h raise RuntimeError("Unknown hashing algorithm in hash: %r" % decoded) diff --git a/tests/conftest.py b/tests/conftest.py index 3128924..768637b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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. diff --git a/tests/test.py b/tests/test.py index 25f2b7c..303a908 100644 --- a/tests/test.py +++ b/tests/test.py @@ -57,7 +57,8 @@ 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='PBKDF2sha1', pbkdf2_iterations=100000) aaa.mailer.use_threads = False return aaa @@ -65,7 +66,7 @@ def aaa(json_db_dir): @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='PBKDF2sha1', pbkdf2_iterations=100000) aaa.mailer.use_threads = False return aaa @@ -101,12 +102,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='PBKDF2sha1', pbkdf2_iterations=100000) def test_initialize_storage(json_db_dir): jb = JsonBackend(json_db_dir.strpath, initialize=True) - Cork(backend=jb) + Cork(backend=jb, preferred_hashing_algorithm='PBKDF2sha1', pbkdf2_iterations=100000) assert json_db_dir.join('users.json').read() == '{}' assert json_db_dir.join('roles.json').read() == '{}' assert json_db_dir.join('register.json').read() == '{}' @@ -122,7 +123,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='PBKDF2sha1', pbkdf2_iterations=100000) def test_loadjson_missing_file(aaa): @@ -143,68 +144,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 diff --git a/tests/test_argon2.py b/tests/test_argon2.py index fe0227a..788ce3d 100644 --- a/tests/test_argon2.py +++ b/tests/test_argon2.py @@ -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 @@ -7,7 +6,7 @@ # Test argon2-based password hashing # -from pytest import raises +from pytest import fixture, raises from time import time import os import shutil @@ -15,50 +14,13 @@ 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): @@ -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' @@ -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" diff --git a/tests/test_flask.py b/tests/test_flask.py index 3704d0e..45305c4 100644 --- a/tests/test_flask.py +++ b/tests/test_flask.py @@ -90,7 +90,8 @@ def setup_mockedadmin(): global aaa global cookie_name setup_dir() - aaa = MockedAdminCork(testdir, smtp_server='localhost', email_sender='test@localhost') + aaa = MockedAdminCork(testdir, smtp_server='localhost', email_sender='test@localhost', + preferred_hashing_algorithm='scrypt') cookie_name = None @@ -99,7 +100,7 @@ def setup_mocked_unauthenticated(): global aaa global cookie_name setup_dir() - aaa = MockedUnauthenticatedCork(testdir) + aaa = MockedUnauthenticatedCork(testdir, preferred_hashing_algorithm='scrypt') cookie_name = None @@ -114,13 +115,13 @@ def teardown_dir(): @with_setup(setup_dir, teardown_dir) def test_init(): - Cork(testdir) + Cork(testdir, preferred_hashing_algorithm='scrypt') @with_setup(setup_dir, teardown_dir) def test_initialize_storage(): jb = JsonBackend(testdir, initialize=True) - Cork(backend=jb) + Cork(backend=jb, preferred_hashing_algorithm='scrypt') with open("%s/users.json" % testdir) as f: assert f.readlines() == ['{}'] with open("%s/roles.json" % testdir) as f: @@ -139,7 +140,7 @@ def test_initialize_storage(): @with_setup(setup_dir, teardown_dir) def test_unable_to_save(): bogus_dir = '/___inexisting_directory___' - Cork(bogus_dir, initialize=True) + Cork(bogus_dir, initialize=True, preferred_hashing_algorithm='scrypt') @with_setup(setup_mockedadmin, teardown_dir) @@ -169,58 +170,6 @@ def test_loadjson_unchanged(): assert mtimes == aaa._store._mtimes -# Test PBKDF2-based password hashing - -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2(): - shash = aaa._hash('user_foo', 'bogus_pwd') - assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash) - assert shash.endswith('='), "hash should end with '='" - assert aaa._verify_password('user_foo', 'bogus_pwd', shash) == True, \ - "Hashing verification should succeed" - -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2_known_hash(): - salt = 's' * 32 - shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) - assert shash == 'cHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzax44AxQgK6uD9q1YWxLos1ispCe1Z7T7pOFK1PwdWEs=' - -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2_known_hash_2(): - salt = '\0' * 32 - shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) - assert shash == 'cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/8Uh4pyEOHoRz4j0lDzAmqb7Dvmo8GpeXwiKTDsuYFw=' - -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2_known_hash_3(): - salt = 'x' * 32 - shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) - assert shash == 'cHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4MEaIU5Op97lmvwX5NpVSTBP8jg8OlrN7c2K8K8tnNks=' - -@raises(AssertionError) -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2_incorrect_hash_len(): - salt = 'x' * 31 # Incorrect length - shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) - -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2_incorrect_hash_value(): - shash = aaa._hash('user_foo', 'bogus_pwd') - assert len(shash) == 88, "hash length should be 88 and is %d" % len(shash) - assert shash.endswith('='), "hash should end with '='" - assert aaa._verify_password('user_foo', '####', shash) == False, \ - "Hashing verification should fail" - assert aaa._verify_password('###', 'bogus_pwd', shash) == False, \ - "Hashing verification should fail" - -@with_setup(setup_mockedadmin, teardown_dir) -def test_password_hashing_PBKDF2_collision(): - salt = 'S' * 32 - hash1 = aaa._hash('user_foo', 'bogus_pwd', salt=salt) - hash2 = aaa._hash('user_foobogus', '_pwd', salt=salt) - assert hash1 != hash2, "Hash collision" - - # Test password hashing for inexistent algorithms @raises(RuntimeError) diff --git a/tests/test_functional.py b/tests/test_functional.py index 9594900..7dbaf70 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -340,6 +340,8 @@ def aaa_unauth(templates_dir, backend): backend=backend, smtp_server='localhost', email_sender='test@localhost', + preferred_hashing_algorithm='PBKDF2sha1', + pbkdf2_iterations=10, ) aaa._mocked_beaker_session = MockedSession() return aaa @@ -353,6 +355,8 @@ def aaa_admin(templates_dir, backend): backend=backend, email_sender='test@localhost', smtp_server='localhost', + preferred_hashing_algorithm='PBKDF2sha1', + pbkdf2_iterations=10, ) aaa._mocked_beaker_session = MockedSession(username='admin') return aaa diff --git a/tests/test_pbkdf2.py b/tests/test_pbkdf2.py new file mode 100644 index 0000000..05cc591 --- /dev/null +++ b/tests/test_pbkdf2.py @@ -0,0 +1,86 @@ +# 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 +# +# Unit testing +# Test PBKDF2-based password hashing +# + +from base64 import b64encode +from pytest import fixture, raises +from time import time +import os +import shutil + +from cork import Cork, JsonBackend, AuthException +import testutils + +HASHLEN = 32 + +@fixture +def aaa(mytmpdir): + aaa = Cork(mytmpdir, smtp_server='localhost', email_sender='test@localhost', + preferred_hashing_algorithm='PBKDF2sha1', pbkdf2_iterations=100000) + return aaa + + +def test_password_hashing_PBKDF2(aaa): + assert aaa.preferred_hashing_algorithm == 'PBKDF2sha1' + 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=HASHLEN) + assert b64encode(h) == b'QTH8vcCFLLqLhxCTnkz6sq+Un3B4RQgWjMPpRC9hfEY=' + +def test_password_hashing_PBKDF2_known_hash(aaa): + assert aaa.preferred_hashing_algorithm == 'PBKDF2sha1' + salt = b's' * HASHLEN + shash = aaa._hash(u'user_foo', u'bogus_pwd', salt=salt) + assert shash == b'cHNzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nz9XaEVyNjrXBEKjKHSVWKcjFEaNAAm66rvwCIjDZPru0=' + +def test_password_hashing_PBKDF2_known_hash_2(aaa): + assert aaa.preferred_hashing_algorithm == 'PBKDF2sha1' + salt = b'\0' * HASHLEN + shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) + assert shash == b'cAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAiIL4LIVJ9DCGDQAsAQG7JYvUjm68ZKQqV/TABeWrYzs=' + + +def test_password_hashing_PBKDF2_known_hash_3(aaa): + assert aaa.preferred_hashing_algorithm == 'PBKDF2sha1' + salt = b'x' * HASHLEN + shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) + assert shash == b'cHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4uaJQV+rua/emhov7hJ819Sdy6MNGiPfL+e6DbPAeQQU=' + + +def test_password_hashing_PBKDF2_incorrect_hash_len(aaa): + salt = b'x' * 31 # Incorrect length + with raises(AssertionError): + shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt) + + +def test_password_hashing_PBKDF2_incorrect_hash_value(aaa): + shash = aaa._hash('user_foo', '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('###', 'bogus_pwd', shash) == False, \ + "Hashing verification should fail" + + +def test_password_hashing_PBKDF2_collision(aaa): + salt = b'S' * HASHLEN + hash1 = aaa._hash('user_foo', u'bogus_pwd', salt=salt) + hash2 = aaa._hash('user_foobogus', u'_pwd', salt=salt) + assert hash1 != hash2, "Hash collision" diff --git a/tests/test_scrypt.py b/tests/test_scrypt.py index 54e866b..317ec7d 100644 --- a/tests/test_scrypt.py +++ b/tests/test_scrypt.py @@ -1,4 +1,3 @@ - # Cork - Authentication module for the Bottle web framework # Copyright (C) 2013 Federico Ceratto and others, see AUTHORS file. # Released under LGPLv3+ license, see LICENSE.txt @@ -7,7 +6,7 @@ # Test scrypt-based password hashing # -from pytest import raises +from pytest import fixture, raises from time import time import os import shutil @@ -15,50 +14,13 @@ from cork import Cork, JsonBackend, AuthException import testutils +HASHLEN = 32 -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='scrypt') + return aaa def test_password_hashing_scrypt(aaa): @@ -70,19 +32,19 @@ def test_password_hashing_scrypt(aaa): def test_password_hashing_scrypt_known_hash(aaa): - salt = b's' * 32 + salt = b's' * HASHLEN shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt') assert shash == b'c3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3Nzc3NzeLt/2Ta8vJOVqimNpN9G1WWxN1hxlUOJDPgH+0wqPpG20XQHFHLlksDIUo2BL4P8BMLBZj7F+cq6UP6pc304LQ==', repr(shash) def test_password_hashing_scrypt_known_hash_2(aaa): - salt = b'\0' * 32 + salt = b'\0' * HASHLEN shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt') assert shash == b'cwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAmu5jQskr2/yX13Yxmc4TYL0MIuSxwo41SVJwn/QueiDdLGkNaEsxlKL37i98YofXxs8xJJAJlC3Xj/9Nx0RNBw==' def test_password_hashing_scrypt_known_hash_3(aaa): - salt = b'x' * 32 + salt = b'x' * HASHLEN shash = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt') assert shash == b'c3h4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4eHh4yKuT1e8lovFZnaaOctivIvYBPkLoKDXX72kf5/nRuGIgyyhiKxxKE4LVYFKFCeVNPQM5m/+LulQkWhO0aB89lA==' @@ -105,7 +67,7 @@ def test_password_hashing_scrypt_incorrect_hash_value(aaa): def test_password_hashing_scrypt_collision(aaa): - salt = b'S' * 32 + salt = b'S' * HASHLEN hash1 = aaa._hash('user_foo', 'bogus_pwd', salt=salt, algo='scrypt') hash2 = aaa._hash('user_foobogus', '_pwd', salt=salt, algo='scrypt') assert hash1 != hash2, "Hash collision"