#51 hmac verification
This commit is contained in:
@@ -7,14 +7,14 @@ local log = require("scada-common.log")
|
||||
local insert = table.insert
|
||||
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
local C_ID = os.getComputerID() ---@type integer computer ID
|
||||
local COMPUTER_ID = os.getComputerID() ---@type integer computer ID
|
||||
|
||||
local max_distance = nil ---@type number|nil maximum acceptable transmission distance
|
||||
local max_distance = nil ---@type number|nil maximum acceptable transmission distance
|
||||
|
||||
---@class comms
|
||||
local comms = {}
|
||||
|
||||
comms.version = "2.0.0"
|
||||
comms.version = "2.1.0"
|
||||
|
||||
---@enum PROTOCOL
|
||||
local PROTOCOL = {
|
||||
@@ -149,6 +149,7 @@ function comms.scada_packet()
|
||||
dest_addr = comms.BROADCAST,
|
||||
seq_num = -1,
|
||||
protocol = PROTOCOL.SCADA_MGMT,
|
||||
mac = "",
|
||||
length = 0,
|
||||
payload = {}
|
||||
}
|
||||
@@ -163,14 +164,13 @@ function comms.scada_packet()
|
||||
---@param payload table
|
||||
function public.make(dest_addr, seq_num, protocol, payload)
|
||||
self.valid = true
|
||||
---@diagnostic disable-next-line: undefined-field
|
||||
self.src_addr = C_ID
|
||||
self.src_addr = COMPUTER_ID
|
||||
self.dest_addr = dest_addr
|
||||
self.seq_num = seq_num
|
||||
self.protocol = protocol
|
||||
self.length = #payload
|
||||
self.payload = payload
|
||||
self.raw = { self.src_addr, self.dest_addr, self.seq_num, self.protocol, self.payload }
|
||||
self.raw = { self.src_addr, self.dest_addr, self.seq_num, self.protocol, self.mac, self.payload }
|
||||
end
|
||||
|
||||
-- parse in a modem message as a SCADA packet
|
||||
@@ -198,43 +198,58 @@ function comms.scada_packet()
|
||||
-- log.debug("comms.scada_packet.receive(): discarding packet with distance " .. distance .. " outside of trusted range")
|
||||
else
|
||||
if type(self.raw) == "table" then
|
||||
if #self.raw == 5 then
|
||||
if #self.raw == 6 then
|
||||
self.src_addr = self.raw[1]
|
||||
self.dest_addr = self.raw[2]
|
||||
self.seq_num = self.raw[3]
|
||||
self.protocol = self.raw[4]
|
||||
self.mac = self.raw[5]
|
||||
|
||||
-- element 5 must be a table
|
||||
if type(self.raw[5]) == "table" then
|
||||
self.length = #self.raw[5]
|
||||
self.payload = self.raw[5]
|
||||
if type(self.raw[6]) == "table" then
|
||||
self.length = #self.raw[6]
|
||||
self.payload = self.raw[6]
|
||||
end
|
||||
else
|
||||
self.src_addr = nil
|
||||
self.dest_addr = nil
|
||||
self.seq_num = nil
|
||||
self.protocol = nil
|
||||
self.mac = ""
|
||||
self.length = 0
|
||||
self.payload = {}
|
||||
end
|
||||
|
||||
-- check if this packet is destined for this device
|
||||
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == C_ID)
|
||||
local is_destination = (self.dest_addr == comms.BROADCAST) or (self.dest_addr == COMPUTER_ID)
|
||||
|
||||
self.valid = is_destination and type(self.src_addr) == "number" and type(self.dest_addr) == "number" and
|
||||
type(self.seq_num) == "number" and type(self.protocol) == "number" and type(self.payload) == "table"
|
||||
self.valid = is_destination and
|
||||
type(self.src_addr) == "number" and
|
||||
type(self.dest_addr) == "number" and
|
||||
type(self.seq_num) == "number" and
|
||||
type(self.protocol) == "number" and
|
||||
type(self.mac) == "string" and
|
||||
type(self.payload) == "table"
|
||||
end
|
||||
end
|
||||
|
||||
return self.valid
|
||||
end
|
||||
|
||||
-- set message authentication code
|
||||
function public.set_mac(code)
|
||||
self.mac = code
|
||||
self.raw = { self.src_addr, self.dest_addr, self.seq_num, self.protocol, self.mac, self.payload }
|
||||
end
|
||||
|
||||
-- public accessors --
|
||||
|
||||
---@nodiscard
|
||||
function public.modem_event() return self.modem_msg_in end
|
||||
---@nodiscard
|
||||
function public.raw_sendable() return self.raw end
|
||||
---@nodiscard
|
||||
function public.raw_verifiable() return { self.src_addr, self.dest_addr, self.seq_num, self.protocol, "", self.payload } end
|
||||
|
||||
---@nodiscard
|
||||
function public.local_channel() return self.modem_msg_in.s_channel end
|
||||
@@ -253,6 +268,8 @@ function comms.scada_packet()
|
||||
---@nodiscard
|
||||
function public.protocol() return self.protocol end
|
||||
---@nodiscard
|
||||
function public.mac() return self.mac end
|
||||
---@nodiscard
|
||||
function public.length() return self.length end
|
||||
---@nodiscard
|
||||
function public.data() return self.payload end
|
||||
|
||||
@@ -2,15 +2,13 @@
|
||||
-- Cryptographic Communications Engine
|
||||
--
|
||||
|
||||
local aes128 = require("lockbox.cipher.aes128")
|
||||
local ctr_mode = require("lockbox.cipher.mode.ctr")
|
||||
local sha1 = require("lockbox.digest.sha1")
|
||||
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 zero_pad = require("lockbox.padding.zero")
|
||||
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")
|
||||
@@ -19,136 +17,56 @@ local crypto = {}
|
||||
|
||||
local c_eng = {
|
||||
key = nil,
|
||||
cipher = nil,
|
||||
decipher = nil,
|
||||
hmac = nil
|
||||
}
|
||||
|
||||
---@alias hex string
|
||||
|
||||
-- initialize cryptographic system
|
||||
function crypto.init(password, server_port)
|
||||
function crypto.init(password)
|
||||
local key_deriv = pbkdf2()
|
||||
|
||||
-- setup PBKDF2
|
||||
-- the primary goal is to just turn our password into a 16 byte key
|
||||
key_deriv.setPassword(password)
|
||||
key_deriv.setSalt("salty_salt_at_" .. server_port)
|
||||
key_deriv.setSalt("pepper")
|
||||
key_deriv.setIterations(32)
|
||||
key_deriv.setBlockLen(8)
|
||||
key_deriv.setDKeyLen(16)
|
||||
|
||||
local start = util.time()
|
||||
local start = util.time_ms()
|
||||
|
||||
key_deriv.setPRF(hmac().setBlockSize(64).setDigest(sha2_256))
|
||||
key_deriv.finish()
|
||||
|
||||
log.dmesg("pbkdf2: key derivation took " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
|
||||
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 cipher
|
||||
c_eng.cipher = ctr_mode.Cipher()
|
||||
c_eng.cipher.setKey(c_eng.key)
|
||||
c_eng.cipher.setBlockCipher(aes128)
|
||||
c_eng.cipher.setPadding(zero_pad)
|
||||
|
||||
-- initialize decipher
|
||||
c_eng.decipher = ctr_mode.Decipher()
|
||||
c_eng.decipher.setKey(c_eng.key)
|
||||
c_eng.decipher.setBlockCipher(aes128)
|
||||
c_eng.decipher.setPadding(zero_pad)
|
||||
|
||||
-- initialize HMAC
|
||||
c_eng.hmac = hmac()
|
||||
c_eng.hmac.setBlockSize(64)
|
||||
c_eng.hmac.setDigest(sha1)
|
||||
c_eng.hmac.setDigest(md5)
|
||||
c_eng.hmac.setKey(c_eng.key)
|
||||
|
||||
log.dmesg("init: completed in " .. (util.time() - start) .. "ms", "CRYPTO", colors.yellow)
|
||||
end
|
||||
|
||||
-- encrypt plaintext
|
||||
---@nodiscard
|
||||
---@param plaintext string
|
||||
---@return table initial_value, string ciphertext
|
||||
function crypto.encrypt(plaintext)
|
||||
local start = util.time()
|
||||
|
||||
-- initial value
|
||||
local iv = {
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255),
|
||||
math.random(0, 255)
|
||||
}
|
||||
|
||||
log.debug("crypto.random: iv random took " .. (util.time() - start) .. "ms")
|
||||
|
||||
start = util.time()
|
||||
|
||||
c_eng.cipher.init()
|
||||
c_eng.cipher.update(stream.fromArray(iv))
|
||||
c_eng.cipher.update(stream.fromString(plaintext))
|
||||
c_eng.cipher.finish()
|
||||
|
||||
local ciphertext = c_eng.cipher.asHex() ---@type hex
|
||||
|
||||
log.debug("crypto.encrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
|
||||
log.debug("ciphertext: " .. util.strval(ciphertext))
|
||||
|
||||
return iv, ciphertext
|
||||
end
|
||||
|
||||
-- decrypt ciphertext
|
||||
---@nodiscard
|
||||
---@param iv string CTR initial value
|
||||
---@param ciphertext string ciphertext hex
|
||||
---@return string plaintext
|
||||
function crypto.decrypt(iv, ciphertext)
|
||||
local start = util.time()
|
||||
|
||||
c_eng.decipher.init()
|
||||
c_eng.decipher.update(stream.fromArray(iv))
|
||||
c_eng.decipher.update(stream.fromHex(ciphertext))
|
||||
c_eng.decipher.finish()
|
||||
|
||||
local plaintext_hex = c_eng.decipher.asHex() ---@type hex
|
||||
|
||||
local plaintext = stream.toString(stream.fromHex(plaintext_hex))
|
||||
|
||||
log.debug("crypto.decrypt: aes128-ctr-mode took " .. (util.time() - start) .. "ms")
|
||||
log.debug("plaintext: " .. util.strval(plaintext))
|
||||
|
||||
return plaintext
|
||||
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_hex string initial value concatenated with ciphertext
|
||||
function crypto.hmac(message_hex)
|
||||
local start = util.time()
|
||||
---@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.fromHex(message_hex))
|
||||
c_eng.hmac.update(stream.fromString(message))
|
||||
c_eng.hmac.finish()
|
||||
|
||||
local hash = c_eng.hmac.asHex() ---@type hex
|
||||
local hash = c_eng.hmac.asHex()
|
||||
|
||||
log.debug("crypto.hmac: hmac-sha1 took " .. (util.time() - start) .. "ms")
|
||||
log.debug("hmac: " .. util.strval(hash))
|
||||
log.debug("crypto.hmac: hmac-md5 took " .. (util.time_ms() - start) .. "ms")
|
||||
log.debug("crypto.hmac: hmac = " .. util.strval(hash))
|
||||
|
||||
return hash
|
||||
end
|
||||
@@ -173,7 +91,6 @@ function crypto.secure_modem(modem)
|
||||
|
||||
-- wrap a modem
|
||||
---@param reconnected_modem table
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
function public.wrap(reconnected_modem)
|
||||
modem = reconnected_modem
|
||||
for key, func in pairs(modem) do
|
||||
@@ -184,58 +101,49 @@ function crypto.secure_modem(modem)
|
||||
-- wrap modem functions, then we replace transmit
|
||||
public.wrap(modem)
|
||||
|
||||
-- send a packet with encryption
|
||||
---@param channel integer
|
||||
---@param reply_channel integer
|
||||
---@param payload table packet raw_sendable
|
||||
function public.transmit(channel, reply_channel, payload)
|
||||
local plaintext = textutils.serialize(payload, { allow_repetitions = true, compact = true })
|
||||
-- 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)
|
||||
|
||||
local iv, ciphertext = crypto.encrypt(plaintext)
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local computed_hmac = crypto.hmac(iv .. ciphertext)
|
||||
packet.set_mac(computed_hmac)
|
||||
|
||||
modem.transmit(channel, reply_channel, { computed_hmac, iv, ciphertext })
|
||||
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 port
|
||||
---@param reply_to integer reply port
|
||||
---@param message any encrypted packet sent with secure_modem.transmit
|
||||
---@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 string side, integer sender, integer reply_to, any plaintext_message, integer distance
|
||||
---@return scada_packet|nil packet received packet if valid and passed authentication check
|
||||
function public.receive(side, sender, reply_to, message, distance)
|
||||
local body = ""
|
||||
local packet = nil
|
||||
local s_packet = comms.scada_packet()
|
||||
|
||||
if type(message) == "table" then
|
||||
if #message == 3 then
|
||||
---@diagnostic disable-next-line: redefined-local
|
||||
local rx_hmac = message[1]
|
||||
local iv = message[2]
|
||||
local ciphertext = message[3]
|
||||
-- parse packet as generic SCADA packet
|
||||
s_packet.receive(side, sender, reply_to, message, distance)
|
||||
|
||||
local computed_hmac = crypto.hmac(iv .. ciphertext)
|
||||
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 rx_hmac == computed_hmac then
|
||||
-- message intact
|
||||
local plaintext = crypto.decrypt(iv, ciphertext)
|
||||
body = textutils.unserialize(plaintext)
|
||||
|
||||
if body == nil then
|
||||
-- failed decryption
|
||||
log.debug("crypto.secure_modem: decryption failed")
|
||||
body = ""
|
||||
end
|
||||
else
|
||||
-- something went wrong
|
||||
log.debug("crypto.secure_modem: hmac mismatch violation")
|
||||
end
|
||||
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 side, sender, reply_to, body, distance
|
||||
return packet
|
||||
end
|
||||
|
||||
return public
|
||||
|
||||
Reference in New Issue
Block a user