-- [ʞ] rd-key.lua
--  ~ lexi hale
--  🄯 GNU AGPL v3
--  ? 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
