-- [ʞ] rd-key.lua
-- ~ lexi hale
-- © GNU EUPL v1.2
-- ? utility for generating and applying 64bit key
-- schedules to RD-5R "codeplug" provisioning profiles
-- > lua rd-key.lua gen sched.kch
-- > lua rd-key.lua apply sched.kch device.img
-- > dmrconfig -w device.img
function B(b, ...)
if not b then return "" end
return string.pack("I1", b) .. B(...)
end
local const = {
kch_header = B(0x9a, 0x1e, 0x2f) .. "kCH";
maxkeys = 16;
keylen = 64/8;
plug = {
kch_start = 0x1370;
kch_end = 0x13f8;
bank_chns = 128;
chn_sz = 56;
bank_sz = 16 + (56 * 128);
chn_regions = {
{top = 128, start = 0x3780};
{top = 1023, start = 0xb1b0};
};
mode = {
analog = 0;
digital = 1;
};
};
}
math.randomseed(os.time())
function log(...) print(string.format(...)) end
function keymask(num)
local mask = 0
for i = 0, num-1 do
mask = mask | 1 << i
end
return string.pack("<I2", mask)
end
function strdec(str)
for i = 1, #str do
if string.byte(str, i) == 0xFF then
return str:sub(1, i-1)
end
end
return str
end
function chans(mem)
local i = 1
local region = 1
return function()
local r = const.plug.chn_regions[region]
::tryagain::
if i > r.top then
region = region + 1
r = const.plug.chn_regions[region]
end
if r == nil then return nil end
local bottom = region > 1 and const.plug.chn_regions[region-1].top or 1
local bank = (i - bottom) // const.plug.bank_chns
local bo = r.start + bank * const.plug.bank_sz
local bankpos = (i - bottom) % const.plug.bank_chns
i = i + 1
-- local bitmap = mem:sub(bo, bo + 16)
-- local bitmap_b = string.byte(bitmap, 1 + bankpos // 8)
-- if bitmap_b & (1 << (bankpos % 8)) == 0 then --- doesn't work
-- goto tryagain
-- end
local chans = bo + 16
local ent = chans + bankpos * const.plug.chn_sz
local rec = mem:sub(1+ent,1+ent + const.plug.chn_sz)
return rec, i-1, 1+ent
end
end
function strchg(str, ofs, sz, new)
local head = string.sub(str, 1, ofs-1)
local tail = string.sub(str, ofs+sz)
local n = head .. new .. tail
if #n ~= #str then
print("old str") dumpline(string.sub(str, ofs, ofs + (sz-1)))
print("new str should be", sz, #new) dumpline(new)
error("bad str chg")
end
return n
end
function dumpline(str)
local function dump(line)
local top="\t"
local bottom="\t"
for i=1,#line do
local val = string.byte(line, i)
if val >= 0x20 and val <= 0x7e then
top = top .. string.format(" %c ", val)
else
top = top .. " : "
end
bottom = bottom .. string.format("%02x ", val)
end
print(top)
print(bottom)
end
local cpl = 32
for i = 0, #str // cpl do
local st = 1 + i*cpl
dump(str:sub(st, st+cpl))
end
end
function applykeychain(chain, file)
local h = io.open(file, 'rb')
if not h then error("could not open file") end
local pl = h:read('a') h:close()
local keylist = B(1,0) -- basic mode
.. keymask(const.maxkeys) -- number of keys
.. B(0,0, 0,0) -- padding??
.. table.concat(chain)
pl = strchg(pl, const.plug.kch_start+1, #keylist, keylist)
for ch, idx, ofs in chans(pl) do
if string.byte(ch, 1) ~= 0xFF then -- is channel defined?
local name = strdec(ch:sub(1,16))
local mode = string.byte(ch, 25)
local function say(fmt, ...)
log("\x1b[1mchannel [%04u] “%s”\x1b[m: " .. fmt, idx, name, ...)
end
local newch
if mode == const.plug.mode.digital then
local privg = string.byte(ch, 42)
local chkey = idx % const.maxkeys
if privg == 0 then
say("marked clear, comms on this channel will NOT be encrypted")
elseif privg ~= chkey then
say("changing key from #%u to #%u", privg, chkey)
newch = strchg(ch, 42, 1, B(chkey))
dirty = true
-- else
-- say("already keyed correctly")
end
else
say("analog channel, cannot be encrypted")
end
if newch then
pl=strchg(pl, ofs, 1+const.plug.chn_sz, newch)
end
end
end
h = io.open(file, 'w+b')
h:write(pl)
h:close()
end
function loadkeychain(file)
local f = io.open(file, "r")
if f:read(#const.kch_header) ~= const.kch_header then
error "not a valid keychain file"
end
local ch = {}
for i=1, const.maxkeys do
ch[i] = f:read(const.keylen)
end
local comment = f:read('a')
f:close()
log("using keychain for %s", comment)
return ch
end
function savekeychain(file, kch)
local f = io.open(file, "w+b")
f:write(const.kch_header)
for i=1, const.maxkeys do
f:write(kch[i])
end
f:write(os.date())
f:close()
end
function genkeychain()
local ch = {}
local rng = io.open("/dev/urandom") or io.open("/dev/random")
function rb(n)
if rng then return rng:read(n) else
log('warning, cannot read /dev/[u]random, random bytes may be low-quality')
local str = ""
for i = 1,n do str = str .. B(math.random(0,0xff)) end
return str
end
end
for i=1,const.maxkeys do
ch[i] = rb(64/8)
end
if rng then rng:close() end
return ch
end
local cmd, keychain, target = ...
if not cmd then
print("usage: lua rd-key.lua (gen|apply|extract) keychain [codeplug]")
else
if keychain == nil or file == "" then keychain = "chain.kch" end
if target == nil or target == "" then target = "device.img" end
if cmd == "gen" then
log("writing new keychain to %s", keychain)
local ch = genkeychain()
savekeychain(keychain, ch)
elseif cmd == "apply" then
log("applying keychain %s to codeplug %s", keychain, target)
local ch = loadkeychain(keychain)
applykeychain(ch, target)
elseif cmd == "extract" then
log("extracting keychain %s from codeplug %s", keychain, target)
log(" -- unimplemented --")
end
end