---
-- A module implementing IPMI protocol (the code is a porting of the Metasploit ipmi scanner:
-- https://github.com/rapid7/metasploit-framework/tree/master/modules/auxiliary/scanner/ipmi)
--
-- @author "Claudiu Perta <claudiu.perta@gmail.com>"
local bin = require "bin"
local bit = require "bit"
local stdnse = require "stdnse"
local table = require "table"
_ENV = stdnse.module("ipmi", stdnse.seeall)

local HAVE_SSL, openssl = pcall(require,"openssl")

PAYLOADS = {
  ["IPMI"] = 0,
  ["PAYLOAD_SOL"]  = 1,
  ["RMCPPLUSOPEN_REQ"] = 0x10,
  ["RMCPPLUSOPEN_REP"] = 0x11,
  ["RAKP1"] = 0x12,
  ["RAKP2"] = 0x13,
  ["RAKP3"] = 0x14,
  ["RAKP4"] = 0x15,
}

RMCP_ERRORS = {
  [1] = "Insufficient resources to create new session \
         (wait for existing sessions to timeout)",

  -- Shouldn't occur.
  [2] = "Invalid Session ID",

  -- Shouldn't occur.
  [3] = "Invalid payload type",

  -- If these happen, we need to enhance our mechanism for detecting
  -- supported auth algorithms.
  [4] = "Invalid authentication algorithm",
  [5] = "Invalid integrity algorithm",

  [6] = "No matching authentication payload",
  [7] = "No matching integrity payload",

  -- This suggests the session was timed out while trying to negotiate,
  -- shouldn't happen.
  [8] = "Inactive Session ID",

  [9] = "Invalid role",
  [0xa] = "Unauthorised role or privilege level requested",
  [0xb] = "Insufficient resources to create a session at the requested role",
  [0xc] = "Invalid username length",
  [0xd] = "Unauthorized name",
  [0xe] = "Unauthorized GUID",
  [0xf] = "Invalid integrity check value",
  [0x10] = "Invalid confidentiality algorithm",
  [0x11] = "No cipher suite match with proposed security algorithms",

  -- Never observed, most likely a bug in xCAT or IPMI device.
  [0x12] = "Illegal or unrecognized parameter",
}

local c = function(byte)
  return string.char(byte)
end

Helper = {

  new = function(self, o)
    local o ={}
    setmetatable(o, self)
    self.__index = self
    return o
  end,

  checksum = function(self, data)
    local sum = 0
    -- TODO(claudiu) Finish implementation.
    local pos, str = bin.unpack("A", data, 0)
  end,

  random_console_session_id = function(self)
    local console_session_id = ""
    local len = 4
    for i = 1, len do
      console_session_id = console_session_id .. string.char(math.random(255))
    end

    return console_session_id
  end,

  channel_auth_request = function(self)
    local data = {
      c(0x06), c(0x00), c(0xff), c(0x07), -- RMCP Header
      c(0x00), c(0x00), c(0x00), c(0x00),
      c(0x00), c(0x00), c(0x00), c(0x00), c(0x00), c(0x09), c(0x20), c(0x18),
      c(0xc8), c(0x81), c(0x00), c(0x38), c(0x8e), c(0x04), c(0xb5)
    }
    return table.concat(data)
  end,

  -- Open rmcpplus_request
  session_open_request = function(self, console_session_id)
    local header = {
      c(0x06), c(0x00), c(0xff), c(0x07),     -- RMCP Header
      c(0x06),                                -- RMCP+ Authentication Type
      c(PAYLOADS["RMCPPLUSOPEN_REQ"]),        -- Payload Type
      c(0x00), c(0x00), c(0x00), c(0x00),     -- Session ID
      c(0x00), c(0x00), c(0x00), c(0x00)      -- Sequence Number
    }

    local data =
      table.concat({
        -- Maximum access
        c(0x00), c(0x00),
        -- Reserved
        c(0x00), c(0x00)
      }) .. console_session_id  ..
      table.concat ({
        c(0x00), c(0x00), c(0x00), c(0x08),
        c(0x01), c(0x00), c(0x00), c(0x00),
        c(0x01), c(0x00), c(0x00), c(0x08),
        -- HMAC-SHA1
        c(0x01), c(0x00), c(0x00), c(0x00),
        c(0x02), c(0x00), c(0x00), c(0x08),
        -- AES Encryption
        c(0x01), c(0x00), c(0x00), c(0x00)
      })

      return bin.pack("<ASA", table.concat(header), #data, data)
  end,

  -- Open rmcpplus_request
  session_open_cipher_zero_request = function(self, console_session_id)
    local header = {
      c(0x06), c(0x00), c(0xff), c(0x07),     -- RMCP Header
      c(0x06),                                -- RMCP+ Authentication Type
      c(PAYLOADS["RMCPPLUSOPEN_REQ"]),        -- Payload Type
      c(0x00), c(0x00), c(0x00), c(0x00),     -- Session ID
      c(0x00), c(0x00), c(0x00), c(0x00)      -- Sequence Number
    }

    local data =
      table.concat({
        -- Maximum access
        c(0x00), c(0x00),
        -- Reserved
        c(0x00), c(0x00)
      }) .. bin.pack("A", console_session_id)  ..
      table.concat ({
        c(0x00), c(0x00), c(0x00), c(0x08),
        -- Cipher 0
        c(0x00), c(0x00), c(0x00), c(0x00),
        c(0x01), c(0x00), c(0x00), c(0x08),
        -- Cipher 0
        c(0x00), c(0x00), c(0x00), c(0x00),
        c(0x02), c(0x00), c(0x00), c(0x08),
        -- No Encryption
        c(0x00), c(0x00), c(0x00), c(0x00)
      })

      return bin.pack("<ASA", table.concat(header), #data, data)
  end,

  rapk_1_request = function(self, bmc_session_id, console_random_id, username)
    return table.concat({
      table.concat({
        c(0x06), c(0x00), c(0xff), c(0x07), -- RMCP Header
        c(0x06),                            -- RMCP + Authentication Tyle
        c(PAYLOADS["RAKP1"]),               -- Payload Type
        c(0x00), c(0x00),
        c(0x00), c(0x00), c(0x00), c(0x00), c(0x00), c(0x00), c(0x21), c(0x00),
        c(0x00), c(0x00), c(0x00), c(0x00)
      }),
      bmc_session_id,
      console_random_id,
      table.concat({
       c(0x14), c(0x00), c(0x00), #username
      }),
      username
    })

  end,

  rapk_hmac_sha1_salt = function(self, con_sid, bmc_sid, con_rid, bmc_rid, bmc_gid,
                                 auth_level, username)
    return table.concat({
      con_sid, bmc_sid, con_rid, bmc_rid, bmc_gid,
      bin.pack("C", auth_level),
      bin.pack("C", #username),
      username
    })
  end,

  verify_rapk_hmac_sha1 = function(self, salt, hash, password)
    if not(HAVE_SSL) then
      return false
    end

    local digest = openssl.hmac('sha1', password, salt)
    return digest == hash
  end,

  parse_channel_auth_reply = function(self, reply)
    local data = {}
    local info = {}
    local pos = 0
    local value

    pos, data["rmcp_version"] = bin.unpack("<C", reply, pos)
    pos, data["rmcp_padding"] = bin.unpack("<C", reply, pos)
    pos, data["rmcp_sequence"] = bin.unpack("<C", reply, pos)

    pos, value = bin.unpack("<C", reply, pos)
    data["rmcp_mtype"] = bit.band(bit.rshift(value, 7), 0x00000001)
    data["rmcp_class"] = bit.band(value, 0x01111111)

    pos, data["session_auth_type"] = bin.unpack("<C", reply, pos)
    pos, data["session_sequence"] = bin.unpack("<I", reply, pos)
    pos, data["session_id"] = bin.unpack("<I", reply, pos)
    pos, data["message_length"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_tgt_address"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_tgt_lun"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_header_checksum"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_src_address"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_src_lun"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_command"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_completion_code"] = bin.unpack("<C", reply, pos)
    pos, data["ipmi_channel"] = bin.unpack("<C", reply, pos)

    pos, value = bin.unpack("<C", reply, pos)
    data["ipmi_compat_20"] =  bit.band(bit.rshift(value, 7), 0x00000001)
    data["ipmi_compat_reserved1"] =  bit.band(bit.rshift(value, 6), 0x00000001)
    data["ipmi_compat_oem_auth"] = bit.band(bit.rshift(value, 5), 0x00000001)
    data["ipmi_compat_password"] = bit.band(bit.rshift(value, 4), 0x00000001)
    data["ipmi_compat_reserved2"] = bit.band(bit.rshift(value, 3), 0x00000001)
    data["ipmi_compat_md5"] =   bit.band(bit.rshift(value, 2), 0x00000001)
    data["ipmi_compat_md2"] =  bit.band(bit.rshift(value, 1), 0x00000001)
    data["ipmi_compat_none"] =  bit.band(value, 0x00000001)

    pos, value = bin.unpack("<C", reply, pos)
    data["ipmi_user_reserved1"] = bit.band(bit.rshift(value, 6), 0x00000011)
    data["ipmi_user_kg"] =  bit.band(bit.rshift(value, 5), 0x00000001)
    data["ipmi_user_disable_message_auth"] = bit.band(bit.rshift(value, 4), 0x00000001)
    data["ipmi_user_disable_user_auth"] =bit.band(bit.rshift(value, 3), 0x00000001)
    data["ipmi_user_non_null"] =  bit.band(bit.rshift(value, 2), 0x00000001)
    data["ipmi_user_null"] =  bit.band(bit.rshift(value, 1), 0x00000001)
    data["ipmi_user_anonymous"] =  bit.band(value, 0x00000001)

    pos, value = bin.unpack("<C", reply, pos)
    data["ipmi_conn_reserved1"] = bit.band(bit.rshift(value, 2), 0x00111111)
    data["ipmi_conn_20"] = bit.band(bit.rshift(value, 1), 0x00000001)
    data["ipmi_conn_15"] = bit.band(value, 0x00000001)

    -- 24 bits OEMID, unpack an int and shift 1 byte to the right
    pos, value = bin.unpack("<I", reply, pos)
    data["ipmi_oem_id"] = bit.rshift(value, 8)
    -- restore one byte position
    pos = pos - 1
    pos, data["ipmi_oem_data"] = bin.unpack("<A", reply, pos)

    -- TODO(claudiu) Make sure 'tonumber' is not needed.
    return data
  end,

  parse_open_session_reply = function(self, reply)
    local data = {}
    local info = {}
    local pos = 0
    local value

    -- 4 bytes Header
    pos, data["rmcp_version"] = bin.unpack("<C", reply, pos)
    pos, data["rmcp_padding"] = bin.unpack("<C", reply, pos)
    pos, data["rmcp_sequence"] = bin.unpack("<C", reply, pos)

    pos, value = bin.unpack("<C", reply, pos)
    -- bit 1
    data["rmcp_mtype"] = bit.band(bit.rshift(value, 7), 0x00000001)
    -- bit [2:8]
    data["rmcp_class"] = bit.band(value, 0x01111111)

    pos, data["session_auth_type"] = bin.unpack("<C", reply, pos)

    pos, value = bin.unpack("<C", reply, pos)
    -- bit 1
    data["session_payload_encrypted"] =  bit.band(bit.rshift(value, 7), 0x00000001)
    -- bit 2
    data["session_payload_authenticated"] =  bit.band(bit.rshift(value, 6), 0x00000001)
    -- bit [3:8]
    data["session_payload_type"] = bit.band(value, 0x00111111)

    pos, data["session_id"] = bin.unpack("<I", reply, pos)
    pos, data["session_sequence"] = bin.unpack("<I", reply, pos)
    pos, data["message_length"] = bin.unpack("<S", reply, pos)
    pos, data["ignored1"] = bin.unpack("<C", reply, pos)
    pos, data["error_code"] = bin.unpack("<S", reply, pos)
    pos, data["ignored2"] = bin.unpack("<S", reply, pos)
    pos, data["console_session_id"] = bin.unpack("<I", reply, pos)
    pos, data["bmc_session_id"] = bin.unpack("<I", reply, pos)

    -- TODO(claudiu) Make sure 'tonumber' is not needed.
    return data
  end
}

return _ENV;
