hackedteam/rcs-db

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

Summary

Maintainability
A
2 hrs
Test Coverage
#!/usr/bin/env ruby

require 'optparse'
require 'ffi'
require 'securerandom'
require 'openssl'
require 'digest/sha1'
require 'pp'
require 'yaml'
require 'time'
require 'date'

module Hasp
  extend FFI::Library

  # we can use the HASP dongle only on windows
  if RbConfig::CONFIG['host_os'] =~ /mingw/
    ffi_lib File.join(File.dirname(File.realpath(__FILE__)), 'ruby_x64.dll')

    ffi_convention :stdcall

    AES_PADDING = 16
    STRUCT_SIZE = 128

    class Info < FFI::Struct
     layout :enc, [:char, STRUCT_SIZE + AES_PADDING]
    end

    attach_function :RI, [:pointer], Info.by_value
    attach_function :DC, [], :int
  end

end

class Dongle
  VERSION = 20120504
  KEY = "\xB3\xE0\x2A\x88\x30\x69\x67\xAA\x21\x74\x23\xCC\x90\x99\x0C\x3C"

    ERROR_INFO = 1
    ERROR_PARSING = 2
    ERROR_LOGIN = 3
    ERROR_RTC = 4
    ERROR_STORAGE = 5

  class << self

    def info

      # fake info for macos
      return {serial: 'off', time: Time.now.getutc, oneshot: 0} if RbConfig::CONFIG['host_os'] =~ /darwin/

      # our info to be returned
      info = {}

      # pick a random IV for the encrypted channel with the DLL
      iv = SecureRandom.random_bytes(16)

      # allocate the memory
      ivp = FFI::MemoryPointer.new(:char , 16)
      ivp.write_bytes iv, 0, 16

      # call the actual method in the DLL
      hasp_info = Hasp.RI(ivp)
      enc = hasp_info[:enc].to_ptr.read_bytes Hasp::STRUCT_SIZE + Hasp::AES_PADDING
      raise "Invalid ENC dongle size: corrupted?" if enc.bytesize != Hasp::STRUCT_SIZE + Hasp::AES_PADDING

      # decrypt the response with the pre-shared KEY
      decipher = OpenSSL::Cipher::Cipher.new('aes-128-cbc')
      decipher.decrypt
      decipher.padding = 1
      decipher.key = KEY
      decipher.iv = iv
      data = decipher.update(enc)
      data << decipher.final

      # parse the data
      version = data.slice!(0..3).unpack('I').first
      raise "Invalid HASP version" if version != VERSION
      info[:version] = version

      info[:serial] = data.slice!(0..31).delete("\x00")

      time = data.slice!(0..7).unpack('Q').first
      time = Time.at(time) unless time == 0
      info[:time] = time

      info[:oneshot] = data.slice!(0..3).unpack('I').first
      info[:error_code] = data.slice!(0..3).unpack('I').first
      info[:error_msg] = data.slice!(0..63).delete("\x00")

      puts "Error #{info[:error_code]} while communicating with HASP token: #{info[:error_msg]}" unless info[:error_code] == 0

      raise "Cannot find hardware token" if info[:error_code] == ERROR_INFO || info[:error_code] == ERROR_PARSING

      return info
    end

    def time
      time = info[:time]
      raise "Cannot get RTC time" if time == 0
      return time
    rescue Exception => e
      puts "Invalid dongle time, contact support for dongle replacement"
      return Time.now.getutc
    end
  end

end


module LicenseChecker
  extend self

  def load_license(lic_file, version)

    raise "No license file found" unless File.exist? lic_file

    lic = {}

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

      # check the authenticity 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] != version
        raise "Invalid License File: incorrect version (#{lic[:version]}) #{version} is needed"
      end

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

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

      if lic[:maintenance].nil?
        raise "Invalid License File: invalid maintenance period"
      end

      if lic[:serial] != 'off'
        puts "Checking for hardware dongle..."
        # get the version from the dongle (can rise exception)
        info = Dongle.info
        puts "Dongle info: " + info.inspect
        raise "Invalid License File: incorrect serial number (#{lic[:serial]}) #{info[:serial]} is needed" if lic[:serial] != info[:serial]
      else
        puts "Hardware dongle not required..."
      end
    end

    return lic
  end

  def aes_encrypt(clear_text, key, padding = 1)
    cipher = OpenSSL::Cipher::Cipher.new('aes-128-cbc')
    cipher.encrypt
    cipher.padding = padding
    cipher.key = key
    cipher.iv = "\x00" * cipher.iv_len
    edata = cipher.update(clear_text)
    edata << cipher.final
    return edata
  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

  # executed from rcs-db-license
  def run!(*argv)
    # 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-license-check [options]"

      opts.on( '-l', '--license FILE', String, 'Load this license file' ) do |file|
        options[:file] = file
      end

      opts.on( '-v', '--version VERSION', String, 'License file should be this version' ) do |version|
        options[:version] = version
      end

      opts.on( '-i', '--info', 'Check license validity and display info' ) 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)

    raise "No license file specified" unless options[:file]
    raise "No version specified" unless options[:version]

    # load the license
    license = load_license options[:file], options[:version]

    # print the dongle infos
    pp Dongle.info if license[:serial] != 'off'

    puts "Version: " + license[:version]
    puts "Expiry: " + license[:expiry].to_s

    return 0
  rescue Exception => e
    puts "Cannot load license: #{e.message}"
    #puts e.backtrace.join("\n")
    return 1
  end

end

if __FILE__ == $0
  exit LicenseChecker.run! *ARGV
end