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