From 6b97dd6d8a01ff56b7f70a160fbaf59ddec5c60f Mon Sep 17 00:00:00 2001 From: Mike Crute Date: Sat, 30 Sep 2017 22:18:26 +0000 Subject: Remove PyCrypto and add Cryptography PyCrypto is no longer supported nor recommended. Cryptography is the replacement package which is actively maintained by the Python core committers. Also Cryptography is bundled as binary wheels for all the platforms we support which removes the install-tiem compiler requirement, making it easier to distribute Pydora. This commit replaces all usages of PyCrypto with Cryptography but is completely API compatible with the previous version. --- pandora/transport.py | 72 ++++++++++++++++++++++++-------- setup.py | 8 ++-- tests/test_pandora/test_clientbuilder.py | 8 ++-- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/pandora/transport.py b/pandora/transport.py index a8f885e..94e5f6d 100644 --- a/pandora/transport.py +++ b/pandora/transport.py @@ -15,7 +15,10 @@ import json import base64 import requests from requests.adapters import HTTPAdapter -from Crypto.Cipher import Blowfish +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives.ciphers import Cipher +from cryptography.hazmat.primitives.ciphers.modes import ECB +from cryptography.hazmat.primitives.ciphers.algorithms import Blowfish from .errors import PandoraException @@ -236,6 +239,51 @@ class APITransport(object): return self._parse_response(result) +class BlowfishCryptor(object): + """Low-Level Blowfish Cryptography + + Handles symmetric Blowfish cryptography of raw byte messages with or + without padding. Does not handle messages that are encoded in other formats + like hex or base64. + """ + + def __init__(self, key): + self.cipher = Cipher( + Blowfish(key.encode("ascii")), ECB(), backend=default_backend()) + + @staticmethod + def _add_padding(data): + block_size = Blowfish.block_size + pad_size = len(data) % block_size + padding = bytes(chr(pad_size) * (block_size - pad_size), "ascii") + return data.encode("utf-8") + padding + + @staticmethod + def _strip_padding(data): + pad_size = int(data[-1]) + if not data[-pad_size:] == bytes((pad_size,)) * pad_size: + raise ValueError('Invalid padding') + return data[:-pad_size] + + @staticmethod + def _make_bytearray(data): + return bytearray(len(data) + (Blowfish.block_size - 1)) + + def decrypt(self, data, strip_padding=True): + buf = self._make_bytearray(data) + dec = self.cipher.decryptor() + len_dec = dec.update_into(data, buf) + data = bytes(buf[:len_dec]) + dec.finalize() + return self._strip_padding(data) if strip_padding else data + + def encrypt(self, data): + data = self._add_padding(data) + enc = self.cipher.encryptor() + buf = self._make_bytearray(data) + len_enc = enc.update_into(data, buf) + return bytes(buf[:len_enc]) + enc.finalize() + + class Encryptor(object): """Pandora Blowfish Encryptor @@ -244,8 +292,8 @@ class Encryptor(object): """ def __init__(self, in_key, out_key): - self.bf_out = Blowfish.new(out_key, Blowfish.MODE_ECB) - self.bf_in = Blowfish.new(in_key, Blowfish.MODE_ECB) + self.bf_out = BlowfishCryptor(out_key) + self.bf_in = BlowfishCryptor(in_key) @staticmethod def _decode_hex(data): @@ -256,22 +304,10 @@ class Encryptor(object): return base64.b16encode(data).lower() def decrypt(self, data): - data = self.bf_out.decrypt(self._decode_hex(data)) - return json.loads(self.strip_padding(data)) + return json.loads(self.bf_out.decrypt(self._decode_hex(data))) def decrypt_sync_time(self, data): - return int(self.bf_in.decrypt(self._decode_hex(data))[4:-2]) - - def add_padding(self, data): - block_size = Blowfish.block_size - pad_size = len(data) % block_size - return data + (chr(pad_size) * (block_size - pad_size)) - - def strip_padding(self, data): - pad_size = int(data[-1]) - if not data[-pad_size:] == bytes((pad_size,)) * pad_size: - raise ValueError('Invalid padding') - return data[:-pad_size] + return int(self.bf_in.decrypt(self._decode_hex(data), False)[4:-2]) def encrypt(self, data): - return self._encode_hex(self.bf_out.encrypt(self.add_padding(data))) + return self._encode_hex(self.bf_out.encrypt(data)) diff --git a/setup.py b/setup.py index 4897822..ba90a9a 100755 --- a/setup.py +++ b/setup.py @@ -56,12 +56,12 @@ setup( "flake8>=3.3", ], tests_require=[ - "mock>=2.0", - "coverage>=4.1", + "mock>=2,<3", + "coverage>=4.1,<5", ], install_requires=[ - "pycrypto>=2.6", - "requests>=2", + "cryptography>=2,<3", + "requests>=2,<3", ], entry_points={ "console_scripts": [ diff --git a/tests/test_pandora/test_clientbuilder.py b/tests/test_pandora/test_clientbuilder.py index 27117b0..5ac6e7d 100644 --- a/tests/test_pandora/test_clientbuilder.py +++ b/tests/test_pandora/test_clientbuilder.py @@ -65,8 +65,8 @@ class TestSettingsDictBuilder(TestCase): @classmethod def _build_minimal(self): return cb.SettingsDictBuilder({ - "DECRYPTION_KEY": "dec", - "ENCRYPTION_KEY": "enc", + "DECRYPTION_KEY": "blowfishkey", + "ENCRYPTION_KEY": "blowfishkey", "PARTNER_USER": "user", "PARTNER_PASSWORD": "pass", "DEVICE": "dev", @@ -75,8 +75,8 @@ class TestSettingsDictBuilder(TestCase): @classmethod def _build_maximal(self): return cb.SettingsDictBuilder({ - "DECRYPTION_KEY": "dec", - "ENCRYPTION_KEY": "enc", + "DECRYPTION_KEY": "blowfishkey", + "ENCRYPTION_KEY": "blowfishkey", "PARTNER_USER": "user", "PARTNER_PASSWORD": "pass", "DEVICE": "dev", -- cgit v1.2.3