hackedteam/rcs-db

View on GitHub
lib/rcs-db/license.rb

Summary

Maintainability
F
6 days
Test Coverage
# encoding: utf-8
#
#  License handling stuff
#

# relative
require_relative 'dongle.rb'
require_relative 'shard.rb'

# from RCS::Common
require 'rcs-common/trace'
require 'rcs-common/crypt'

# system
require 'yaml'
require 'pp'
require 'optparse'

class NoLicenseError < StandardError
  attr_reader :msg

  def initialize(msg)
    @msg = msg
  end

  def to_s
    @msg
  end
end

class LicenseManager
  include Singleton
  include RCS::Tracer
  include RCS::Crypt

  LICENSE_VERSION = '9.2'

  LICENSE_FILE = 'rcs.lic'

  DONT_STEAL_RCS = "Ò€‹›fifl‡°·‚æ…¬˚∆˙©ƒ∂ß´®†¨ˆøΩ≈ç√∫˜µ≤¡™£¢∞§¶•ªº"

  attr_reader :limits

  def initialize
    # default values.
    # you have at least:
    #   - one user to login to the system
    #   - one collector to receive data
    #   - cannot create agents (neither demo nor real)
    @limits = {:type => 'reusable',
               :serial => "off",
               :version => LICENSE_VERSION,
               :users => 1,
               :agents => {:total => 0,
                              :desktop => 0,
                              :mobile => 0,
                              :windows => [false, false],
                              :osx => [false, false],
                              :linux => [false, false],
                              :winmo => [false, false],
                              :winphone => [false, false],
                              :ios => [false, false],
                              :blackberry => [false, false],
                              :symbian => [false, false],
                              :android => [false, false]},
               :alerting => false,
               :correlation => false,
               :intelligence => false,
               :connectors => false,
               :rmi => [false, false],
               :nia => [0, false],
               :shards => 1,
               :exploits => false,
               :deletion => false,
               :modify => false,
               :archive => false,
               :scout => true,
               :ocr => true,
               :translation => false,
               :collectors => {:collectors => 1, :anonymizers => 0}}
  end

  def load_license(periodic = false)

    # load the license file
    lic_file = File.join $execution_directory, RCS::DB::Config::CONF_DIR, LICENSE_FILE

    unless File.exist? lic_file
      trace :fatal, "No license file found"
      exit!
    end

    trace :info, "Loading license limits #{lic_file}" unless periodic

    File.open(lic_file, "rb") do |f|
      lic = YAML.load(f.read)

      # check the authenticity of the license
      unless crypt_check(lic)
        trace :fatal, 'Invalid License File: corrupted integrity check'
        exit!
      end

      # the license is not for this version
      if lic[:version] != LICENSE_VERSION
        trace :fatal, 'Invalid License File: incorrect version'
        exit!
      end

      # use local time if the dongle presence is not enforced
      if lic[:serial] == 'off'
        time = Time.now.getutc
      else
        time = RCS::DB::Dongle.time
      end

      if not lic[:expiry].nil? and Time.parse(lic[:expiry]).getutc < time
        trace :fatal, "Invalid License File: license expired on #{Time.parse(lic[:expiry]).getutc}"
        exit!
      end

      if lic[:maintenance].nil?
        trace :fatal, "Invalid License File: invalid maintenance period"
        exit!
      end

      # load only licenses valid for the current dongle's serial and current version
      add_limits lic
    end

    # sanity check
    if @limits[:agents][:total] < @limits[:agents][:desktop] or @limits[:agents][:total] < @limits[:agents][:mobile]
      trace :fatal, 'Invalid License File: total is lower than desktop or mobile'
      exit!
    end

    begin
      if @limits[:serial] != 'off'
        trace :info, "Checking for hardware dongle..." unless periodic
        # get the version from the dongle (can rise exception)
        info = RCS::DB::Dongle.info
        trace :info, "HASP info: " + info.inspect
        raise 'Invalid License File: incorrect serial number' if @limits[:serial] != info[:serial]
        raise 'Cannot read storage from token' if @limits[:type] == 'oneshot' && (info[:error_code] == RCS::DB::Dongle::ERROR_LOGIN || info[:error_code] == RCS::DB::Dongle::ERROR_STORAGE)
      else
        trace :info, "Hardware dongle not required..." unless periodic
      end
    rescue Exception => e
      trace :fatal, e.message
      exit!
    end

    return true
  end

  def new_license(file)
    file = "#{ENV['CWD']}/#{file}" if !File.exist?(file) and ENV['CWD']
    raise "file not found" unless File.exist?(file)

    trace :info, "Loading new license file #{file}"

    content = File.open(file, "rb") {|f| f.read}
    lic = YAML.load(content)

    # check the autenticity of the license
    unless crypt_check(lic)
      raise 'Invalid License File: corrupted integrity check'
    end

    # the license is not for this version
    if lic[:version] != LICENSE_VERSION
      raise 'Invalid License File: incorrect version'
    end

    if lic[:serial] != 'off'
      trace :info, "Checking for hardware dongle..."
      # get the version from the dongle (can rise exception)
      info = RCS::DB::Dongle.info
      trace :info, "HASP info: " + info.inspect
      raise 'Invalid License File: incorrect serial number' if lic[:serial] != info[:serial]
      raise 'Cannot read storage from token' if lic[:type] == 'oneshot' && (info[:error_code] == RCS::DB::Dongle::ERROR_LOGIN || info[:error_code] == RCS::DB::Dongle::ERROR_STORAGE)
    else
      trace :info, "Hardware dongle not required..."
    end

    # save the new license file
    lic_file = File.join $execution_directory, RCS::DB::Config::CONF_DIR, LICENSE_FILE
    File.open(lic_file, "wb") {|f| f.write content}

    # load the new one
    load_license(true)

    trace :info, "New license file saved"
  end

  def add_limits(limit)

    @limits[:magic] = limit[:check]

    @limits[:type] = limit[:type]
    @limits[:serial] = limit[:serial]

    @limits[:expiry] = limit[:expiry].nil? ? nil : Time.parse(limit[:expiry]).getutc
    @limits[:maintenance] = Time.parse(limit[:maintenance]).getutc

    @limits[:users] = limit[:users]

    @limits[:agents][:total] = limit[:agents][:total]
    @limits[:agents][:mobile] = limit[:agents][:mobile]
    @limits[:agents][:desktop] = limit[:agents][:desktop]

    @limits[:agents][:windows] = limit[:agents][:windows]
    @limits[:agents][:osx] = limit[:agents][:osx]
    @limits[:agents][:linux] = limit[:agents][:linux]
    @limits[:agents][:winmo] = limit[:agents][:winmo]
    @limits[:agents][:winphone] = limit[:agents][:winphone]
    @limits[:agents][:symbian] = limit[:agents][:symbian]
    @limits[:agents][:ios] = limit[:agents][:ios]
    @limits[:agents][:blackberry] = limit[:agents][:blackberry]
    @limits[:agents][:android] = limit[:agents][:android]
    
    @limits[:collectors][:collectors] = limit[:collectors][:collectors] unless limit[:collectors][:collectors].nil?
    @limits[:collectors][:anonymizers] = limit[:collectors][:anonymizers] unless limit[:collectors][:anonymizers].nil?

    @limits[:nia] = limit[:nia]
    @limits[:rmi] = limit[:rmi]

    @limits[:alerting] = limit[:alerting]
    @limits[:connectors] = limit[:connectors]

    @limits[:shards] = limit[:shards]
    @limits[:exploits] = limit[:exploits]

    @limits[:deletion] = limit[:deletion]
    @limits[:modify] = limit[:modify]

    @limits[:archive] = limit[:archive]

    @limits[:scout] = limit[:scout]

    @limits[:encbits] = limit[:digest_enc]

    @limits[:ocr] = limit[:ocr]
    @limits[:translation] = limit[:translation]
    @limits[:correlation] = limit[:correlation]
    @limits[:intelligence] = limit[:intelligence]

    @limits[:hostname_sync] = limit[:hostname_sync]
  end

  
  def burn_one_license(type, platform)

    # check if the platform can be used
    unless @limits[:agents][platform][0]
      trace :warn, "You don't have a license for #{platform.to_s}. Queuing agent..."
      return false
    end

    unless check(:maintenance)
      trace :warn, "Maintenance period expired. Queuing agent..."
      return false
    end

    if @limits[:type] == 'reusable'
      # reusable licenses don't consume any license slot but we have to check
      # the number of already active agents in the db
      desktop = Item.where(_kind: 'agent', type: 'desktop', status: 'open', demo: false, deleted: false).count
      mobile = Item.where(_kind: 'agent', type: 'mobile', status: 'open', demo: false, deleted: false).count

      if desktop + mobile >= @limits[:agents][:total]
        trace :warn, "You don't have enough total license to received data. Queuing..."
        return false
      end
      if type == :desktop and desktop < @limits[:agents][:desktop]
        trace :info, "Using a reusable license: #{type.to_s} #{platform.to_s}"
        return true
      end
      if type == :mobile and mobile < @limits[:agents][:mobile]
        trace :info, "Using a reusable license: #{type.to_s} #{platform.to_s}"
        return true
      end

      trace :warn, "You don't have enough license for #{type.to_s}. Queuing..."
      return false
    end

    if @limits[:type] == 'oneshot'

      # do we have available license on the dongle?
      if RCS::DB::Dongle.info[:count] > 0
        trace :info, "Using a oneshot license: #{type.to_s} #{platform.to_s}"
        RCS::DB::Dongle.decrement
        return true
      else
        trace :warn, "You don't have enough license to received data. Queuing..."
        return false
      end
    end

    return false
  end

  def can_build_platform(platform, demo)

    # enforce demo flag if not build
    demo = true unless LicenseManager.instance.limits[:agents][platform][0]

    # remove demo flag if not enabled
    demo = false unless LicenseManager.instance.limits[:agents][platform][1]

    # if not build and not demo, raise
    if not LicenseManager.instance.limits[:agents][platform].inject(:|)
      raise NoLicenseError.new("Cannot build #{platform}, NO license")
    end

    return demo
  end

  def check(field)
    # these check are performed just before the creation of an object.
    # thus the comparison is strictly < and not <=
    case (field)
      when :users
        if ::User.where(enabled: true).count < @limits[:users]
          return true
        end

      when :collectors
        if Collector.where(type: 'local').count < @limits[:collectors][:collectors]
          return true
        end

      when :anonymizers
        if Collector.where(type: 'remote').count < @limits[:collectors][:anonymizers]
          return true
        end

      when :injectors
        if Injector.count < @limits[:nia][0]
          return true
        end

      when :alerting
        return @limits[:alerting]

      when :rmi
        return @limits[:rmi]

      when :exploits
        return @limits[:exploits]

      when :deletion
        return @limits[:deletion]

      when :modify
        return @limits[:modify]

      when :archive
        return @limits[:archive]

      when :scout
        return @limits[:scout]

      when :translation
        return @limits[:translation]

      when :correlation
        return @limits[:correlation]

      when :intelligence
        return @limits[:intelligence]

      when :ocr
        return @limits[:ocr]

      when :connectors
        return @limits[:connectors]

      when :shards
        if RCS::DB::Shard.count < @limits[:shards]
          return true
        end

      when :maintenance
        return Time.now.getutc <= @limits[:maintenance]
    end

    trace :warn, 'LICENCE EXCEEDED: ' + field.to_s
    return false
  end

  def store_in_db
    db = RCS::DB::DB.instance.session
    db['license'].find().upsert(@limits)
  end

  def load_from_db
    db = RCS::DB::DB.instance.session
    db['license'].find({}).first
  end

  def periodic_check
    begin

      # periodically check for license file
      load_license(true)

      # add it to the database so it is accessible to all the components (other than db)
      store_in_db

      # check the consistency of the database (if someone tries to tamper it)
      if ::User.where(enabled: true).count > @limits[:users]
        trace :fatal, "LICENCE EXCEEDED: Number of users is greater than license file. Fixing..."
        # fix by disabling the last updated user
        offending = ::User.where(enabled: true).order_by([[:updated_at, :desc]]).first
        offending[:enabled] = false
        trace :warn, "Disabling user '#{offending[:name]}'"
        offending.save
      end

      if ::Collector.local.count > @limits[:collectors][:collectors]
        trace :fatal, "LICENCE EXCEEDED: Number of collector is greater than license file. Fixing..."
        # fix by deleting the collector
        offending = ::Collector.local.order_by([[:updated_at, :desc]]).first
        trace :warn, "Deleting collector '#{offending[:name]}' #{offending[:address]}"
        # clear the chain of (possible) anonymizers
        next_id = offending['next'][0]
        begin
          break if next_id.nil?
          curr = ::Collector.find(next_id)
          trace :warn, "Fixing the anonymizer chain: #{curr['name']}"
          next_id = curr['next'][0]
          curr.prev = [nil]
          curr.next = [nil]
          curr.save
        end until next_id.nil?
        offending.destroy
      end
      if ::Collector.remote.count > @limits[:collectors][:anonymizers]
        trace :fatal, "LICENCE EXCEEDED: Number of anonymizers is greater than license file. Fixing..."
        # fix by deleting the collector
        offending = ::Collector.remote.order_by([[:updated_at, :desc]]).first
        trace :warn, "Deleting anonymizer '#{offending[:name]}' #{offending[:address]}"
        # clear the chain of (possible) anonymizers
        next_id = offending['next'][0]
        begin
          break if next_id.nil?
          curr = ::Collector.find(next_id)
          trace :warn, "Fixing the anonymizer chain: #{curr['name']}"
          next_id = curr['next'][0]
          curr.prev = [nil]
          curr.next = [nil]
          curr.save
        end until next_id.nil?
        offending.destroy
      end

      if ::Injector.count > @limits[:nia][0]
        trace :fatal, "LICENCE EXCEEDED: Number of injectors is greater than license file. Fixing..."
        # fix by deleting the injector
        offending = ::Injector.order_by([[:updated_at, :desc]]).first
        trace :warn, "Deleting injector '#{offending[:name]}' #{offending[:address]}"
        offending.destroy
      end

      if ::Item.agents.where(type: 'desktop', status: 'open', demo: false, deleted: false).count > @limits[:agents][:desktop]
        trace :fatal, "LICENCE EXCEEDED: Number of agents(desktop) is greater than license file. Fixing..."
        # fix by queuing the last updated agent
        offending = ::Item.where(_kind: 'agent', type: 'desktop', status: 'open', demo: false).order_by([[:updated_at, :desc]]).first
        offending[:status] = 'queued'
        trace :warn, "Queuing agent '#{offending[:name]}' #{offending[:desc]}"
        offending.save
      end

      if ::Item.agents.where(type: 'mobile', status: 'open', demo: false, deleted: false).count > @limits[:agents][:mobile]
        trace :fatal, "LICENCE EXCEEDED: Number of agents(mobile) is greater than license file. Fixing..."
        # fix by queuing the last updated agent
        offending = ::Item.where(_kind: 'agent', type: 'mobile', status: 'open', demo: false).order_by([[:updated_at, :desc]]).first
        offending[:status] = 'queued'
        trace :warn, "Queuing agent '#{offending[:name]}' #{offending[:desc]}"
        offending.save
      end

      if ::Item.agents.where(status: 'open', demo: false, deleted: false).count > @limits[:agents][:total]
        trace :fatal, "LICENCE EXCEEDED: Number of agent(total) is greater than license file. Fixing..."
        # fix by queuing the last updated agent
        offending = ::Item.where(_kind: 'agent', status: 'open', demo: false).order_by([[:updated_at, :desc]]).first
        offending[:status] = 'queued'
        trace :warn, "Queuing agent '#{offending[:name]}' #{offending[:desc]}"
        offending.save
      end

      if @limits[:alerting] == false
        if Alert.count > 0
          trace :fatal, "LICENCE EXCEEDED: Alerting is not enabled in the license file. Fixing..."
          ::Alert.update_all(enabled: false)
        end
      end

      # check if someone modifies manually the items
      ::Item.only_checksum_arguments.each do |item|
        next if item[:_kind] == 'global'
        if item.cs != item.calculate_checksum
          trace :fatal, "TAMPERED ITEM: [#{item._id}] [#{item._kind}] #{item.name}"
          exit!
        end
      end

    rescue Exception => e
      trace :fatal, "Cannot perform license check: #{e.message}"
      #trace :fatal, "Cannot perform license check: #{e.backtrace}"
      exit!
    end
  end


  def crypt_check(hash)
    # check the date digest (hidden expiration)
    return false if hash[:digest_seed] and Time.now.to_i > hash[:digest_seed].unpack('I').first

    # first check on signature
    content = hash.reject {|k,v| k == :integrity or k == :signature}.to_s
    check = Digest::HMAC.hexdigest(content, "əɹnʇɐuƃıs ɐ ʇou sı sıɥʇ", Digest::SHA2)
    return false if hash[:signature] != check

    # second check on integrity
    content = hash.reject {|k,v| k == :integrity}.to_s
    check = aes_encrypt(Digest::SHA2.digest(content), Digest::SHA2.digest("€ ∫∑x=1 ∆t π™")).unpack('H*').first
    return false if hash[:integrity] != check

    return true
  end


  def counters
    counters = {:users => User.where(enabled: true).count,
                :agents => {:total => Item.agents.where(status: 'open', demo: false, deleted: false).count,
                               :desktop => Item.agents.where(type: 'desktop', status: 'open', demo: false, deleted: false).count,
                               :mobile => Item.agents.where(type: 'mobile', status: 'open', demo: false, deleted: false).count},
                :collectors => {:collectors => Collector.local.count,
                                :anonymizers => Collector.remote.count},
                :nia => Injector.count,
                :shards => RCS::DB::Shard.count}

    return counters
  end

  def run(options)

    # save the new file if requested
    new_license(options[:new_license]) if options[:new_license]

    # load the license file
    load_license

    # print the current license
    pp RCS::DB::Dongle.info if @limits[:serial] != 'off'

    if options[:check]
      puts "Version: " + @limits[:version]
      puts "Expiry: " + @limits[:expiry].to_s
    else
      pp @limits.select {|x| not [:magic, :encbits].include? x}
    end

    return 0
  rescue Exception => e
    trace :fatal, "Cannot load license: #{e.message}"
    return 1
  end

  # executed from rcs-db-license
  def self.run!(*argv)
    # reopen the class and declare any empty trace method
    # if called from command line, we don't have the trace facility
    self.class_eval do
      def trace(level, message)
        puts message
      end
    end

    # This hash will hold all of the options parsed from the command-line by OptionParser.
    options = {}

    optparse = OptionParser.new do |opts|
      # Set a banner, displayed at the top of the help screen.
      opts.banner = "Usage: rcs-db-license [options]"

      opts.on( '-n', '--new FILE', String, 'Load a new license file into the system' ) do |file|
        options[:new_license] = file
      end

      opts.on( '-c', '--check', 'Check license validity' ) do
        options[:check] = true
      end

      # This displays the help screen
      opts.on( '-h', '--help', 'Display this screen' ) do
        puts opts
        return 0
      end
    end

    optparse.parse(argv)

    # execute the configurator
    return LicenseManager.instance.run(options)
  end

end