From c8b79bf686113c3418d8d65ff0a1920da2b409bc Mon Sep 17 00:00:00 2001 From: Micah Anderson Date: Thu, 14 Nov 2013 16:35:32 -0500 Subject: initial commit --- tests/__init__.py | 0 tests/ciphertexts.csv | 6 + tests/hashvectors.csv | 5 + tests/test_scrypt.py | 285 ++++++++++++++++++++++++++++++++++++++++++++++ tests/test_scrypt_py2x.py | 51 +++++++++ tests/test_scrypt_py3x.py | 56 +++++++++ 6 files changed, 403 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/ciphertexts.csv create mode 100644 tests/hashvectors.csv create mode 100644 tests/test_scrypt.py create mode 100644 tests/test_scrypt_py2x.py create mode 100644 tests/test_scrypt_py3x.py (limited to 'tests') diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/ciphertexts.csv b/tests/ciphertexts.csv new file mode 100644 index 0000000..7594dd4 --- /dev/null +++ b/tests/ciphertexts.csv @@ -0,0 +1,6 @@ +input,password,maxtime,maxmem,maxmemfrac,ciphertext +message,password,0.01,0,0.0625,736372797074000a00000008000000019f6d3fe5e9423a12d330e35089befdfbb476c7d4faea91492a2561f942c1599701aba424220218b9f81812df06d7cf2a281fd0fdbc7c9d978c335bf5209b1062ee2e49993c4d3a37d347ad6bf0eaecc2fe579a6f320b0acf475882c222c0ba34a7ac5379bedc82358bb3f736ea31d4b824e8bc95c75579 +message,password,5.0000,0.0000,0.1250,736372797074000a00000008000000cb974d55992fea307caa3593205c8851cd56b3ddfd241ee7b1c075cfd2e2f871dddcee71b6bc2b6b075caa1699ea58ce32e9fcd802b18069828201692380574e23e84c2d39d6a951c9c2401dea1a0fa44195b01fca9332f7aac223c84251c69a28037788e09cf297a003a62e2e7c5b6f039e3c1d21fa5da6 +message,password,0.5000,0.0000,0.0625,736372797074000a00000008000000141429bb22aef840e1dde29564dada8f2a77fc2855fe8c9e27e5bf5df4a0eec330344e8471bf83e2466a28acc9d1813a3e50e64697fb8e9c7c17954950f16a5b78fe114d8b147c5936802fe52b17f83e6da9de884257f2a938aa37c2e92b1c33cd9afd0f5e91baba29be4b1c0709bd2a31a4c52394f9b0d4 +message,password,0.5000,0.0000,0.1250,736372797074000a0000000800000014e9baa49b57b2e5b561df54121d870532714f063f295c15ca48e32b677e8e11126ab8da24683951749a19eede36ea3768cb01e8eda6c0f570db7051e07e99d2d687ce28f96aa2e6afa8a0a8c5d902c847e041e14435a22d873c4e676f7a7f5ab9107171b583d02724c056707640931a3c160b9e0ef87bae +message,password,0.5000,10485760.0000,0.1250,736372797074000a0000000800000014e53c55cd89b6e2de639c1621d92e79212e5f4fc793387435486427eb8adeb192382b5b6397f98ac66fa528a13cba01bd611082369fe1e2244a01ba8329ff6fcaa2db5e87fb75683ae6fc636d873c2e004a0209b947c7b8e805174d65ec0ac2cb597baadae6cf092c33a5096590860b51570faa89e39bf0 \ No newline at end of file diff --git a/tests/hashvectors.csv b/tests/hashvectors.csv new file mode 100644 index 0000000..bcf6b40 --- /dev/null +++ b/tests/hashvectors.csv @@ -0,0 +1,5 @@ +password,salt,n,r,p,hexhash +,,16,1,1,77d6576238657b203b19ca42c18a0497f16b4844e3074ae8dfdffa3fede21442fcd0069ded0948f8326a753a0fc81f17e8d3e0fb2e0d3628cf35e20c38d18906 +password,NaCl,1024,8,16,fdbabe1c9d3472007856e7190d01e9fe7c6ad7cbc8237830e77376634b3731622eaf30d92e22a3886ff109279d9830dac727afb94a83ee6d8360cbdfa2cc0640 +pleaseletmein,SodiumChloride,16384,8,1,7023bdcb3afd7348461c06cd81fd38ebfda8fbba904f8e3ea9b543f6545da1f2d5432955613f0fcf62d49705242a9af9e61e85dc0d651e40dfcf017b45575887 +pleaseletmein,SodiumChloride,32768,16,4,cbc397a9b5f5a53048c5b9f039ee1246d9532c8089fb346a4ab47cd0701febf18652b1ee042e070d1b6c631c43fd05ececd5b165ee1c2ffc1a2e98406fc2cd52 \ No newline at end of file diff --git a/tests/test_scrypt.py b/tests/test_scrypt.py new file mode 100644 index 0000000..4c5a207 --- /dev/null +++ b/tests/test_scrypt.py @@ -0,0 +1,285 @@ +# -*- coding: utf-8 -*- + +from os import urandom +from os.path import dirname, abspath, sep +from sys import version_info +from csv import reader +from binascii import a2b_hex, b2a_hex +import base64 +import json + +if ((version_info > (3, 2, 0, 'final', 0)) or + (version_info > (2, 7, 0, 'final', 0) and version_info < (3, 0, 0, 'final', 0))): + import unittest as testm +else: + try: + import unittest2 as testm + except ImportError: + print("Please install unittest2 to run the test suite") + exit(-1) + +import scrypt + + +class TestScrypt(testm.TestCase): + + def setUp(self): + self.input = "message" + self.password = "password" + self.longinput = str(urandom(100000)) + self.five_minutes = 300.0 + self.five_seconds = 5.0 + self.one_byte = 1 # in Bytes + self.one_megabyte = 1024 * 1024 # in Bytes + self.ten_megabytes = 10 * self.one_megabyte + base_dir = dirname(abspath(__file__)) + sep + cvf = open(base_dir + "ciphertexts.csv", "r") + ciphertxt_reader = reader(cvf, dialect="excel") + self.ciphertexts = [] + for row in ciphertxt_reader: + self.ciphertexts.append(row) + cvf.close() + self.ciphertext = a2b_hex(bytes(self.ciphertexts[1][5].encode('ascii'))) + + def test_encrypt_decrypt(self): + """Test encrypt for simple encryption and decryption""" + s = scrypt.encrypt(self.input, self.password, 0.1) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt(self): + """Test encrypt takes input and password strings as + positional arguments and produces ciphertext""" + s = scrypt.encrypt(self.input, self.password) + self.assertEqual(len(s), 128 + len(self.input)) + + def test_encrypt_input_and_password_as_keywords(self): + """Test encrypt for input and password accepted as keywords""" + s = scrypt.encrypt(password=self.password, input=self.input) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_missing_input_keyword_argument(self): + """Test encrypt raises TypeError if keyword argument missing input""" + self.assertRaises(TypeError, lambda: scrypt.encrypt(password=self.password)) + + def test_encrypt_missing_password_positional_argument(self): + """Test encrypt raises TypeError if second positional argument missing + (password)""" + self.assertRaises(TypeError, lambda: scrypt.encrypt(self.input)) + + def test_encrypt_missing_both_required_positional_arguments(self): + """Test encrypt raises TypeError if both positional arguments missing + (input and password)""" + self.assertRaises(TypeError, lambda: scrypt.encrypt()) + + def test_encrypt_maxtime_positional(self): + """Test encrypt maxtime accepts maxtime at position 3""" + s = scrypt.encrypt(self.input, self.password, 0.01) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxtime_key(self): + """Test encrypt maxtime accepts maxtime as keyword argument""" + s = scrypt.encrypt(self.input, self.password, maxtime=0.01) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxmem_positional(self): + """Test encrypt maxmem accepts 4th positional argument and exactly + (1 megabyte) of storage to use for V array""" + s = scrypt.encrypt(self.input, self.password, 0.01, self.one_megabyte) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxmem_undersized(self): + """Test encrypt maxmem accepts (< 1 megabyte) of storage to use for V array""" + s = scrypt.encrypt(self.input, self.password, 0.01, self.one_byte) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxmem_in_normal_range(self): + """Test encrypt maxmem accepts (> 1 megabyte) of storage to use for V array""" + s = scrypt.encrypt(self.input, + self.password, + 0.01, + self.ten_megabytes) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxmem_keyword_argument(self): + """Test encrypt maxmem accepts exactly (1 megabyte) of storage to use for + V array""" + s = scrypt.encrypt(self.input, + self.password, + maxmem=self.one_megabyte, + maxtime=0.01) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxmemfrac_positional(self): + """Test encrypt maxmemfrac accepts 5th positional argument of 1/16 total + memory for V array""" + s = scrypt.encrypt(self.input, self.password, 0.01, 0, 0.0625) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_maxmemfrac_keyword_argument(self): + """Test encrypt maxmemfrac accepts keyword argument of 1/16 total memory for + V array""" + s = scrypt.encrypt(self.input, self.password, maxmemfrac=0.0625, + maxtime=0.01) + m = scrypt.decrypt(s, self.password) + self.assertEqual(m, self.input) + + def test_encrypt_long_input(self): + """Test encrypt accepts long input for encryption""" + s = scrypt.encrypt(self.longinput, self.password, 0.1) + self.assertEqual(len(s), 128 + len(self.longinput)) + + def test_encrypt_raises_error_on_invalid_keyword(self): + """Test encrypt raises TypeError if invalid keyword used in argument""" + self.assertRaises(TypeError, lambda: scrypt.encrypt(self.input, + self.password, nonsense="Raise error")) + + def test_decrypt_from_csv_ciphertexts(self): + """Test decrypt function with precalculated combinations""" + for row in self.ciphertexts[1:]: + h = scrypt.decrypt(a2b_hex(bytes(row[5].encode('ascii'))), row[1]) + self.assertEqual(bytes(h.encode("ascii")), row[0].encode("ascii")) + + def test_decrypt_maxtime_positional(self): + """Test decrypt function accepts third positional argument""" + m = scrypt.decrypt(self.ciphertext, self.password, self.five_seconds) + self.assertEqual(m, self.input) + + def test_decrypt_maxtime_keyword_argument(self): + """Test decrypt function accepts maxtime keyword argument""" + m = scrypt.decrypt(maxtime=1.0, input=self.ciphertext, password=self.password) + self.assertEqual(m, self.input) + + def test_decrypt_maxmem_positional(self): + """Test decrypt function accepts fourth positional argument""" + m = scrypt.decrypt(self.ciphertext, self.password, self.five_minutes, self.ten_megabytes) + self.assertEqual(m, self.input) + + def test_decrypt_maxmem_keyword_argument(self): + """Test decrypt function accepts maxmem keyword argument""" + m = scrypt.decrypt(maxmem=self.ten_megabytes, input=self.ciphertext, password=self.password) + self.assertEqual(m, self.input) + + def test_decrypt_maxmemfrac_positional(self): + """Test decrypt function accepts maxmem keyword argument""" + m = scrypt.decrypt(self.ciphertext, self.password, self.five_minutes, self.one_megabyte, 0.0625) + self.assertEqual(m, self.input) + + def test_decrypt_maxmemfrac_keyword_argument(self): + """Test decrypt function accepts maxmem keyword argument""" + m = scrypt.decrypt(maxmemfrac=0.625, input=self.ciphertext, password=self.password) + self.assertEqual(m, self.input) + + def test_decrypt_raises_error_on_too_little_time(self): + """Test decrypt function raises scrypt.error raised if insufficient time allowed for + ciphertext decryption""" + s = scrypt.encrypt(self.input, self.password, 0.1) + self.assertRaises(scrypt.error, + lambda: scrypt.decrypt(s, self.password, .01)) + + +class TestScryptHash(testm.TestCase): + + def setUp(self): + self.input = "message" + self.password = "password" + self.salt = "NaCl" + self.hashes = [] + base_dir = dirname(abspath(__file__)) + sep + hvf = open(base_dir + "hashvectors.csv", "r") + hash_reader = reader(hvf, dialect="excel") + for row in hash_reader: + self.hashes.append(row) + hvf.close() + + def test_hash_vectors_from_csv(self): + """Test hash function with precalculated combinations""" + for row in self.hashes[1:]: + h = scrypt.hash(row[0], row[1], int(row[2]), int(row[3]), int(row[4])) + hhex = b2a_hex(h) + self.assertEqual(hhex, bytes(row[5].encode("utf-8"))) + + def test_hash_buflen_keyword(self): + """Test hash takes keyword valid buflen""" + h64 = scrypt.hash(self.input, self.salt, buflen=64) + h128 = scrypt.hash(self.input, self.salt, buflen=128) + self.assertEqual(len(h64), 64) + self.assertEqual(len(h128), 128) + + def test_hash_n_positional(self): + """Test hash accepts valid N in position 3""" + h = scrypt.hash(self.input, self.salt, 256) + self.assertEqual(len(h), 64) + + def test_hash_n_keyword(self): + """Test hash takes keyword valid N""" + h = scrypt.hash(N=256, password=self.input, salt=self.salt) + self.assertEqual(len(h), 64) + + def test_hash_r_positional(self): + """Test hash accepts valid r in position 4""" + h = scrypt.hash(self.input, self.salt, 256, 16) + self.assertEqual(len(h), 64) + + def test_hash_r_keyword(self): + """Test hash takes keyword valid r""" + h = scrypt.hash(r=16, password=self.input, salt=self.salt) + self.assertEqual(len(h), 64) + + def test_hash_p_positional(self): + """Test hash accepts valid p in position 5""" + h = scrypt.hash(self.input, self.salt, 256, 8, 2) + self.assertEqual(len(h), 64) + + def test_hash_p_keyword(self): + """Test hash takes keyword valid p""" + h = scrypt.hash(p=4, password=self.input, salt=self.salt) + self.assertEqual(len(h), 64) + + def test_hash_raises_error_on_p_equals_zero(self): + """Test hash raises scrypt error on illegal parameter value (p = 0)""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, p=0)) + + def test_hash_raises_error_on_negative_p(self): + """Test hash raises scrypt error on illegal parameter value (p < 0)""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, p=-1)) + + def test_hash_raises_error_on_r_equals_zero(self): + """Test hash raises scrypt error on illegal parameter value (r = 0)""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, r=0)) + + def test_hash_raises_error_on_negative_r(self): + """Test hash raises scrypt error on illegal parameter value (r < 1)""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, r=-1)) + + def test_hash_raises_error_r_p_over_limit(self): + """Test hash raises scrypt error when parameters r multiplied by p over limit 2**30""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, r=2, p=2 ** 29)) + + def test_hash_raises_error_n_not_power_of_two(self): + """Test hash raises scrypt error when parameter N is not a power of two {2, 4, 8, 16, etc}""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, N=3)) + + def test_hash_raises_error_n_under_limit(self): + """Test hash raises scrypt error when parameter N under limit of 1""" + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, N=1)) + self.assertRaises(scrypt.error, + lambda: scrypt.hash(self.input, self.salt, N=-1)) + +if __name__ == "__main__": + testm.main() diff --git a/tests/test_scrypt_py2x.py b/tests/test_scrypt_py2x.py new file mode 100644 index 0000000..811cf37 --- /dev/null +++ b/tests/test_scrypt_py2x.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +from sys import version_info + +if ((version_info > (3, 2, 0, 'final', 0)) or + (version_info > (2, 7, 0, 'final', 0) and version_info < (3, 0, 0, 'final', 0))): + import unittest as testm +else: + try: + import unittest2 as testm + except ImportError: + print("Please install unittest2 to run the test suite") + exit(-1) + +import scrypt + + +@testm.skipIf(version_info > (3, 0, 0, 'final', 0), "Tests for Python 2 only") +class TestScryptForPython2(testm.TestCase): + + def setUp(self): + self.input = "message" + self.password = "password" + self.unicode_text = '\xe1\x93\x84\xe1\x93\x87\xe1\x95\x97\xe1\x92\xbb\xe1\x92\xa5\xe1\x90\x85\xe1\x91\xa6'.decode('utf-8') + + def test_py2_encrypt_fails_on_unicode_input(self): + """Test Py2 encrypt raises TypeError when Unicode input passed""" + self.assertRaises(TypeError, lambda: scrypt.encrypt(self.unicode_text, self.password)) + + def test_py2_encrypt_fails_on_unicode_password(self): + """Test Py2 encrypt raises TypeError when Unicode password passed""" + self.assertRaises(TypeError, lambda: scrypt.encrypt(self.input, self.unicode_text)) + + def test_py2_encrypt_returns_string(self): + """Test Py2 encrypt returns str""" + e = scrypt.encrypt(self.input, self.password, 0.1) + self.assertTrue(isinstance(e, str)) + + def test_py2_decrypt_returns_string(self): + """Test Py2 decrypt returns str""" + s = scrypt.encrypt(self.input, self.password, 0.1) + m = scrypt.decrypt(s, self.password) + self.assertTrue(isinstance(m, str)) + + def test_py2_hash_returns_string(self): + """Test Py2 hash return str""" + h = scrypt.hash(self.input, self.password) + self.assertTrue(isinstance(h, str)) + +if __name__ == "__main__": + testm.main() diff --git a/tests/test_scrypt_py3x.py b/tests/test_scrypt_py3x.py new file mode 100644 index 0000000..e03e098 --- /dev/null +++ b/tests/test_scrypt_py3x.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- + +from sys import version_info + +if ((version_info > (3, 2, 0, 'final', 0)) or + (version_info > (2, 7, 0, 'final', 0) and version_info < (3, 0, 0, 'final', 0))): + import unittest as testm +else: + try: + import unittest2 as testm + except ImportError: + print("Please install unittest2 to run the test suite") + exit(-1) + +import scrypt + + +@testm.skipIf(version_info < (3, 0, 0, 'final', 0), "Tests for Python 3 only") +class TestScryptForPy3(testm.TestCase): + + def setUp(self): + self.input = "message" + self.password = "password" + self.byte_text = b'\xe1\x93\x84\xe1\x93\x87\xe1\x95\x97\xe1\x92\xbb\xe1\x92\xa5\xe1\x90\x85\xe1\x91\xa6' + self.unicode_text = self.byte_text.decode('utf-8', "strict") + + def test_py3_encrypt_allows_bytes_input(self): + """Test Py3 encrypt allows unicode input""" + s = scrypt.encrypt(self.byte_text, self.password, 0.1) + m = scrypt.decrypt(s, self.password) + self.assertEqual(bytes(m.encode("utf-8")), self.byte_text) + + def test_py3_encrypt_allows_bytes_password(self): + """Test Py3 encrypt allows unicode password""" + s = scrypt.encrypt(self.input, self.byte_text, 0.1) + m = scrypt.decrypt(s, self.byte_text) + self.assertEqual(m, self.input) + + def test_py3_encrypt_returns_bytes(self): + """Test Py3 encrypt return bytes""" + s = scrypt.encrypt(self.input, self.password, 0.1) + self.assertTrue(isinstance(s, bytes)) + + def test_py3_decrypt_returns_unicode_string(self): + """Test Py3 decrypt returns Unicode UTF-8 string""" + s = scrypt.encrypt(self.input, self.password, 0.1) + m = scrypt.decrypt(s, self.password) + self.assertTrue(isinstance(m, str)) + + def test_py3_hash_returns_bytes(self): + """Test Py3 hash return bytes""" + h = scrypt.hash(self.input, self.password) + self.assertTrue(isinstance(h, bytes)) + +if __name__ == "__main__": + testm.main() -- cgit v1.2.3