diff --git a/coordinator/coordinator.lua b/coordinator/coordinator.lua index f6e8038..59661da 100644 --- a/coordinator/coordinator.lua +++ b/coordinator/coordinator.lua @@ -212,13 +212,13 @@ end -- coordinator communications ---@nodiscard ---@param version string coordinator version ----@param modem table modem device +---@param nic nic network interface device ---@param crd_channel integer port of configured supervisor ---@param svr_channel integer listening port for supervisor replys ---@param pkt_channel integer listening port for pocket API ---@param range integer trusted device connection range ---@param sv_watchdog watchdog -function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) +function coordinator.comms(version, nic, crd_channel, svr_channel, pkt_channel, range, sv_watchdog) local self = { sv_linked = false, sv_addr = comms.BROADCAST, @@ -234,16 +234,12 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel -- PRIVATE FUNCTIONS -- - -- configure modem channels - local function _conf_channels() - modem.closeAll() - modem.open(crd_channel) - end + -- configure network channels + nic.closeAll() + nic.open(crd_channel) - _conf_channels() - - -- link modem to apisessions - apisessions.init(modem) + -- link nic to apisessions + apisessions.init(nic) -- send a packet to the supervisor ---@param msg_type SCADA_MGMT_TYPE|SCADA_CRDN_TYPE @@ -263,7 +259,7 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel pkt.make(msg_type, msg) s_pkt.make(self.sv_addr, self.sv_seq_num, protocol, pkt.raw_sendable()) - modem.transmit(svr_channel, crd_channel, s_pkt.raw_sendable()) + nic.transmit(svr_channel, crd_channel, s_pkt) self.sv_seq_num = self.sv_seq_num + 1 end @@ -277,7 +273,7 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel m_pkt.make(SCADA_MGMT_TYPE.ESTABLISH, { ack }) s_pkt.make(packet.src_addr(), packet.seq_num() + 1, PROTOCOL.SCADA_MGMT, m_pkt.raw_sendable()) - modem.transmit(pkt_channel, crd_channel, s_pkt.raw_sendable()) + nic.transmit(pkt_channel, crd_channel, s_pkt) self.last_api_est_acks[packet.src_addr()] = ack end @@ -297,14 +293,6 @@ function coordinator.comms(version, modem, crd_channel, svr_channel, pkt_channel ---@class coord_comms local public = {} - -- reconnect a newly connected modem - ---@param new_modem table - function public.reconnect_modem(new_modem) - modem = new_modem - apisessions.relink_modem(new_modem) - _conf_channels() - end - -- close the connection to the server function public.close() sv_watchdog.cancel() diff --git a/coordinator/session/apisessions.lua b/coordinator/session/apisessions.lua index 17988f5..1ea1beb 100644 --- a/coordinator/session/apisessions.lua +++ b/coordinator/session/apisessions.lua @@ -10,7 +10,7 @@ local pocket = require("coordinator.session.pocket") local apisessions = {} local self = { - modem = nil, + nic = nil, next_id = 0, sessions = {} } @@ -31,7 +31,7 @@ local function _api_handle_outq(session) if msg ~= nil then if msg.qtype == mqueue.TYPE.PACKET then -- handle a packet to be sent - self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) + self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message) elseif msg.qtype == mqueue.TYPE.COMMAND then -- handle instruction/notification elseif msg.qtype == mqueue.TYPE.DATA then @@ -58,7 +58,7 @@ local function _shutdown(session) while session.out_queue.ready() do local msg = session.out_queue.pop() if msg ~= nil and msg.qtype == mqueue.TYPE.PACKET then - self.modem.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message.raw_sendable()) + self.nic.transmit(config.PKT_CHANNEL, config.CRD_CHANNEL, msg.message) end end @@ -68,15 +68,9 @@ end -- PUBLIC FUNCTIONS -- -- initialize apisessions ----@param modem table -function apisessions.init(modem) - self.modem = modem -end - --- re-link the modem ----@param modem table -function apisessions.relink_modem(modem) - self.modem = modem +---@param nic nic +function apisessions.init(nic) + self.nic = nic end -- find a session by remote port diff --git a/coordinator/startup.lua b/coordinator/startup.lua index 5a68afc..52b790c 100644 --- a/coordinator/startup.lua +++ b/coordinator/startup.lua @@ -6,6 +6,7 @@ require("/initenv").init_env() local crash = require("scada-common.crash") local log = require("scada-common.log") +local network = require("scada-common.network") local ppm = require("scada-common.ppm") local tcd = require("scada-common.tcd") local util = require("scada-common.util") @@ -20,7 +21,7 @@ local sounder = require("coordinator.sounder") local apisessions = require("coordinator.session.apisessions") -local COORDINATOR_VERSION = "v0.16.1" +local COORDINATOR_VERSION = "v0.17.0" local println = util.println local println_ts = util.println_ts @@ -147,8 +148,9 @@ local function main() conn_watchdog.cancel() log.debug("startup> conn watchdog created") - -- start comms, open all channels - local coord_comms = coordinator.comms(COORDINATOR_VERSION, modem, config.CRD_CHANNEL, config.SVR_CHANNEL, + -- init network interface then start comms + local nic = network.nic(modem) + local coord_comms = coordinator.comms(COORDINATOR_VERSION, nic, config.CRD_CHANNEL, config.SVR_CHANNEL, config.PKT_CHANNEL, config.TRUSTED_RANGE, conn_watchdog) log.debug("startup> comms init") log_comms("comms initialized") @@ -218,8 +220,6 @@ local function main() local date_format = util.trinary(config.TIME_24_HOUR, "%X \x04 %A, %B %d %Y", "%r \x04 %A, %B %d %Y") - local no_modem = false - if ui_ok then -- start connection watchdog conn_watchdog.feed() @@ -239,8 +239,9 @@ local function main() if type ~= nil and device ~= nil then if type == "modem" then -- we only really care if this is our wireless modem - if device == modem then - no_modem = true + -- if it is another modem, handle other peripheral losses separately + if nic.is_modem(device) then + nic.disconnect() log_sys("comms modem disconnected") println_ts("wireless modem disconnected!") @@ -254,6 +255,7 @@ local function main() end elseif type == "monitor" then if renderer.is_monitor_used(device) then + ---@todo will be handled properly in #249 -- "halt and catch fire" style handling local msg = "lost a configured monitor, system will now exit" println_ts(msg) @@ -275,9 +277,7 @@ local function main() if type == "modem" then if device.isWireless() then -- reconnected modem - no_modem = false - modem = device - coord_comms.reconnect_modem(modem) + nic.connect(device) log_sys("comms modem reconnected") println_ts("wireless modem reconnected.") @@ -289,6 +289,7 @@ local function main() log_sys("wired modem reconnected") end -- elseif type == "monitor" then + ---@todo will be handled properly in #249 -- not supported, system will exit on loss of in-use monitors elseif type == "speaker" then local msg = "alarm sounder speaker reconnected" @@ -322,7 +323,7 @@ local function main() renderer.close_ui() sounder.stop() - if not no_modem then + if nic.connected() then -- try to re-connect to the supervisor if not init_connect_sv() then break end ui_ok = init_start_ui() @@ -350,7 +351,7 @@ local function main() renderer.close_ui() sounder.stop() - if not no_modem then + if nic.connected() then -- try to re-connect to the supervisor if not init_connect_sv() then break end ui_ok = init_start_ui() diff --git a/lockbox/init.lua b/lockbox/init.lua index 0031a50..9323451 100644 --- a/lockbox/init.lua +++ b/lockbox/init.lua @@ -1,5 +1,8 @@ local Lockbox = {}; +-- cc-mek-scada lockbox version +Lockbox.VERSION = "1.0" + --[[ package.path = "./?.lua;" .. "./cipher/?.lua;" diff --git a/pocket/pocket.lua b/pocket/pocket.lua index 0281e92..c0b5f77 100644 --- a/pocket/pocket.lua +++ b/pocket/pocket.lua @@ -17,14 +17,14 @@ local pocket = {} -- pocket coordinator + supervisor communications ---@nodiscard ---@param version string pocket version ----@param modem table modem device +---@param nic nic network interface device ---@param pkt_channel integer pocket comms channel ---@param svr_channel integer supervisor access channel ---@param crd_channel integer coordinator access channel ---@param range integer trusted device connection range ---@param sv_watchdog watchdog ---@param api_watchdog watchdog -function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) +function pocket.comms(version, nic, pkt_channel, svr_channel, crd_channel, range, sv_watchdog, api_watchdog) local self = { sv = { linked = false, @@ -47,13 +47,9 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran -- PRIVATE FUNCTIONS -- - -- configure modem channels - local function _conf_channels() - modem.closeAll() - modem.open(pkt_channel) - end - - _conf_channels() + -- configure network channels + nic.closeAll() + nic.open(pkt_channel) -- send a management packet to the supervisor ---@param msg_type SCADA_MGMT_TYPE @@ -65,7 +61,7 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran pkt.make(msg_type, msg) s_pkt.make(self.sv.addr, self.sv.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - modem.transmit(svr_channel, pkt_channel, s_pkt.raw_sendable()) + nic.transmit(svr_channel, pkt_channel, s_pkt) self.sv.seq_num = self.sv.seq_num + 1 end @@ -79,7 +75,7 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran pkt.make(msg_type, msg) s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.SCADA_MGMT, pkt.raw_sendable()) - modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) + nic.transmit(crd_channel, pkt_channel, s_pkt) self.api.seq_num = self.api.seq_num + 1 end @@ -93,7 +89,7 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran -- pkt.make(msg_type, msg) -- s_pkt.make(self.api.addr, self.api.seq_num, PROTOCOL.COORD_API, pkt.raw_sendable()) - -- modem.transmit(crd_channel, pkt_channel, s_pkt.raw_sendable()) + -- nic.transmit(crd_channel, pkt_channel, s_pkt) -- self.api.seq_num = self.api.seq_num + 1 -- end @@ -124,13 +120,6 @@ function pocket.comms(version, modem, pkt_channel, svr_channel, crd_channel, ran ---@class pocket_comms local public = {} - -- reconnect a newly connected modem - ---@param new_modem table - function public.reconnect_modem(new_modem) - modem = new_modem - _conf_channels() - end - -- close connection to the supervisor function public.close_sv() sv_watchdog.cancel() diff --git a/pocket/startup.lua b/pocket/startup.lua index 7afd208..7b2e493 100644 --- a/pocket/startup.lua +++ b/pocket/startup.lua @@ -6,6 +6,7 @@ require("/initenv").init_env() local crash = require("scada-common.crash") local log = require("scada-common.log") +local network = require("scada-common.network") local ppm = require("scada-common.ppm") local tcd = require("scada-common.tcd") local util = require("scada-common.util") @@ -17,7 +18,7 @@ local coreio = require("pocket.coreio") local pocket = require("pocket.pocket") local renderer = require("pocket.renderer") -local POCKET_VERSION = "alpha-v0.4.5" +local POCKET_VERSION = "alpha-v0.5.0" local println = util.println local println_ts = util.println_ts @@ -88,8 +89,9 @@ local function main() log.debug("startup> conn watchdogs created") - -- start comms, open all channels - local pocket_comms = pocket.comms(POCKET_VERSION, modem, config.PKT_CHANNEL, config.SVR_CHANNEL, + -- init network interface then start comms + local nic = network.nic(modem) + local pocket_comms = pocket.comms(POCKET_VERSION, nic, config.PKT_CHANNEL, config.SVR_CHANNEL, config.CRD_CHANNEL, config.TRUSTED_RANGE, conn_wd.sv, conn_wd.api) log.debug("startup> comms init") diff --git a/scada-common/comms.lua b/scada-common/comms.lua index bd43706..99a3c7c 100644 --- a/scada-common/comms.lua +++ b/scada-common/comms.lua @@ -205,7 +205,7 @@ function comms.scada_packet() self.protocol = self.raw[4] self.mac = self.raw[5] - -- element 5 must be a table + -- element 6 must be a table if type(self.raw[6]) == "table" then self.length = #self.raw[6] self.payload = self.raw[6] diff --git a/scada-common/crypto.lua b/scada-common/crypto.lua deleted file mode 100644 index 0dae940..0000000 --- a/scada-common/crypto.lua +++ /dev/null @@ -1,152 +0,0 @@ --- --- Cryptographic Communications Engine --- - -local md5 = require("lockbox.digest.md5") -local sha2_256 = require("lockbox.digest.sha2_256") -local pbkdf2 = require("lockbox.kdf.pbkdf2") -local hmac = require("lockbox.mac.hmac") -local stream = require("lockbox.util.stream") -local array = require("lockbox.util.array") -local comms = require("scada-common.comms") - -local log = require("scada-common.log") -local util = require("scada-common.util") - -local crypto = {} - -local c_eng = { - key = nil, - hmac = nil -} - --- initialize cryptographic system -function crypto.init(password) - local key_deriv = pbkdf2() - - -- setup PBKDF2 - key_deriv.setPassword(password) - key_deriv.setSalt("pepper") - key_deriv.setIterations(32) - key_deriv.setBlockLen(8) - key_deriv.setDKeyLen(16) - - local start = util.time_ms() - - key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha2_256)) - key_deriv.finish() - - local message = "pbkdf2 key derivation took " .. (util.time_ms() - start) .. "ms" - log.dmesg(message, "CRYPTO", colors.yellow) - log.info("crypto.init: " .. message) - - 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) - - message = "init: completed in " .. (util.time_ms() - start) .. "ms" - log.dmesg(message, "CRYPTO", colors.yellow) - log.info("crypto." .. message) -end - --- generate HMAC of message ----@nodiscard ----@param message string initial value concatenated with ciphertext -function crypto.hmac(message) - local start = util.time_ms() - - c_eng.hmac.init() - c_eng.hmac.update(stream.fromString(message)) - c_eng.hmac.finish() - - local hash = c_eng.hmac.asHex() - - log.debug("crypto.hmac: hmac-md5 took " .. (util.time_ms() - start) .. "ms") - log.debug("crypto.hmac: hmac = " .. util.strval(hash)) - - return hash -end - --- wrap a modem as a secure modem to send encrypted traffic ----@param modem table modem to wrap -function crypto.secure_modem(modem) - ---@class secure_modem - ---@field open function - ---@field isOpen function - ---@field close function - ---@field closeAll function - ---@field isWireless function - ---@field getNamesRemote function - ---@field isPresentRemote function - ---@field getTypeRemote function - ---@field hasTypeRemote function - ---@field getMethodsRemote function - ---@field callRemote function - ---@field getNameLocal function - local public = {} - - -- wrap a modem - ---@param reconnected_modem table - function public.wrap(reconnected_modem) - modem = reconnected_modem - for key, func in pairs(modem) do - public[key] = func - end - end - - -- wrap modem functions, then we replace transmit - public.wrap(modem) - - -- send a packet with message authentication - ---@param packet scada_packet packet raw_sendable - function public.transmit(packet) - local start = util.time_ms() - local message = textutils.serialize(packet.raw_verifiable(), { allow_repetitions = true, compact = true }) - local computed_hmac = crypto.hmac(message) - - packet.set_mac(computed_hmac) - - log.debug("crypto.transmit: data processing took " .. (util.time_ms() - start) .. "ms") - - modem.transmit(packet.remote_channel(), packet.local_channel(), packet.raw_sendable()) - 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 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 - local s_packet = comms.scada_packet() - - -- parse packet as generic SCADA packet - s_packet.receive(side, sender, reply_to, message, distance) - - if s_packet.is_valid() then - local start = util.time_ms() - local packet_hmac = s_packet.mac() - local computed_hmac = crypto.hmac(textutils.serialize(s_packet.raw_verifiable(), { allow_repetitions = true, compact = true })) - - if packet_hmac == computed_hmac then - log.debug("crypto.secure_modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") - packet = s_packet - else - log.debug("crypto.secure_modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") - end - end - - return packet - end - - return public -end - -return crypto diff --git a/scada-common/network.lua b/scada-common/network.lua new file mode 100644 index 0000000..fbc4750 --- /dev/null +++ b/scada-common/network.lua @@ -0,0 +1,225 @@ +-- +-- Network Communications +-- + +local md5 = require("lockbox.digest.md5") +local sha256 = require("lockbox.digest.sha2_256") +local pbkdf2 = require("lockbox.kdf.pbkdf2") +local hmac = require("lockbox.mac.hmac") +local stream = require("lockbox.util.stream") +local array = require("lockbox.util.array") +local comms = require("scada-common.comms") + +local log = require("scada-common.log") +local util = require("scada-common.util") + +local network = {} + +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 + key_deriv.setPassword(passkey) + key_deriv.setSalt("pepper") + key_deriv.setIterations(32) + key_deriv.setBlockLen(8) + key_deriv.setDKeyLen(16) + key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha256)) + 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 + +-- generate HMAC of message +---@nodiscard +---@param message string initial value concatenated with ciphertext +local function compute_hmac(message) + local start = util.time_ms() + + c_eng.hmac.init() + c_eng.hmac.update(stream.fromString(message)) + c_eng.hmac.finish() + + local hash = c_eng.hmac.asHex() + + log.debug("compute_hmac(): hmac-md5 = " .. util.strval(hash) .. " (took " .. (util.time_ms() - start) .. "ms)") + + return hash +end + +-- NIC: Network Interface Controller
+-- utilizes HMAC-MD5 for message authentication, if enabled +---@param modem table modem to use +function network.nic(modem) + local self = { + connected = (modem ~= nil), + 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 = {} + + -- 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 + + -- link all public functions except for transmit + 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 this NIC has a connected modem + ---@nodiscard + function public.connected() return self.connected end + + -- check if a peripheral is this modem + ---@nodiscard + ---@param device table + function public.is_modem(device) return device == modem end + + -- wrap modem functions, then create custom transmit + public.connect(modem) + + function public.open(channel) + if self.connected then + modem.open(channel) + + local already_open = false + for i = 1, #self.channels do + if self.channels[i] == channel then + already_open = true + break + end + end + + if not already_open then + table.insert(self.channels, channel) + end + end + end + + function public.close(channel) + if self.connected then + modem.close(channel) + for i = 1, #self.channels do + if self.channels[i] == channel then + table.remove(self.channels, i) + return + end + end + end + end + + function public.closeAll() + if self.connected then + modem.closeAll() + self.channels = {} + end + end + + -- send a packet, with message authentication if configured + ---@param dest_channel integer destination channel + ---@param local_channel integer local channel + ---@param packet scada_packet packet raw_sendable + function public.transmit(dest_channel, local_channel, packet) + if self.connected then + if c_eng.hmac ~= nil then + local start = util.time_ms() + local message = textutils.serialize(packet.raw_verifiable(), { allow_repetitions = true, compact = true }) + local computed_hmac = compute_hmac(message) + + packet.set_mac(computed_hmac) + + log.debug("crypto.modem.transmit: data processing took " .. (util.time_ms() - start) .. "ms") + end + + modem.transmit(dest_channel, local_channel, packet.raw_sendable()) + 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() + + -- parse packet as generic SCADA packet + s_packet.receive(side, sender, reply_to, message, distance) + + if s_packet.is_valid() then + if c_eng.hmac ~= nil then + local start = util.time_ms() + local packet_hmac = s_packet.mac() + local computed_hmac = compute_hmac(textutils.serialize(s_packet.raw_verifiable(), { allow_repetitions = true, compact = true })) + + if packet_hmac == computed_hmac then + log.debug("crypto.modem.receive: HMAC verified in " .. (util.time_ms() - start) .. "ms") + packet = s_packet + else + log.debug("crypto.modem.receive: HMAC failed verification in " .. (util.time_ms() - start) .. "ms") + end + else + packet = s_packet + end + end + end + + return packet + end + + return public +end + +return network