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/.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/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 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..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)
@@ -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/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 da5ecef98..000000000
--- a/LICENSE.txt
+++ /dev/null
@@ -1,30 +0,0 @@
-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
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/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..e34c166eb
--- /dev/null
+++ b/features/step_definitions/crypto_steps.rb
@@ -0,0 +1,99 @@
+# 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, _|
+ @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
+ 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|
+ 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(@encrypt_status || @decrypt_status).not_to eq nil
+ expect(@encrypt_status || @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 0614ef0e3..e11c814b8 100644
--- a/lib/pubnub/client.rb
+++ b/lib/pubnub/client.rb
@@ -1,13 +1,16 @@
+# 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/modules/crypto/module'
+
require 'pubnub/schemas/envelope_schema'
require 'pubnub/event'
@@ -183,6 +186,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}"
@@ -339,6 +343,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)
@@ -386,9 +398,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] = Crypto::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/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/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..42c0f5206 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 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
@@ -217,6 +217,14 @@ def compute_random_iv(data)
ck.call(data)
end
+ # Data processing crypto module.
+ #
+ # @return [Pubnub::Crypto::CryptoProvider, nil] Crypto module for data encryption and
+ # decryption.
+ def crypto_module
+ @crypto_module
+ end
+
def error_message(parsed_response)
parsed_response['message']
rescue StandardError
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 2637f43e8..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,23 +68,30 @@ 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
def valid_envelope(parsed_response, req_res_objects)
messages = parsed_response[0]
- 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)
+ # 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)
+ # 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 6c7d2f27b..bff735e4c 100644
--- a/lib/pubnub/formatter.rb
+++ b/lib/pubnub/formatter.rb
@@ -41,17 +41,28 @@ 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
+ # 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
+ 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
- message
+ json_message
end
# Quite lazy way, but good enough for current usage
@@ -100,7 +111,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..a65b32d27
--- /dev/null
+++ b/lib/pubnub/modules/crypto/crypto_module.rb
@@ -0,0 +1,159 @@
+module Pubnub
+ module Crypto
+ # 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 < 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)]
+ 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.
+ #
+ # @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)]
+ 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 || {}
+ super()
+ 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)
+ if data.nil? || data.empty?
+ puts 'Pubnub :: DECRYPTION ERROR: Empty data for decryption'
+ return nil
+ end
+
+ header = Crypto::CryptorHeader.parse(data)
+ 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'
+ raise UnknownCryptorError, {
+ message: "Decrypting data created by unknown cryptor. Please make sure to register
+#{identifier} or update SDK."
+ }
+ end
+
+ 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?
+
+ cryptor.decrypt(EncryptedData.new(encrypted_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
+
+ # 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
+
+ # 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/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..446c112b6
--- /dev/null
+++ b/lib/pubnub/modules/crypto/cryptor.rb
@@ -0,0 +1,73 @@
+# 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..c77690d08
--- /dev/null
+++ b/lib/pubnub/modules/crypto/cryptor_header.rb
@@ -0,0 +1,251 @@
+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] 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 && identifier != '\x00\x00\x00\x00'
+ 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 > 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,
+ create_header_data(version.to_i, identifier.to_s, data_size.to_i)
+ )
+ 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.
+ 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 missing or empty.'
+ }
+ 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/modules/crypto/cryptors/aes_cbc_cryptor.rb b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb
new file mode 100644
index 000000000..2c37b4ef1
--- /dev/null
+++ b/lib/pubnub/modules/crypto/cryptors/aes_cbc_cryptor.rb
@@ -0,0 +1,67 @@
+# 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'
+ super()
+ end
+
+ def identifier
+ 'ACRH'
+ end
+
+ def encrypt(data)
+ if data.nil? || data.empty?
+ puts 'Pubnub :: ENCRYPTION ERROR: Empty data for encryption'
+ return nil
+ end
+
+ 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
+ puts "Pubnub :: ENCRYPTION ERROR: #{e}"
+ nil
+ end
+
+ def decrypt(data)
+ if data.metadata.length != BLOCK_SIZE
+ puts "Pubnub :: DECRYPTION ERROR: Unexpected initialization vector length:
+#{data.metadata.length} bytes (#{BLOCK_SIZE} bytes is expected)"
+ 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
+ puts "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..6782b30db
--- /dev/null
+++ b/lib/pubnub/modules/crypto/cryptors/legacy_cryptor.rb
@@ -0,0 +1,84 @@
+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'
+ super()
+ end
+
+ def identifier
+ '\x00\x00\x00\x00'
+ end
+
+ def encrypt(data)
+ if data.nil? || data.empty?
+ puts 'Pubnub :: ENCRYPTION ERROR: Empty data for encryption'
+ return nil
+ end
+
+ 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
+ puts "Pubnub :: ENCRYPTION ERROR: #{e}"
+ nil
+ end
+
+ def decrypt(data)
+ encrypted_data = data.data
+ 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 "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'
+ 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
+ puts "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 3d64d77c6..a460db538 100644
--- a/lib/pubnub/subscribe_event/formatter.rb
+++ b/lib/pubnub/subscribe_event/formatter.rb
@@ -33,13 +33,12 @@ def build_error_envelopes(_parsed_response, error, req_res_objects)
end
def decipher_payload(message)
- 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
+ # TODO: Uncomment code below when cryptor implementations will be added.
+ 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)
+ rescue StandardError, UnknownCryptorError
message[:payload]
end
@@ -51,7 +50,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
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
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) }
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