Skip to content

Commit

Permalink
refactor(crypto): decouple crypto into module
Browse files Browse the repository at this point in the history
Decouple code which works with data encryption and decryption into separate module.
  • Loading branch information
parfeon committed Sep 29, 2023
1 parent d1223fe commit 80398f6
Show file tree
Hide file tree
Showing 11 changed files with 531 additions and 13 deletions.
18 changes: 18 additions & 0 deletions lib/pubnub/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down Expand Up @@ -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
# <i>cipher_key</i> and not <i>crypto_module</i>).
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
Expand Down
2 changes: 1 addition & 1 deletion lib/pubnub/crypto.rb → lib/pubnub/cryptor.rb
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
3 changes: 3 additions & 0 deletions lib/pubnub/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,7 @@ class RequestError < Error

class ResponseError < Error
end

class UnknownCryptorError < Error
end
end
18 changes: 13 additions & 5 deletions lib/pubnub/event.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion lib/pubnub/events/history.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
26 changes: 23 additions & 3 deletions lib/pubnub/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
116 changes: 116 additions & 0 deletions lib/pubnub/modules/crypto/crypto_module.rb
Original file line number Diff line number Diff line change
@@ -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
# <i>LegacyCrypto</i>. In addition to the <i>LegacyCrypto</i> for data
# decryption, the <i>AesCbcCryptor</i> 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
# <i>LegacyCrypto</i>. In addition to the <i>LegacyCrypto</i> for data
# decryption, the <i>AesCbcCryptor</i> 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<Cryptor>, 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<String, Cryptor>] 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
31 changes: 31 additions & 0 deletions lib/pubnub/modules/crypto/crypto_provider.rb
Original file line number Diff line number Diff line change
@@ -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 <b>PubNub</b> 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 <i>nil</i> 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 <i>nil</i> in case of decryption
# error.
#
# @raise [UnknownCryptorError] If the <i>cryptor</i> for data processing is
# not registered.
def decrypt(data)
raise NotImplementedError, 'Subclass should provide "decrypt" method implementation.'
end
end
end
end
72 changes: 72 additions & 0 deletions lib/pubnub/modules/crypto/cryptor.rb
Original file line number Diff line number Diff line change
@@ -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 <i>metadata</i> may contain:
# * initialization vector
# * cipher key identifier
# * encrypted <i>data</i> 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 <i>cryptor</i> needs to handle it later.
#
# @param data [String] Outcome of successful cryptor <i>encrypt</i> method
# call.
# @param metadata [String, nil] Additional information is provided by
# <i>cryptor</i> 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
# <i>CryptorProvider</i> implementation for data encryption and decryption.
class Cryptor
# Identifier will be encoded into cryptor data header and passed along
# with encrypted <i>data</i> and <i>metadata</i>.
#
# The identifier <b>must</b> 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 <i>nil</i> 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 <i>nil</i> in case of decryption
# error.
def decrypt(data)
raise NotImplementedError, 'Subclass should provide "decrypt" method implementation.'
end
end
end
end
Loading

0 comments on commit 80398f6

Please sign in to comment.