local _G = require "_G"
local bin = require "bin"
local nmap = require "nmap"
local stdnse = require "stdnse"
local string = require "string"
local table = require "table"
local math = require "math"
local io = require "io"


description = [[
A very basic IKE library.

The current funcionality includes:
	1. Generating a Main or Aggressive Mode IKE request packet with a variable amount of transforms and a vpn group.
	2. Sending a packet
	3. Receiving the response
	4. Parsing the response for VIDs
	5. Searching for the VIDs in 'ike-fingerprints.lua'
	6. returning a parsed info table

This library is meant for extension, which could include:
	1. complete parsing of the response packet (might allow for better fingerprinting)
	2. adding more options to the request packet
		vendor field (might give better fingerprinting of services, e.g. Checkpoint)
	3. backoff pattern analyses
	...

An a implementation resembling 'ike-scan' could be built.
]]


_ENV = stdnse.module("ike", stdnse.seeall)

author = "Jesper Kueckelhahn"
license = "Same as Nmap--See http://nmap.org/book/man-legal.html"
categories = {"discovery", "safe"}

local ENC_METHODS = {
	["des"]     = 0x80010001,
	["3des"]    = 0x80010005,
	["cast"]    = 0x80010006,
	["aes/128"] = { 0x80010007, 0x800E0080 },
	["aes/192"] = { 0x80010007, 0x800E00C0 },
	["aes/256"] = { 0x80010007, 0x800E0100 },
}

local AUTH_TYPES = {
	["psk"]    = 0x80030001,
	["rsa"]    = 0x80030003,
	["ECDSA"]  = 0x80030008,
	["Hybrid"] = 0x8003FADD,
	["XAUTH"]  = 0x8003FDE9,
}

local HASH_ALGORITHM = {
	["md5"]      = 0x80020001,
	["sha1"]     = 0x80020002,
	["sha2-256"] = 0x80020004,
	["sha2-384"] = 0x80020005,
	["sha2-512"] = 0x80020006,
}

local GROUP_DESCRIPTION = {
	["768"]  = 0x80040001,
	["1024"] = 0x80040002,
	["1536"] = 0x80040005,
	["2048"] = 0x0004000E,
}

local EXCHANGE_MODE = {
	["Main"]       = 0x02,
	["Aggressive"] = 0x04,
}

local PROTOCOL_IDS = {
	["tcp"] = "06",
	["udp"] = "11",
}

-- Response packet types
local EXCHANGE_TYPE = {
	["02"] = "Main",
	["04"] = "Aggressive",
	["05"] = "Informational",
}

-- Payload names
local PAYLOADS = {
	["00"] = "None",
	["01"] = "SA",
	["03"] = "Transform",
	["04"] = "Key Exchange",
	["05"] = "ID",
	["08"] = "Hash",
	["0A"] = "Nonce",
	["0D"] = "VID",
}


-- Load the fingerprint file
-- (located in: nselib/data/ike-fingerprints.lua)
--
local function load_fingerprints()
	local file, filename_full, fingerprints

	-- Check if fingerprints are cached
	if(nmap.registry.ike_fingerprints ~= nil) then
		stdnse.print_debug(1, "ike: Loading cached fingerprints")
		return nmap.registry.ike_fingerprints
	end

	-- Try and find the file
	-- If it isn't in Nmap's directories, take it as a direct path
	filename_full = nmap.fetchfile('nselib/data/ike-fingerprints.lua')

	-- Load the file
	stdnse.print_debug(1, "ike: Loading fingerprints: %s", filename_full)
	local env = setmetatable({fingerprints = {}}, {__index = _G});
	file = loadfile(filename_full, "t", env)
	if( not(file) ) then
		stdnse.print_debug(1, "ike: Couldn't load the file: %s", filename_full)
		return false, "Couldn't load fingerprint file: " .. filename_full
	end
	file()
	fingerprints = env.fingerprints

	-- Check there are fingerprints to use
	if(#fingerprints == 0 ) then
		return false, "No fingerprints were loaded after processing ".. filename_full
	end

	return true, fingerprints
end


-- generate a random hex-string of length 'length'
--
local function generate_random(length)
	local rnd = ""

	for i=1, length do
		rnd = rnd .. string.format("%.2X", math.random(255))
	end
	return rnd
end


-- convert a string to a hex-string (of the ASCII representation)
--
local function convert_to_hex(id)
	local hex_str = ""
	for c in string.gmatch(id, ".") do
		hex_str = hex_str .. string.format("%X", c:byte())
	end
	return hex_str
end


-- Extract Payloads
local function extract_payloads(packet)

	-- packet only contains HDR
	if packet:len() < 61 then return {} end

	local np = packet:sub(33,34)	-- next payload
	local index = 61		-- starting point for search
	local ike_headers = {}		-- ike headers
	local payload = ''

	-- loop over packet
	while PAYLOADS[np] ~= "None" and index <= packet:len() do
		local payload_length = tonumber("0x"..packet:sub(index, index+3)) * 2
		payload = string.lower(packet:sub(index+4, index+payload_length-5))

		-- debug
		if PAYLOADS[np] == 'VID' then
			stdnse.print_debug(2, 'IKE: Found IKE Header: %s: %s - %s', np, PAYLOADS[np], payload)
		else
			stdnse.print_debug(2, 'IKE: Found IKE Header: %s: %s', np, PAYLOADS[np])
		end

		-- Store payload
		if ike_headers[PAYLOADS[np]] == nil then
			ike_headers[PAYLOADS[np]] = {payload}
		else
			table.insert(ike_headers[PAYLOADS[np]], payload)
		end

		-- find the next payload type
		np = packet:sub(index-4, index-3)

		-- jump to the next payload
		index = index + payload_length
	end
	return ike_headers

end




-- Search the fingerprint database for matches
--	This is a (currently) divided into two parts
--		1) version detection based on single fingerprints
--		2) version detection based on the order of all vendor ids
--
--	NOTE: the second step currently only has support for CISCO devices
--
-- Input is a table of collected vendor-ids, output is a table
-- with fields:
--	vendor, version, name, attributes (table), guess (table), os
local function lookup(vendor_ids)
	if vendor_ids == {} or vendor_ids == nil then return {} end

	-- concat all vids to one string
	local all_vids = ''
	for _,vid in pairs(vendor_ids) do all_vids = all_vids .. vid end

	-- the results
	local info = {
		vendor = nil,
		attribs = {},
	}

	local status, fingerprints
	status, fingerprints = load_fingerprints()

	if status then

		-- loop over the vendor_ids returned in ike request
		for _,vendor_id in pairs(vendor_ids) do

			-- loop over the fingerprints found in database
			for _,row in pairs(fingerprints) do

				if vendor_id:find(row.fingerprint) then

					-- if a match is found, check if it's a version detection or attribute
					if row.category == 'vendor' then
						local debug_string = ''
						if row.vendor  ~= nil then debug_string = debug_string .. row.vendor .. ' ' end
						if row.version ~= nil then debug_string = debug_string .. row.version       end
						stdnse.print_debug(2, "IKE: Fingerprint: %s matches %s", vendor_id,  debug_string)

						-- Only store the first match
						if info.vendor == nil then
							-- the fingerprint contains information about the VID
							info.vendor = row
						end

					elseif row.category == 'attribute' then
						info.attribs[ #info.attribs + 1] = row
						stdnse.print_debug(2, "IKE: Attribute: %s matches %s", vendor_id, row.text)
						break
					end
				end
			end
		end
	end


	---------------------------------------------------
	-- Search for the order of the vids
	-- Uses category 'vid_ordering'
	---

	-- search in the 'vid_ordering' category
	local debug_string = ''
	for _,row in pairs(fingerprints) do

		if row.category == 'vid_ordering' and all_vids:find(row.fingerprint) then

			-- Use ordering information if there where no vendor matches from prevoius step
			if info.vendor == nil then
				info.vendor = row

				-- Debugging info
				debug_string = ''
				if info.vendor.vendor  ~= nil then debug_string = debug_string .. info.vendor.vendor  .. ' ' end
				if info.vendor.version ~= nil then debug_string = debug_string .. info.vendor.version .. ' ' end
				if info.vendor.ostype  ~= nil then debug_string = debug_string .. info.vendor.ostype         end
				stdnse.print_debug(2, 'IKE: No vendor match, but ordering match found: %s', debug_string)

				return info

			-- Update OS based on ordering
			elseif info.vendor.vendor == row.vendor then
				info.vendor.ostype = row.ostype

				-- Debugging info
				debug_string = ''
				if info.vendor.vendor	~= nil then debug_string = debug_string .. info.vendor.vendor  .. ' to ' end
				if row.ostype			~= nil then debug_string = debug_string .. row.ostype end
				stdnse.print_debug(2, 'IKE: Vendor and ordering match. OS updated: %s', debug_string)

				return info

			-- Only print debugging information if conflicting information is detected
			else
				-- Debugging info
				debug_string = ''
				if info.vendor.vendor	~= nil then debug_string = debug_string .. info.vendor.vendor  .. ' vs ' end
				if row.vendor			~= nil then debug_string = debug_string .. row.vendor end
				stdnse.print_debug(2, 'IKE: Found an ordering match, but vendors do not match. %s', debug_string)

			end
		end
	end

	return info
end


-- Handle a response packet
--	A very limited response parser
--	Currently only the VIDs are extracted
--	This could be made more advanced to
--  allow for fingerprinting via the order
--	of the returned headers
---
function response(packet)
	local resp = { ["mode"] = "", ["info"] = nil, ['vids']={}, ['success'] = false }

	if packet:len() > 38 then

		-- extract the return type
		local resp_type = EXCHANGE_TYPE[packet:sub(37,38)]
		local ike_headers = {}

		-- simple check that the type is something other than 'Informational'
		-- as this type does not include VIDs
		if resp_type ~= "Informational" then
			resp["mode"]	= resp_type

			ike_headers = extract_payloads(packet)

			-- Extract the VIDs
			resp['vids']	= ike_headers['VID']

			-- search for fingerprints
			resp["info"]	= lookup(resp['vids'])

			-- indicate that a packet 'useful' packet was returned
			resp['success'] = true
		end
	end

	return resp
end


-- Send a request
-- The 'packet' argument must be generated by the function 'request'
-- and is a hex string
--
function send_request( host, port, packet )

	local socket = nmap.new_socket()
	local s_status, r_status, data, i, hexstring, _

	-- lock resource (port 500/udp)
	local mutex = nmap.mutex("ike_port_500");
	mutex "lock";

	-- send the request packet
	socket:set_timeout(1000)
	socket:bind(nil, port.number)
	socket:connect(host, port, "udp")
	s_status,_ = socket:send(packet)

	-- receive answer
	if s_status then
		r_status, data = socket:receive_lines(1)

		if r_status then
			i, hexstring = bin.unpack("H" .. data:len(), data)
			socket:close()

			-- release mutex
			mutex "done";
			return response(hexstring)
		else
			socket:close()
		end
	else
		socket:close()
	end

	-- release mutex
	mutex "done";

	return {}
end

-- Create the aggressive part of a packet
--	Aggressive mode includes the user-id, so the
--	length of this has to be taken into account
--
local function generate_aggressive(port, protocol, id, diffie)
	local hex_port = string.format("%.4X", port)
	local hex_prot = PROTOCOL_IDS[protocol]
	local id_len = string.format("%.4X", 8 + id:len())

	-- get length of key data based on diffie
	local key_length
	if diffie == 1 then
		key_length = 96
	elseif diffie == 2 then
		key_length = 128
	elseif diffie == 5 then
		key_length = 192
	end

	return bin.pack(">SHHSSHSHCHHH",
		-- Key Exchange
		0x0a00					, -- Next payload (Nonce)
		string.format("%04X", key_length+4)	, -- Length (132-bit)
		generate_random(key_length)		, -- Random key data

		-- Nonce
		0x0500					, -- Next payload (Identification)
		0x0018					, -- Length (24)
		generate_random(20)			, -- Nonce data

		-- Identification
		0x0000					, -- Next Payload (None)
		id_len					, -- Payload length (id + 8)
		0x03					, -- ID Type (USER_FQDN)
		hex_prot				, -- Protocol ID (UDP)
		hex_port				, -- Port (500)
		convert_to_hex(id)			  -- Id Data (as hex)
	)
end


-- Create the transform
--	AES encryption needs an extra value to define the key length
--	Currently only DES, 3DES and AES encryption is supported
--
local function generate_transform(auth, encryption, hash, group, number, total)
	local key_length, trans_length, aes_enc, sep, enc
	local next_payload, payload_number

	-- handle special case of aes
	if encryption:sub(1,3) == "aes" then
		trans_length = 0x0028
		enc = ENC_METHODS[encryption][1]
		key_length	= ENC_METHODS[encryption][2]
	else
		trans_length = 0x0024
		enc = ENC_METHODS[encryption]
		key_length = nil
	end

	-- check if there are more transforms
	if number == total then
		next_payload = 0x0000 -- none
	else
		next_payload = 0x0300 -- transform
	end

	-- set the payload number
	payload_number = string.format("%.2X", number)

	local trans = bin.pack(">SSHCSIIII",
		next_payload		, -- Next payload
		trans_length		, -- Transform length
		payload_number		, -- Transform number
		0x01			, -- Transform ID (IKE)
		0x0000			, -- spacers ?
		enc			, -- Encryption algorithm
		HASH_ALGORITHM[hash]	, -- Hash algorithm
		AUTH_TYPES[auth]	, -- Authentication method
		GROUP_DESCRIPTION[group]  -- Group Description
	)

	if key_length ~= nil then
		trans = trans .. bin.pack(">I", key_length) -- only set for aes
	end

	trans = trans .. bin.pack(">IL",
		0x800b0001		, -- Life type (seconds)
		0x000c000400007080	  -- Life duration (28800)
	)

	return trans
end


-- Generate multiple transforms
--	Input nust be a table of complete transforms
--
local function generate_transforms(transform_table)
	local transforms = ''

	for i,t in pairs(transform_table) do
		transforms = transforms .. generate_transform(t.auth, t.encryption, t.hash, t.group, i, #transform_table)
	end

	return transforms
end


-- Create a request packet
--	Support for multiple transforms, which minimizes the
--	the amount of traffic/packets needed to be sendt
--
function request(port, proto, mode, transforms, diffie, id)
	local payload_after_sa, str_aggressive, l, l_sa, l_pro
	local number_transforms, transform_string

	transform_string = generate_transforms(transforms)
	number_transforms = string.format("%.2X", #transforms)

	-- check for aggressive vs Main mode
	if mode == "Aggressive" then
		str_aggressive = generate_aggressive(port, proto, id, diffie)
		payload_after_sa = 0x0400
	else
		str_aggressive = ""
		payload_after_sa = 0x0000
	end


	-- calculate lengths
	l		= string.format("%.8X", 48 + transform_string:len() + str_aggressive:len())
	l_sa	= string.format("%.4X", 20 + transform_string:len())
	l_pro	= string.format("%.4X", 8 + transform_string:len())

	-- Build the packet
	local packet = bin.pack(">HLCCCCIHSHIISHCCCH",
		generate_random(8)	, -- Initiator cookie
		0x0000000000000000	, -- Responder cookie
		0x01			, -- Next payload (SA)
		0x10			, -- Version
		EXCHANGE_MODE[mode]	, -- Exchange type
		0x00			, -- Flags
		0x00000000		, -- Message id
		l			, -- packet length


		-- Security Association
		payload_after_sa	, -- Next payload (Key exchange, if aggressive mode)
		l_sa			, -- Length
		0x00000001		, -- IPSEC
		0x00000001		, -- Situation

		--## Proposal
		0x0000			, -- Next payload (None)
		l_pro			, -- Payload length
		0x01			, -- Proposal number
		0x01			, -- Protocol ID (ISAKMP)
		0x00			, -- SPI Size
		number_transforms	  -- Proposal transforms
	)

	packet = packet .. transform_string	 -- transform

	if mode == 'Aggressive' then
		packet = packet .. str_aggressive
	end

	return packet
end


return _ENV
