2023-06-21 23:04:39 +00:00
|
|
|
--
|
|
|
|
-- Network Communications
|
|
|
|
--
|
|
|
|
|
2023-07-30 16:24:54 +00:00
|
|
|
local comms = require("scada-common.comms")
|
|
|
|
local log = require("scada-common.log")
|
|
|
|
local util = require("scada-common.util")
|
|
|
|
|
2023-06-21 23:04:39 +00:00
|
|
|
local md5 = require("lockbox.digest.md5")
|
2024-02-20 00:28:12 +00:00
|
|
|
local sha1 = require("lockbox.digest.sha1")
|
2023-06-21 23:04:39 +00:00
|
|
|
local pbkdf2 = require("lockbox.kdf.pbkdf2")
|
|
|
|
local hmac = require("lockbox.mac.hmac")
|
|
|
|
local stream = require("lockbox.util.stream")
|
|
|
|
local array = require("lockbox.util.array")
|
|
|
|
|
2023-07-30 16:24:54 +00:00
|
|
|
---@class scada_net_interface
|
2023-06-21 23:04:39 +00:00
|
|
|
local network = {}
|
|
|
|
|
2023-07-30 16:24:54 +00:00
|
|
|
-- cryptography engine
|
2023-06-21 23:04:39 +00:00
|
|
|
local c_eng = {
|
|
|
|
key = nil,
|
|
|
|
hmac = nil
|
|
|
|
}
|
|
|
|
|
|
|
|
-- initialize message authentication system
|
|
|
|
---@param passkey string facility passkey
|
|
|
|
---@return integer init_time milliseconds init took
|
|
|
|
function network.init_mac(passkey)
|
|
|
|
local start = util.time_ms()
|
|
|
|
|
|
|
|
local key_deriv = pbkdf2()
|
|
|
|
|
|
|
|
-- setup PBKDF2
|
2024-02-20 00:28:12 +00:00
|
|
|
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha1))
|
|
|
|
key_deriv.setBlockLen(20)
|
|
|
|
key_deriv.setDKeyLen(20)
|
|
|
|
key_deriv.setIterations(256)
|
2023-06-21 23:04:39 +00:00
|
|
|
key_deriv.setSalt("pepper")
|
2024-02-20 00:28:12 +00:00
|
|
|
key_deriv.setPassword(passkey)
|
2023-06-21 23:04:39 +00:00
|
|
|
key_deriv.finish()
|
|
|
|
|
|
|
|
c_eng.key = array.fromHex(key_deriv.asHex())
|
|
|
|
|
|
|
|
-- initialize HMAC
|
|
|
|
c_eng.hmac = hmac()
|
|
|
|
c_eng.hmac.setBlockSize(64)
|
|
|
|
c_eng.hmac.setDigest(md5)
|
|
|
|
c_eng.hmac.setKey(c_eng.key)
|
|
|
|
|
|
|
|
local init_time = util.time_ms() - start
|
|
|
|
log.info("network.init_mac completed in " .. init_time .. "ms")
|
|
|
|
|
|
|
|
return init_time
|
|
|
|
end
|
|
|
|
|
2024-02-23 00:25:55 +00:00
|
|
|
-- de-initialize message authentication system
|
|
|
|
function network.deinit_mac()
|
|
|
|
c_eng.key, c_eng.hmac = nil, nil
|
|
|
|
end
|
|
|
|
|
2023-06-21 23:04:39 +00:00
|
|
|
-- generate HMAC of message
|
|
|
|
---@nodiscard
|
|
|
|
---@param message string initial value concatenated with ciphertext
|
|
|
|
local function compute_hmac(message)
|
2023-06-27 22:36:16 +00:00
|
|
|
-- local start = util.time_ms()
|
2023-06-21 23:04:39 +00:00
|
|
|
|
|
|
|
c_eng.hmac.init()
|
|
|
|
c_eng.hmac.update(stream.fromString(message))
|
|
|
|
c_eng.hmac.finish()
|
|
|
|
|
|
|
|
local hash = c_eng.hmac.asHex()
|
|
|
|
|
2023-06-27 22:36:16 +00:00
|
|
|
-- log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)")
|
2023-06-21 23:04:39 +00:00
|
|
|
|
|
|
|
return hash
|
|
|
|
end
|
|
|
|
|
|
|
|
-- NIC: Network Interface Controller<br>
|
|
|
|
-- utilizes HMAC-MD5 for message authentication, if enabled
|
|
|
|
---@param modem table modem to use
|
|
|
|
function network.nic(modem)
|
|
|
|
local self = {
|
2024-06-29 18:10:58 +00:00
|
|
|
connected = true, -- used to avoid costly MAC calculations if modem isn't even present
|
2023-06-21 23:04:39 +00:00
|
|
|
channels = {}
|
|
|
|
}
|
|
|
|
|
|
|
|
---@class nic
|
|
|
|
---@field open function
|
|
|
|
---@field isOpen function
|
|
|
|
---@field close function
|
|
|
|
---@field closeAll function
|
|
|
|
---@field isWireless function
|
|
|
|
---@field getNameLocal function
|
|
|
|
---@field getNamesRemote function
|
|
|
|
---@field isPresentRemote function
|
|
|
|
---@field getTypeRemote function
|
|
|
|
---@field hasTypeRemote function
|
|
|
|
---@field getMethodsRemote function
|
|
|
|
---@field callRemote function
|
|
|
|
local public = {}
|
|
|
|
|
2023-06-23 18:12:41 +00:00
|
|
|
-- check if this NIC has a connected modem
|
|
|
|
---@nodiscard
|
2023-07-11 22:22:09 +00:00
|
|
|
function public.is_connected() return self.connected end
|
2023-06-23 18:12:41 +00:00
|
|
|
|
2023-06-21 23:04:39 +00:00
|
|
|
-- connect to a modem peripheral
|
|
|
|
---@param reconnected_modem table
|
|
|
|
function public.connect(reconnected_modem)
|
|
|
|
modem = reconnected_modem
|
|
|
|
self.connected = true
|
|
|
|
|
|
|
|
-- open previously opened channels
|
|
|
|
for _, channel in ipairs(self.channels) do
|
|
|
|
modem.open(channel)
|
|
|
|
end
|
|
|
|
|
2024-06-29 02:27:55 +00:00
|
|
|
-- link all public functions except for transmit, open, and close
|
2023-06-21 23:04:39 +00:00
|
|
|
for key, func in pairs(modem) do
|
|
|
|
if key ~= "transmit" and key ~= "open" and key ~= "close" and key ~= "closeAll" then public[key] = func end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- flag this NIC as no longer having a connected modem (usually do to peripheral disconnect)
|
|
|
|
function public.disconnect() self.connected = false end
|
|
|
|
|
|
|
|
-- check if a peripheral is this modem
|
|
|
|
---@nodiscard
|
|
|
|
---@param device table
|
|
|
|
function public.is_modem(device) return device == modem end
|
|
|
|
|
2023-06-23 18:12:41 +00:00
|
|
|
-- wrap modem functions, then create custom functions
|
2023-06-21 23:04:39 +00:00
|
|
|
public.connect(modem)
|
|
|
|
|
2023-06-23 18:12:41 +00:00
|
|
|
-- open a channel on the modem<br>
|
|
|
|
-- if disconnected *after* opening, previousy opened channels will be re-opened on reconnection
|
|
|
|
---@param channel integer
|
2023-06-21 23:04:39 +00:00
|
|
|
function public.open(channel)
|
2023-06-25 17:06:03 +00:00
|
|
|
modem.open(channel)
|
2023-06-21 23:04:39 +00:00
|
|
|
|
2023-06-25 17:06:03 +00:00
|
|
|
local already_open = false
|
|
|
|
for i = 1, #self.channels do
|
|
|
|
if self.channels[i] == channel then
|
|
|
|
already_open = true
|
|
|
|
break
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
2023-06-25 17:06:03 +00:00
|
|
|
end
|
2023-06-21 23:04:39 +00:00
|
|
|
|
2023-06-25 17:06:03 +00:00
|
|
|
if not already_open then
|
|
|
|
table.insert(self.channels, channel)
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-06-23 18:12:41 +00:00
|
|
|
-- close a channel on the modem
|
|
|
|
---@param channel integer
|
2023-06-21 23:04:39 +00:00
|
|
|
function public.close(channel)
|
2023-06-25 17:06:03 +00:00
|
|
|
modem.close(channel)
|
|
|
|
|
|
|
|
for i = 1, #self.channels do
|
|
|
|
if self.channels[i] == channel then
|
|
|
|
table.remove(self.channels, i)
|
|
|
|
return
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-06-23 18:12:41 +00:00
|
|
|
-- close all channels on the modem
|
2023-06-21 23:04:39 +00:00
|
|
|
function public.closeAll()
|
2023-06-25 17:06:03 +00:00
|
|
|
modem.closeAll()
|
|
|
|
self.channels = {}
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
-- send a packet, with message authentication if configured
|
|
|
|
---@param dest_channel integer destination channel
|
|
|
|
---@param local_channel integer local channel
|
2023-06-27 22:36:16 +00:00
|
|
|
---@param packet scada_packet packet
|
2023-06-21 23:04:39 +00:00
|
|
|
function public.transmit(dest_channel, local_channel, packet)
|
|
|
|
if self.connected then
|
2024-06-29 18:10:58 +00:00
|
|
|
local tx_packet = packet ---@type authd_packet|scada_packet
|
2023-06-27 22:36:16 +00:00
|
|
|
|
2023-06-21 23:04:39 +00:00
|
|
|
if c_eng.hmac ~= nil then
|
2023-06-27 22:36:16 +00:00
|
|
|
-- local start = util.time_ms()
|
|
|
|
tx_packet = comms.authd_packet()
|
2023-06-21 23:04:39 +00:00
|
|
|
|
2023-06-27 22:36:16 +00:00
|
|
|
---@cast tx_packet authd_packet
|
|
|
|
tx_packet.make(packet, compute_hmac)
|
2023-06-21 23:04:39 +00:00
|
|
|
|
2024-06-29 02:27:55 +00:00
|
|
|
-- log.debug("network.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms")
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
|
2023-06-27 22:36:16 +00:00
|
|
|
modem.transmit(dest_channel, local_channel, tx_packet.raw_sendable())
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
-- parse in a modem message as a network packet
|
|
|
|
---@nodiscard
|
|
|
|
---@param side string modem side
|
|
|
|
---@param sender integer sender channel
|
|
|
|
---@param reply_to integer reply channel
|
|
|
|
---@param message any packet sent with or without message authentication
|
|
|
|
---@param distance integer transmission distance
|
|
|
|
---@return scada_packet|nil packet received packet if valid and passed authentication check
|
|
|
|
function public.receive(side, sender, reply_to, message, distance)
|
|
|
|
local packet = nil
|
|
|
|
|
|
|
|
if self.connected then
|
|
|
|
local s_packet = comms.scada_packet()
|
|
|
|
|
2023-06-27 22:36:16 +00:00
|
|
|
if c_eng.hmac ~= nil then
|
|
|
|
-- parse packet as an authenticated SCADA packet
|
|
|
|
local a_packet = comms.authd_packet()
|
|
|
|
a_packet.receive(side, sender, reply_to, message, distance)
|
2023-06-21 23:04:39 +00:00
|
|
|
|
2023-06-27 22:36:16 +00:00
|
|
|
if a_packet.is_valid() then
|
2024-06-29 02:27:55 +00:00
|
|
|
s_packet.receive(side, sender, reply_to, a_packet.data(), distance)
|
|
|
|
|
|
|
|
if s_packet.is_valid() then
|
2024-06-29 18:10:58 +00:00
|
|
|
-- local start = util.time_ms()
|
2024-06-29 02:27:55 +00:00
|
|
|
local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_header(), { allow_repetitions = true, compact = true }))
|
2023-06-21 23:04:39 +00:00
|
|
|
|
2024-06-29 02:27:55 +00:00
|
|
|
if a_packet.mac() == computed_hmac then
|
|
|
|
-- log.debug("network.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms")
|
2024-06-29 18:10:58 +00:00
|
|
|
s_packet.stamp_authenticated()
|
|
|
|
else
|
2024-06-29 02:27:55 +00:00
|
|
|
-- log.debug("network.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms")
|
|
|
|
end
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
end
|
2023-06-27 22:36:16 +00:00
|
|
|
else
|
|
|
|
-- parse packet as a generic SCADA packet
|
|
|
|
s_packet.receive(side, sender, reply_to, message, distance)
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
2023-06-27 22:36:16 +00:00
|
|
|
|
|
|
|
if s_packet.is_valid() then packet = s_packet end
|
2023-06-21 23:04:39 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
return packet
|
|
|
|
end
|
|
|
|
|
|
|
|
return public
|
|
|
|
end
|
|
|
|
|
|
|
|
return network
|