diff options
-rw-r--r-- | pandora/transport.py | 72 | ||||
-rwxr-xr-x | setup.py | 8 | ||||
-rw-r--r-- | 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 | |||
15 | import base64 | 15 | import base64 |
16 | import requests | 16 | import requests |
17 | from requests.adapters import HTTPAdapter | 17 | from requests.adapters import HTTPAdapter |
18 | from Crypto.Cipher import Blowfish | 18 | from cryptography.hazmat.backends import default_backend |
19 | from cryptography.hazmat.primitives.ciphers import Cipher | ||
20 | from cryptography.hazmat.primitives.ciphers.modes import ECB | ||
21 | from cryptography.hazmat.primitives.ciphers.algorithms import Blowfish | ||
19 | 22 | ||
20 | from .errors import PandoraException | 23 | from .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 | ||
242 | class 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 | |||
239 | class Encryptor(object): | 287 | class 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)) |
@@ -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", |