# Copyright (c) 2012 CyberLeo, All Rights Reserved.
# http://wiki.cyberleo.net/wiki/CyberLeo/COPYRIGHT?version=4

require 'openssl'
require 'socket'
require 'zlib'

# Public: CocKnocker, the Cryptographically Obfuscated Contact Knocker
#
# This class is responsible for computing a cryptographic sequence of ports to
# knock, via UDP packets, to tickle a firewall to allow the source IP through.
class CocKnocker
  # Public: Raised when 'generation' wraps 8 bits and none of the packets have
  # been deemed acceptable. This should, in practice, never occur unless you
  # have done something seriously wrong.
  class Unacceptable < StandardError; end

  # Public: See initializer documentation for descriptions of these attributes.
  attr_accessor :key, :tgt_ip, :my_ip

  # Public: A list of ports which should be avoided when knocking, due to their
  # special meaning or likelihood of being blocked by either source or target
  # internet service providers.
  BAD_PORTS = [ 0, 25, 135, 139, 445 ].freeze

  # Public: Packet encapsulation and handling class.
  #
  # Packet format is as follows:
  #   uint8_t  generation - Integer to alter packet; increment this to generate
  #                         a different packet in case the generated port list
  #                         contains items that are found in the BAD_PORTS list.
  #   uint32_t ip         - IP address of the node attempting to knock (the
  #                         'client'), to mitigate replay attacks.
  #   uint32_t time       - UNIX Epoch timestamp of when the knock is occuring,
  #                         to mitigate replay attacks.
  #   uint32_t crc        - CRC32 checksum of the first 9 bytes of the encoded
  #                         representation of the packet, to ensure a successful
  #                         decode.
  # All values are encoded in network byte order.
  class Packet
    # Public: See class documentation for descriptions of these attributes.
    attr_accessor :generation, :ip, :time

    # Public: Compute the CRC32 checksum of the passed binary packet string, to
    # ensure that it matches what is encoded in the packet itself.
    #
    # string packet - Encoded packet to verify
    #
    # Returns true if the computed CRC32 checksum of the first 9 bytes of the
    # provided packet matches the CRC32 checksum embedded in the last four
    # bytes; false otherwise.
    def self.valid?(packet)
      packet.unpack('C5NN').last == Zlib.crc32(packet[0..8])
    end

    # Public: Unpack a packet for use.
    #
    # string packet - Encoded packet to decode
    # Returns a Packet instance containing the decoded values; or false if the
    # checksum does not match.
    def self.unpack(packet)
      return false unless self.valid?(packet)
      data = packet.unpack('C5NN')
      generation = data[0]
      ip = data[1..4].map(&:to_s).join('.')
      time = data[5]
      crc = data[6]
      self.new(generation, ip, time)
    end

    # Public: Instance initializer
    #
    # integer generation - See class documentation
    # string ip          - See class documentation
    # integer time       - See class documentation
    def initialize(generation, ip, time)
      @generation = generation
      @ip = ip
      @time = time
    end

    # Internal: Transform the generation, IP, time, and checksum into a packed
    # binary string, according to the format set forth in the class docs.
    #
    # Returns a string containing the packed information.
    def pack
      a = [ generation ]
      a.concat(ip.split('.').map(&:to_i))
      a << time
      p = a.pack('C5N')
      a << Zlib.crc32(p)
      a.pack('C5NN')
    end
    alias :to_s :pack
  end

  # Public: Instance initializer
  #
  # key    - OpenSSL::PKey::RSA object representing a 192-bit RSA private key;
  #          used to obfuscate packets prior to their transmission, and to offer
  #          a means of authenticating the knocker during decryption.
  # tgt_ip - String IP address of the firewall that should receive the knocks.
  # my_ip  - String public IP address of the device knocking; should match the
  #          IP seen by the firewall, to ensure security against replay attacks.
  def initialize(key, tgt_ip, my_ip)
    @key = key
    @tgt_ip = tgt_ip
    @my_ip = my_ip
    @generation = 0
    @time = Time.now
  end

  # Internal: Craft and return a packet object corresponding to the attributes
  def packet
    Packet.new(@generation, my_ip, @time.to_i)
  end

  # Internal: Encrypt the packet to the provided key
  #
  # Returns a string containing the packet encrypted against the key.
  def encrypted_packet
    key.private_encrypt(packet.to_s)
  end

  # Internal: Checks if the provided port list is acceptable.
  #
  # Returns true if the provided list does not contain any ports on the
  # BAD_PORTS list, false otherwise.
  def acceptable?(list)
    BAD_PORTS - list == BAD_PORTS
  end

  # Internal: Format a packet into a sequenced series of port numbers suitable for
  # knocking.
  #
  # Returns an array of integers between 0 and 65535 inclusive.
  def port_list
    out = []
    stack = encrypted_packet.unpack('C*')
    while a = stack.shift
      b = stack.shift
      c = stack.shift
      out << ( ( a & 0xff ) << 4 ) + ( ( b & 0xf0 ) >> 4 )
      out << ( ( b & 0x0f ) << 8 ) + ( c & 0xff )
    end
    out.each_with_index {|p, i| out[i] = ( ( p << 4 ) + i ) }
    out
  end

  # Internal: Same as #port_list, but makes sure the port list is #acceptable?
  #
  # Returns an array of integers between 0 and 65535 inclusive, excluding ports
  # in the BAD_PORTS list.
  def safe_port_list
    return @list if @list
    @list = nil
    until @list && acceptable?(@list)
      @list = port_list
      @generation += 1
      raise Unacceptable if @generation > 255
    end
    @list
  end

  # Public: Perform a knock sequence
  def knock!
    sock = UDPSocket.new
    safe_port_list.each {|port|
      sock.send('', 0, tgt_ip, port)
    }
  end

  class Listener
    def self.decode(key, list)
      # Sort the stack, to ensure the packets are in the correct order, and
      # strip off packet indexes
      stack = list.sort {|a,b|
        ( a & 0x000f ) <=> ( b & 0x000f )
      }.map {|port|
        ( port & 0xfff0 ) >> 4
      }
      # Decode 12b16b encoding
      out = []
      while one = stack.shift
        two = stack.shift
        out << ( ( one & 0xff0 ) >> 4 )
        out << ( ( one & 0x00f ) << 4 ) + ( ( two & 0xf00 ) >> 8 )
        out << ( two & 0x0ff )
      end
      # Decrypt packet
      data = out.pack('C*')
      data = key.public_decrypt(data)
      Packet.unpack(data)
    end
  end
end

key = OpenSSL::PKey::RSA.new(192)
my_ip = '10.0.0.2'
tgt_ip = '10.0.0.1'

#CocKnocker.new(key, tgt_ip, my_ip).knock!
c = CocKnocker.new(key, tgt_ip, my_ip)
puts c.packet.inspect
p = c.safe_port_list.sort
puts CocKnocker::Listener.decode(key, p).inspect