diff --git a/examples/crypto.py b/examples/crypto.py index 63f42237..9b2c0294 100644 --- a/examples/crypto.py +++ b/examples/crypto.py @@ -14,7 +14,7 @@ def PNFactory(cipher_mode=AES.MODE_GCM, fallback_cipher_mode=AES.MODE_CBC) -> Pu config.publish_key = getenv('PN_KEY_PUBLISH') config.subscribe_key = getenv('PN_KEY_SUBSCRIBE') config.secret_key = getenv('PN_KEY_SECRET') - config.cipher_key = getenv('PN_KEY_CIPHER') + config.cipher_key = getenv('PN_KEY_CIPHER', 'my_secret_cipher_key') config.user_id = 'experiment' config.cipher_mode = cipher_mode config.fallback_cipher_mode = fallback_cipher_mode @@ -57,5 +57,8 @@ def PNFactory(cipher_mode=AES.MODE_GCM, fallback_cipher_mode=AES.MODE_CBC) -> Pu pubnub.crypto = PubNubCryptoModule({ PubNubAesCbcCryptor.CRYPTOR_ID: PubNubAesCbcCryptor('myCipherKey') }, PubNubAesCbcCryptor) -encrypted = pubnub.crypto.encrypt('My Secret Text') # encrypted wih AES cryptor and `myCipherKey` cipher key + +text_to_encrypt = 'My Secret Text' +encrypted = pubnub.crypto.encrypt(text_to_encrypt) # encrypted wih AES cryptor and `myCipherKey` cipher key decrypted = pubnub.crypto.decrypt(encrypted) +print(f'Source: {text_to_encrypt}\nEncrypted: {encrypted}\nDecrypted: {decrypted}') diff --git a/examples/encrypted_publish.py b/examples/encrypted_publish.py new file mode 100644 index 00000000..ae4d45c3 --- /dev/null +++ b/examples/encrypted_publish.py @@ -0,0 +1,22 @@ +from os import getenv +from pubnub.pubnub import PubNub +from pubnub.pnconfiguration import PNConfiguration +from pubnub.crypto import AesCbcCryptoModule + +config = PNConfiguration() +config.publish_key = getenv('PUBLISH_KEY', 'demo') +config.subscribe_key = getenv('SUBSCRIBE_KEY', 'demo') +config.cipher_key = getenv('CIPHER_KEY', 'my_cipher_key') +config.uuid = 'example-python' +config.crypto_module = AesCbcCryptoModule(config) + +pubnub = PubNub(config) + +message = 'Plaintext_message' +if config.cipher_key and not config.crypto_module: + message = f'cryptodome({type(config.crypto)})' +if config.crypto_module: + message = f'crypto_module({type(config.crypto_module)})' + +pubnub.publish().channel('example').message(message).sync() +print(f'published: {message}') diff --git a/examples/fetch_messages.py b/examples/fetch_messages.py new file mode 100644 index 00000000..d859ad7b --- /dev/null +++ b/examples/fetch_messages.py @@ -0,0 +1,16 @@ + +from os import getenv +from pubnub.pubnub import PubNub +from pubnub.pnconfiguration import PNConfiguration + +config = PNConfiguration() +config.publish_key = getenv('PUBLISH_KEY', 'demo') +config.subscribe_key = getenv('SUBSCRIBE_KEY', 'demo') +config.cipher_key = getenv('CIPHER_KEY', 'my_cipher_key') +config.uuid = 'example' +config.cipher_key = "my_cipher_key" +pubnub = PubNub(config) + +messages = pubnub.fetch_messages().channels('example').count(30).decrypt_messages().sync() +for msg in messages.result.channels['example']: + print(msg.message, f' !! Error during decryption: {msg.error}' if msg.error else '') diff --git a/examples/pubnub_asyncio/subscribe.py b/examples/pubnub_asyncio/subscribe.py new file mode 100644 index 00000000..025398fe --- /dev/null +++ b/examples/pubnub_asyncio/subscribe.py @@ -0,0 +1,36 @@ +import asyncio + +from os import getenv +from pubnub.callbacks import SubscribeCallback +from pubnub.crypto import AesCbcCryptoModule +from pubnub.pnconfiguration import PNConfiguration +from pubnub.pubnub_asyncio import PubNubAsyncio + +config = PNConfiguration() +config.publish_key = getenv('PUBLISH_KEY', 'demo') +config.subscribe_key = getenv('SUBSCRIBE_KEY', 'demo') +config.cipher_key = getenv('CIPHER_KEY', 'my_cipher_key') +config.crypto_module = AesCbcCryptoModule(config) +config.uuid = 'example-python' +config.enable_subscribe = True + +pubnub = PubNubAsyncio(config) + + +class PrinterCallback(SubscribeCallback): + def status(self, pubnub, status): + print(status.category.name) + + def message(self, pubnub, message): + print(message.message) + + +async def main(): + pubnub.add_listener(PrinterCallback()) + pubnub.subscribe().channels("example").execute() + + await asyncio.sleep(500) + + +loop = asyncio.get_event_loop() +loop.run_until_complete(main()) diff --git a/pubnub/crypto.py b/pubnub/crypto.py index 9942573f..f61269f5 100644 --- a/pubnub/crypto.py +++ b/pubnub/crypto.py @@ -1,8 +1,7 @@ import hashlib import json -import random import logging - +import secrets from base64 import decodebytes, encodebytes, b64decode, b64encode from Cryptodome.Cipher import AES @@ -69,7 +68,7 @@ def extract_random_iv(self, message, use_random_iv): def get_initialization_vector(self, use_random_iv): if self.pubnub_configuration.use_random_initialization_vector or use_random_iv: - return "{0:016}".format(random.randint(0, 9999999999999999)) + return secrets.token_urlsafe(16)[:16] else: return Initial16bytes diff --git a/pubnub/crypto_core.py b/pubnub/crypto_core.py index 1b7b9cf0..33b38bbe 100644 --- a/pubnub/crypto_core.py +++ b/pubnub/crypto_core.py @@ -1,6 +1,5 @@ import hashlib import json -import random import secrets from abc import abstractmethod @@ -111,7 +110,7 @@ def extract_random_iv(self, message, use_random_iv): def get_initialization_vector(self, use_random_iv) -> bytes: if self.use_random_iv or use_random_iv: - return bytes("{0:016}".format(random.randint(0, 9999999999999999)), 'utf-8') + return secrets.token_bytes(16) else: return self.Initial16bytes diff --git a/pubnub/endpoints/fetch_messages.py b/pubnub/endpoints/fetch_messages.py index 14773a4b..b9da9f9f 100644 --- a/pubnub/endpoints/fetch_messages.py +++ b/pubnub/endpoints/fetch_messages.py @@ -33,6 +33,7 @@ def __init__(self, pubnub): self._include_message_actions = None self._include_message_type = None self._include_uuid = None + self._decrypt_messages = False def channels(self, channels): utils.extend_list(self._channels, channels) @@ -76,6 +77,10 @@ def include_uuid(self, include_uuid): self._include_uuid = include_uuid return self + def decrypt_messages(self, decrypt: bool = True): + self._decrypt_messages = decrypt + return self + def custom_params(self): params = {'max': int(self._count)} @@ -155,7 +160,8 @@ def create_response(self, envelope): # pylint: disable=W0221 json_input=envelope, include_message_actions=self._include_message_actions, start_timetoken=self._start, - end_timetoken=self._end) + end_timetoken=self._end, + crypto_module=self.pubnub.crypto if self._decrypt_messages else None) def request_timeout(self): return self.pubnub.config.non_subscribe_request_timeout diff --git a/pubnub/endpoints/pubsub/fire.py b/pubnub/endpoints/pubsub/fire.py index 0a9ac3db..547ee6b0 100644 --- a/pubnub/endpoints/pubsub/fire.py +++ b/pubnub/endpoints/pubsub/fire.py @@ -43,11 +43,15 @@ def meta(self, meta): def build_data(self): if self._use_post is True: - cipher = self.pubnub.config.cipher_key - if cipher is not None: - return '"' + self.pubnub.config.crypto.encrypt(cipher, utils.write_value_as_string(self._message)) + '"' - else: - return utils.write_value_as_string(self._message) + stringified_message = utils.write_value_as_string(self._message) + if self.pubnub.config.crypto_module: + stringified_message = '"' + self.pubnub.config.crypto_module.encrypt(stringified_message) + '"' + elif self.pubnub.config.cipher_key is not None: # The legacy way + stringified_message = '"' + self.pubnub.config.crypto.encrypt( + self.pubnub.config.cipher_key, + stringified_message) + '"' + + return stringified_message else: return None @@ -67,11 +71,13 @@ def build_path(self): self.pubnub.config.subscribe_key, utils.url_encode(self._channel), 0) else: - cipher = self.pubnub.config.cipher_key stringified_message = utils.write_value_as_string(self._message) - - if cipher is not None: - stringified_message = '"' + self.pubnub.config.crypto.encrypt(cipher, stringified_message) + '"' + if self.pubnub.config.crypto_module: + stringified_message = '"' + self.pubnub.config.crypto_module.encrypt(stringified_message) + '"' + elif self.pubnub.config.cipher_key is not None: # The legacy way + stringified_message = '"' + self.pubnub.config.crypto.encrypt( + self.pubnub.config.cipher_key, + stringified_message) + '"' stringified_message = utils.url_encode(stringified_message) diff --git a/pubnub/endpoints/pubsub/publish.py b/pubnub/endpoints/pubsub/publish.py index 8fe15ad2..3be282af 100644 --- a/pubnub/endpoints/pubsub/publish.py +++ b/pubnub/endpoints/pubsub/publish.py @@ -56,11 +56,15 @@ def ttl(self, ttl): def build_data(self): if self._use_post is True: - cipher = self.pubnub.config.cipher_key - if cipher is not None: - return '"' + self.pubnub.config.crypto.encrypt(cipher, utils.write_value_as_string(self._message)) + '"' - else: - return utils.write_value_as_string(self._message) + stringified_message = utils.write_value_as_string(self._message) + + if self.pubnub.config.crypto_module: + stringified_message = '"' + self.pubnub.config.crypto_module.encrypt(stringified_message) + '"' + elif self.pubnub.config.cipher_key is not None: # The legacy way + stringified_message = '"' + self.pubnub.config.crypto.encrypt( + self.pubnub.config.cipher_key, + stringified_message) + '"' + return stringified_message else: return None @@ -99,11 +103,13 @@ def build_path(self): self.pubnub.config.subscribe_key, utils.url_encode(self._channel), 0) else: - cipher = self.pubnub.config.cipher_key stringified_message = utils.write_value_as_string(self._message) - - if cipher is not None: - stringified_message = '"' + self.pubnub.config.crypto.encrypt(cipher, stringified_message) + '"' + if self.pubnub.config.crypto_module: + stringified_message = '"' + self.pubnub.config.crypto_module.encrypt(stringified_message) + '"' + elif self.pubnub.config.cipher_key is not None: # The legacy way + stringified_message = '"' + self.pubnub.config.crypto.encrypt( + self.pubnub.config.cipher_key, + stringified_message) + '"' stringified_message = utils.url_encode(stringified_message) diff --git a/pubnub/models/consumer/history.py b/pubnub/models/consumer/history.py index cbd5a637..9d421a44 100644 --- a/pubnub/models/consumer/history.py +++ b/pubnub/models/consumer/history.py @@ -1,4 +1,5 @@ import binascii +from pubnub.exceptions import PubNubException class PNHistoryResult(object): @@ -61,7 +62,7 @@ def decrypt(self, cipher_key): class PNFetchMessagesResult(object): - def __init__(self, channels, start_timetoken, end_timetoken): + def __init__(self, channels, start_timetoken, end_timetoken, error: Exception = None): self.channels = channels self.start_timetoken = start_timetoken self.end_timetoken = end_timetoken @@ -70,13 +71,23 @@ def __str__(self): return "Fetch messages result for range %d..%d" % (self.start_timetoken, self.end_timetoken) @classmethod - def from_json(cls, json_input, include_message_actions=False, start_timetoken=None, end_timetoken=None): + def from_json(cls, json_input, include_message_actions=False, start_timetoken=None, end_timetoken=None, + crypto_module=None): channels = {} for key, entry in json_input['channels'].items(): channels[key] = [] for item in entry: - message = PNFetchMessageItem(item['message'], item['timetoken']) + try: + error = None + item_message = crypto_module.decrypt(item['message']) if crypto_module else item['message'] + except Exception as decryption_error: + if type(decryption_error) not in [PubNubException, binascii.Error, ValueError]: + raise decryption_error + item_message = item['message'] + error = decryption_error + + message = PNFetchMessageItem(item_message, item['timetoken'], error=error) if 'uuid' in item: message.uuid = item['uuid'] if 'message_type' in item: @@ -101,11 +112,12 @@ def from_json(cls, json_input, include_message_actions=False, start_timetoken=No class PNFetchMessageItem(object): - def __init__(self, message, timetoken, meta=None, actions=None): + def __init__(self, message, timetoken, meta=None, actions=None, error: Exception = None): self.message = message self.meta = meta self.timetoken = timetoken self.actions = actions + self.error = error def __str__(self): return "Fetch message item with tt: %s and content: %s" % (self.timetoken, self.message) diff --git a/pubnub/pnconfiguration.py b/pubnub/pnconfiguration.py index 72d3ecfa..de00581d 100644 --- a/pubnub/pnconfiguration.py +++ b/pubnub/pnconfiguration.py @@ -136,8 +136,6 @@ def file_crypto(self) -> PubNubCrypto: @property def crypto_module(self): - if not self._crypto_module: - self._crypto_module = self.DEFAULT_CRYPTO_MODULE(self) return self._crypto_module @crypto_module.setter diff --git a/pubnub/pubnub_core.py b/pubnub/pubnub_core.py index 87c4a86a..e36abca1 100644 --- a/pubnub/pubnub_core.py +++ b/pubnub/pubnub_core.py @@ -137,7 +137,10 @@ def uuid(self): @property def crypto(self) -> PubNubCryptoModule: - return self.__crypto if self.__crypto else self.config.crypto_module + crypto_module = self.__crypto or self.config.crypto_module + if not crypto_module and self.config.cipher_key: + crypto_module = self.config.DEFAULT_CRYPTO_MODULE(self.config) + return crypto_module @crypto.setter def crypto(self, crypto: PubNubCryptoModule): diff --git a/scripts/run-tests.py b/scripts/run-tests.py index 9b6ee82b..80ff48a0 100755 --- a/scripts/run-tests.py +++ b/scripts/run-tests.py @@ -12,7 +12,6 @@ os.chdir(os.path.join(REPO_ROOT)) tcmn = 'py.test tests --cov=pubnub --ignore=tests/manual/' -tcmn_ee = 'PN_ENABLE_EVENT_ENGINE=True pytest tests/integrational/asyncio/' fcmn = 'flake8 --exclude=scripts/,src/,.cache,.git,.idea,.tox,._trial_temp/,venv/ --ignore F811,E402' @@ -21,6 +20,5 @@ def run(command): run(tcmn) -run(tcmn_ee) # moved to separate action # run(fcmn)