aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2017-09-30 22:18:26 +0000
committerMike Crute <mike@crute.us>2017-09-30 23:09:18 +0000
commit6b97dd6d8a01ff56b7f70a160fbaf59ddec5c60f (patch)
tree8b563221ef8bcb1cc9f4f7c739c791dc21142ee9
parentfdfb18a78e0014d362a2e0654eabf706094e4f09 (diff)
downloadpydora-6b97dd6d8a01ff56b7f70a160fbaf59ddec5c60f.tar.bz2
pydora-6b97dd6d8a01ff56b7f70a160fbaf59ddec5c60f.tar.xz
pydora-6b97dd6d8a01ff56b7f70a160fbaf59ddec5c60f.zip
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.
-rw-r--r--pandora/transport.py72
-rwxr-xr-xsetup.py8
-rw-r--r--tests/test_pandora/test_clientbuilder.py8
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
15import base64 15import base64
16import requests 16import requests
17from requests.adapters import HTTPAdapter 17from requests.adapters import HTTPAdapter
18from Crypto.Cipher import Blowfish 18from cryptography.hazmat.backends import default_backend
19from cryptography.hazmat.primitives.ciphers import Cipher
20from cryptography.hazmat.primitives.ciphers.modes import ECB
21from cryptography.hazmat.primitives.ciphers.algorithms import Blowfish
19 22
20from .errors import PandoraException 23from .errors import PandoraException
21 24
@@ -236,6 +239,51 @@ class APITransport(object):
236 return self._parse_response(result) 239 return self._parse_response(result)
237 240
238 241
242class BlowfishCryptor(object):
243 """Low-Level Blowfish Cryptography
244
245 Handles symmetric Blowfish cryptography of raw byte messages with or
246 without padding. Does not handle messages that are encoded in other formats
247 like hex or base64.
248 """
249
250 def __init__(self, key):
251 self.cipher = Cipher(
252 Blowfish(key.encode("ascii")), ECB(), backend=default_backend())
253
254 @staticmethod
255 def _add_padding(data):
256 block_size = Blowfish.block_size
257 pad_size = len(data) % block_size
258 padding = bytes(chr(pad_size) * (block_size - pad_size), "ascii")
259 return data.encode("utf-8") + padding
260
261 @staticmethod
262 def _strip_padding(data):
263 pad_size = int(data[-1])
264 if not data[-pad_size:] == bytes((pad_size,)) * pad_size:
265 raise ValueError('Invalid padding')
266 return data[:-pad_size]
267
268 @staticmethod
269 def _make_bytearray(data):
270 return bytearray(len(data) + (Blowfish.block_size - 1))
271
272 def decrypt(self, data, strip_padding=True):
273 buf = self._make_bytearray(data)
274 dec = self.cipher.decryptor()
275 len_dec = dec.update_into(data, buf)
276 data = bytes(buf[:len_dec]) + dec.finalize()
277 return self._strip_padding(data) if strip_padding else data
278
279 def encrypt(self, data):
280 data = self._add_padding(data)
281 enc = self.cipher.encryptor()
282 buf = self._make_bytearray(data)
283 len_enc = enc.update_into(data, buf)
284 return bytes(buf[:len_enc]) + enc.finalize()
285
286
239class Encryptor(object): 287class Encryptor(object):
240 """Pandora Blowfish Encryptor 288 """Pandora Blowfish Encryptor
241 289
@@ -244,8 +292,8 @@ class Encryptor(object):
244 """ 292 """
245 293
246 def __init__(self, in_key, out_key): 294 def __init__(self, in_key, out_key):
247 self.bf_out = Blowfish.new(out_key, Blowfish.MODE_ECB) 295 self.bf_out = BlowfishCryptor(out_key)
248 self.bf_in = Blowfish.new(in_key, Blowfish.MODE_ECB) 296 self.bf_in = BlowfishCryptor(in_key)
249 297
250 @staticmethod 298 @staticmethod
251 def _decode_hex(data): 299 def _decode_hex(data):
@@ -256,22 +304,10 @@ class Encryptor(object):
256 return base64.b16encode(data).lower() 304 return base64.b16encode(data).lower()
257 305
258 def decrypt(self, data): 306 def decrypt(self, data):
259 data = self.bf_out.decrypt(self._decode_hex(data)) 307 return json.loads(self.bf_out.decrypt(self._decode_hex(data)))
260 return json.loads(self.strip_padding(data))
261 308
262 def decrypt_sync_time(self, data): 309 def decrypt_sync_time(self, data):
263 return int(self.bf_in.decrypt(self._decode_hex(data))[4:-2]) 310 return int(self.bf_in.decrypt(self._decode_hex(data), False)[4:-2])
264
265 def add_padding(self, data):
266 block_size = Blowfish.block_size
267 pad_size = len(data) % block_size
268 return data + (chr(pad_size) * (block_size - pad_size))
269
270 def strip_padding(self, data):
271 pad_size = int(data[-1])
272 if not data[-pad_size:] == bytes((pad_size,)) * pad_size:
273 raise ValueError('Invalid padding')
274 return data[:-pad_size]
275 311
276 def encrypt(self, data): 312 def encrypt(self, data):
277 return self._encode_hex(self.bf_out.encrypt(self.add_padding(data))) 313 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(
56 "flake8>=3.3", 56 "flake8>=3.3",
57 ], 57 ],
58 tests_require=[ 58 tests_require=[
59 "mock>=2.0", 59 "mock>=2,<3",
60 "coverage>=4.1", 60 "coverage>=4.1,<5",
61 ], 61 ],
62 install_requires=[ 62 install_requires=[
63 "pycrypto>=2.6", 63 "cryptography>=2,<3",
64 "requests>=2", 64 "requests>=2,<3",
65 ], 65 ],
66 entry_points={ 66 entry_points={
67 "console_scripts": [ 67 "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):
65 @classmethod 65 @classmethod
66 def _build_minimal(self): 66 def _build_minimal(self):
67 return cb.SettingsDictBuilder({ 67 return cb.SettingsDictBuilder({
68 "DECRYPTION_KEY": "dec", 68 "DECRYPTION_KEY": "blowfishkey",
69 "ENCRYPTION_KEY": "enc", 69 "ENCRYPTION_KEY": "blowfishkey",
70 "PARTNER_USER": "user", 70 "PARTNER_USER": "user",
71 "PARTNER_PASSWORD": "pass", 71 "PARTNER_PASSWORD": "pass",
72 "DEVICE": "dev", 72 "DEVICE": "dev",
@@ -75,8 +75,8 @@ class TestSettingsDictBuilder(TestCase):
75 @classmethod 75 @classmethod
76 def _build_maximal(self): 76 def _build_maximal(self):
77 return cb.SettingsDictBuilder({ 77 return cb.SettingsDictBuilder({
78 "DECRYPTION_KEY": "dec", 78 "DECRYPTION_KEY": "blowfishkey",
79 "ENCRYPTION_KEY": "enc", 79 "ENCRYPTION_KEY": "blowfishkey",
80 "PARTNER_USER": "user", 80 "PARTNER_USER": "user",
81 "PARTNER_PASSWORD": "pass", 81 "PARTNER_PASSWORD": "pass",
82 "DEVICE": "dev", 82 "DEVICE": "dev",