From b1742bfdb602d216632ffc350ef93cb39218a494 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dobrza=C5=84ski?= Date: Thu, 28 Sep 2023 17:39:08 +0200 Subject: [PATCH 01/18] =?UTF-8?q?fix:=20=C2=AF\=5F(=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pubnub/crypto/aes_cbc_cryptor.rb | 29 +++++++++++++++++++++ lib/pubnub/crypto/cryptor.rb | 13 ++++++++++ lib/pubnub/crypto/legacy_cryptor.rb | 39 ++++++++++++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 lib/pubnub/crypto/aes_cbc_cryptor.rb create mode 100644 lib/pubnub/crypto/cryptor.rb create mode 100644 lib/pubnub/crypto/legacy_cryptor.rb diff --git a/lib/pubnub/crypto/aes_cbc_cryptor.rb b/lib/pubnub/crypto/aes_cbc_cryptor.rb new file mode 100644 index 000000000..b73134d93 --- /dev/null +++ b/lib/pubnub/crypto/aes_cbc_cryptor.rb @@ -0,0 +1,29 @@ +module Pubnub::Crypto + class AesCbcCryptor + include Cryptor + BLOCK_SIZE = 16 + + def initialize(cipher_key) + @identifier = 'ACRH' + @alg = 'AES-256-CBC' + @cipher_key = cipher_key + end + + def encrypt(data) + cipher = OpenSSL::Cipher.new(@alg).encrypt + cipher.key = @cipher_key + cipher.iv = OpenSSL::Random.random_bytes BLOCK_SIZE + encoded_message = cipher.update data + encoded_message << cipher.final + end + + def decrypt(data, iv) + cipher = OpenSSL::Cipher.new(@alg).decrypt + cipher.key = @cipher_key + cipher.iv = iv + + decrypted = cipher.update data + decrypted << cipher.final + end + end +end diff --git a/lib/pubnub/crypto/cryptor.rb b/lib/pubnub/crypto/cryptor.rb new file mode 100644 index 000000000..79562e7f9 --- /dev/null +++ b/lib/pubnub/crypto/cryptor.rb @@ -0,0 +1,13 @@ +module Pubnub::Crypto::Cryptor + attr_reader :identifier + attr_reader :alg + attr_reader :cipher_key + + def encrypt(data) + raise 'Not Implemented' + end + + def decrypt(data, iv) + raise 'Not Implemented' + end +end \ No newline at end of file diff --git a/lib/pubnub/crypto/legacy_cryptor.rb b/lib/pubnub/crypto/legacy_cryptor.rb new file mode 100644 index 000000000..a083d92e3 --- /dev/null +++ b/lib/pubnub/crypto/legacy_cryptor.rb @@ -0,0 +1,39 @@ +module Pubnub::Crypto + class LegacyCryptor + include Cryptor + BLOCK_SIZE = 16 + + def initialize(cipher_key, use_random_iv) + @identifier = '' + @alg = 'AES-256-CBC' + @cipher_key = cipher_key + @iv = use_random_iv ? nil : '0123456789012345' + end + + def encrypt(data) + cipher = OpenSSL::Cipher.new(@alg).encrypt + cipher.key = @cipher_key + cipher.iv = !@iv.nil? ? @iv : OpenSSL::Random.random_bytes(BLOCK_SIZE) + encoded_message = cipher.update data + encoded_message << cipher.final + + Base64.strict_encode64(encoded_message) + end + + def decrypt(data, iv) + iv = @iv + + undecoded_text = Base64.strict_decode64(data) + if data.length > 16 && !@iv.nil? + iv = undecoded_text.slice!(0..15) + end + + cipher = OpenSSL::Cipher.new(@alg).decrypt + cipher.key = @cipher_key + cipher.iv = iv + + decrypted = cipher.update undecoded_text + decrypted << cipher.final + end + end +end From 1064993d926952f67f88138787858fbf09f38e66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Dobrza=C5=84ski?= Date: Thu, 28 Sep 2023 17:40:20 +0200 Subject: [PATCH 02/18] =?UTF-8?q?fix:=20=C2=AF\=5F(=E3=83=84)=5F/=C2=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/pubnub/crypto/aes_cbc_cryptor.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/pubnub/crypto/aes_cbc_cryptor.rb b/lib/pubnub/crypto/aes_cbc_cryptor.rb index b73134d93..4afd25c98 100644 --- a/lib/pubnub/crypto/aes_cbc_cryptor.rb +++ b/lib/pubnub/crypto/aes_cbc_cryptor.rb @@ -15,14 +15,18 @@ def encrypt(data) cipher.iv = OpenSSL::Random.random_bytes BLOCK_SIZE encoded_message = cipher.update data encoded_message << cipher.final + + Base64.strict_encode64(encoded_message) end def decrypt(data, iv) + undecoded_text = Base64.strict_decode64(data) + cipher = OpenSSL::Cipher.new(@alg).decrypt cipher.key = @cipher_key cipher.iv = iv - decrypted = cipher.update data + decrypted = cipher.update undecoded_text decrypted << cipher.final end end From 80398f616e161d7c4004320f366b48900d8a187f Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Fri, 29 Sep 2023 14:02:33 +0300 Subject: [PATCH 03/18] refactor(crypto): decouple crypto into module Decouple code which works with data encryption and decryption into separate module. --- lib/pubnub/client.rb | 18 ++ lib/pubnub/{crypto.rb => cryptor.rb} | 2 +- lib/pubnub/error.rb | 3 + lib/pubnub/event.rb | 18 +- lib/pubnub/events/history.rb | 7 +- lib/pubnub/formatter.rb | 26 +- lib/pubnub/modules/crypto/crypto_module.rb | 116 +++++++++ lib/pubnub/modules/crypto/crypto_provider.rb | 31 +++ lib/pubnub/modules/crypto/cryptor.rb | 72 ++++++ lib/pubnub/modules/crypto/cryptor_header.rb | 238 +++++++++++++++++++ lib/pubnub/subscribe_event/formatter.rb | 13 +- 11 files changed, 531 insertions(+), 13 deletions(-) rename lib/pubnub/{crypto.rb => cryptor.rb} (99%) create mode 100644 lib/pubnub/modules/crypto/crypto_module.rb create mode 100644 lib/pubnub/modules/crypto/crypto_provider.rb create mode 100644 lib/pubnub/modules/crypto/cryptor.rb create mode 100644 lib/pubnub/modules/crypto/cryptor_header.rb diff --git a/lib/pubnub/client.rb b/lib/pubnub/client.rb index 0614ef0e3..fac75ed31 100644 --- a/lib/pubnub/client.rb +++ b/lib/pubnub/client.rb @@ -183,6 +183,7 @@ def initialize(options) clean_env prepare_env validate! @env + setup_crypto_module @telemetry = Telemetry.new Pubnub.logger.debug('Pubnub::Client') do "Created new Pubnub::Client instance. Version: #{Pubnub::VERSION}" @@ -386,9 +387,26 @@ def setup_app(options) Concurrent.global_logger = Pubnub.logger @subscriber = Subscriber.new(self) options[:user_id] = options[:uuid] if options[:user_id].nil? + + if options[:cipher_key] && options[:crypto_module] + puts 'It is expected that only cipherKey or cryptoModule will be configured ' \ + 'at once. PubNub client will use the configured cryptoModule.' + end + @env = options end + # Complete crypto module configuration + # Create crypto module if it is required by user (specified + # cipher_key and not crypto_module). + def setup_crypto_module + random_iv = @env[:random_iv] + key = @env[:cipher_key] + + # Create crypto module if it is not specified + @env[:crypto_module] = CryptoModule.new_legacy(key, random_iv) if key && @env[:crypto_module].nil? + end + def prepare_env assign_defaults setup_pools diff --git a/lib/pubnub/crypto.rb b/lib/pubnub/cryptor.rb similarity index 99% rename from lib/pubnub/crypto.rb rename to lib/pubnub/cryptor.rb index 6402b97d8..b61d0b368 100644 --- a/lib/pubnub/crypto.rb +++ b/lib/pubnub/cryptor.rb @@ -1,7 +1,7 @@ # Toplevel Pubnub module. module Pubnub # Internal Crypto class used for message encryption and decryption - class Crypto + class Cryptor def initialize(cipher_key, use_random_iv) @alg = 'AES-256-CBC' sha256_key = Digest::SHA256.hexdigest(cipher_key.to_s) diff --git a/lib/pubnub/error.rb b/lib/pubnub/error.rb index 7d3510aff..19f2ec7ba 100644 --- a/lib/pubnub/error.rb +++ b/lib/pubnub/error.rb @@ -51,4 +51,7 @@ class RequestError < Error class ResponseError < Error end + + class UnknownCryptorError < Error + end end diff --git a/lib/pubnub/event.rb b/lib/pubnub/event.rb index 640cbb45a..06279ce48 100644 --- a/lib/pubnub/event.rb +++ b/lib/pubnub/event.rb @@ -113,12 +113,12 @@ def enable_format_group? def operation_http_method case @event when Pubnub::Constants::OPERATION_DELETE, Pubnub::Constants::OPERATION_REMOVE_MESSAGE_ACTION, - Pubnub::Constants::OPERATION_REMOVE_CHANNEL_METADATA, Pubnub::Constants::OPERATION_REMOVE_UUID_METADATA, - Pubnub::Constants::OPERATION_REVOKE_TOKEN + Pubnub::Constants::OPERATION_REMOVE_CHANNEL_METADATA, Pubnub::Constants::OPERATION_REMOVE_UUID_METADATA, + Pubnub::Constants::OPERATION_REVOKE_TOKEN 'delete' when Pubnub::Constants::OPERATION_SET_UUID_METADATA, Pubnub::Constants::OPERATION_SET_CHANNEL_METADATA, - Pubnub::Constants::OPERATION_SET_CHANNEL_MEMBERS, Pubnub::Constants::OPERATION_SET_MEMBERSHIPS, - Pubnub::Constants::OPERATION_REMOVE_CHANNEL_MEMBERS, Pubnub::Constants::OPERATION_REMOVE_MEMBERSHIPS + Pubnub::Constants::OPERATION_SET_CHANNEL_MEMBERS, Pubnub::Constants::OPERATION_SET_MEMBERSHIPS, + Pubnub::Constants::OPERATION_REMOVE_CHANNEL_MEMBERS, Pubnub::Constants::OPERATION_REMOVE_MEMBERSHIPS 'patch' when Pubnub::Constants::OPERATION_ADD_MESSAGE_ACTION 'post' @@ -170,7 +170,7 @@ def handle(response, request) def create_variables_from_options(options) variables = %w[channel channels message http_sync callback - ssl cipher_key random_iv secret_key auth_key + ssl cipher_key random_iv cryptor_module secret_key auth_key publish_key subscribe_key timetoken action_timetoken message_timetoken open_timeout read_timeout idle_timeout heartbeat group action read write delete manage ttl presence start @@ -217,6 +217,14 @@ def compute_random_iv(data) ck.call(data) end + # Data processing crypto module. + # + # @return [CryptoModule, nil] Crypto module for data encryption and + # decryption. + def crypto_module + @app.env[:crypto_module] + end + def error_message(parsed_response) parsed_response['message'] rescue StandardError diff --git a/lib/pubnub/events/history.rb b/lib/pubnub/events/history.rb index 2637f43e8..b9812956d 100644 --- a/lib/pubnub/events/history.rb +++ b/lib/pubnub/events/history.rb @@ -74,10 +74,15 @@ def decrypt_history(message, crypto) def valid_envelope(parsed_response, req_res_objects) messages = parsed_response[0] + # TODO: Uncomment code below when cryptor implementations will be added. + # if crypto_module && messages + # crypto = crypto_module + # messages = messages.map { |message| decrypt_history(message, crypto) } + # end if (@cipher_key || @app.env[:cipher_key] || @cipher_key_selector || @app.env[:cipher_key_selector]) && messages cipher_key = compute_cipher_key(parsed_response) random_iv = compute_random_iv(parsed_response) - crypto = Crypto.new(cipher_key, random_iv) + crypto = Cryptor.new(cipher_key, random_iv) messages = messages.map { |message| decrypt_history(message, crypto) } end diff --git a/lib/pubnub/formatter.rb b/lib/pubnub/formatter.rb index 6c7d2f27b..af86c4366 100644 --- a/lib/pubnub/formatter.rb +++ b/lib/pubnub/formatter.rb @@ -42,9 +42,9 @@ def format_uuid(uuids, should_encode = true) end # Transforms message to json and encode it - def format_message(message, cipher_key = "", use_random_iv = false, uri_escape = true) + def format_message(message, cipher_key = '', use_random_iv = false, uri_escape = true) if cipher_key && !cipher_key.empty? - pc = Pubnub::Crypto.new(cipher_key, use_random_iv) + pc = Pubnub::Cryptor.new(cipher_key, use_random_iv) message = pc.encrypt(message).to_json message = Addressable::URI.escape(message) if uri_escape else @@ -54,6 +54,26 @@ def format_message(message, cipher_key = "", use_random_iv = false, uri_escape = message end + # TODO: Uncomment code below when cryptor implementations will be added. + # Transforms message to json and encode it. + # + # @param message [Hash, String, Integer, Boolean] Message data which + # should be formatted. + # @param crypto [Crypto::CryptoProvider, nil] Crypto which should be used to + # encrypt message data. + # @param uri_escape [Boolean, nil] Whether formatted message should escape + # to be used as part of URI or not. + # @return [String, nil] Formatted message data. + # def format_message(message, crypto = nil, uri_escape = true) + # json_message = message.to_json + # json_message = crypto&.encrypt(json_message) || '' unless crypto.nil? + # if uri_escape + # json_message = Formatter.encode(json_message) if crypto.nil? + # json_message = Addressable::URI.escape(json_message).to_s unless crypto.nil? + # end + # json_message + # end + # Quite lazy way, but good enough for current usage def classify_method(method) method.split('_').map(&:capitalize).join @@ -100,7 +120,7 @@ def make_uuid_array(uuid) # Parses string to JSON def parse_json(string) [JSON.parse(string), nil] - rescue JSON::ParserError => _error + rescue JSON::ParserError => _e [nil, JSON::ParserError] end diff --git a/lib/pubnub/modules/crypto/crypto_module.rb b/lib/pubnub/modules/crypto/crypto_module.rb new file mode 100644 index 000000000..9a9ad82d8 --- /dev/null +++ b/lib/pubnub/modules/crypto/crypto_module.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +module Pubnub + # Crypto module for data processing. + # + # The PubNub client uses a module to encrypt and decrypt sent data in a way + # that's compatible with previous versions (if additional cryptors have been + # registered). + class CryptoModule < Crypto::CryptoProvider + # Legacy AES-CBC cryptor based module. + # + # Data encryption and decryption will be done by default using the + # LegacyCrypto. In addition to the LegacyCrypto for data + # decryption, the AesCbcCryptor will be registered for + # future-compatibility (which will help with gradual application updates). + # + # @param cipher_key [String] Key for data encryption and decryption. + # @param use_random_iv [Boolean] Whether random IV should be used for data decryption. + def self.new_aes_cbc(cipher_key, use_random_iv) + # TODO: Create AES-CBC based crypto module + end + + # Legacy AES-CBC cryptor based module. + # + # Data encryption and decryption will be done by default using the + # LegacyCrypto. In addition to the LegacyCrypto for data + # decryption, the AesCbcCryptor will be registered for + # future-compatibility (which will help with gradual application updates). + # + # @param cipher_key [String] Key for data encryption and decryption. + # @param use_random_iv [Boolean] Whether random IV should be used for data decryption. + def self.new_legacy(cipher_key, use_random_iv) + # TODO: Create legacy AES-CBC based crypto module + end + + # Create crypto module. + # @param default [Cryptor] Default cryptor used to encrypt and decrypt data. + # @param cryptors [Array, nil] Additional cryptors which will be + # used to decrypt data encrypted by previously used cryptors. + def initialize(default, cryptors) + if default.nil? + raise ArgumentError, { + message: '\'default\' cryptor required for data encryption.' + } + end + + @default = default + @cryptors = cryptors&.each_with_object({}) do |value, hash| + hash[value.identifier] = value + end || {} + end + + def encrypt(data) + # Encrypting provided data. + encrypted_data = default_cryptor.encrypt(data) + return nil if encrypted_data.nil? + + payload = Crypto::CryptorHeader.new(default_cryptor.identifier, encrypted_data.metadata).to_s + payload << encrypted_data.metadata unless encrypted_data.metadata.nil? + payload << encrypted_data.data + end + + def decrypt(data) + header = Crypto::CryptorHeader.parse(data) + cryptor = cryptor(header&.identifier || '\x00\x00\x00\x00') + + # Check whether there is a cryptor to decrypt data or not. + if cryptor.nil? + identifier = header&.identifier || 'UNKN' + raise UnknownCryptorError, { + message: "Decrypting data created by unknown cryptor. Please make sure to register +#{identifier} or update SDK." + } + end + + metadata_size = header&.data_size || 0 + metadata = metadata_size.positive? ? data[0..metadata_size] : nil + data = data[metadata_size..-1] + + return nil if data.nil? + + cryptor.decrypt(EncryptedData.new(data, metadata)) + end + + private + + # Cryptor used by the module by default to encrypt data. + # + # @return [Cryptor] Default cryptor used to encrypt and decrypt data. + def default_cryptor + @default + end + + # Additional cryptors that can be used to decrypt data if the + # default_cryptor can't. + # + # @return [Hash] Map of Cryptor to their identifiers. + def additional_cryptors + @cryptors + end + + # Find cryptor with a specified identifier. + # + # Data decryption can only be done with registered cryptors. An identifier + # in the cryptor data header is used to identify a suitable cryptor. + # + # @param identifier [String] A unicode cryptor identifier. + # @return [Cryptor, nil] Target cryptor or `nil` in case there is none with + # the specified identifier. + def cryptor(identifier) + return default_cryptor if default_cryptor.identifier == identifier + + additional_cryptors.fetch(identifier, nil) + end + end +end \ No newline at end of file diff --git a/lib/pubnub/modules/crypto/crypto_provider.rb b/lib/pubnub/modules/crypto/crypto_provider.rb new file mode 100644 index 000000000..12d1f411e --- /dev/null +++ b/lib/pubnub/modules/crypto/crypto_provider.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Pubnub + module Crypto + # Base class which is used to implement a module that can be used to + # configure PubNub client or for manual data encryption and + # decryption. + class CryptoProvider + # Encrypt provided data. + # + # @param data [String] Source data for encryption. + # @return [String, nil] Encrypted data or nil in case of encryption + # error. + def encrypt(data) + raise NotImplementedError, 'Subclass should provide "encrypt" method implementation.' + end + + # Decrypt provided data. + # + # @param data [String] Encrypted data for decryption. + # @return [String, nil] Decrypted data or nil in case of decryption + # error. + # + # @raise [UnknownCryptorError] If the cryptor for data processing is + # not registered. + def decrypt(data) + raise NotImplementedError, 'Subclass should provide "decrypt" method implementation.' + end + end + end +end diff --git a/lib/pubnub/modules/crypto/cryptor.rb b/lib/pubnub/modules/crypto/cryptor.rb new file mode 100644 index 000000000..1e93944d5 --- /dev/null +++ b/lib/pubnub/modules/crypto/cryptor.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module Pubnub + module Crypto + # Encrypted data representation object. + # + # Objects contain both encrypted data and additional data created by cryptor + # that will be required to decrypt the data. + class EncryptedData + # Cryptor may provide here any information which will be useful when data + # should be decrypted. + # + # For example metadata may contain: + # * initialization vector + # * cipher key identifier + # * encrypted data length + # + # @return [String, nil] Cryptor-defined information. + attr_reader :metadata + + # Encrypted data. + # + # @return [String] Encrypted data. + attr_reader :data + + # Create encrypted data object. + # + # An object used to keep track of the results of data encryption and the + # additional data the cryptor needs to handle it later. + # + # @param data [String] Outcome of successful cryptor encrypt method + # call. + # @param metadata [String, nil] Additional information is provided by + # cryptor so that encrypted data can be handled later. + def initialize(data, metadata = nil) + @data = data + @metadata = metadata + end + end + + # Base class which is used to implement cryptor that should be used with + # CryptorProvider implementation for data encryption and decryption. + class Cryptor + # Identifier will be encoded into cryptor data header and passed along + # with encrypted data and metadata. + # + # The identifier must be 4 bytes long. + # @return [String] Unique cryptor identifier. + def identifier + raise NotImplementedError, 'Subclass should provide "identifier" method implementation.' + end + + # Encrypt provided data. + # + # @param data [String] Source data for encryption. + # @return [EncryptedData, nil] Encrypted data or nil in case of + # encryption error. + def encrypt(data) + raise NotImplementedError, 'Subclass should provide "encrypt" method implementation.' + end + + # Decrypt provided data. + # + # @param data [EncryptedData] Encrypted data for decryption. + # @return [String, nil] Decrypted data or nil in case of decryption + # error. + def decrypt(data) + raise NotImplementedError, 'Subclass should provide "decrypt" method implementation.' + end + end + end +end diff --git a/lib/pubnub/modules/crypto/cryptor_header.rb b/lib/pubnub/modules/crypto/cryptor_header.rb new file mode 100644 index 000000000..8992c4db1 --- /dev/null +++ b/lib/pubnub/modules/crypto/cryptor_header.rb @@ -0,0 +1,238 @@ +# frozen_string_literal: true + +module Pubnub + module Crypto + # Cryptor data header. + # + # This instance used to parse header from received data and encode into + # binary for sending. + class CryptorHeader + module Versions + # Currently used cryptor data header schema version. + CURRENT_VERSION = 1 + + # Base class for cryptor data schema. + class CryptorHeaderData + # Cryptor header version. + # + # @return [Integer] Cryptor header version. + def version + raise NotImplementedError, 'Subclass should provide "version" method implementation.' + end + + # Cryptor identifier. + # + # @return [String] Identifier of the cryptor which has been used to + # encrypt data. + def identifier + raise NotImplementedError, 'Subclass should provide "identifier" method implementation.' + end + + # Cryptor-defined data size. + # + # @return [Integer] Cryptor-defined data size. + def data_size + raise NotImplementedError, 'Subclass should provide "data_size" method implementation.' + end + end + + # v1 cryptor header schema. + # + # This header consists of: + # * sentinel (4 bytes) + # * version (1 byte) + # * cryptor identifier (4 bytes) + # * cryptor data size (1 byte if less than 255 and 3 bytes in other cases) + # * cryptor-defined data + class CryptorHeaderV1Data < CryptorHeaderData + # Identifier of the cryptor which has been used to encrypt data. + # + # @return [String] Identifier of the cryptor which has been used to + # encrypt data. + attr_reader :identifier + + # Cryptor-defined data size. + # + # @return [Integer] Cryptor-defined data size. + attr_reader :data_size + + # Create cryptor header data. + # + # @param identifier [String] Identifier of the cryptor which has been + # used to encrypt data. + # @param data_size [Integer] Cryptor-defined data size. + def initialize(identifier, data_size) + @identifier = identifier + @data_size = data_size + end + + def version + 1 + end + end + end + + # Create cryptor header. + # + # @param identifier [String, nil] Identifier of the cryptor which has been + # used to encrypt data. + # @param metadata [String, nil] Cryptor-defined information. + def initialize(identifier = nil, metadata = nil) + @data = if identifier + Versions::CryptorHeaderV1Data.new( + identifier.to_s, + metadata&.length || 0 + ) + end + end + + # Parse cryptor header data to create instance. + # + # @param data [String] Data which may contain cryptor header + # information. + # @return [CryptorHeader, nil] Header instance or nil in case of + # encrypted data parse error. + # + # @raise [ArgumentError] Raise an exception if data is nil + # or empty. + # @raise [UnknownCryptorError] Raise an exception if, during cryptor + # header data parsing, an unknown cryptor header version is encountered. + def self.parse(data) + if data.nil? || data.empty? + raise ArgumentError, { + message: '\'data\' is required and should not be empty.' + } + end + + # Data is too short to be encrypted. Assume legacy cryptor without + # header. + return CryptorHeader.new if data.length < 4 || data.unpack('A4').last != 'PNED' + + # Malformed crypto header. + return nil if data.length < 10 + + # Unpack header bytes. + _, version, identifier, data_size = data.unpack('A4 C A4 C') + + # Check whether version is within known range. + if version > CryptorHeader.current_version + raise UnknownCryptorError, { + message: 'Decrypting data created by unknown cryptor.' + } + end + + if data_size == 255 + data_size = data.unpack('A4 C A4 C n').last if data.length >= 12 + return CryptorHeader.new if data.length < 12 + end + + header = CryptorHeader.new + header.send( + :update_header_data, + CryptorHeader.create_header_data(version.to_i, identifier.to_s, data_size.to_i) + ) + header + end + + # Crypto header version Version module. + # + # @return [Integer] One of known versions from Version module. + def version + header_data&.version || 0 + end + + # Identifier of the cryptor which has been used to encrypt data. + # + # @return [String, nil] Identifier of the cryptor which has been used to + # encrypt data. + def identifier + header_data&.identifier || nil + end + + # Cryptor-defined information size. + # + # @return [Integer] Cryptor-defined information size. + def data_size + header_data&.data_size || 0 + end + + # Create cryptor header data object. + # + # @param version [Integer] Cryptor header data schema version. + # @param identifier [String] Encrypting cryptor identifier. + # @param size [Integer] Cryptor-defined data size + # @return [Versions::CryptorHeaderData] Cryptor header data. + def self.create_header_data(version, identifier, size) + Versions::CryptorHeaderV1Data.new(identifier, size) if version == 1 + end + + # Crypto header which is currently used to encrypt data. + # + # @return [Integer] Current cryptor header version. + def self.current_version + Versions::CURRENT_VERSION + end + + # Serialize cryptor header. + # + # @return [String] Cryptor header data, which is serialized as a binary + # string. + # + # @raise [ArgumentError] Raise an exception if a cryptor identifier + # is not provided for a non-legacy cryptor. + def to_s + # We don't need to serialize header for legacy cryptor. + return '' if version.zero? + + cryptor_identifier = identifier + if cryptor_identifier.nil? || cryptor_identifier.empty? + raise ArgumentError, { + message: '\'identifier\' is required for encryption' + } + end + + header_bytes = ['PNED', version, cryptor_identifier] + if data_size < 255 + header_bytes.push(data_size) + else + header_bytes.push(255, data_size) + end + + header_bytes.pack(data_size < 255 ? 'A4 C A4 C' : 'A4 C A4 C n') + end + + private + + # Versioned cryptor header data + # + # @return [Versions::CryptorHeaderData, nil] Cryptor header data. + def header_data + @data + end + + # Update crypto header version. + # + # @param data [Versions::CryptorHeaderData] Header version number parsed from binary data. + def update_header_data(data) + @data = data + end + + # Update crypto header version. + # + # @param value [Integer] Header version number parsed from binary data. + def update_version(value) + @version = value + end + + # Update cryptor-defined data size. + # + # @param value [Integer] Cryptor-defined data size parsed from binary + # data. + def update_data_size(value) + @data_size = value + end + + private_class_method :create_header_data, :current_version + end + end +end diff --git a/lib/pubnub/subscribe_event/formatter.rb b/lib/pubnub/subscribe_event/formatter.rb index 3d64d77c6..3f46aa9aa 100644 --- a/lib/pubnub/subscribe_event/formatter.rb +++ b/lib/pubnub/subscribe_event/formatter.rb @@ -33,13 +33,19 @@ def build_error_envelopes(_parsed_response, error, req_res_objects) end def decipher_payload(message) + # TODO: Uncomment code below when cryptor implementations will be added. + # return message[:payload] if message[:channel].end_with?('-pnpres') || crypto_module.nil? + # + # crypto = crypto_module + # JSON.parse(crypto.decrypt(message[:payload]), quirks_mode: true) + return message[:payload] if message[:channel].end_with?('-pnpres') || (@app.env[:cipher_key].nil? && @cipher_key.nil? && @cipher_key_selector.nil? && @env[:cipher_key_selector].nil?) data = message.reject { |k, _v| k == :payload } cipher_key = compute_cipher_key(data) random_iv = compute_random_iv(data) - crypto = Pubnub::Crypto.new(cipher_key, random_iv) + crypto = Pubnub::Cryptor.new(cipher_key, random_iv) JSON.parse(crypto.decrypt(message[:payload]), quirks_mode: true) - rescue StandardError + rescue StandardError, UnknownCryptorError message[:payload] end @@ -51,7 +57,8 @@ def build_non_error_envelopes(parsed_response, req_res_objects) # STATUS envelopes = if messages.empty? [plain_envelope(req_res_objects, timetoken)] - else # RESULT + else + # RESULT messages.map do |message| encrypted_envelope(req_res_objects, message, timetoken) end From 0d642c5bec52ed621309a1dd1d62a4cb87946af9 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Fri, 29 Sep 2023 14:09:29 +0300 Subject: [PATCH 04/18] fix(crypto): revert crypto module name --- lib/pubnub/{cryptor.rb => crypto.rb} | 2 +- lib/pubnub/events/history.rb | 2 +- lib/pubnub/formatter.rb | 2 +- lib/pubnub/subscribe_event/formatter.rb | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename lib/pubnub/{cryptor.rb => crypto.rb} (99%) diff --git a/lib/pubnub/cryptor.rb b/lib/pubnub/crypto.rb similarity index 99% rename from lib/pubnub/cryptor.rb rename to lib/pubnub/crypto.rb index b61d0b368..6402b97d8 100644 --- a/lib/pubnub/cryptor.rb +++ b/lib/pubnub/crypto.rb @@ -1,7 +1,7 @@ # Toplevel Pubnub module. module Pubnub # Internal Crypto class used for message encryption and decryption - class Cryptor + class Crypto def initialize(cipher_key, use_random_iv) @alg = 'AES-256-CBC' sha256_key = Digest::SHA256.hexdigest(cipher_key.to_s) diff --git a/lib/pubnub/events/history.rb b/lib/pubnub/events/history.rb index b9812956d..e03f705c4 100644 --- a/lib/pubnub/events/history.rb +++ b/lib/pubnub/events/history.rb @@ -82,7 +82,7 @@ def valid_envelope(parsed_response, req_res_objects) if (@cipher_key || @app.env[:cipher_key] || @cipher_key_selector || @app.env[:cipher_key_selector]) && messages cipher_key = compute_cipher_key(parsed_response) random_iv = compute_random_iv(parsed_response) - crypto = Cryptor.new(cipher_key, random_iv) + crypto = Crypto.new(cipher_key, random_iv) messages = messages.map { |message| decrypt_history(message, crypto) } end diff --git a/lib/pubnub/formatter.rb b/lib/pubnub/formatter.rb index af86c4366..423b20c8f 100644 --- a/lib/pubnub/formatter.rb +++ b/lib/pubnub/formatter.rb @@ -44,7 +44,7 @@ def format_uuid(uuids, should_encode = true) # Transforms message to json and encode it def format_message(message, cipher_key = '', use_random_iv = false, uri_escape = true) if cipher_key && !cipher_key.empty? - pc = Pubnub::Cryptor.new(cipher_key, use_random_iv) + pc = Pubnub::Crypto.new(cipher_key, use_random_iv) message = pc.encrypt(message).to_json message = Addressable::URI.escape(message) if uri_escape else diff --git a/lib/pubnub/subscribe_event/formatter.rb b/lib/pubnub/subscribe_event/formatter.rb index 3f46aa9aa..633c9f328 100644 --- a/lib/pubnub/subscribe_event/formatter.rb +++ b/lib/pubnub/subscribe_event/formatter.rb @@ -43,7 +43,7 @@ def decipher_payload(message) data = message.reject { |k, _v| k == :payload } cipher_key = compute_cipher_key(data) random_iv = compute_random_iv(data) - crypto = Pubnub::Cryptor.new(cipher_key, random_iv) + crypto = Pubnub::Crypto.new(cipher_key, random_iv) JSON.parse(crypto.decrypt(message[:payload]), quirks_mode: true) rescue StandardError, UnknownCryptorError message[:payload] From 486c2e897a658434ca5e66e5b321737ef17c7a19 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Fri, 29 Sep 2023 14:16:57 +0300 Subject: [PATCH 05/18] fix: use proper module when initialize CryptoModule --- lib/pubnub/client.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/pubnub/client.rb b/lib/pubnub/client.rb index fac75ed31..d9e082c21 100644 --- a/lib/pubnub/client.rb +++ b/lib/pubnub/client.rb @@ -402,9 +402,9 @@ def setup_app(options) def setup_crypto_module random_iv = @env[:random_iv] key = @env[:cipher_key] - + # Create crypto module if it is not specified - @env[:crypto_module] = CryptoModule.new_legacy(key, random_iv) if key && @env[:crypto_module].nil? + @env[:crypto_module] = Crypto::CryptoModule.new_legacy(key, random_iv) if key && @env[:crypto_module].nil? end def prepare_env From ec5759cef535ada98a4558e4db50131c16340513 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Sat, 30 Sep 2023 00:15:52 +0300 Subject: [PATCH 06/18] fix(crypto): fix issues discovered with unit tests --- lib/pubnub/client.rb | 14 +- lib/pubnub/crypto/aes_cbc_cryptor.rb | 33 --- lib/pubnub/crypto/cryptor.rb | 13 - lib/pubnub/crypto/legacy_cryptor.rb | 39 --- lib/pubnub/event.rb | 6 +- lib/pubnub/events/add_message_action.rb | 4 +- lib/pubnub/events/grant_token.rb | 2 +- lib/pubnub/events/history.rb | 27 ++- lib/pubnub/events/publish.rb | 10 +- lib/pubnub/events/remove_channel_members.rb | 6 +- lib/pubnub/events/remove_channel_metadata.rb | 2 +- lib/pubnub/events/remove_memberships.rb | 6 +- lib/pubnub/events/remove_uuid_metadata.rb | 2 +- lib/pubnub/events/set_channel_members.rb | 6 +- lib/pubnub/events/set_channel_metadata.rb | 4 +- lib/pubnub/events/set_memberships.rb | 6 +- lib/pubnub/events/set_uuid_metadata.rb | 4 +- lib/pubnub/events/signal.rb | 2 +- lib/pubnub/events/subscribe.rb | 5 + lib/pubnub/formatter.rb | 44 ++-- lib/pubnub/modules/crypto/crypto_module.rb | 228 ++++++++++-------- lib/pubnub/modules/crypto/cryptor.rb | 1 + lib/pubnub/modules/crypto/cryptor_header.rb | 25 +- .../crypto/cryptors/aes_cbc_cryptor.rb | 63 +++++ .../modules/crypto/cryptors/legacy_cryptor.rb | 76 ++++++ lib/pubnub/modules/crypto/module.rb | 8 + lib/pubnub/subscribe_event/formatter.rb | 18 +- spec/lib/multiple_ciphers_spec.rb | 2 + 28 files changed, 402 insertions(+), 254 deletions(-) delete mode 100644 lib/pubnub/crypto/aes_cbc_cryptor.rb delete mode 100644 lib/pubnub/crypto/cryptor.rb delete mode 100644 lib/pubnub/crypto/legacy_cryptor.rb create mode 100644 lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb create mode 100644 lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb create mode 100644 lib/pubnub/modules/crypto/module.rb diff --git a/lib/pubnub/client.rb b/lib/pubnub/client.rb index d9e082c21..21f7ff186 100644 --- a/lib/pubnub/client.rb +++ b/lib/pubnub/client.rb @@ -1,13 +1,17 @@ +# frozen_string_literal: true + require 'base64' require 'pubnub/error' require 'pubnub/uuid' require 'pubnub/formatter' -require 'pubnub/crypto' require 'pubnub/constants' require 'pubnub/configuration' require 'pubnub/subscribe_callback' +# require 'pubnub/crypto' +require 'pubnub/modules/crypto/module' + require 'pubnub/schemas/envelope_schema' require 'pubnub/event' @@ -340,6 +344,14 @@ def set_token(token) @env[:token] = token end + # Data processing crypto module. + # + # @return [Pubnub::Crypto::CryptoProvider, nil] Crypto module for data encryption and + # decryption. + def crypto_module + @env[:crypto_module] + end + private def create_state_pools(event) diff --git a/lib/pubnub/crypto/aes_cbc_cryptor.rb b/lib/pubnub/crypto/aes_cbc_cryptor.rb deleted file mode 100644 index 4afd25c98..000000000 --- a/lib/pubnub/crypto/aes_cbc_cryptor.rb +++ /dev/null @@ -1,33 +0,0 @@ -module Pubnub::Crypto - class AesCbcCryptor - include Cryptor - BLOCK_SIZE = 16 - - def initialize(cipher_key) - @identifier = 'ACRH' - @alg = 'AES-256-CBC' - @cipher_key = cipher_key - end - - def encrypt(data) - cipher = OpenSSL::Cipher.new(@alg).encrypt - cipher.key = @cipher_key - cipher.iv = OpenSSL::Random.random_bytes BLOCK_SIZE - encoded_message = cipher.update data - encoded_message << cipher.final - - Base64.strict_encode64(encoded_message) - end - - def decrypt(data, iv) - undecoded_text = Base64.strict_decode64(data) - - cipher = OpenSSL::Cipher.new(@alg).decrypt - cipher.key = @cipher_key - cipher.iv = iv - - decrypted = cipher.update undecoded_text - decrypted << cipher.final - end - end -end diff --git a/lib/pubnub/crypto/cryptor.rb b/lib/pubnub/crypto/cryptor.rb deleted file mode 100644 index 79562e7f9..000000000 --- a/lib/pubnub/crypto/cryptor.rb +++ /dev/null @@ -1,13 +0,0 @@ -module Pubnub::Crypto::Cryptor - attr_reader :identifier - attr_reader :alg - attr_reader :cipher_key - - def encrypt(data) - raise 'Not Implemented' - end - - def decrypt(data, iv) - raise 'Not Implemented' - end -end \ No newline at end of file diff --git a/lib/pubnub/crypto/legacy_cryptor.rb b/lib/pubnub/crypto/legacy_cryptor.rb deleted file mode 100644 index a083d92e3..000000000 --- a/lib/pubnub/crypto/legacy_cryptor.rb +++ /dev/null @@ -1,39 +0,0 @@ -module Pubnub::Crypto - class LegacyCryptor - include Cryptor - BLOCK_SIZE = 16 - - def initialize(cipher_key, use_random_iv) - @identifier = '' - @alg = 'AES-256-CBC' - @cipher_key = cipher_key - @iv = use_random_iv ? nil : '0123456789012345' - end - - def encrypt(data) - cipher = OpenSSL::Cipher.new(@alg).encrypt - cipher.key = @cipher_key - cipher.iv = !@iv.nil? ? @iv : OpenSSL::Random.random_bytes(BLOCK_SIZE) - encoded_message = cipher.update data - encoded_message << cipher.final - - Base64.strict_encode64(encoded_message) - end - - def decrypt(data, iv) - iv = @iv - - undecoded_text = Base64.strict_decode64(data) - if data.length > 16 && !@iv.nil? - iv = undecoded_text.slice!(0..15) - end - - cipher = OpenSSL::Cipher.new(@alg).decrypt - cipher.key = @cipher_key - cipher.iv = iv - - decrypted = cipher.update undecoded_text - decrypted << cipher.final - end - end -end diff --git a/lib/pubnub/event.rb b/lib/pubnub/event.rb index 06279ce48..42c0f5206 100644 --- a/lib/pubnub/event.rb +++ b/lib/pubnub/event.rb @@ -170,7 +170,7 @@ def handle(response, request) def create_variables_from_options(options) variables = %w[channel channels message http_sync callback - ssl cipher_key random_iv cryptor_module secret_key auth_key + ssl cipher_key random_iv crypto_module secret_key auth_key publish_key subscribe_key timetoken action_timetoken message_timetoken open_timeout read_timeout idle_timeout heartbeat group action read write delete manage ttl presence start @@ -219,10 +219,10 @@ def compute_random_iv(data) # Data processing crypto module. # - # @return [CryptoModule, nil] Crypto module for data encryption and + # @return [Pubnub::Crypto::CryptoProvider, nil] Crypto module for data encryption and # decryption. def crypto_module - @app.env[:crypto_module] + @crypto_module end def error_message(parsed_response) diff --git a/lib/pubnub/events/add_message_action.rb b/lib/pubnub/events/add_message_action.rb index 874811429..176452859 100644 --- a/lib/pubnub/events/add_message_action.rb +++ b/lib/pubnub/events/add_message_action.rb @@ -13,8 +13,8 @@ def initialize(options, app) def fire Pubnub.logger.debug('Pubnub::Add Message Action') { "Fired event #{self.class}" } - type_payload = { type: @type, value: @value} - body = Formatter.format_message(type_payload, '', false, false) + type_payload = { type: @type, value: @value } + body = Formatter.format_message(type_payload, nil, false) response = send_request(body, { "Content-Type": 'application/json' }) diff --git a/lib/pubnub/events/grant_token.rb b/lib/pubnub/events/grant_token.rb index dc474eb1c..611defa4f 100644 --- a/lib/pubnub/events/grant_token.rb +++ b/lib/pubnub/events/grant_token.rb @@ -34,7 +34,7 @@ def fire patterns: prepare_permissions(:pattern, @channels, @channel_groups, @uuids, @spaces_permissions, @users_permissions) }.select { |_, v| v } } - body = Formatter.format_message(raw_body, "", false, false) + body = Formatter.format_message(raw_body, nil, false) response = send_request(body, { "Content-Type": "application/json" }) envelopes = fire_callbacks(handle(response, uri)) diff --git a/lib/pubnub/events/history.rb b/lib/pubnub/events/history.rb index e03f705c4..5961080dc 100644 --- a/lib/pubnub/events/history.rb +++ b/lib/pubnub/events/history.rb @@ -8,6 +8,11 @@ class History < SingleEvent def initialize(options, app) @event = :history @telemetry_name = :l_hist + + # Override crypto module if custom cipher key has been used. + random_iv = options.key?(:random_iv) ? options[:random_iv] : true + options[:crypto_module] = Crypto::CryptoModule.new_legacy(options[:cipher_key], random_iv) if options[:cipher_key] + super end @@ -63,11 +68,13 @@ def parameters(*_args) def decrypt_history(message, crypto) if @include_token || @include_meta - message['message'] = JSON.parse(crypto.decrypt(message['message']), quirks_mode: true) + encrypted_message = Base64.decode64(message['message']) + message['message'] = JSON.parse(crypto.decrypt(encrypted_message), quirks_mode: true) message else - JSON.parse(crypto.decrypt(message), quirks_mode: true) + encrypted_message = Base64.decode64(message) + JSON.parse(crypto.decrypt(encrypted_message), quirks_mode: true) end end @@ -75,16 +82,16 @@ def valid_envelope(parsed_response, req_res_objects) messages = parsed_response[0] # TODO: Uncomment code below when cryptor implementations will be added. - # if crypto_module && messages - # crypto = crypto_module - # messages = messages.map { |message| decrypt_history(message, crypto) } - # end - if (@cipher_key || @app.env[:cipher_key] || @cipher_key_selector || @app.env[:cipher_key_selector]) && messages - cipher_key = compute_cipher_key(parsed_response) - random_iv = compute_random_iv(parsed_response) - crypto = Crypto.new(cipher_key, random_iv) + if crypto_module && messages + crypto = crypto_module messages = messages.map { |message| decrypt_history(message, crypto) } end + # if (@cipher_key || @app.env[:cipher_key] || @cipher_key_selector || @app.env[:cipher_key_selector]) && messages + # cipher_key = compute_cipher_key(parsed_response) + # random_iv = compute_random_iv(parsed_response) + # crypto = Crypto.new(cipher_key, random_iv) + # messages = messages.map { |message| decrypt_history(message, crypto) } + # end start = parsed_response[1] finish = parsed_response[2] diff --git a/lib/pubnub/events/publish.rb b/lib/pubnub/events/publish.rb index d32eb69f5..34c51ffd5 100644 --- a/lib/pubnub/events/publish.rb +++ b/lib/pubnub/events/publish.rb @@ -10,6 +10,11 @@ class Publish < SingleEvent def initialize(options, app) @event = :publish @telemetry_name = :l_pub + + # Override crypto module if custom cipher key has been used. + random_iv = options.key?(:random_iv) ? options[:random_iv] : true + options[:crypto_module] = Crypto::CryptoModule.new_legacy(options[:cipher_key], random_iv) if options[:cipher_key] + super @sequence_number = sequence_number! @origination_time_token = @app.generate_ortt @@ -25,9 +30,8 @@ def initialize(options, app) def fire Pubnub.logger.debug('Pubnub::Publish') { "Fired event #{self.class}" } - if @compressed - compressed_body = Formatter.format_message(@message, @cipher_key, @random_iv, false) + compressed_body = Formatter.format_message(@message, @crypto_module, false) response = send_request(compressed_body) else response = send_request @@ -72,7 +76,7 @@ def path '0', Formatter.format_channel(@channel, true), '0', - Formatter.format_message(@message, @cipher_key, @random_iv) + Formatter.format_message(@message, @crypto_module) ] rpath.pop if @compressed diff --git a/lib/pubnub/events/remove_channel_members.rb b/lib/pubnub/events/remove_channel_members.rb index 90165c130..3fdf4b285 100644 --- a/lib/pubnub/events/remove_channel_members.rb +++ b/lib/pubnub/events/remove_channel_members.rb @@ -42,7 +42,7 @@ def fire { uuid: { id: member } } end - body = Formatter.format_message({ delete: members }, "", @random_iv, false) + body = Formatter.format_message({ delete: members }, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) @@ -83,11 +83,11 @@ def path def valid_envelope(parsed_response, req_res_objects) members = parsed_response['data'].map { |channel_member| member = Hash.new - channel_member.each{ |k,v| member[k.to_sym] = v } + channel_member.each { |k, v| member[k.to_sym] = v } unless member[:uuid].nil? uuid_metadata = Hash.new - member[:uuid].each{ |k,v| uuid_metadata[k.to_sym] = v } + member[:uuid].each { |k, v| uuid_metadata[k.to_sym] = v } uuid_metadata[:updated] = Date._parse(uuid_metadata[:updated]) unless uuid_metadata[:updated].nil? member[:uuid] = uuid_metadata end diff --git a/lib/pubnub/events/remove_channel_metadata.rb b/lib/pubnub/events/remove_channel_metadata.rb index 9f05286b0..76ea84319 100644 --- a/lib/pubnub/events/remove_channel_metadata.rb +++ b/lib/pubnub/events/remove_channel_metadata.rb @@ -17,7 +17,7 @@ def initialize(options, app) def fire Pubnub.logger.debug('Pubnub::RemoveChannelMetadata') { "Fired event #{self.class}" } - body = Formatter.format_message(@data, "", @random_iv, false) + body = Formatter.format_message(@data, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) diff --git a/lib/pubnub/events/remove_memberships.rb b/lib/pubnub/events/remove_memberships.rb index 6e3ea08c7..1baeda46e 100644 --- a/lib/pubnub/events/remove_memberships.rb +++ b/lib/pubnub/events/remove_memberships.rb @@ -42,7 +42,7 @@ def fire { channel: { id: membership } } end - body = Formatter.format_message({ delete: memberships }, "", @random_iv, false) + body = Formatter.format_message({ delete: memberships }, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) @@ -83,11 +83,11 @@ def path def valid_envelope(parsed_response, req_res_objects) memberships = parsed_response['data'].map { |uuid_membership| membership = Hash.new - uuid_membership.each{ |k,v| membership[k.to_sym] = v } + uuid_membership.each { |k, v| membership[k.to_sym] = v } unless membership[:channel].nil? channel_metadata = Hash.new - membership[:channel].each{ |k,v| channel_metadata[k.to_sym] = v } + membership[:channel].each { |k, v| channel_metadata[k.to_sym] = v } channel_metadata[:updated] = Date._parse(channel_metadata[:updated]) unless channel_metadata[:updated].nil? membership[:channel] = channel_metadata end diff --git a/lib/pubnub/events/remove_uuid_metadata.rb b/lib/pubnub/events/remove_uuid_metadata.rb index fe8a8aaff..efd6e2c89 100644 --- a/lib/pubnub/events/remove_uuid_metadata.rb +++ b/lib/pubnub/events/remove_uuid_metadata.rb @@ -17,7 +17,7 @@ def initialize(options, app) def fire Pubnub.logger.debug('Pubnub::RemoveUuidMetadata') { "Fired event #{self.class}" } - body = Formatter.format_message(@data, "", @random_iv, false) + body = Formatter.format_message(@data, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) diff --git a/lib/pubnub/events/set_channel_members.rb b/lib/pubnub/events/set_channel_members.rb index c21b31a94..8db950152 100644 --- a/lib/pubnub/events/set_channel_members.rb +++ b/lib/pubnub/events/set_channel_members.rb @@ -45,7 +45,7 @@ def fire member_object end - body = Formatter.format_message({ set: members }, "", @random_iv, false) + body = Formatter.format_message({ set: members }, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) @@ -86,11 +86,11 @@ def path def valid_envelope(parsed_response, req_res_objects) members = parsed_response['data'].map { |channel_member| member = Hash.new - channel_member.each{ |k,v| member[k.to_sym] = v } + channel_member.each { |k, v| member[k.to_sym] = v } unless member[:uuid].nil? uuid_metadata = Hash.new - member[:uuid].each{ |k,v| uuid_metadata[k.to_sym] = v } + member[:uuid].each { |k, v| uuid_metadata[k.to_sym] = v } uuid_metadata[:updated] = Date._parse(uuid_metadata[:updated]) unless uuid_metadata[:updated].nil? member[:uuid] = uuid_metadata end diff --git a/lib/pubnub/events/set_channel_metadata.rb b/lib/pubnub/events/set_channel_metadata.rb index 5f368b2d6..7e3aba6cf 100644 --- a/lib/pubnub/events/set_channel_metadata.rb +++ b/lib/pubnub/events/set_channel_metadata.rb @@ -27,7 +27,7 @@ def initialize(options, app) def fire Pubnub.logger.debug('Pubnub::SetChannelMetadata') { "Fired event #{self.class}" } - body = Formatter.format_message(@metadata, "", @random_iv, false) + body = Formatter.format_message(@metadata, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) @@ -60,7 +60,7 @@ def path def valid_envelope(parsed_response, req_res_objects) data = parsed_response['data'] metadata = Hash.new - data.each{ |k,v| metadata[k.to_sym] = v } + data.each { |k, v| metadata[k.to_sym] = v } metadata[:updated] = Date._parse(metadata[:updated]) unless metadata[:updated].nil? Pubnub::Envelope.new( diff --git a/lib/pubnub/events/set_memberships.rb b/lib/pubnub/events/set_memberships.rb index 9a8a914f2..b582b4e34 100644 --- a/lib/pubnub/events/set_memberships.rb +++ b/lib/pubnub/events/set_memberships.rb @@ -45,7 +45,7 @@ def fire membership_object end - body = Formatter.format_message({ set: memberships }, "", @random_iv, false) + body = Formatter.format_message({ set: memberships }, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) @@ -86,11 +86,11 @@ def path def valid_envelope(parsed_response, req_res_objects) memberships = parsed_response['data'].map { |uuid_membership| membership = Hash.new - uuid_membership.each{ |k,v| membership[k.to_sym] = v } + uuid_membership.each { |k, v| membership[k.to_sym] = v } unless membership[:channel].nil? channel_metadata = Hash.new - membership[:channel].each{ |k,v| channel_metadata[k.to_sym] = v } + membership[:channel].each { |k, v| channel_metadata[k.to_sym] = v } channel_metadata[:updated] = Date._parse(channel_metadata[:updated]) unless channel_metadata[:updated].nil? membership[:channel] = channel_metadata end diff --git a/lib/pubnub/events/set_uuid_metadata.rb b/lib/pubnub/events/set_uuid_metadata.rb index a79cd7f0a..cefa7cd57 100644 --- a/lib/pubnub/events/set_uuid_metadata.rb +++ b/lib/pubnub/events/set_uuid_metadata.rb @@ -28,7 +28,7 @@ def initialize(options, app) def fire Pubnub.logger.debug('Pubnub::SetUuidMetadata') { "Fired event #{self.class}" } - body = Formatter.format_message(@metadata, "", @random_iv, false) + body = Formatter.format_message(@metadata, nil, false) response = send_request(body) envelopes = fire_callbacks(handle(response, uri)) @@ -61,7 +61,7 @@ def path def valid_envelope(parsed_response, req_res_objects) data = parsed_response['data'] metadata = Hash.new - data.each{ |k,v| metadata[k.to_sym] = v } + data.each { |k, v| metadata[k.to_sym] = v } metadata[:updated] = Date._parse(metadata[:updated]) unless metadata[:updated].nil? Pubnub::Envelope.new( diff --git a/lib/pubnub/events/signal.rb b/lib/pubnub/events/signal.rb index 1c7d152c9..67c31c90d 100644 --- a/lib/pubnub/events/signal.rb +++ b/lib/pubnub/events/signal.rb @@ -39,7 +39,7 @@ def path '0', Formatter.format_channel(@channel, true), '0', - Formatter.format_message(@message, @cipher_key, @random_iv) + Formatter.format_message(@message, @crypto_module) ].join('/') end diff --git a/lib/pubnub/events/subscribe.rb b/lib/pubnub/events/subscribe.rb index 6a0ef6ad9..5348518e5 100644 --- a/lib/pubnub/events/subscribe.rb +++ b/lib/pubnub/events/subscribe.rb @@ -8,6 +8,11 @@ class Subscribe < SubscribeEvent def initialize(options, app) @event = :subscribe + + # Override crypto module if custom cipher key has been used. + random_iv = options.key?(:random_iv) ? options[:random_iv] : true + options[:crypto_module] = Crypto::CryptoModule.new_legacy(options[:cipher_key], random_iv) if options[:cipher_key] + super app.apply_state(self) end diff --git a/lib/pubnub/formatter.rb b/lib/pubnub/formatter.rb index 423b20c8f..f9bf35265 100644 --- a/lib/pubnub/formatter.rb +++ b/lib/pubnub/formatter.rb @@ -42,17 +42,17 @@ def format_uuid(uuids, should_encode = true) end # Transforms message to json and encode it - def format_message(message, cipher_key = '', use_random_iv = false, uri_escape = true) - if cipher_key && !cipher_key.empty? - pc = Pubnub::Crypto.new(cipher_key, use_random_iv) - message = pc.encrypt(message).to_json - message = Addressable::URI.escape(message) if uri_escape - else - message = message.to_json - message = Formatter.encode(message) if uri_escape - end - message - end + # def format_message(message, cipher_key = '', use_random_iv = false, uri_escape = true) + # if cipher_key && !cipher_key.empty? + # pc = Pubnub::Crypto.new(cipher_key, use_random_iv) + # message = pc.encrypt(message).to_json + # message = Addressable::URI.escape(message) if uri_escape + # else + # message = message.to_json + # message = Formatter.encode(message) if uri_escape + # end + # message + # end # TODO: Uncomment code below when cryptor implementations will be added. # Transforms message to json and encode it. @@ -64,15 +64,19 @@ def format_message(message, cipher_key = '', use_random_iv = false, uri_escape = # @param uri_escape [Boolean, nil] Whether formatted message should escape # to be used as part of URI or not. # @return [String, nil] Formatted message data. - # def format_message(message, crypto = nil, uri_escape = true) - # json_message = message.to_json - # json_message = crypto&.encrypt(json_message) || '' unless crypto.nil? - # if uri_escape - # json_message = Formatter.encode(json_message) if crypto.nil? - # json_message = Addressable::URI.escape(json_message).to_s unless crypto.nil? - # end - # json_message - # end + def format_message(message, crypto = nil, uri_escape = true) + json_message = message.to_json + if crypto + encrypted_data = crypto&.encrypt(json_message) + json_message = Base64.strict_encode64(encrypted_data).to_json unless encrypted_data.nil? + end + + if uri_escape + json_message = Formatter.encode(json_message) if crypto.nil? + json_message = Addressable::URI.escape(json_message).to_s unless crypto.nil? + end + json_message + end # Quite lazy way, but good enough for current usage def classify_method(method) diff --git a/lib/pubnub/modules/crypto/crypto_module.rb b/lib/pubnub/modules/crypto/crypto_module.rb index 9a9ad82d8..289ae33f9 100644 --- a/lib/pubnub/modules/crypto/crypto_module.rb +++ b/lib/pubnub/modules/crypto/crypto_module.rb @@ -1,116 +1,154 @@ -# frozen_string_literal: true - module Pubnub - # Crypto module for data processing. - # - # The PubNub client uses a module to encrypt and decrypt sent data in a way - # that's compatible with previous versions (if additional cryptors have been - # registered). - class CryptoModule < Crypto::CryptoProvider - # Legacy AES-CBC cryptor based module. - # - # Data encryption and decryption will be done by default using the - # LegacyCrypto. In addition to the LegacyCrypto for data - # decryption, the AesCbcCryptor will be registered for - # future-compatibility (which will help with gradual application updates). + module Crypto + # Crypto module for data processing. # - # @param cipher_key [String] Key for data encryption and decryption. - # @param use_random_iv [Boolean] Whether random IV should be used for data decryption. - def self.new_aes_cbc(cipher_key, use_random_iv) - # TODO: Create AES-CBC based crypto module - end - - # Legacy AES-CBC cryptor based module. - # - # Data encryption and decryption will be done by default using the - # LegacyCrypto. In addition to the LegacyCrypto for data - # decryption, the AesCbcCryptor will be registered for - # future-compatibility (which will help with gradual application updates). - # - # @param cipher_key [String] Key for data encryption and decryption. - # @param use_random_iv [Boolean] Whether random IV should be used for data decryption. - def self.new_legacy(cipher_key, use_random_iv) - # TODO: Create legacy AES-CBC based crypto module - end + # The PubNub client uses a module to encrypt and decrypt sent data in a way + # that's compatible with previous versions (if additional cryptors have been + # registered). + class CryptoModule < CryptoProvider + # AES-CBC cryptor based module. + # + # Data encryption and decryption will be done by default + # using the AesCbcCryptor. In addition to the AesCbcCryptor + # for data decryption, the LegacyCryptor will be registered + # for backward-compatibility. + # + # @param cipher_key [String] Key for data encryption and decryption. + # @param use_random_iv [Boolean] Whether random IV should be used for data + # decryption. + # + # @raise [ArgumentError] If the cipher_key is missing or empty. + def self.new_aes_cbc(cipher_key, use_random_iv) + if cipher_key.nil? || cipher_key.empty? + raise ArgumentError, { + message: '\'cipher_key\' is missing or empty.' + } + end + + CryptoModule.new AesCbcCryptor.new(cipher_key), [LegacyCryptor.new(cipher_key, use_random_iv)], cipher_key, use_random_iv + end - # Create crypto module. - # @param default [Cryptor] Default cryptor used to encrypt and decrypt data. - # @param cryptors [Array, nil] Additional cryptors which will be - # used to decrypt data encrypted by previously used cryptors. - def initialize(default, cryptors) - if default.nil? - raise ArgumentError, { - message: '\'default\' cryptor required for data encryption.' - } + # Legacy AES-CBC cryptor based module. + # + # Data encryption and decryption will be done by default + # using the LegacyCrypto. In addition to the LegacyCrypto + # for data decryption, the AesCbcCryptor will be registered + # for future-compatibility (which will help with gradual application + # updates). + # + # @param cipher_key [String] Key for data encryption and decryption. + # @param use_random_iv [Boolean] Whether random IV should be used for data + # decryption. + # + # @raise [ArgumentError] If the cipher_key is missing or empty. + def self.new_legacy(cipher_key, use_random_iv) + if cipher_key.nil? || cipher_key.empty? + raise ArgumentError, { + message: '\'cipher_key\' is missing or empty.' + } + end + + CryptoModule.new LegacyCryptor.new(cipher_key, use_random_iv), [AesCbcCryptor.new(cipher_key)], cipher_key, use_random_iv end - @default = default - @cryptors = cryptors&.each_with_object({}) do |value, hash| - hash[value.identifier] = value - end || {} - end + # Create crypto module. + # + # @param default [Cryptor] Default cryptor used to encrypt and decrypt + # data. + # @param cryptors [Array, nil] Additional cryptors which will be + # used to decrypt data encrypted by previously used cryptors. + def initialize(default, cryptors, key = nil, random = true) + if default.nil? + raise ArgumentError, { + message: '\'default\' cryptor required for data encryption.' + } + end + + @___cipher_key = key + @___use_random_iv = random + + @default = default + @cryptors = cryptors&.each_with_object({}) do |value, hash| + hash[value.identifier] = value + end || {} + end - def encrypt(data) - # Encrypting provided data. - encrypted_data = default_cryptor.encrypt(data) - return nil if encrypted_data.nil? + def encrypt(data) + # Encrypting provided data. + encrypted_data = default_cryptor.encrypt(data) + return nil if encrypted_data.nil? - payload = Crypto::CryptorHeader.new(default_cryptor.identifier, encrypted_data.metadata).to_s - payload << encrypted_data.metadata unless encrypted_data.metadata.nil? - payload << encrypted_data.data - end + payload = Crypto::CryptorHeader.new(default_cryptor.identifier, encrypted_data.metadata).to_s + payload << encrypted_data.metadata unless encrypted_data.metadata.nil? + payload << encrypted_data.data + end - def decrypt(data) - header = Crypto::CryptorHeader.parse(data) - cryptor = cryptor(header&.identifier || '\x00\x00\x00\x00') + def decrypt(data) + header = Crypto::CryptorHeader.parse(data) + cryptor_identifier = header&.identifier || '\x00\x00\x00\x00' + cryptor = cryptor cryptor_identifier - # Check whether there is a cryptor to decrypt data or not. - if cryptor.nil? - identifier = header&.identifier || 'UNKN' - raise UnknownCryptorError, { - message: "Decrypting data created by unknown cryptor. Please make sure to register + # Check whether there is a cryptor to decrypt data or not. + if cryptor.nil? + identifier = header&.identifier || 'UNKN' + raise UnknownCryptorError, { + message: "Decrypting data created by unknown cryptor. Please make sure to register #{identifier} or update SDK." - } - end + } + end - metadata_size = header&.data_size || 0 - metadata = metadata_size.positive? ? data[0..metadata_size] : nil - data = data[metadata_size..-1] + encrypted_data = data[(header&.length || 0)..-1] + metadata = metadata encrypted_data, (header&.data_size || 0) - return nil if data.nil? + # Check whether there is still some data for processing or not. + return nil if encrypted_data.nil? || encrypted_data.empty? - cryptor.decrypt(EncryptedData.new(data, metadata)) - end + cryptor.decrypt(EncryptedData.new(encrypted_data, metadata)) + end - private + private - # Cryptor used by the module by default to encrypt data. - # - # @return [Cryptor] Default cryptor used to encrypt and decrypt data. - def default_cryptor - @default - end + # Cryptor used by the module by default to encrypt data. + # + # @return [Cryptor] Default cryptor used to encrypt and decrypt data. + def default_cryptor + @default + end - # Additional cryptors that can be used to decrypt data if the - # default_cryptor can't. - # - # @return [Hash] Map of Cryptor to their identifiers. - def additional_cryptors - @cryptors - end + # Additional cryptors that can be used to decrypt data if the + # default_cryptor can't. + # + # @return [Hash] Map of Cryptor to their identifiers. + def additional_cryptors + @cryptors + end - # Find cryptor with a specified identifier. - # - # Data decryption can only be done with registered cryptors. An identifier - # in the cryptor data header is used to identify a suitable cryptor. - # - # @param identifier [String] A unicode cryptor identifier. - # @return [Cryptor, nil] Target cryptor or `nil` in case there is none with - # the specified identifier. - def cryptor(identifier) - return default_cryptor if default_cryptor.identifier == identifier + # Extract metadata information from source data. + # + # @param data [String, nil] Encrypted data from which cryptor metadata + # should be extracted. + # @param size [Integer] Size of cryptor-defined data. + # @return [String, nil] Extracted metadata or nil in case if + # size is 0. + def metadata(data, size) + return nil if !data || !size.positive? + + data&.slice!(0..(size - 1)) + end - additional_cryptors.fetch(identifier, nil) + # Find cryptor with a specified identifier. + # + # Data decryption can only be done with registered cryptors. An identifier + # in the cryptor data header is used to identify a suitable cryptor. + # + # @param identifier [String] A unicode cryptor identifier. + # @return [Cryptor, nil] Target cryptor or `nil` in case there is none + # with the specified identifier. + def cryptor(identifier) + return default_cryptor if default_cryptor.identifier == identifier + + additional_cryptors.fetch(identifier, nil) + end end end end \ No newline at end of file diff --git a/lib/pubnub/modules/crypto/cryptor.rb b/lib/pubnub/modules/crypto/cryptor.rb index 1e93944d5..446c112b6 100644 --- a/lib/pubnub/modules/crypto/cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptor.rb @@ -45,6 +45,7 @@ class Cryptor # with encrypted data and metadata. # # The identifier must be 4 bytes long. + # # @return [String] Unique cryptor identifier. def identifier raise NotImplementedError, 'Subclass should provide "identifier" method implementation.' diff --git a/lib/pubnub/modules/crypto/cryptor_header.rb b/lib/pubnub/modules/crypto/cryptor_header.rb index 8992c4db1..ac620395a 100644 --- a/lib/pubnub/modules/crypto/cryptor_header.rb +++ b/lib/pubnub/modules/crypto/cryptor_header.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - module Pubnub module Crypto # Cryptor data header. @@ -74,11 +72,11 @@ def version # Create cryptor header. # - # @param identifier [String, nil] Identifier of the cryptor which has been - # used to encrypt data. + # @param identifier [String] Identifier of the cryptor which has been used + # to encrypt data. # @param metadata [String, nil] Cryptor-defined information. def initialize(identifier = nil, metadata = nil) - @data = if identifier + @data = if identifier && identifier != '\x00\x00\x00\x00' Versions::CryptorHeaderV1Data.new( identifier.to_s, metadata&.length || 0 @@ -134,6 +132,21 @@ def self.parse(data) header end + # Overall header size. + # + # Full header size which includes: + # * sentinel + # * version + # * cryptor identifier + # * cryptor data size + # * cryptor-defined fields size. + def length + # Legacy payload doesn't have header. + return 0 if @data.nil? + + 9 + (data_size < 255 ? 1 : 3) + end + # Crypto header version Version module. # # @return [Integer] One of known versions from Version module. @@ -187,7 +200,7 @@ def to_s cryptor_identifier = identifier if cryptor_identifier.nil? || cryptor_identifier.empty? raise ArgumentError, { - message: '\'identifier\' is required for encryption' + message: '\'identifier\' is missing or empty.' } end diff --git a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb new file mode 100644 index 000000000..649a50778 --- /dev/null +++ b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module Pubnub + module Crypto + # AES-256-CBC cryptor. + # + # The cryptor provides _encryption_ and _decryption_ using AES-256 in + # CBC mode with a cipher key and random initialization vector. + # When it is registered as a secondary with other cryptors, it will provide + # backward compatibility with previously encrypted data. + class AesCbcCryptor < Cryptor + # AES-128 CBC block size. + BLOCK_SIZE = 16 + + # Create AES-256-CBC cryptor instance. + # + # @param cipher_key [String] Key for data encryption and + # decryption. + def initialize(cipher_key) + @cipher_key = Digest::SHA256.digest(cipher_key) + @alg = 'AES-256-CBC' + end + + def identifier + 'ACRH' + end + + def encrypt(data) + iv = OpenSSL::Random.random_bytes BLOCK_SIZE + cipher = OpenSSL::Cipher.new(@alg).encrypt + cipher.key = @cipher_key + cipher.iv = iv + + encoded_message = cipher.update data + encoded_message << cipher.final + Crypto::EncryptedData.new(encoded_message, iv) + rescue StandardError => e + Pubnub.error('Pubnub') { "ENCRYPTION ERROR: #{e}" } + nil + end + + def decrypt(data) + if data.metadata.length != BLOCK_SIZE + Pubnub.error('Pubnub') do + "DECRYPTION ERROR: Unexpected initialization vector length: +#{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" + end + return nil + end + + cipher = OpenSSL::Cipher.new(@alg).decrypt + cipher.key = @cipher_key + cipher.iv = data.metadata + + decrypted = cipher.update data.data + decrypted << cipher.final + rescue StandardError => e + Pubnub.error('Pubnub') { "DECRYPTION ERROR: #{e}" } + nil + end + end + end +end diff --git a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb new file mode 100644 index 000000000..54b08318d --- /dev/null +++ b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb @@ -0,0 +1,76 @@ +module Pubnub + module Crypto + # Legacy cryptor. + # + # The cryptor provides _encryption_ and _decryption_ using `AES-256 in + # CBC mode with a cipher key and configurable initialization vector + # randomness. + # When it is registered as a secondary with other cryptors, it will provide + # backward compatibility with previously encrypted data. + # + # Important: It has been reported that the digest from cipherKey has + # low entropy, and it is suggested to use AesCbcCryptor instead. + class LegacyCryptor < Cryptor + # AES-128 CBC block size. + BLOCK_SIZE = 16 + + # Create legacy cryptor instance. + # + # @param cipher_key [String] Key for data encryption and + # decryption. + # @param use_random_iv [Boolean] Whether random IV should be used. + def initialize(cipher_key, use_random_iv = true) + @alg = 'AES-256-CBC' + @original_cipher_key = cipher_key + @cipher_key = Digest::SHA256.hexdigest(cipher_key.to_s).slice(0, 32) + @iv = use_random_iv ? nil : '0123456789012345' + end + + def identifier + '\x00\x00\x00\x00' + end + + def encrypt(data) + iv = @iv || OpenSSL::Random.random_bytes(BLOCK_SIZE) + cipher = OpenSSL::Cipher.new(@alg).encrypt + cipher.key = @cipher_key + cipher.iv = iv + + encoded_message = '' + encoded_message << iv if @iv.nil? && iv + encoded_message << cipher.update(data) + encoded_message << cipher.final + Crypto::EncryptedData.new(encoded_message) + rescue StandardError => e + Pubnub.error('Pubnub') { "ENCRYPTION ERROR: #{e}" } + nil + end + + def decrypt(data) + encrypted_data = data.data + iv = if @iv.nil? + encrypted_data.slice!(0..(BLOCK_SIZE - 1)) if encrypted_data.length >= BLOCK_SIZE + else + @iv + end + if iv.length != BLOCK_SIZE + Pubnub.error('Pubnub') do + "DECRYPTION ERROR: Unexpected initialization vector length: + #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" + end + return nil + end + + cipher = OpenSSL::Cipher.new(@alg).decrypt + cipher.key = @cipher_key + cipher.iv = iv + + decrypted = cipher.update encrypted_data + decrypted << cipher.final + rescue StandardError => e + Pubnub.error('Pubnub') { "DECRYPTION ERROR: #{e}" } + nil + end + end + end +end diff --git a/lib/pubnub/modules/crypto/module.rb b/lib/pubnub/modules/crypto/module.rb new file mode 100644 index 000000000..c35a56561 --- /dev/null +++ b/lib/pubnub/modules/crypto/module.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +require 'pubnub/modules/crypto/cryptor' +require 'pubnub/modules/crypto/cryptors/aes_cbc_cryptor' +require 'pubnub/modules/crypto/cryptors/legacy_cryptor' +require 'pubnub/modules/crypto/crypto_provider' +require 'pubnub/modules/crypto/cryptor_header' +require 'pubnub/modules/crypto/crypto_module' diff --git a/lib/pubnub/subscribe_event/formatter.rb b/lib/pubnub/subscribe_event/formatter.rb index 633c9f328..5cb6ad988 100644 --- a/lib/pubnub/subscribe_event/formatter.rb +++ b/lib/pubnub/subscribe_event/formatter.rb @@ -34,17 +34,17 @@ def build_error_envelopes(_parsed_response, error, req_res_objects) def decipher_payload(message) # TODO: Uncomment code below when cryptor implementations will be added. - # return message[:payload] if message[:channel].end_with?('-pnpres') || crypto_module.nil? + return message[:payload] if message[:channel].end_with?('-pnpres') || crypto_module.nil? + + encrypted_message = Base64.decode64(message[:payload]) + JSON.parse(crypto_module.decrypt(encrypted_message), quirks_mode: true) # - # crypto = crypto_module + # return message[:payload] if message[:channel].end_with?('-pnpres') || (@app.env[:cipher_key].nil? && @cipher_key.nil? && @cipher_key_selector.nil? && @env[:cipher_key_selector].nil?) + # data = message.reject { |k, _v| k == :payload } + # cipher_key = compute_cipher_key(data) + # random_iv = compute_random_iv(data) + # crypto = Pubnub::Crypto.new(cipher_key, random_iv) # JSON.parse(crypto.decrypt(message[:payload]), quirks_mode: true) - - return message[:payload] if message[:channel].end_with?('-pnpres') || (@app.env[:cipher_key].nil? && @cipher_key.nil? && @cipher_key_selector.nil? && @env[:cipher_key_selector].nil?) - data = message.reject { |k, _v| k == :payload } - cipher_key = compute_cipher_key(data) - random_iv = compute_random_iv(data) - crypto = Pubnub::Crypto.new(cipher_key, random_iv) - JSON.parse(crypto.decrypt(message[:payload]), quirks_mode: true) rescue StandardError, UnknownCryptorError message[:payload] end diff --git a/spec/lib/multiple_ciphers_spec.rb b/spec/lib/multiple_ciphers_spec.rb index 5b86a59c3..4d46b7ff1 100644 --- a/spec/lib/multiple_ciphers_spec.rb +++ b/spec/lib/multiple_ciphers_spec.rb @@ -42,6 +42,8 @@ http_sync: true, ).first + puts '---------------------------------------' + expect(e0.result[:data][:message]).to eq "Some test message" expect(e1.result[:data][:message]).to eq "Another test message" end From 3ce68636a9fd05e64f3fd84008f41ef86e41c28b Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Sat, 30 Sep 2023 00:21:10 +0300 Subject: [PATCH 07/18] chore: update direct 'addressable' dependency --- pubnub.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubnub.gemspec b/pubnub.gemspec index 4102baa4a..1126451fe 100644 --- a/pubnub.gemspec +++ b/pubnub.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.required_ruby_version = '>= 2.4' - spec.add_dependency 'addressable', '>= 2.0.0' + spec.add_dependency 'addressable', '>= 2.8.0' spec.add_dependency 'concurrent-ruby', '~> 1.1.5' spec.add_dependency 'concurrent-ruby-edge', '~> 0.5.0' spec.add_dependency 'dry-validation', '~> 1.0' From 603f24f8749a277f2107670b51484533f9e3a8b6 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Sat, 30 Sep 2023 00:28:21 +0300 Subject: [PATCH 08/18] chore: update Gemfile.lock --- Gemfile.lock | 231 +++++++++++++++++++++++++-------------------------- 1 file changed, 112 insertions(+), 119 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a25e36d6a..24dffb923 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: pubnub (5.2.2) - addressable (>= 2.0.0) + addressable (>= 2.8.0) concurrent-ruby (~> 1.1.5) concurrent-ruby-edge (~> 0.5.0) dry-validation (~> 1.0) @@ -13,155 +13,147 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.7.0) - public_suffix (>= 2.0.2, < 5.0) - ast (2.4.1) - awesome_print (1.8.0) - binding_of_caller (0.8.0) + addressable (2.8.5) + public_suffix (>= 2.0.2, < 6.0) + ast (2.4.2) + awesome_print (1.9.2) + base64 (0.1.1) + binding_of_caller (1.0.0) debug_inspector (>= 0.0.1) builder (3.2.4) codacy-coverage (2.2.1) simplecov coderay (1.1.3) - concurrent-ruby (1.1.6) + concurrent-ruby (1.1.10) concurrent-ruby-edge (0.5.0) concurrent-ruby (~> 1.1.5) - crack (0.4.3) - safe_yaml (~> 1.0.0) - cucumber (7.0.0) + crack (0.4.5) + rexml + cucumber (9.0.2) builder (~> 3.2, >= 3.2.4) - cucumber-core (~> 10.0, >= 10.0.1) - cucumber-create-meta (~> 6.0, >= 6.0.1) - cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) - cucumber-gherkin (~> 20.0, >= 20.0.1) - cucumber-html-formatter (~> 16.0, >= 16.0.1) - cucumber-messages (~> 17.0, >= 17.0.1) - cucumber-wire (~> 6.0, >= 6.0.1) - diff-lcs (~> 1.4, >= 1.4.4) - mime-types (~> 3.3, >= 3.3.1) - multi_test (~> 0.1, >= 0.1.2) - sys-uname (~> 1.2, >= 1.2.2) - cucumber-core (10.0.1) - cucumber-gherkin (~> 20.0, >= 20.0.1) - cucumber-messages (~> 17.0, >= 17.0.1) - cucumber-tag-expressions (~> 3.0, >= 3.0.1) - cucumber-create-meta (6.0.1) - cucumber-messages (~> 17.0, >= 17.0.1) - sys-uname (~> 1.2, >= 1.2.2) - cucumber-cucumber-expressions (12.1.3) - cucumber-gherkin (20.0.1) - cucumber-messages (~> 17.0, >= 17.0.1) - cucumber-html-formatter (16.0.1) - cucumber-messages (~> 17.0, >= 17.0.1) - cucumber-messages (17.1.1) - cucumber-tag-expressions (3.0.1) - cucumber-wire (6.1.1) - cucumber-core (~> 10.0, >= 10.0.1) - cucumber-cucumber-expressions (~> 12.1, >= 12.1.2) - cucumber-messages (~> 17.0, >= 17.0.1) - debug_inspector (0.0.3) - diff-lcs (1.4.4) + cucumber-ci-environment (~> 9.2, >= 9.2.0) + cucumber-core (~> 11.1, >= 11.1.0) + cucumber-cucumber-expressions (~> 16.1, >= 16.1.2) + cucumber-gherkin (>= 24, < 26.2.1) + cucumber-html-formatter (~> 20.4, >= 20.4.0) + cucumber-messages (>= 19, < 23) + diff-lcs (~> 1.5, >= 1.5.0) + mini_mime (~> 1.1, >= 1.1.5) + multi_test (~> 1.1, >= 1.1.0) + sys-uname (~> 1.2, >= 1.2.3) + cucumber-ci-environment (9.2.0) + cucumber-core (11.1.0) + cucumber-gherkin (>= 24, < 27) + cucumber-messages (>= 19, < 22) + cucumber-tag-expressions (~> 4.1, >= 4.1.0) + cucumber-cucumber-expressions (16.1.2) + cucumber-gherkin (26.2.0) + cucumber-messages (>= 19.1.4, < 22.1) + cucumber-html-formatter (20.4.0) + cucumber-messages (>= 18.0, < 22.1) + cucumber-messages (21.0.1) + cucumber-tag-expressions (4.1.0) + debug_inspector (1.1.0) + diff-lcs (1.5.0) docile (1.4.0) - dry-configurable (0.11.6) - concurrent-ruby (~> 1.0) - dry-core (~> 0.4, >= 0.4.7) - dry-equalizer (~> 0.2) - dry-container (0.7.2) + dry-configurable (1.1.0) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-core (1.0.1) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.9) + zeitwerk (~> 2.6) + dry-inflector (1.0.0) + dry-initializer (3.1.1) + dry-logic (1.5.0) concurrent-ruby (~> 1.0) - dry-equalizer (0.3.0) - dry-inflector (0.2.0) - dry-initializer (3.0.3) - dry-logic (1.0.6) + dry-core (~> 1.0, < 2) + zeitwerk (~> 2.6) + dry-schema (1.13.3) concurrent-ruby (~> 1.0) - dry-core (~> 0.2) - dry-equalizer (~> 0.2) - dry-schema (1.5.2) - concurrent-ruby (~> 1.0) - dry-configurable (~> 0.8, >= 0.8.3) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-logic (~> 1.0) - dry-types (~> 1.4) - dry-types (1.4.0) + dry-logic (>= 1.4, < 2) + dry-types (>= 1.7, < 2) + zeitwerk (~> 2.6) + dry-types (1.7.1) concurrent-ruby (~> 1.0) - dry-container (~> 0.3) - dry-core (~> 0.4, >= 0.4.4) - dry-equalizer (~> 0.3) - dry-inflector (~> 0.1, >= 0.1.2) - dry-logic (~> 1.0, >= 1.0.2) - dry-validation (1.5.3) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) + dry-validation (1.10.0) concurrent-ruby (~> 1.0) - dry-container (~> 0.7, >= 0.7.1) - dry-core (~> 0.4) - dry-equalizer (~> 0.2) + dry-core (~> 1.0, < 2) dry-initializer (~> 3.0) - dry-schema (~> 1.5) - ffi (1.13.1) - ffi (1.13.1-java) + dry-schema (>= 1.12, < 2) + zeitwerk (~> 2.6) + ffi (1.16.2) + ffi (1.16.2-java) hashdiff (1.0.1) httpclient (2.8.3) interception (0.5) - json (2.3.1) - json (2.3.1-java) + json (2.6.3) + json (2.6.3-java) + language_server-protocol (3.17.0.3) method_source (1.0.0) - mime-types (3.3.1) - mime-types-data (~> 3.2015) - mime-types-data (3.2021.0901) - multi_test (0.1.2) - parallel (1.19.2) - parser (2.7.1.4) + mini_mime (1.1.5) + multi_test (1.1.0) + parallel (1.23.0) + parser (3.2.2.3) ast (~> 2.4.1) - pry (0.13.1) + racc + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.13.1-java) + pry (0.14.2-java) coderay (~> 1.1) method_source (~> 1.0) spoon (~> 0.0) pry-rescue (1.5.2) interception (>= 0.5) pry (>= 0.12.0) - pry-stack_explorer (0.4.11) - binding_of_caller (~> 0.7) + pry-stack_explorer (0.6.1) + binding_of_caller (~> 1.0) pry (~> 0.13) - public_suffix (4.0.5) - rainbow (3.0.0) - regexp_parser (1.7.1) - rexml (3.2.4) - rr (1.2.1) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.2) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.2) + public_suffix (5.0.3) + racc (1.7.1) + racc (1.7.1-java) + rainbow (3.1.1) + regexp_parser (2.8.1) + rexml (3.2.6) + rr (3.1.0) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) + rspec-support (~> 3.12.0) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.9.3) - rubocop (0.89.0) + rspec-support (3.12.1) + rubocop (1.56.4) + base64 (~> 0.1.1) + json (~> 2.3) + language_server-protocol (>= 3.17.0) parallel (~> 1.10) - parser (>= 2.7.1.1) + parser (>= 3.2.2.3) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.7) - rexml - rubocop-ast (>= 0.1.0, < 1.0) + regexp_parser (>= 1.8, < 3.0) + rexml (>= 3.2.5, < 4.0) + rubocop-ast (>= 1.28.1, < 2.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 1.4.0, < 2.0) - rubocop-ast (0.3.0) - parser (>= 2.7.1.4) - ruby-progressbar (1.10.1) - safe_yaml (1.0.5) - simplecov (0.21.2) + unicode-display_width (>= 2.4.0, < 3.0) + rubocop-ast (1.29.0) + parser (>= 3.2.1.0) + ruby-progressbar (1.13.0) + simplecov (0.22.0) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) @@ -169,15 +161,16 @@ GEM simplecov_json_formatter (0.1.4) spoon (0.0.6) ffi - sys-uname (1.2.2) + sys-uname (1.2.3) ffi (~> 1.1) - timers (4.3.0) - unicode-display_width (1.7.0) - vcr (6.0.0) - webmock (3.8.3) - addressable (>= 2.3.6) + timers (4.3.5) + unicode-display_width (2.4.2) + vcr (6.2.0) + webmock (3.19.1) + addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) + zeitwerk (2.6.12) PLATFORMS java From 5a7f6c920df48bc40285c55b99801e33e4fc8c6e Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 2 Oct 2023 00:55:33 +0300 Subject: [PATCH 09/18] refactor(crypto): move old crypto to specs Move old crypto code to specs for tests purpose only. test(contract): add steps definitions for crypto module fix(crypto): decrypt empty return immediately Return empty data from `decrypt` methods immediately if encrypted data is empty. --- Gemfile.lock | 16 +--- features/step_definitions/access_steps.rb | 2 - features/step_definitions/crypto_steps.rb | 93 +++++++++++++++++++ features/support/cryptor.rb | 58 ++++++++++++ features/support/hooks.rb | 1 - lib/pubnub/client.rb | 1 - lib/pubnub/crypto.rb | 70 -------------- lib/pubnub/modules/crypto/crypto_module.rb | 20 ++-- lib/pubnub/modules/crypto/cryptor_header.rb | 4 +- .../crypto/cryptors/aes_cbc_cryptor.rb | 12 ++- .../modules/crypto/cryptors/legacy_cryptor.rb | 13 +-- pubnub.gemspec | 2 +- 12 files changed, 181 insertions(+), 111 deletions(-) create mode 100644 features/step_definitions/crypto_steps.rb create mode 100644 features/support/cryptor.rb delete mode 100644 lib/pubnub/crypto.rb diff --git a/Gemfile.lock b/Gemfile.lock index 24dffb923..216ebf051 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -2,7 +2,7 @@ PATH remote: . specs: pubnub (5.2.2) - addressable (>= 2.8.0) + addressable (>= 2.0.0) concurrent-ruby (~> 1.1.5) concurrent-ruby-edge (~> 0.5.0) dry-validation (~> 1.0) @@ -89,12 +89,10 @@ GEM dry-schema (>= 1.12, < 2) zeitwerk (~> 2.6) ffi (1.16.2) - ffi (1.16.2-java) hashdiff (1.0.1) httpclient (2.8.3) interception (0.5) json (2.6.3) - json (2.6.3-java) language_server-protocol (3.17.0.3) method_source (1.0.0) mini_mime (1.1.5) @@ -106,10 +104,6 @@ GEM pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.14.2-java) - coderay (~> 1.1) - method_source (~> 1.0) - spoon (~> 0.0) pry-rescue (1.5.2) interception (>= 0.5) pry (>= 0.12.0) @@ -118,7 +112,6 @@ GEM pry (~> 0.13) public_suffix (5.0.3) racc (1.7.1) - racc (1.7.1-java) rainbow (3.1.1) regexp_parser (2.8.1) rexml (3.2.6) @@ -159,8 +152,6 @@ GEM simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - spoon (0.0.6) - ffi sys-uname (1.2.3) ffi (~> 1.1) timers (4.3.5) @@ -173,8 +164,7 @@ GEM zeitwerk (2.6.12) PLATFORMS - java - ruby + arm64-darwin-21 DEPENDENCIES awesome_print @@ -194,4 +184,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.1.4 + 2.4.6 diff --git a/features/step_definitions/access_steps.rb b/features/step_definitions/access_steps.rb index eaa6184fc..2f65efb02 100644 --- a/features/step_definitions/access_steps.rb +++ b/features/step_definitions/access_steps.rb @@ -254,5 +254,3 @@ Then('the error detail message is not empty') do expect(parse_error_body(@global_state[:last_call_res])["error"]["message"].empty?).to eq false end - - diff --git a/features/step_definitions/crypto_steps.rb b/features/step_definitions/crypto_steps.rb new file mode 100644 index 000000000..fabe5825f --- /dev/null +++ b/features/step_definitions/crypto_steps.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true +require 'pubnub' + +Given(/^Crypto module with '([^']*)' cryptor$/) do |cryptor_id| + @cryptor_ids = [cryptor_id] +end + +Given(/^Crypto module with default '([^']*)' and additional '([^']*)' cryptors$/) do |cryptor_id1, cryptor_id2| + @cryptor_ids = [cryptor_id1, cryptor_id2] +end + +Given(/^Legacy code with '([^']*)' cipher key and '(random|constant|-)' vector$/) do |cipher_key, use_random_iv| + use_random_iv = use_random_iv != 'constant' + @legacy_cryptor = Cryptor.new cipher_key, use_random_iv +end + +Then(/^with '([^']*)' cipher key$/) do |cipher_key| + @cipher_key = cipher_key +end + +Then(/^with '(random|constant|-)' vector$/) do |use_random_iv| + @use_random_iv = use_random_iv != 'constant' +end + +When(/^I encrypt '([^']*)' file as '([^']*)'$/) do |file_name, type| + @source_file_name = file_name + @source_file_content = File.binread "sdk-specifications/features/encryption/assets/#{file_name}" + @encrypted_content = crypto_module.encrypt @source_file_content + expect(@encrypted_content).not_to eq nil +end + +When(/^I decrypt '([^']*)' file$/) do |file_name| + file_content = File.binread "sdk-specifications/features/encryption/assets/#{file_name}" + + begin + @decrypted_content = crypto_module.decrypt file_content + rescue Pubnub::UnknownCryptorError + @decrypt_status = 'unknown cryptor error' + end + @decrypt_status = 'decryption error' if @decrypted_content.nil? && @decrypt_status.nil? + @decrypt_status = 'success' if !@decrypted_content.nil? && @decrypt_status.nil? +end + +When(/^I decrypt '([^']*)' file as '([^']*)'$/) do |file_name, _| + file_content = File.binread "sdk-specifications/features/encryption/assets/#{file_name}" + + begin + @decrypted_content = crypto_module.decrypt file_content + rescue Pubnub::UnknownCryptorError + @decrypt_status = 'unknown cryptor error' + end + @decrypt_status = 'decryption error' if @decrypted_content.nil? && @decrypt_status.nil? + @decrypt_status = 'success' if !@decrypted_content.nil? && @decrypt_status.nil? +end + +Then(/^Decrypted file content equal to the '([^']*)' file content$/) do |file_name| + file_content = File.binread "sdk-specifications/features/encryption/assets/#{file_name}" + expect(@decrypted_content).not_to eq nil + expect(@decrypted_content).to eq file_content +end + +Then('Successfully decrypt an encrypted file with legacy code') do + expect(@legacy_cryptor).not_to eq nil + base64_encoded = Base64.strict_encode64(@encrypted_content) + decrypted_content = @legacy_cryptor.decrypt(base64_encoded) + expect(decrypted_content).not_to eq nil + expect(decrypted_content).to eq @source_file_content +end + +Then(/^I receive '([^']*)'$/) do |outcome| + expect(@decrypt_status).not_to eq nil + expect(@decrypt_status).to eq outcome +end + +# Crypto module +# +# @return [Pubnub::Crypto::CryptoModule] Crypto module instance. +def crypto_module + cryptors = [] + @cryptor_ids.each do |cryptor_id| + cryptor = if cryptor_id == 'acrh' + Pubnub::Crypto::AesCbcCryptor.new @cipher_key + elsif cryptor_id == 'legacy' + Pubnub::Crypto::LegacyCryptor.new @cipher_key, @use_random_iv + end + cryptors.push(cryptor) unless cryptor.nil? + end + + raise ArgumentError, "No crypto identifiers specified: #{@cryptor_ids}" if cryptors.empty? + + default_cryptor = cryptors.shift + Pubnub::Crypto::CryptoModule.new default_cryptor, cryptors unless default_cryptor.nil? +end diff --git a/features/support/cryptor.rb b/features/support/cryptor.rb new file mode 100644 index 000000000..091be4d79 --- /dev/null +++ b/features/support/cryptor.rb @@ -0,0 +1,58 @@ +# Internal Crypto class used for message encryption and decryption +class Cryptor + def initialize(cipher_key, use_random_iv) + @alg = 'AES-256-CBC' + sha256_key = Digest::SHA256.hexdigest(cipher_key.to_s) + @key = sha256_key.slice(0, 32) + @using_random_iv = use_random_iv + @iv = @using_random_iv == true ? random_iv : '0123456789012345' + end + + def encrypt(message) + aes = OpenSSL::Cipher.new(@alg) + aes.encrypt + aes.key = @key + aes.iv = @iv + + json_message = message.to_json + cipher = @using_random_iv == true ? @iv : '' + cipher << aes.update(json_message) + cipher << aes.final + + Base64.strict_encode64(cipher) + end + + def decrypt(cipher_text) + undecoded_text = Base64.decode64(cipher_text) + iv = @iv + + if cipher_text.length > 16 && @using_random_iv == true + iv = undecoded_text.slice!(0..15) + end + + decode_cipher = OpenSSL::Cipher.new(@alg).decrypt + decode_cipher.key = @key + decode_cipher.iv = iv + + plain_text = decryption(undecoded_text, decode_cipher) + + plain_text + end + + private + + def decryption(cipher_text, decode_cipher) + plain_text = decode_cipher.update(cipher_text) + plain_text << decode_cipher.final + rescue StandardError => e + puts "Pubnub :: DECRYPTION ERROR: #{e}" + '"DECRYPTION ERROR"' + end + + private + + def random_iv + random_bytes = Random.new.bytes(16).unpack('NnnnnN') + format('%08x%04x%04x', *random_bytes) + end +end \ No newline at end of file diff --git a/features/support/hooks.rb b/features/support/hooks.rb index b27fc57af..876d9a379 100644 --- a/features/support/hooks.rb +++ b/features/support/hooks.rb @@ -8,7 +8,6 @@ @pn_configuration = {} when_mock_server_used { - puts "Using mock" expect(ENV['SERVER_HOST']).not_to be_nil expect(ENV['SERVER_PORT']).not_to be_nil @pn_configuration = { diff --git a/lib/pubnub/client.rb b/lib/pubnub/client.rb index 21f7ff186..e11c814b8 100644 --- a/lib/pubnub/client.rb +++ b/lib/pubnub/client.rb @@ -9,7 +9,6 @@ require 'pubnub/configuration' require 'pubnub/subscribe_callback' -# require 'pubnub/crypto' require 'pubnub/modules/crypto/module' require 'pubnub/schemas/envelope_schema' diff --git a/lib/pubnub/crypto.rb b/lib/pubnub/crypto.rb deleted file mode 100644 index 6402b97d8..000000000 --- a/lib/pubnub/crypto.rb +++ /dev/null @@ -1,70 +0,0 @@ -# Toplevel Pubnub module. -module Pubnub - # Internal Crypto class used for message encryption and decryption - class Crypto - def initialize(cipher_key, use_random_iv) - @alg = 'AES-256-CBC' - sha256_key = Digest::SHA256.hexdigest(cipher_key.to_s) - @key = sha256_key.slice(0, 32) - @using_random_iv = use_random_iv - @iv = @using_random_iv == true ? random_iv : '0123456789012345' - end - - def encrypt(message) - aes = OpenSSL::Cipher.new(@alg) - aes.encrypt - aes.key = @key - aes.iv = @iv - - json_message = message.to_json - cipher = @using_random_iv == true ? @iv : '' - cipher << aes.update(json_message) - cipher << aes.final - - Base64.strict_encode64(cipher) - end - - def decrypt(cipher_text) - undecoded_text = Base64.decode64(cipher_text) - iv = @iv - - if cipher_text.length > 16 && @using_random_iv == true - iv = undecoded_text.slice!(0..15) - end - - decode_cipher = OpenSSL::Cipher.new(@alg).decrypt - decode_cipher.key = @key - decode_cipher.iv = iv - - plain_text = decryption(undecoded_text, decode_cipher) - load_json(plain_text) - - Pubnub.logger.debug('Pubnub') { 'Finished decrypting' } - - plain_text - end - - private - - def decryption(cipher_text, decode_cipher) - plain_text = decode_cipher.update(cipher_text) - plain_text << decode_cipher.final - rescue StandardError => e - Pubnub.error('Pubnub') { "DECRYPTION ERROR #{e}" } - '"DECRYPTION ERROR"' - end - - def load_json(plain_text) - JSON.load(plain_text) - rescue JSON::ParserError - JSON.load("[#{plain_text}]")[0] - end - - private - - def random_iv - random_bytes = Random.new.bytes(16).unpack('NnnnnN') - format('%08x%04x%04x', *random_bytes) - end - end -end diff --git a/lib/pubnub/modules/crypto/crypto_module.rb b/lib/pubnub/modules/crypto/crypto_module.rb index 289ae33f9..3d5d8a78f 100644 --- a/lib/pubnub/modules/crypto/crypto_module.rb +++ b/lib/pubnub/modules/crypto/crypto_module.rb @@ -25,7 +25,7 @@ def self.new_aes_cbc(cipher_key, use_random_iv) } end - CryptoModule.new AesCbcCryptor.new(cipher_key), [LegacyCryptor.new(cipher_key, use_random_iv)], cipher_key, use_random_iv + CryptoModule.new AesCbcCryptor.new(cipher_key), [LegacyCryptor.new(cipher_key, use_random_iv)] end # Legacy AES-CBC cryptor based module. @@ -48,7 +48,7 @@ def self.new_legacy(cipher_key, use_random_iv) } end - CryptoModule.new LegacyCryptor.new(cipher_key, use_random_iv), [AesCbcCryptor.new(cipher_key)], cipher_key, use_random_iv + CryptoModule.new LegacyCryptor.new(cipher_key, use_random_iv), [AesCbcCryptor.new(cipher_key)] end # Create crypto module. @@ -57,20 +57,18 @@ def self.new_legacy(cipher_key, use_random_iv) # data. # @param cryptors [Array, nil] Additional cryptors which will be # used to decrypt data encrypted by previously used cryptors. - def initialize(default, cryptors, key = nil, random = true) + def initialize(default, cryptors) if default.nil? raise ArgumentError, { message: '\'default\' cryptor required for data encryption.' } end - @___cipher_key = key - @___use_random_iv = random - @default = default @cryptors = cryptors&.each_with_object({}) do |value, hash| hash[value.identifier] = value end || {} + super() end def encrypt(data) @@ -85,20 +83,22 @@ def encrypt(data) def decrypt(data) header = Crypto::CryptorHeader.parse(data) - cryptor_identifier = header&.identifier || '\x00\x00\x00\x00' + return nil if header.nil? + + cryptor_identifier = header.identifier || '\x00\x00\x00\x00' cryptor = cryptor cryptor_identifier # Check whether there is a cryptor to decrypt data or not. if cryptor.nil? - identifier = header&.identifier || 'UNKN' + identifier = header.identifier || 'UNKN' raise UnknownCryptorError, { message: "Decrypting data created by unknown cryptor. Please make sure to register #{identifier} or update SDK." } end - encrypted_data = data[(header&.length || 0)..-1] - metadata = metadata encrypted_data, (header&.data_size || 0) + encrypted_data = data[header.length..-1] + metadata = metadata encrypted_data, header.data_size # Check whether there is still some data for processing or not. return nil if encrypted_data.nil? || encrypted_data.empty? diff --git a/lib/pubnub/modules/crypto/cryptor_header.rb b/lib/pubnub/modules/crypto/cryptor_header.rb index ac620395a..c77690d08 100644 --- a/lib/pubnub/modules/crypto/cryptor_header.rb +++ b/lib/pubnub/modules/crypto/cryptor_header.rb @@ -113,7 +113,7 @@ def self.parse(data) _, version, identifier, data_size = data.unpack('A4 C A4 C') # Check whether version is within known range. - if version > CryptorHeader.current_version + if version > current_version raise UnknownCryptorError, { message: 'Decrypting data created by unknown cryptor.' } @@ -127,7 +127,7 @@ def self.parse(data) header = CryptorHeader.new header.send( :update_header_data, - CryptorHeader.create_header_data(version.to_i, identifier.to_s, data_size.to_i) + create_header_data(version.to_i, identifier.to_s, data_size.to_i) ) header end diff --git a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb index 649a50778..e327a8291 100644 --- a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb @@ -19,6 +19,7 @@ class AesCbcCryptor < Cryptor def initialize(cipher_key) @cipher_key = Digest::SHA256.digest(cipher_key) @alg = 'AES-256-CBC' + super() end def identifier @@ -35,16 +36,17 @@ def encrypt(data) encoded_message << cipher.final Crypto::EncryptedData.new(encoded_message, iv) rescue StandardError => e - Pubnub.error('Pubnub') { "ENCRYPTION ERROR: #{e}" } + puts "Pubnub :: ENCRYPTION ERROR: #{e}" nil end def decrypt(data) + # OpenSSL can't work with empty data. + return '' unless data.data.length.positive? + if data.metadata.length != BLOCK_SIZE - Pubnub.error('Pubnub') do - "DECRYPTION ERROR: Unexpected initialization vector length: + puts "Pubnub :: DECRYPTION ERROR: Unexpected initialization vector length: #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" - end return nil end @@ -55,7 +57,7 @@ def decrypt(data) decrypted = cipher.update data.data decrypted << cipher.final rescue StandardError => e - Pubnub.error('Pubnub') { "DECRYPTION ERROR: #{e}" } + puts "Pubnub :: DECRYPTION ERROR: #{e}" nil end end diff --git a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb index 54b08318d..1d6539164 100644 --- a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb @@ -24,6 +24,7 @@ def initialize(cipher_key, use_random_iv = true) @original_cipher_key = cipher_key @cipher_key = Digest::SHA256.hexdigest(cipher_key.to_s).slice(0, 32) @iv = use_random_iv ? nil : '0123456789012345' + super() end def identifier @@ -42,11 +43,14 @@ def encrypt(data) encoded_message << cipher.final Crypto::EncryptedData.new(encoded_message) rescue StandardError => e - Pubnub.error('Pubnub') { "ENCRYPTION ERROR: #{e}" } + puts "Pubnub :: ENCRYPTION ERROR: #{e}" nil end def decrypt(data) + # OpenSSL can't work with empty data. + return '' unless data.data.length.positive? + encrypted_data = data.data iv = if @iv.nil? encrypted_data.slice!(0..(BLOCK_SIZE - 1)) if encrypted_data.length >= BLOCK_SIZE @@ -54,10 +58,7 @@ def decrypt(data) @iv end if iv.length != BLOCK_SIZE - Pubnub.error('Pubnub') do - "DECRYPTION ERROR: Unexpected initialization vector length: - #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" - end + puts "DECRYPTION ERROR: Unexpected initialization vector length: #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" return nil end @@ -68,7 +69,7 @@ def decrypt(data) decrypted = cipher.update encrypted_data decrypted << cipher.final rescue StandardError => e - Pubnub.error('Pubnub') { "DECRYPTION ERROR: #{e}" } + puts "Pubnub :: DECRYPTION ERROR: #{e}" nil end end diff --git a/pubnub.gemspec b/pubnub.gemspec index 1126451fe..4102baa4a 100644 --- a/pubnub.gemspec +++ b/pubnub.gemspec @@ -18,7 +18,7 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.required_ruby_version = '>= 2.4' - spec.add_dependency 'addressable', '>= 2.8.0' + spec.add_dependency 'addressable', '>= 2.0.0' spec.add_dependency 'concurrent-ruby', '~> 1.1.5' spec.add_dependency 'concurrent-ruby-edge', '~> 0.5.0' spec.add_dependency 'dry-validation', '~> 1.0' From ac463fd2a55ed8a2d86bcd6263e705b92bce29bc Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 2 Oct 2023 01:10:15 +0300 Subject: [PATCH 10/18] fix: fix platforms list in lock file --- Gemfile.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile.lock b/Gemfile.lock index 216ebf051..bafff2ed8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -165,6 +165,7 @@ GEM PLATFORMS arm64-darwin-21 + x86_64-linux DEPENDENCIES awesome_print From 0b3efcc79591514b4f88a25626cd59dc3c6b6a45 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 2 Oct 2023 01:14:20 +0300 Subject: [PATCH 11/18] chore: revert Gemfile lock to master --- Gemfile.lock | 236 +++++++++++++++++++++++++++------------------------ 1 file changed, 126 insertions(+), 110 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bafff2ed8..5169c284a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,159 +13,175 @@ PATH GEM remote: https://rubygems.org/ specs: - addressable (2.8.5) - public_suffix (>= 2.0.2, < 6.0) - ast (2.4.2) - awesome_print (1.9.2) - base64 (0.1.1) - binding_of_caller (1.0.0) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + ast (2.4.1) + awesome_print (1.8.0) + binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) builder (3.2.4) codacy-coverage (2.2.1) simplecov coderay (1.1.3) - concurrent-ruby (1.1.10) + concurrent-ruby (1.1.6) concurrent-ruby-edge (0.5.0) concurrent-ruby (~> 1.1.5) - crack (0.4.5) - rexml - cucumber (9.0.2) + crack (0.4.3) + safe_yaml (~> 1.0.0) + cucumber (7.0.0) builder (~> 3.2, >= 3.2.4) - cucumber-ci-environment (~> 9.2, >= 9.2.0) - cucumber-core (~> 11.1, >= 11.1.0) - cucumber-cucumber-expressions (~> 16.1, >= 16.1.2) - cucumber-gherkin (>= 24, < 26.2.1) - cucumber-html-formatter (~> 20.4, >= 20.4.0) - cucumber-messages (>= 19, < 23) - diff-lcs (~> 1.5, >= 1.5.0) - mini_mime (~> 1.1, >= 1.1.5) - multi_test (~> 1.1, >= 1.1.0) - sys-uname (~> 1.2, >= 1.2.3) - cucumber-ci-environment (9.2.0) - cucumber-core (11.1.0) - cucumber-gherkin (>= 24, < 27) - cucumber-messages (>= 19, < 22) - cucumber-tag-expressions (~> 4.1, >= 4.1.0) - cucumber-cucumber-expressions (16.1.2) - cucumber-gherkin (26.2.0) - cucumber-messages (>= 19.1.4, < 22.1) - cucumber-html-formatter (20.4.0) - cucumber-messages (>= 18.0, < 22.1) - cucumber-messages (21.0.1) - cucumber-tag-expressions (4.1.0) - debug_inspector (1.1.0) - diff-lcs (1.5.0) + cucumber-core (~> 10.0, >= 10.0.1) + cucumber-create-meta (~> 6.0, >= 6.0.1) + cucumber-cucumber-expressions (~> 12.1, >= 12.1.1) + cucumber-gherkin (~> 20.0, >= 20.0.1) + cucumber-html-formatter (~> 16.0, >= 16.0.1) + cucumber-messages (~> 17.0, >= 17.0.1) + cucumber-wire (~> 6.0, >= 6.0.1) + diff-lcs (~> 1.4, >= 1.4.4) + mime-types (~> 3.3, >= 3.3.1) + multi_test (~> 0.1, >= 0.1.2) + sys-uname (~> 1.2, >= 1.2.2) + cucumber-core (10.0.1) + cucumber-gherkin (~> 20.0, >= 20.0.1) + cucumber-messages (~> 17.0, >= 17.0.1) + cucumber-tag-expressions (~> 3.0, >= 3.0.1) + cucumber-create-meta (6.0.1) + cucumber-messages (~> 17.0, >= 17.0.1) + sys-uname (~> 1.2, >= 1.2.2) + cucumber-cucumber-expressions (12.1.3) + cucumber-gherkin (20.0.1) + cucumber-messages (~> 17.0, >= 17.0.1) + cucumber-html-formatter (16.0.1) + cucumber-messages (~> 17.0, >= 17.0.1) + cucumber-messages (17.1.1) + cucumber-tag-expressions (3.0.1) + cucumber-wire (6.1.1) + cucumber-core (~> 10.0, >= 10.0.1) + cucumber-cucumber-expressions (~> 12.1, >= 12.1.2) + cucumber-messages (~> 17.0, >= 17.0.1) + debug_inspector (0.0.3) + diff-lcs (1.4.4) docile (1.4.0) - dry-configurable (1.1.0) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-core (1.0.1) + dry-configurable (0.11.6) + concurrent-ruby (~> 1.0) + dry-core (~> 0.4, >= 0.4.7) + dry-equalizer (~> 0.2) + dry-container (0.7.2) + concurrent-ruby (~> 1.0) + dry-configurable (~> 0.1, >= 0.1.3) + dry-core (0.4.9) concurrent-ruby (~> 1.0) - zeitwerk (~> 2.6) - dry-inflector (1.0.0) - dry-initializer (3.1.1) - dry-logic (1.5.0) + dry-equalizer (0.3.0) + dry-inflector (0.2.0) + dry-initializer (3.0.3) + dry-logic (1.0.6) concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) - zeitwerk (~> 2.6) - dry-schema (1.13.3) + dry-core (~> 0.2) + dry-equalizer (~> 0.2) + dry-schema (1.5.2) concurrent-ruby (~> 1.0) - dry-configurable (~> 1.0, >= 1.0.1) - dry-core (~> 1.0, < 2) + dry-configurable (~> 0.8, >= 0.8.3) + dry-core (~> 0.4) + dry-equalizer (~> 0.2) dry-initializer (~> 3.0) - dry-logic (>= 1.4, < 2) - dry-types (>= 1.7, < 2) - zeitwerk (~> 2.6) - dry-types (1.7.1) + dry-logic (~> 1.0) + dry-types (~> 1.4) + dry-types (1.4.0) concurrent-ruby (~> 1.0) - dry-core (~> 1.0) - dry-inflector (~> 1.0) - dry-logic (~> 1.4) - zeitwerk (~> 2.6) - dry-validation (1.10.0) + dry-container (~> 0.3) + dry-core (~> 0.4, >= 0.4.4) + dry-equalizer (~> 0.3) + dry-inflector (~> 0.1, >= 0.1.2) + dry-logic (~> 1.0, >= 1.0.2) + dry-validation (1.5.3) concurrent-ruby (~> 1.0) - dry-core (~> 1.0, < 2) + dry-container (~> 0.7, >= 0.7.1) + dry-core (~> 0.4) + dry-equalizer (~> 0.2) dry-initializer (~> 3.0) - dry-schema (>= 1.12, < 2) - zeitwerk (~> 2.6) - ffi (1.16.2) + dry-schema (~> 1.5) + ffi (1.13.1) + ffi (1.13.1-java) hashdiff (1.0.1) httpclient (2.8.3) interception (0.5) - json (2.6.3) - language_server-protocol (3.17.0.3) + json (2.3.1) + json (2.3.1-java) method_source (1.0.0) - mini_mime (1.1.5) - multi_test (1.1.0) - parallel (1.23.0) - parser (3.2.2.3) + mime-types (3.3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2021.0901) + multi_test (0.1.2) + parallel (1.19.2) + parser (2.7.1.4) ast (~> 2.4.1) - racc - pry (0.14.2) + pry (0.13.1) coderay (~> 1.1) method_source (~> 1.0) + pry (0.13.1-java) + coderay (~> 1.1) + method_source (~> 1.0) + spoon (~> 0.0) pry-rescue (1.5.2) interception (>= 0.5) pry (>= 0.12.0) - pry-stack_explorer (0.6.1) - binding_of_caller (~> 1.0) + pry-stack_explorer (0.4.11) + binding_of_caller (~> 0.7) pry (~> 0.13) - public_suffix (5.0.3) - racc (1.7.1) - rainbow (3.1.1) - regexp_parser (2.8.1) - rexml (3.2.6) - rr (3.1.0) - rspec (3.12.0) - rspec-core (~> 3.12.0) - rspec-expectations (~> 3.12.0) - rspec-mocks (~> 3.12.0) - rspec-core (3.12.2) - rspec-support (~> 3.12.0) - rspec-expectations (3.12.3) + public_suffix (4.0.5) + rainbow (3.0.0) + regexp_parser (1.7.1) + rexml (3.2.4) + rr (1.2.1) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) - rspec-mocks (3.12.6) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.12.0) + rspec-support (~> 3.9.0) rspec-retry (0.6.2) rspec-core (> 3.3) - rspec-support (3.12.1) - rubocop (1.56.4) - base64 (~> 0.1.1) - json (~> 2.3) - language_server-protocol (>= 3.17.0) + rspec-support (3.9.3) + rubocop (0.89.0) parallel (~> 1.10) - parser (>= 3.2.2.3) + parser (>= 2.7.1.1) rainbow (>= 2.2.2, < 4.0) - regexp_parser (>= 1.8, < 3.0) - rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.28.1, < 2.0) + regexp_parser (>= 1.7) + rexml + rubocop-ast (>= 0.1.0, < 1.0) ruby-progressbar (~> 1.7) - unicode-display_width (>= 2.4.0, < 3.0) - rubocop-ast (1.29.0) - parser (>= 3.2.1.0) - ruby-progressbar (1.13.0) - simplecov (0.22.0) + unicode-display_width (>= 1.4.0, < 2.0) + rubocop-ast (0.3.0) + parser (>= 2.7.1.4) + ruby-progressbar (1.10.1) + safe_yaml (1.0.5) + simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) simplecov_json_formatter (~> 0.1) simplecov-html (0.12.3) simplecov_json_formatter (0.1.4) - sys-uname (1.2.3) + spoon (0.0.6) + ffi + sys-uname (1.2.2) ffi (~> 1.1) - timers (4.3.5) - unicode-display_width (2.4.2) - vcr (6.2.0) - webmock (3.19.1) - addressable (>= 2.8.0) + timers (4.3.0) + unicode-display_width (1.7.0) + vcr (6.0.0) + webmock (3.8.3) + addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - zeitwerk (2.6.12) PLATFORMS - arm64-darwin-21 - x86_64-linux + java + ruby DEPENDENCIES awesome_print @@ -185,4 +201,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.4.6 + 2.1.4 \ No newline at end of file From 706f4c35eaad6d1abe36a1e0f799f021051f8113 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 2 Oct 2023 01:20:59 +0300 Subject: [PATCH 12/18] chore(codeowners): update codeowners list --- .github/CODEOWNERS | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c3bf0a3aa..30ea3fca3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,3 @@ -* @parfeon @CraigLn @kleewho @seba-aln -.github/* @parfeon @CraigLn @kleewho @seba-aln -README.md @techwritermat @kazydek +* @parfeon @MikeDobrzan @kleewho @seba-aln +.github/* @parfeon @MikeDobrzan @kleewho @seba-aln +README.md @techwritermat From 7c38f0a188055d961a37b73b07ccbe210a37fd74 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 3 Oct 2023 02:35:03 +0300 Subject: [PATCH 13/18] test: update steps definition for new feature steps --- Gemfile.lock | 2 +- lib/pubnub/modules/crypto/crypto_module.rb | 5 +++++ .../modules/crypto/cryptors/aes_cbc_cryptor.rb | 8 +++++--- .../modules/crypto/cryptors/legacy_cryptor.rb | 17 ++++++++++++----- 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5169c284a..a25e36d6a 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -201,4 +201,4 @@ DEPENDENCIES webmock BUNDLED WITH - 2.1.4 \ No newline at end of file + 2.1.4 diff --git a/lib/pubnub/modules/crypto/crypto_module.rb b/lib/pubnub/modules/crypto/crypto_module.rb index 3d5d8a78f..729a0e3a6 100644 --- a/lib/pubnub/modules/crypto/crypto_module.rb +++ b/lib/pubnub/modules/crypto/crypto_module.rb @@ -82,6 +82,11 @@ def encrypt(data) end def decrypt(data) + unless data.length.positive? + puts 'Pubnub :: DECRYPTION ERROR: Empty data for decryption' + nil + end + header = Crypto::CryptorHeader.parse(data) return nil if header.nil? diff --git a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb index e327a8291..068aa6572 100644 --- a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb @@ -27,6 +27,11 @@ def identifier end def encrypt(data) + if data.nil? || data.empty? + puts 'Pubnub :: ENCRYPTION ERROR: Empty data for encryption' + nil + end + iv = OpenSSL::Random.random_bytes BLOCK_SIZE cipher = OpenSSL::Cipher.new(@alg).encrypt cipher.key = @cipher_key @@ -41,9 +46,6 @@ def encrypt(data) end def decrypt(data) - # OpenSSL can't work with empty data. - return '' unless data.data.length.positive? - if data.metadata.length != BLOCK_SIZE puts "Pubnub :: DECRYPTION ERROR: Unexpected initialization vector length: #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" diff --git a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb index 1d6539164..b78811248 100644 --- a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb @@ -32,6 +32,11 @@ def identifier end def encrypt(data) + if data.nil? || data.empty? + puts 'Pubnub :: ENCRYPTION ERROR: Empty data for encryption' + nil + end + iv = @iv || OpenSSL::Random.random_bytes(BLOCK_SIZE) cipher = OpenSSL::Cipher.new(@alg).encrypt cipher.key = @cipher_key @@ -48,20 +53,22 @@ def encrypt(data) end def decrypt(data) - # OpenSSL can't work with empty data. - return '' unless data.data.length.positive? - encrypted_data = data.data - iv = if @iv.nil? + iv = if @iv.nil? && encrypted_data.length >= BLOCK_SIZE encrypted_data.slice!(0..(BLOCK_SIZE - 1)) if encrypted_data.length >= BLOCK_SIZE else @iv end if iv.length != BLOCK_SIZE - puts "DECRYPTION ERROR: Unexpected initialization vector length: #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" + puts "Pubnub :: DECRYPTION ERROR: Unexpected initialization vector length: #{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)" return nil end + unless encrypted_data.length.positive? + puts 'Pubnub :: DECRYPTION ERROR: Empty data for decryption' + nil + end + cipher = OpenSSL::Cipher.new(@alg).decrypt cipher.key = @cipher_key cipher.iv = iv From 0119565599b3e30af3362525b54cfc8a355e9520 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 3 Oct 2023 03:18:29 +0300 Subject: [PATCH 14/18] fix: fix errors discovered with contract tests --- .tool-versions | 2 +- Gemfile | 2 +- Gemfile.lock | 10 +++++----- features/step_definitions/crypto_steps.rb | 14 ++++++++++---- lib/pubnub/modules/crypto/crypto_module.rb | 4 ++-- .../modules/crypto/cryptors/aes_cbc_cryptor.rb | 2 +- .../modules/crypto/cryptors/legacy_cryptor.rb | 4 ++-- 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/.tool-versions b/.tool-versions index 75d16c6f0..f2a971aa7 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1 +1 @@ -ruby jruby-9.3.8.0 +ruby 3.2.2 diff --git a/Gemfile b/Gemfile index 76fe82685..a14432579 100644 --- a/Gemfile +++ b/Gemfile @@ -17,7 +17,7 @@ end group :development, :test do gem 'awesome_print' - gem 'pry' + gem 'pry', '>= 0.14.2' gem 'pry-rescue' gem 'pry-stack_explorer' end diff --git a/Gemfile.lock b/Gemfile.lock index a25e36d6a..5835d8427 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -100,8 +100,8 @@ GEM dry-equalizer (~> 0.2) dry-initializer (~> 3.0) dry-schema (~> 1.5) - ffi (1.13.1) - ffi (1.13.1-java) + ffi (1.16.2) + ffi (1.16.2-java) hashdiff (1.0.1) httpclient (2.8.3) interception (0.5) @@ -115,10 +115,10 @@ GEM parallel (1.19.2) parser (2.7.1.4) ast (~> 2.4.1) - pry (0.13.1) + pry (0.14.2) coderay (~> 1.1) method_source (~> 1.0) - pry (0.13.1-java) + pry (0.14.2-java) coderay (~> 1.1) method_source (~> 1.0) spoon (~> 0.0) @@ -187,7 +187,7 @@ DEPENDENCIES awesome_print codacy-coverage cucumber - pry + pry (>= 0.14.2) pry-rescue pry-stack_explorer pubnub! diff --git a/features/step_definitions/crypto_steps.rb b/features/step_definitions/crypto_steps.rb index fabe5825f..e34c166eb 100644 --- a/features/step_definitions/crypto_steps.rb +++ b/features/step_definitions/crypto_steps.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# require 'pubnub' Given(/^Crypto module with '([^']*)' cryptor$/) do |cryptor_id| @@ -22,11 +23,16 @@ @use_random_iv = use_random_iv != 'constant' end -When(/^I encrypt '([^']*)' file as '([^']*)'$/) do |file_name, type| +When(/^I encrypt '([^']*)' file as '([^']*)'$/) do |file_name, _| @source_file_name = file_name @source_file_content = File.binread "sdk-specifications/features/encryption/assets/#{file_name}" @encrypted_content = crypto_module.encrypt @source_file_content - expect(@encrypted_content).not_to eq nil + if file_name.include? 'empty' + @encrypt_status = 'encryption error' if @encrypted_content.nil? && @encrypt_status.nil? + @encrypt_status = 'success' if !@encrypted_content.nil? && @encrypt_status.nil? + else + expect(@encrypted_content).not_to eq nil + end end When(/^I decrypt '([^']*)' file$/) do |file_name| @@ -68,8 +74,8 @@ end Then(/^I receive '([^']*)'$/) do |outcome| - expect(@decrypt_status).not_to eq nil - expect(@decrypt_status).to eq outcome + expect(@encrypt_status || @decrypt_status).not_to eq nil + expect(@encrypt_status || @decrypt_status).to eq outcome end # Crypto module diff --git a/lib/pubnub/modules/crypto/crypto_module.rb b/lib/pubnub/modules/crypto/crypto_module.rb index 729a0e3a6..a65b32d27 100644 --- a/lib/pubnub/modules/crypto/crypto_module.rb +++ b/lib/pubnub/modules/crypto/crypto_module.rb @@ -82,9 +82,9 @@ def encrypt(data) end def decrypt(data) - unless data.length.positive? + if data.nil? || data.empty? puts 'Pubnub :: DECRYPTION ERROR: Empty data for decryption' - nil + return nil end header = Crypto::CryptorHeader.parse(data) diff --git a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb index 068aa6572..2c37b4ef1 100644 --- a/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb @@ -29,7 +29,7 @@ def identifier def encrypt(data) if data.nil? || data.empty? puts 'Pubnub :: ENCRYPTION ERROR: Empty data for encryption' - nil + return nil end iv = OpenSSL::Random.random_bytes BLOCK_SIZE diff --git a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb index b78811248..6782b30db 100644 --- a/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb +++ b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb @@ -34,7 +34,7 @@ def identifier def encrypt(data) if data.nil? || data.empty? puts 'Pubnub :: ENCRYPTION ERROR: Empty data for encryption' - nil + return nil end iv = @iv || OpenSSL::Random.random_bytes(BLOCK_SIZE) @@ -66,7 +66,7 @@ def decrypt(data) unless encrypted_data.length.positive? puts 'Pubnub :: DECRYPTION ERROR: Empty data for decryption' - nil + return nil end cipher = OpenSSL::Cipher.new(@alg).decrypt From 11583484a586cc5e07b29f7c9088d9ee56526cc0 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Tue, 3 Oct 2023 10:38:08 +0300 Subject: [PATCH 15/18] refactor: apply changes after review --- lib/pubnub/formatter.rb | 13 ------------- lib/pubnub/subscribe_event/formatter.rb | 7 ------- 2 files changed, 20 deletions(-) diff --git a/lib/pubnub/formatter.rb b/lib/pubnub/formatter.rb index f9bf35265..bff735e4c 100644 --- a/lib/pubnub/formatter.rb +++ b/lib/pubnub/formatter.rb @@ -41,19 +41,6 @@ def format_uuid(uuids, should_encode = true) end end - # Transforms message to json and encode it - # def format_message(message, cipher_key = '', use_random_iv = false, uri_escape = true) - # if cipher_key && !cipher_key.empty? - # pc = Pubnub::Crypto.new(cipher_key, use_random_iv) - # message = pc.encrypt(message).to_json - # message = Addressable::URI.escape(message) if uri_escape - # else - # message = message.to_json - # message = Formatter.encode(message) if uri_escape - # end - # message - # end - # TODO: Uncomment code below when cryptor implementations will be added. # Transforms message to json and encode it. # diff --git a/lib/pubnub/subscribe_event/formatter.rb b/lib/pubnub/subscribe_event/formatter.rb index 5cb6ad988..a460db538 100644 --- a/lib/pubnub/subscribe_event/formatter.rb +++ b/lib/pubnub/subscribe_event/formatter.rb @@ -38,13 +38,6 @@ def decipher_payload(message) encrypted_message = Base64.decode64(message[:payload]) JSON.parse(crypto_module.decrypt(encrypted_message), quirks_mode: true) - # - # return message[:payload] if message[:channel].end_with?('-pnpres') || (@app.env[:cipher_key].nil? && @cipher_key.nil? && @cipher_key_selector.nil? && @env[:cipher_key_selector].nil?) - # data = message.reject { |k, _v| k == :payload } - # cipher_key = compute_cipher_key(data) - # random_iv = compute_random_iv(data) - # crypto = Pubnub::Crypto.new(cipher_key, random_iv) - # JSON.parse(crypto.decrypt(message[:payload]), quirks_mode: true) rescue StandardError, UnknownCryptorError message[:payload] end From 220f87a32e7a1c912f3bba592d06d0f3a8efb3fb Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 16 Oct 2023 10:16:12 +0300 Subject: [PATCH 16/18] docs(license): update license --- LICENSE.txt | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index da5ecef98..65ff4dd36 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,30 +1,4 @@ -PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks -Copyright (c) 2013-2014 PubNub Inc. -http://www.pubnub.com/ -http://www.pubnub.com/terms - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -PubNub Real-time Cloud-Hosted Push API and Push Notification Client Frameworks -Copyright (c) 2013-2014 PubNub Inc. -http://www.pubnub.com/ -http://www.pubnub.com/terms \ No newline at end of file +PubNub Software Development Kit License Agreement +Copyright © 2023 PubNub Inc. All rights reserved. +Subject to the terms and conditions of the license, you are hereby granted a non-exclusive, worldwide, royalty-free license to (a) copy and modify the software in source code or binary form for use with the software services and interfaces provided by PubNub, and (b) redistribute unmodified copies of the software to third parties. The software may not be incorporated in or used to provide any product or service competitive with the products and services of PubNub. The above copyright notice and this license shall be included in or with all copies or substantial portions of the software. +This license does not grant you permission to use the trade names, trademarks, service marks, or product names of PubNub, except as required for reasonable and customary use in describing the origin of the software and reproducing the content of this license.THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file From c8c377ba0cea7b21c352412c812914e342ae0575 Mon Sep 17 00:00:00 2001 From: Serhii Mamontov Date: Mon, 16 Oct 2023 14:12:59 +0300 Subject: [PATCH 17/18] docs(license): update license file --- LICENSE | 29 +++++++++++++++++++++++++++++ LICENSE.txt | 4 ---- pubnub.gemspec | 4 ++-- 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 LICENSE delete mode 100644 LICENSE.txt diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..5e1ef1880 --- /dev/null +++ b/LICENSE @@ -0,0 +1,29 @@ +PubNub Software Development Kit License Agreement +Copyright © 2023 PubNub Inc. All rights reserved. + +Subject to the terms and conditions of the license, you are hereby granted +a non-exclusive, worldwide, royalty-free license to (a) copy and modify +the software in source code or binary form for use with the software services +and interfaces provided by PubNub, and (b) redistribute unmodified copies +of the software to third parties. The software may not be incorporated in +or used to provide any product or service competitive with the products +and services of PubNub. + +The above copyright notice and this license shall be included +in or with all copies or substantial portions of the software. + +This license does not grant you permission to use the trade names, trademarks, +service marks, or product names of PubNub, except as required for reasonable +and customary use in describing the origin of the software and reproducing +the content of this license. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF +ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF +CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +https://www.pubnub.com/ +https://www.pubnub.com/terms diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 65ff4dd36..000000000 --- a/LICENSE.txt +++ /dev/null @@ -1,4 +0,0 @@ -PubNub Software Development Kit License Agreement -Copyright © 2023 PubNub Inc. All rights reserved. -Subject to the terms and conditions of the license, you are hereby granted a non-exclusive, worldwide, royalty-free license to (a) copy and modify the software in source code or binary form for use with the software services and interfaces provided by PubNub, and (b) redistribute unmodified copies of the software to third parties. The software may not be incorporated in or used to provide any product or service competitive with the products and services of PubNub. The above copyright notice and this license shall be included in or with all copies or substantial portions of the software. -This license does not grant you permission to use the trade names, trademarks, service marks, or product names of PubNub, except as required for reasonable and customary use in describing the origin of the software and reproducing the content of this license.THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL PUBNUB OR THE AUTHORS OR COPYRIGHT HOLDERS OF THE SOFTWARE BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/pubnub.gemspec b/pubnub.gemspec index 4102baa4a..0a5ee66f2 100644 --- a/pubnub.gemspec +++ b/pubnub.gemspec @@ -9,8 +9,8 @@ Gem::Specification.new do |spec| spec.email = ['support@pubnub.com'] spec.summary = 'PubNub Official Ruby gem.' spec.description = 'Ruby anywhere in the world in 250ms with PubNub!' - spec.homepage = 'http://github.com/pubnub/ruby' - spec.license = 'MIT' + spec.homepage = 'https://github.com/pubnub/ruby' + spec.licenses = %w[MIT LicenseRef-LICENSE] spec.files = `git ls-files -z`.split("\x0").grep_v(/^(test|spec|fixtures)/) spec.executables = spec.files.grep(%r{^bin\/}) { |f| File.basename(f) } From 1a083f615161323f7e8b4dd7e37717345b77263b Mon Sep 17 00:00:00 2001 From: PubNub Release Bot <120067856+pubnub-release-bot@users.noreply.github.com> Date: Mon, 16 Oct 2023 11:34:10 +0000 Subject: [PATCH 18/18] PubNub SDK v5.3.0 release. --- .pubnub.yml | 15 +++++++++++---- CHANGELOG.md | 9 +++++++++ Gemfile.lock | 2 +- VERSION | 2 +- lib/pubnub/version.rb | 2 +- 5 files changed, 23 insertions(+), 7 deletions(-) diff --git a/.pubnub.yml b/.pubnub.yml index 331a77e10..cf458d9b1 100644 --- a/.pubnub.yml +++ b/.pubnub.yml @@ -1,6 +1,13 @@ --- -version: "5.2.2" +version: "5.3.0" changelog: + - date: 2023-10-16 + version: v5.3.0 + changes: + - type: feature + text: "Add crypto module that allows to configure SDK to encrypt and decrypt messages." + - type: bug + text: "Improved security of crypto implementation by adding enhanced AES-CBC cryptor." - date: 2023-03-14 version: v5.2.2 changes: @@ -663,7 +670,7 @@ sdks: - x86-64 - distribution-type: package distribution-repository: RubyGems - package-name: pubnub-5.2.2.gem + package-name: pubnub-5.3.0.gem location: https://rubygems.org/gems/pubnub requires: - name: addressable @@ -768,8 +775,8 @@ sdks: - x86-64 - distribution-type: library distribution-repository: GitHub release - package-name: pubnub-5.2.2.gem - location: https://github.com/pubnub/ruby/releases/download/v5.2.2/pubnub-5.2.2.gem + package-name: pubnub-5.3.0.gem + location: https://github.com/pubnub/ruby/releases/download/v5.3.0/pubnub-5.3.0.gem requires: - name: addressable min-version: 2.0.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index f4939b6f1..d45a5f06b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## v5.3.0 +October 16 2023 + +#### Added +- Add crypto module that allows to configure SDK to encrypt and decrypt messages. + +#### Fixed +- Improved security of crypto implementation by adding enhanced AES-CBC cryptor. + ## v5.2.2 March 14 2023 diff --git a/Gemfile.lock b/Gemfile.lock index 5835d8427..18a2bc9e8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - pubnub (5.2.2) + pubnub (5.3.0) addressable (>= 2.0.0) concurrent-ruby (~> 1.1.5) concurrent-ruby-edge (~> 0.5.0) diff --git a/VERSION b/VERSION index ce7f2b425..03f488b07 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -5.2.2 +5.3.0 diff --git a/lib/pubnub/version.rb b/lib/pubnub/version.rb index 290168aa7..11ae6978f 100644 --- a/lib/pubnub/version.rb +++ b/lib/pubnub/version.rb @@ -1,4 +1,4 @@ # Toplevel Pubnub module. module Pubnub - VERSION = '5.2.2'.freeze + VERSION = '5.3.0'.freeze end