class Ubiq::Decryption

Class to provide data decryption, either as a simple single function call or as a piecewise where the entire data element isn't available at once or is too large to process in a single call.

Public Class Methods

new(creds) click to toggle source
# File lib/ubiq/decrypt.rb, line 18
def initialize(creds)
  # Initialize the decryption module object
  # Set the credentials in instance varibales to be used among methods
  # the server to which to make the request
  raise 'Some of your credentials are missing, please check!' unless validate_creds(creds)

  @host = creds.host.blank? ? UBIQ_HOST : creds.host

  # The client's public API key (used to identify the client to the server
  @papi = creds.access_key_id

  # The client's secret API key (used to authenticate HTTP requests)
  @sapi = creds.secret_signing_key

  # The client's secret RSA encryption key/password (used to decrypt the
  # client's RSA key from the server). This key is not retained by this object.
  @srsa = creds.secret_crypto_access_key

  @decryption_ready = true
  @decryption_started = false

end

Public Instance Methods

begin() click to toggle source
# File lib/ubiq/decrypt.rb, line 49
def begin
  # Begin the decryption process

  # This interface does not take any cipher text in its arguments
  # in an attempt to maintain an API that corresponds to the
  # encryption object. In doing so, the work that can take place
  # in this function is limited. without any data, there is no
  # way to determine which key is in use or decrypt any data.
  #
  # this function simply throws an error if starting an decryption
  # while one is already in progress, and initializes the internal
  # buffer

  raise 'Decryption is not ready' unless @decryption_ready

  raise 'Decryption Already Started' if @decryption_started

  raise 'Decryption already in progress' if @key.present? && @key.key?('dec')

  @decryption_started = true
  @data = ''
end
close() click to toggle source
# File lib/ubiq/decrypt.rb, line 238
def close
  raise 'Decryption currently running' if @decryption_started

  # Reset the internal state of the decryption object
  if @key.present?
    if @key['uses'].positive?
      query_url = "#{endpoint}/#{@key['finger_print']}/#{@key['session']}"
      url = "#{endpoint_base}/decryption/key/#{@key['finger_print']}/#{@key['session']}"
      query = { uses: @key['uses'] }
      headers = Auth.build_headers(@papi, @sapi, query_url, query, @host, 'patch')
      response = HTTParty.patch(
        url,
        body: query.to_json,
        headers: headers
      )
      remove_instance_variable(:@data)
      remove_instance_variable(:@key)
    end
  end
end
end() click to toggle source
# File lib/ubiq/decrypt.rb, line 210
def end
  raise 'Decryption is not Started' unless @decryption_started

  # The update function always maintains tag-size bytes in
  # the buffer because this function provides no data parameter.
  # by the time the caller calls this function, all data must
  # have already been input to the decryption object.

  sz = @data.length - @algo[:tag_length]

  raise 'Invalid Tag!' if sz.negative?

  if sz.zero?
    @key['dec'].auth_tag = @data
    begin
      pt = @key['dec'].final
      # Delete the decryptor context
      @key.delete('dec')
      # Return the decrypted plain data
      @decryption_started = false
      return pt
    rescue Exception
      print 'Invalid cipher data or tag!'
      return ''
    end
  end
end
endpoint() click to toggle source
# File lib/ubiq/decrypt.rb, line 45
def endpoint
  '/api/v0/decryption/key'
end
endpoint_base() click to toggle source
# File lib/ubiq/decrypt.rb, line 41
def endpoint_base
  @host + '/api/v0'
end
update(data) click to toggle source
# File lib/ubiq/decrypt.rb, line 72
def update(data)
  # Decryption of cipher text is performed here
  # Cipher text must be passed to this function in the order in which
  # it was output from the encryption.update function.

  # Each encryption has a header on it that identifies the algorithm
  # used  and an encryption of the data key that was used to encrypt
  # the original plain text. there is no guarantee how much of that
  # data will be passed to this function or how many times this
  # function will be called to process all of the data. to that end,
  # this function buffers data internally, when it is unable to
  # process it.
  #
  # The function buffers data internally until the entire header is
  # received. once the header has been received, the encrypted data
  # key is sent to the server for decryption. after the header has
  # been successfully handled, this function always decrypts all of
  # the data in its internal buffer *except* for however many bytes
  # are specified by the algorithm's tag size. see the end() function
  # for details.

  raise 'Decryption is not Started' unless @decryption_started

  # Append the incoming data in the internal data buffer
  @data += data

  # if there is no key or 'dec' member of key, then the code is
  # still trying to build a complete header
  if !@key.present? || !@key.key?('dec')
    struct_length = [1, 1, 1, 1, 1].pack('CCCCn').length
    packed_struct = @data[0...struct_length]

    # Does the buffer contain enough of the header to
    # determine the lengths of the initialization vector
    # and the key?
    if @data.length > struct_length
      # Unpack the values packed in encryption
      version, flags, algorithm_id, iv_length, key_length = packed_struct.unpack('CCCCn')

      # verify flag are correct and version is 0
      raise 'invalid encryption header' if (version != 0 ) || ((flags & ~Algo::UBIQ_HEADER_V0_FLAG_AAD) != 0)

      # Does the buffer contain the entire header?
      if @data.length > struct_length + iv_length + key_length
        # Extract the initialization vector
        iv = @data[struct_length...iv_length + struct_length]
        # Extract the encryped key
        encrypted_key = @data[struct_length + iv_length...key_length + struct_length + iv_length]
        # Remove the header from the buffer
        @data = @data[struct_length + iv_length + key_length..-1]

        # generate a local identifier for the key
        hash_sha512 = OpenSSL::Digest::SHA512.new
        hash_sha512 << encrypted_key
        client_id = hash_sha512.digest

        if @key.present?
          close if @key['client_id'] != client_id
        end

        # IF key object not exists, request a new one from the server
        unless @key.present?
          url = endpoint_base + '/decryption/key'
          query = { encrypted_data_key: Base64.strict_encode64(encrypted_key) }
          headers = Auth.build_headers(@papi, @sapi, endpoint, query, @host, 'post')

          response = HTTParty.post(
            url,
            body: query.to_json,
            headers: headers
          )

          # Response status is 200 OK
          if response.code == WEBrick::HTTPStatus::RC_OK
            @key = {}
            @key['finger_print'] = response['key_fingerprint']
            @key['client_id'] = client_id
            @key['session'] = response['encryption_session']

            # Get the algorithm name from the internal algorithm id in the header
            @key['algorithm'] = Algo.new.find_alg(algorithm_id)

            encrypted_private_key = response['encrypted_private_key']
            # Decrypt the encryped private key using SRSA
            private_key = OpenSSL::PKey::RSA.new(encrypted_private_key, @srsa)

            wrapped_data_key = response['wrapped_data_key']
            # Decode WDK from base64 format
            wdk = Base64.strict_decode64(wrapped_data_key)
            # Use private key to decrypt the wrapped data key
            dk = private_key.private_decrypt(wdk, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING)

            @key['raw'] = dk
            @key['uses'] = 0
          else
            # Raise the error if response is not 200
            raise "HTTPError Response: Expected 201, got #{response.code}"
          end
        end

        # If the key object exists, create a new decryptor
        # with the initialization vector from the header and
        # the decrypted key (which is either new from the
        # server or cached from the previous decryption). in
        # either case, increment the key usage

        if @key.present?
          @algo = Algo.new.get_algo(@key['algorithm'])
          @key['dec'] = Algo.new.decryptor(@algo, @key['raw'], iv)
          # Documentation indicates the auth_data has to be set AFTER auth_tag
          # but we get an OpenSSL error when it is set AFTER an update call.
          # Checking OpenSSL documentation, there is not a requirement to set
          # auth_data before auth_tag so Ruby documentation seems to be
          # wrong.  This approach works and is compatible with the encrypted
          # data produced by the other languages' client library
          if (flags & Algo::UBIQ_HEADER_V0_FLAG_AAD) != 0
             @key['dec'].auth_data = packed_struct + iv + encrypted_key
          end
          @key['uses'] += 1
        end
      end
    end
  end

  # if the object has a key and a decryptor, then decrypt whatever
  # data is in the buffer, less any data that needs to be saved to
  # serve as the tag.
  plain_text = ''
  if @key.present? && @key.key?('dec')
    size = @data.length - @algo[:tag_length]
    if size.positive?
      plain_text = @key['dec'].update(@data[0..size - 1])
      @data = @data[size..-1]
    end
    return plain_text
  end
end