diff --git a/.travis.yml b/.travis.yml index 665e3fd..385e7de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,10 @@ python: - "2.6" - "2.7" - "3.3" + - "3.4" before_install: - "sudo apt-get update" - "sudo apt-get install swig libnettle4" -install: +install: - "pip install -r requirements.txt -r requirements-tests.txt --use-mirrors" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} != '3' ]]; then pip install -r requirements-tests-py2.txt --use-mirrors; fi" - - "if [[ ${TRAVIS_PYTHON_VERSION::1} = '3' ]]; then pip install -r requirements-tests-py3.txt --use-mirrors; fi" -script: "nosetests -v" +script: "py.test -v" diff --git a/onepassword/_pbkdf2_pycrypto.py b/onepassword/_pbkdf2_pycrypto.py deleted file mode 100644 index 176dae6..0000000 --- a/onepassword/_pbkdf2_pycrypto.py +++ /dev/null @@ -1,17 +0,0 @@ -import Crypto.Hash.HMAC -import Crypto.Protocol.KDF - -from .hashes import SHA1, SHA512 -from .util import make_utf8 - - -def pbkdf2_sha1(password, salt, length, iterations): - password, salt = make_utf8(password, salt) - prf = lambda p, s: Crypto.Hash.HMAC.new(p, s, digestmod=SHA1).digest() - return Crypto.Protocol.KDF.PBKDF2(password=password, salt=salt, dkLen=length, count=iterations, prf=prf) - - -def pbkdf2_sha512(password, salt, length, iterations): - password, salt = make_utf8(password, salt) - prf = lambda p, s: Crypto.Hash.HMAC.new(p, s, digestmod=SHA512).digest() - return Crypto.Protocol.KDF.PBKDF2(password=password, salt=salt, dkLen=length, count=iterations, prf=prf) diff --git a/onepassword/crypt_util.py b/onepassword/crypt_util.py index 31d4976..41da4d5 100644 --- a/onepassword/crypt_util.py +++ b/onepassword/crypt_util.py @@ -5,15 +5,19 @@ import math import struct -import Crypto.Cipher.AES -import Crypto.Hash.HMAC - from . import padding from . import pbkdf1 from . import pbkdf2 -from .hashes import MD5, SHA256, SHA512 from .util import make_utf8 +from cryptography.exceptions import InvalidSignature +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from cryptography.hazmat.primitives.hashes import MD5, SHA256, SHA512, Hash +from cryptography.hazmat.primitives.hmac import HMAC + +_backend = default_backend() + # 8 bytes for "opdata1" # 8 bytes for plaintext length @@ -55,8 +59,11 @@ def a_decrypt_key(key_obj, password, aes_size=A_AES_SIZE): keys = pbkdf2.pbkdf2_sha1(password=password, salt=salt, length=2*key_size, iterations=iterations) key = keys[:key_size] iv = keys[key_size:] - aes_er = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv) - potential_key = padding.pkcs5_unpad(aes_er.decrypt(data)) + + aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=_backend) + decryptor = aes.decryptor() + potential_key = padding.pkcs5_unpad(decryptor.update(data) + decryptor.finalize()) + validation = base64.b64decode(key_obj['validation']) decrypted_validation = a_decrypt_item(validation, potential_key) if decrypted_validation != potential_key: @@ -88,10 +95,14 @@ def a_decrypt_item(data, key, aes_size=A_AES_SIZE): nkey = pb_gen.read(key_size) iv = pb_gen.read(key_size) else: - nkey = MD5.new(key).digest() + digest = Hash(MD5(), backend=_backend) + digest.update(key) + nkey = digest.finalize() iv = '\x00'*key_size - aes_er = Crypto.Cipher.AES.new(nkey, Crypto.Cipher.AES.MODE_CBC, iv) - return padding.pkcs5_unpad(aes_er.decrypt(data)) + + aes = Cipher(algorithms.AES(nkey), modes.CBC(iv), backend=_backend) + decryptor = aes.decryptor() + return padding.pkcs5_unpad(decryptor.update(data) + decryptor.finalize()) def opdata1_unpack(data): @@ -118,11 +129,15 @@ def opdata1_decrypt_key(data, key, hmac_key, aes_size=C_AES_SIZE, ignore_hmac=Fa key_size = KEY_SIZE[aes_size] iv, cryptext, expected_hmac = struct.unpack("=16s64s32s", data) if not ignore_hmac: - verifier = Crypto.Hash.HMAC.new(key=hmac_key, msg=(iv + cryptext), digestmod=SHA256) - if verifier.digest() != expected_hmac: + verifier = HMAC(hmac_key, SHA256(), backend=_backend) + verifier.update(iv + cryptext) + try: + verifier.verify(expected_hmac) + except InvalidSignature: raise ValueError("HMAC did not match for opdata1 key") - decryptor = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv) - decrypted = decryptor.decrypt(cryptext) + aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=_backend) + decryptor = aes.decryptor() + decrypted = decryptor.update(cryptext) + decryptor.finalize() crypto_key, mac_key = decrypted[:key_size], decrypted[key_size:] return crypto_key, mac_key @@ -132,7 +147,9 @@ def opdata1_decrypt_master_key(data, key, hmac_key, aes_size=C_AES_SIZE, ignore_ bare_key = opdata1_decrypt_item(data, key, hmac_key, aes_size=aes_size, ignore_hmac=ignore_hmac) # XXX: got the following step from jeff@agilebits (as opposed to the # docs anywhere) - hashed_key = SHA512.new(bare_key).digest() + digest = Hash(SHA512(), backend=_backend) + digest.update(bare_key) + hashed_key = digest.finalize() return hashed_key[:key_size], hashed_key[key_size:] @@ -142,17 +159,20 @@ def opdata1_decrypt_item(data, key, hmac_key, aes_size=C_AES_SIZE, ignore_hmac=F assert len(data) >= OPDATA1_MINIMUM_SIZE plaintext_length, iv, cryptext, expected_hmac, hmac_d_data = opdata1_unpack(data) if not ignore_hmac: - verifier = Crypto.Hash.HMAC.new(key=hmac_key, msg=hmac_d_data, digestmod=SHA256) - got_hmac = verifier.digest() - if len(got_hmac) != len(expected_hmac): + verifier = HMAC(hmac_key, SHA256(), backend=_backend) + verifier.update(hmac_d_data) + if len(verifier.copy().finalize()) != len(expected_hmac): raise ValueError("Got unexpected HMAC length (expected %d bytes, got %d bytes)" % ( len(expected_hmac), len(got_hmac) )) - if got_hmac != expected_hmac: + try: + verifier.verify(expected_hmac) + except InvalidSignature: raise ValueError("HMAC did not match for opdata1 record") - decryptor = Crypto.Cipher.AES.new(key, Crypto.Cipher.AES.MODE_CBC, iv) - decrypted = decryptor.decrypt(cryptext) + aes = Cipher(algorithms.AES(key), modes.CBC(iv), backend=_backend) + decryptor = aes.decryptor() + decrypted = decryptor.update(cryptext) + decryptor.finalize() unpadded = padding.ab_unpad(decrypted, plaintext_length) return unpadded @@ -171,7 +191,7 @@ def opdata1_derive_keys(password, salt, iterations=1000, aes_size=C_AES_SIZE): def opdata1_verify_overall_hmac(hmac_key, item): - verifier = Crypto.Hash.HMAC.new(key=hmac_key, digestmod=SHA256) + verifier = HMAC(hmac_key, SHA256(), backend=_backend) for key, value in sorted(item.items()): if key == 'hmac': continue @@ -182,6 +202,7 @@ def opdata1_verify_overall_hmac(hmac_key, item): verifier.update(key.encode('utf-8')) verifier.update(value) expected = base64.b64decode(item['hmac']) - got = verifier.digest() - if got != expected: + try: + verifier.verify(expected) + except InvalidSignature: raise ValueError("HMAC did not match for data dictionary") diff --git a/onepassword/hashes.py b/onepassword/hashes.py deleted file mode 100644 index b085f25..0000000 --- a/onepassword/hashes.py +++ /dev/null @@ -1,10 +0,0 @@ -try: - from Crypto.Hash import MD5 - from Crypto.Hash import SHA as SHA1 - from Crypto.Hash import SHA256 - from Crypto.Hash import SHA512 -except ImportError: - import hashlib - MD5 = hashlib.md5 - SHA256 = hashlib.sha256 - SHA512 = hashlib.sha512 diff --git a/onepassword/padding.py b/onepassword/padding.py index f53a684..e401bf7 100644 --- a/onepassword/padding.py +++ b/onepassword/padding.py @@ -1,4 +1,4 @@ -from . import random_util +import os import six @@ -24,7 +24,7 @@ def pkcs5_unpad(string): return string[:-amount_of_padding] -def ab_pad(string, block_size=16, random_generator=random_util.sort_of_random_bytes): +def ab_pad(string, block_size=16, random_generator=os.urandom): """AgileBits custom pad a string to the given block size Arguments: diff --git a/onepassword/pbkdf2.py b/onepassword/pbkdf2.py index dab8175..f23693b 100644 --- a/onepassword/pbkdf2.py +++ b/onepassword/pbkdf2.py @@ -6,13 +6,7 @@ pbkdf2_sha1 = pbkdf2_sha1 pbkdf2_sha512 = pbkdf2_sha512 except ImportError: - try: - from ._pbkdf2_cryptography import pbkdf2_sha1, pbkdf2_sha512 - # make pyflakes happy - pbkdf2_sha1 = pbkdf2_sha1 - pbkdf2_sha512 = pbkdf2_sha512 - except ImportError: - from ._pbkdf2_pycrypto import pbkdf2_sha1, pbkdf2_sha512 - # make pyflakes happy - pbkdf2_sha1 = pbkdf2_sha1 - pbkdf2_sha512 = pbkdf2_sha512 + from ._pbkdf2_cryptography import pbkdf2_sha1, pbkdf2_sha512 + # make pyflakes happy + pbkdf2_sha1 = pbkdf2_sha1 + pbkdf2_sha512 = pbkdf2_sha512 diff --git a/onepassword/random_util.py b/onepassword/random_util.py deleted file mode 100644 index f186bbb..0000000 --- a/onepassword/random_util.py +++ /dev/null @@ -1,38 +0,0 @@ -"""Random sources""" - -import os -import random - -import six - - -# If someone's truly paranoid and wants to contribute -# code that knows how to talk to an EGD for really really -# strong randomness, I would not say no to that. but it's -# almost always safer/smarter to just use /dev/random and -# trust that your sysadmin knows how to use the EGD - - -def really_random_bytes(l): - """Return bytes that should be cryptographically strong (generally, a - PRNG regularly seeded with real-world entropy""" - with open("/dev/random", "rb") as f: - return f.read(l) - - -def sort_of_random_bytes(l): - """Return bytes that may be cryptographically strong or may be - PRNG-based depending on the operating system status""" - return os.urandom(l) - - -def barely_random_bytes(l): - """Return bytes that appear random but are not cryptographically - strong""" - return b''.join(six.int2byte(random.randrange(0, 255)) for b in six.moves.range(l)) - - -def not_random_bytes(l): - """Return bytes that are not at all random, but suitable for use as - testing filler""" - return b''.join(six.int2byte(x % 255) for x in six.moves.range(l)) diff --git a/requirements-tests-py2.txt b/requirements-tests-py2.txt deleted file mode 100644 index 9a23970..0000000 --- a/requirements-tests-py2.txt +++ /dev/null @@ -1 +0,0 @@ -unittest2 diff --git a/requirements-tests-py3.txt b/requirements-tests-py3.txt deleted file mode 100644 index 62f3769..0000000 --- a/requirements-tests-py3.txt +++ /dev/null @@ -1 +0,0 @@ -unittest2py3k diff --git a/requirements-tests.txt b/requirements-tests.txt index 6af2de0..c043cb9 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -1,2 +1,3 @@ mock>=1.0 -nose>=1.3 +pytest>=2.7.2 +pytest-benchmark>=2.5.0 diff --git a/requirements.txt b/requirements.txt index 4608010..a70eb18 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,3 @@ simplejson>=2.1.0 -pycrypto>=2.0 cryptography>=0.9.3 six>=1.4.0 diff --git a/tests/helpers.py b/tests/helpers.py deleted file mode 100644 index 6beb7d0..0000000 --- a/tests/helpers.py +++ /dev/null @@ -1,11 +0,0 @@ -from contextlib import contextmanager - - -@contextmanager -def assert_raises(exc_klass): - excepted = None - try: - yield - except Exception as exc: - excepted = exc - assert type(excepted) == exc_klass diff --git a/tests/integration/agilekeychain_tests.py b/tests/integration/test_agilekeychain.py similarity index 64% rename from tests/integration/agilekeychain_tests.py rename to tests/integration/test_agilekeychain.py index eb229c0..cf0f2a3 100644 --- a/tests/integration/agilekeychain_tests.py +++ b/tests/integration/test_agilekeychain.py @@ -1,21 +1,19 @@ import os.path -from unittest2 import TestCase - import onepassword.keychain -class AgileKeychainIntegrationTestCase(TestCase): +class TestAgileKeychainIntegration: test_file_root = os.path.realpath(os.path.join(__file__, '..', '..', '..', 'data', 'sample.agilekeychain')) def test_open(self): c = onepassword.keychain.AKeychain(self.test_file_root) c.unlock("george") - self.assertEqual(len(c.items), 2) + assert len(c.items) == 2 def test_item_parsing(self): c = onepassword.keychain.AKeychain(self.test_file_root) c.unlock("george") google = c.get_by_uuid('00925AACC28B482ABFE650FCD42F82CD') - self.assertEqual(google.title, 'Google') - self.assertEqual(google.decrypt()['fields'][1]['value'], 'test_password') + assert google.title == 'Google' + assert google.decrypt()['fields'][1]['value'] == 'test_password' diff --git a/tests/integration/cloudkeychain_tests.py b/tests/integration/test_cloudkeychain.py similarity index 67% rename from tests/integration/cloudkeychain_tests.py rename to tests/integration/test_cloudkeychain.py index fc4865d..76f1d38 100644 --- a/tests/integration/cloudkeychain_tests.py +++ b/tests/integration/test_cloudkeychain.py @@ -1,11 +1,11 @@ import os.path -from unittest2 import TestCase +import pytest import onepassword.keychain -class CloudKeychainIntegrationTestCase(TestCase): +class TestCloudKeychainIntegration: test_file_root = os.path.realpath(os.path.join(__file__, '..', '..', '..', 'data', 'sample.cloudkeychain')) def test_open(self): @@ -16,5 +16,5 @@ def test_item_parsing(self): c = onepassword.keychain.CKeychain(self.test_file_root) c.unlock("fred") skype_item = c.get_by_uuid('2A632FDD32F5445E91EB5636C7580447') - self.assertEqual(skype_item.title, 'Skype') - self.assertEqual(skype_item.decrypt()['fields'][1]['value'], 'dej3ur9unsh5ian1and5') + assert skype_item.title == 'Skype' + assert skype_item.decrypt()['fields'][1]['value'] == 'dej3ur9unsh5ian1and5' diff --git a/tests/unit/padding_tests.py b/tests/unit/padding_tests.py deleted file mode 100644 index 08de959..0000000 --- a/tests/unit/padding_tests.py +++ /dev/null @@ -1,44 +0,0 @@ -from unittest2 import TestCase - -from onepassword import padding -import six - - -class PKCS5PaddingTestCase(TestCase): - """Test our PKCS#5 padding""" - VECTORS = ( - (b"", 1, b"\x01"), - (b"abcd", 8, b"abcd\x04\x04\x04\x04"), - (b"abcdefg\x00", 16, b"abcdefg\x00\x08\x08\x08\x08\x08\x08\x08\x08"), - ) - - def test_pad(self): - for unpadded, bs, padded in self.VECTORS: - self.assertEqual(padding.pkcs5_pad(unpadded, bs), padded) - - def test_unpad(self): - for unpadded, _, padded in self.VECTORS: - self.assertEqual(padding.pkcs5_unpad(padded), unpadded) - self.assertEqual(padding.pkcs5_unpad(""), "") - - -class ABPaddingTestCase(TestCase): - """Test the custom AgileBits padding""" - VECTORS = ( - (b"", 4, b"\x00\x00\x00\x00"), - (b"ab", 4, b"\x00\x00ab"), - (b"abcd", 4, b"\x00\x00\x00\x00abcd"), - (b"\x01\x02", 10, b"\x00\x00\x00\x00\x00\x00\x00\x00\x01\x02"), - ) - - def zeros(self, count): - return b''.join([six.int2byte(0) for x in range(count)]) - - def test_pad(self): - for unpadded, bs, padded in self.VECTORS: - self.assertEqual(padding.ab_pad(unpadded, bs, random_generator=self.zeros), padded) - - def test_unpad(self): - for unpadded, _, padded in self.VECTORS: - size = len(unpadded) - self.assertEqual(padding.ab_unpad(padded, size), unpadded) diff --git a/tests/unit/pbkdf2_tests.py b/tests/unit/pbkdf2_tests.py deleted file mode 100644 index 8297e80..0000000 --- a/tests/unit/pbkdf2_tests.py +++ /dev/null @@ -1,75 +0,0 @@ -from __future__ import print_function - -from functools import wraps -from unittest2 import TestCase - - -def ignore_import_error(f): - @wraps(f) - def wrapper(*args, **kwargs): - try: - f(*args, **kwargs) - except ImportError as ex: - print('ignoring ImportError: {0}'.format(ex)) - return wrapper - - -class PBKDF2SHA1TestCase(TestCase): - VECTORS = ( - (b'password', b'', 1, b'\x87T\xc3,d\xb0\xf5$\xfcP\xc0\x0fx\x815\xde'), - (b'password', b'', 16, b'\x0bP|}r\x13\xe9\x9e\x0f\x0cj\xd0\xbdH\xa7\xc9'), - (b'password', b'salt', 1, b'\x0c`\xc8\x0f\x96\x1f\x0eq\xf3\xa9\xb5$\xaf`\x12\x06'), - (b'password', b'salt', 16, b'\x1e\x84Lf\xb5|\x0e\xed\xf6\xfdx\x1b\xca\xfc\xe8"'), - (b'password', b'salt', 163840, b'\xc2\x03/\xb4\xfe\xf4\xa8n\x15\\\x1a\x93kY\xa9\xda'), - ) - - def test_vectors_pycrypto(self): - from onepassword import _pbkdf2_pycrypto - for password, salt, iterations, expected_key in self.VECTORS: - generated = _pbkdf2_pycrypto.pbkdf2_sha1(password, salt, length=16, iterations=iterations) - self.assertEqual(generated, expected_key) - - @ignore_import_error - def test_vectors_cryptography(self): - from onepassword import _pbkdf2_cryptography - for password, salt, iterations, expected_key in self.VECTORS: - generated = _pbkdf2_cryptography.pbkdf2_sha1(password, salt, length=16, iterations=iterations) - self.assertEqual(generated, expected_key) - - @ignore_import_error - def test_vectors_nettle(self): - from onepassword import _pbkdf2_nettle - for password, salt, iterations, expected_key in self.VECTORS: - generated = _pbkdf2_nettle.pbkdf2_sha1(password, salt, length=16, iterations=iterations) - self.assertEqual(generated, expected_key) - - -class PBKDF2SHA512TestCase(TestCase): - VECTORS = ( - (b'password', b'', 1, b'\xae\x16\xcem\xfdJj\x0c B\x1f\xf8\x0e\xb3\xbaJ'), - (b'password', b'', 16, b'T\xe1\xd5T\xa6{\x15\x1d\x19;\x82\nbXbI'), - (b'password', b'salt', 1, b'\x86\x7fp\xcf\x1a\xde\x02\xcf\xf3u%\x99\xa3\xa5=\xc4'), - (b'password', b'salt', 16, b'\x884\xdc\xaf\xec\xf51&\xcc\xfeMF\xc6v\x16M'), - (b'password', b'salt', 163840, b'|\xc2\xa2i\xe7\xa2j\x9e\x8f\xfb\x93\xd7\xb7f\x88\x05'), - ) - - def test_vectors_pycrypto(self): - from onepassword import _pbkdf2_pycrypto - for password, salt, iterations, expected_key in self.VECTORS: - generated = _pbkdf2_pycrypto.pbkdf2_sha512(password, salt, length=16, iterations=iterations) - self.assertEqual(generated, expected_key) - - @ignore_import_error - def test_vectors_cryptography(self): - from onepassword import _pbkdf2_cryptography - for password, salt, iterations, expected_key in self.VECTORS: - generated = _pbkdf2_cryptography.pbkdf2_sha512(password, salt, length=16, iterations=iterations) - self.assertEqual(generated, expected_key) - - @ignore_import_error - def test_vectors_nettle(self): - from onepassword import _pbkdf2_nettle - for password, salt, iterations, expected_key in self.VECTORS: - generated = _pbkdf2_nettle.pbkdf2_sha512(password, salt, length=16, iterations=iterations) - self.assertEqual(generated, expected_key) - diff --git a/tests/unit/random_tests.py b/tests/unit/random_tests.py deleted file mode 100644 index 9f6bb50..0000000 --- a/tests/unit/random_tests.py +++ /dev/null @@ -1,24 +0,0 @@ -from unittest2 import TestCase -from onepassword import random_util - - -class RandomTestCase(TestCase): - # just make sure that all of the functions return the right number - # of bytes for now - BYTES = 512 - - def test_not_random(self): - bytez = random_util.not_random_bytes(self.BYTES) - self.assertEqual(len(bytez), self.BYTES) - - def test_barely_random(self): - bytez = random_util.barely_random_bytes(self.BYTES) - self.assertEqual(len(bytez), self.BYTES) - - def test_sort_of_random(self): - bytez = random_util.sort_of_random_bytes(self.BYTES) - self.assertEqual(len(bytez), self.BYTES) - - def test_really_random(self): - bytez = random_util.really_random_bytes(self.BYTES) - self.assertEqual(len(bytez), self.BYTES) diff --git a/tests/unit/crypt_util_tests.py b/tests/unit/test_crypt_util.py similarity index 85% rename from tests/unit/crypt_util_tests.py rename to tests/unit/test_crypt_util.py index d585fc4..c45750b 100644 --- a/tests/unit/crypt_util_tests.py +++ b/tests/unit/test_crypt_util.py @@ -2,15 +2,14 @@ import hashlib import hmac import struct -from unittest2 import TestCase import simplejson +import pytest from onepassword import crypt_util -from ..helpers import assert_raises -class HexizeTestCase(TestCase): +class TestHexize: VECTORS = ( (b'', b''), (b'\x00', b'00'), @@ -18,16 +17,15 @@ class HexizeTestCase(TestCase): (b'\x00,123', b'002C313233'), ) - def test_hexize_simple(self): - for unhexed, hexed in self.VECTORS: - self.assertEqual(crypt_util.hexize(unhexed), hexed) + @pytest.mark.parametrize('unhexed, hexed', VECTORS) + def test_hexize_simple(self, unhexed, hexed): + assert crypt_util.hexize(unhexed) == hexed - def test_unhexize_simple(self): - for unhexed, hexed in self.VECTORS: - self.assertEqual(crypt_util.unhexize(hexed), unhexed) + @pytest.mark.parametrize('unhexed, hexed', VECTORS) + def test_unhexize_simple(self, unhexed, hexed): + assert crypt_util.unhexize(hexed) == unhexed - -class OPData1KeyDerivationTestCase(TestCase): +class TestOPData1KeyDerivation: VECTORS = ( (('', b''), (b'\xcb\x93\tl:\x02\xbe\xeb\x1c_\xac6v\\\x90\x11\xfe\x99\xf8\xd8\xeab6`H\xfc\x98\xcb\x98\xdf\xea\x8f', b'O\x8d0U\xa5\xef\x9bz\xf2\x97s\xad\x82R\x95Ti9\x9d%\xd3\nS1(\x89(X\x1f\xb8n\xcb')), (('', b'', 10000), (b'I\xb4\xa7!=\xfc\xeeN\xad\xde\xc1\xe2\x1e\xa6\xfc\x8b\x9a,FZ\xe7\xcdPOA\x1e\xeek!\xd2\xe5\xef', b'v4\x8a\xe1\xa9\xea\xa8\x1bUUm\x13\xa2CM\t\x02,\xc4\x07\xd9\x13bF\xef5(\x05\xf4\xb4\xab\xb5')), @@ -36,12 +34,12 @@ class OPData1KeyDerivationTestCase(TestCase): (('fred', b''), (b'P\x9b\xe2\xb9\xc0C"\xaf\xf2>\xc0zF\xe8\xff\x06j\x88\x91\xe3\t\x82\x96VZ0\x8e\xd6\x11\xcc\xa7\xd4', b'b$\x81(\xd4\xf4\x0e8M\xf0\x0c\x18)!r\xcf\x02>\xf3hK_\x95\xa4\x8c\xa0\x91\x9c\xf97 W')), ) - def test_vectors(self): - for args, expected in self.VECTORS: - self.assertEqual(crypt_util.opdata1_derive_keys(*args), expected) + @pytest.mark.parametrize('args, expected', VECTORS) + def test_vectors(self, args, expected): + assert crypt_util.opdata1_derive_keys(*args) == expected -class OPData1UnpackTestCase(TestCase): +class TestOPData1Unpack: def build_opdata1(self): header = b"opdata01" plaintext = b"" @@ -54,26 +52,26 @@ def build_opdata1(self): return header + msg def test_bad_header_fails(self): - with assert_raises(TypeError): + with pytest.raises(TypeError): crypt_util.opdata1_unpack(b"") - with assert_raises(TypeError): + with pytest.raises(TypeError): crypt_util.opdata1_unpack(b"opdata02abcdef") def test_basic(self): packed = self.build_opdata1() plaintext_length, iv, cryptext, expected_hmac, _ = crypt_util.opdata1_unpack(packed) - self.assertEqual(plaintext_length, 0) - self.assertEqual(cryptext, b"") + assert plaintext_length == 0 + assert cryptext == b"" def test_basic_auto_b64decode(self): packed = self.build_opdata1() packed = base64.b64encode(packed) plaintext_length, iv, cryptext, expected_hmac, _ = crypt_util.opdata1_unpack(packed) - self.assertEqual(plaintext_length, 0) - self.assertEqual(cryptext, b"") + assert plaintext_length == 0 + assert cryptext == b"" -class OPdata1DecryptTestCase(TestCase): +class TestOPdata1Decrypt: """test specific data from the example keychain provided by AgileBits""" DERIVED_KEY = b'`\x8c\x9f\x19
-a -nosalt -p < /dev/null VECTORS = ( @@ -14,16 +14,16 @@ class PBKDF1TestCase(TestCase): (b'012345678910111231415161717', b'F7560045C70A96DB', b'2E14B2EC7E2F8CDC18F15BB773CCD6F2', b'5C8AADA268F9B86F960DF0464AE5E981'), ) - def test_vectors(self): - for password, hex_salt, expected_key, expected_iv in self.VECTORS: - salt = crypt_util.unhexize(hex_salt) - pb_gen = pbkdf1.PBKDF1(password, salt) - derived_key = pb_gen.read(16) - derived_iv = pb_gen.read(16) - hex_derived_key = crypt_util.hexize(derived_key) - hex_derived_iv = crypt_util.hexize(derived_iv) - self.assertEqual(hex_derived_key, expected_key) - self.assertEqual(hex_derived_iv, expected_iv) + @pytest.mark.parametrize('password, hex_salt, expected_key, expected_iv', VECTORS) + def test_vectors(self, password, hex_salt, expected_key, expected_iv): + salt = crypt_util.unhexize(hex_salt) + pb_gen = pbkdf1.PBKDF1(password, salt) + derived_key = pb_gen.read(16) + derived_iv = pb_gen.read(16) + hex_derived_key = crypt_util.hexize(derived_key) + hex_derived_iv = crypt_util.hexize(derived_iv) + assert hex_derived_key == expected_key + assert hex_derived_iv == expected_iv def test_count(self): # can't use vectors as easily here because openssl never passes @@ -38,10 +38,10 @@ def test_count(self): pb_gen = pbkdf1.PBKDF1(key, salt, iterations=4, hash_algo=mock_md5) keyish = pb_gen.read(16) ivish = pb_gen.read(16) - self.assertEqual((keyish, ivish), (sigil[:-16], sigil[-16:])) - self.assertEqual(mock_md5.mock_calls, [ + assert (keyish, ivish) == (sigil[:-16], sigil[-16:]) + assert mock_md5.mock_calls == [ mock.call(key+salt), mock.call(sigil), mock.call(sigil), mock.call(sigil), - ]) + ] diff --git a/tests/unit/test_pbkdf2.py b/tests/unit/test_pbkdf2.py new file mode 100644 index 0000000..b5e2129 --- /dev/null +++ b/tests/unit/test_pbkdf2.py @@ -0,0 +1,64 @@ +from __future__ import print_function + +from functools import wraps + +import pytest + + +@pytest.mark.benchmark(group='PBKDF2 SHA1') +class TestPBKDF2SHA1: + VECTORS = ( + (b'password', b'', 1, b'\x87T\xc3,d\xb0\xf5$\xfcP\xc0\x0fx\x815\xde'), + (b'password', b'', 16, b'\x0bP|}r\x13\xe9\x9e\x0f\x0cj\xd0\xbdH\xa7\xc9'), + (b'password', b'salt', 1, b'\x0c`\xc8\x0f\x96\x1f\x0eq\xf3\xa9\xb5$\xaf`\x12\x06'), + (b'password', b'salt', 16, b'\x1e\x84Lf\xb5|\x0e\xed\xf6\xfdx\x1b\xca\xfc\xe8"'), + (b'password', b'salt', 163840, b'\xc2\x03/\xb4\xfe\xf4\xa8n\x15\\\x1a\x93kY\xa9\xda'), + ) + + @pytest.mark.parametrize('password, salt, iterations, expected_key', VECTORS) + def test_vectors_cryptography(self, password, salt, iterations, expected_key): + from onepassword import _pbkdf2_cryptography + generated = _pbkdf2_cryptography.pbkdf2_sha1(password, salt, length=16, iterations=iterations) + assert generated == expected_key + + @pytest.mark.parametrize('password, salt, iterations, expected_key', VECTORS) + def test_vectors_nettle(self, password, salt, iterations, expected_key): + _pbkdf2_nettle = pytest.importorskip('onepassword._pbkdf2_nettle') + generated = _pbkdf2_nettle.pbkdf2_sha1(password, salt, length=16, iterations=iterations) + assert generated == expected_key + + def test_benchmark_cryptography(self, benchmark): + benchmark(self.test_vectors_cryptography, *self.VECTORS[-1]) + + def test_benchmark_nettle(self, benchmark): + benchmark(self.test_vectors_nettle, *self.VECTORS[-1]) + + +@pytest.mark.benchmark(group='PBKDF2 SHA512') +class TestPBKDF2SHA512: + VECTORS = ( + (b'password', b'', 1, b'\xae\x16\xcem\xfdJj\x0c B\x1f\xf8\x0e\xb3\xbaJ'), + (b'password', b'', 16, b'T\xe1\xd5T\xa6{\x15\x1d\x19;\x82\nbXbI'), + (b'password', b'salt', 1, b'\x86\x7fp\xcf\x1a\xde\x02\xcf\xf3u%\x99\xa3\xa5=\xc4'), + (b'password', b'salt', 16, b'\x884\xdc\xaf\xec\xf51&\xcc\xfeMF\xc6v\x16M'), + (b'password', b'salt', 163840, b'|\xc2\xa2i\xe7\xa2j\x9e\x8f\xfb\x93\xd7\xb7f\x88\x05'), + ) + + @pytest.mark.parametrize('password, salt, iterations, expected_key', VECTORS) + def test_vectors_cryptography(self, password, salt, iterations, expected_key): + from onepassword import _pbkdf2_cryptography + generated = _pbkdf2_cryptography.pbkdf2_sha512(password, salt, length=16, iterations=iterations) + assert generated == expected_key + + @pytest.mark.parametrize('password, salt, iterations, expected_key', VECTORS) + def test_vectors_nettle(self, password, salt, iterations, expected_key): + _pbkdf2_nettle = pytest.importorskip('onepassword._pbkdf2_nettle') + generated = _pbkdf2_nettle.pbkdf2_sha512(password, salt, length=16, iterations=iterations) + assert generated == expected_key + + def test_benchmark_cryptography(self, benchmark): + benchmark(self.test_vectors_cryptography, *self.VECTORS[-1]) + + def test_benchmark_nettle(self, benchmark): + benchmark(self.test_vectors_nettle, *self.VECTORS[-1]) +