client/luascripts/lf_t55xx_multiwriter.lua
local getopt = require('getopt')
local utils = require('utils')
local ac = require('ansicolors')
local os = require('os')
local dash = string.rep('--', 32)
local dir = os.getenv('HOME') .. '/.proxmark3/logs/'
local logfile = (io.popen('dir /a-d /o-d /tw /b/s "' .. dir .. '" 2>nul:'):read("*a"):match("%C+"))
local command = core.console
local pm3 = require('pm3')
p = pm3.pm3()
command('clear')
author = ' jareckib - 12.03.2025'
version = ' v1.06'
mod = ' 20.03.2025'
desc = [[

  This simple script stores 1, 2 or 3 different EM4102 on a single T5577.
  There is an option to enter the number engraved on the fob in decimal form.
  The script can therefore be useful if the original EM4102 doesn't work but
  has an engraved ID number. By entering such an ID as a single EM4102, we
  can create a working copy of our damaged fob.
  A tag T5577 created in this way works with the following USB readers:

  - ACM08Y
  - ACM26C
  - Sycreader R60D
  - Elatech Multitech TWN4
]]
usage = [[
  script run lf_t55xx_multiwriter
]]
arguments = [[
  script run lf_t55xx_multiwriter -h    : this help
]]

local function help()
    print()
    print(ac.yellow..'  Author:'..ac.reset..author)
    print(ac.yellow..'  Version:'..ac.reset..version)
    print(ac.yellow..'  Modification date:'..ac.reset..mod)
    print(desc)
    print(ac.cyan .. '  Usage' .. ac.reset)
    print(usage)
    print(ac.cyan .. '  Arguments' .. ac.reset)
    print(arguments)
end

local function sleep(n)
    os.execute("sleep " ..tonumber(n))
end

function wait(msec)
   local t = os.clock()
   repeat
   until os.clock() > t + msec * 1e-3
end

local function timer(n)
    while n > 0 do
        io.write(ac.cyan.."::::: "..ac.yellow.. tonumber(n) ..ac.yellow.." sec "..ac.cyan..":::::\r"..ac.reset)
        sleep(1)
        io.flush()
        n = n-1
    end
end

local function reset_log_file()
    local file = io.open(logfile, "w+")
    file:write("")
    file:close()
end

local function read_log_file(logfile)
    local file = io.open(logfile, "r")
    if not file then
        return nil
    end
    local content = file:read("*all")
    file:close()
    return content
end

local function extract_uid(log_content)
    for line in log_content:gmatch("[^\r\n]+") do
        local uid = line:match("%[%s*%+%]%s*EM%s*410x%s*ID%s*([A-F0-9]+)")
        if uid then
            return uid
        end
    end
    return nil
end

local function hex_to_bin(hex_value)
    if not hex_value:match("^[A-Fa-f0-9]+$") or #hex_value ~= 10 then
        error("Invalid UID format. Must be a valid 5-byte HEX value.")
    end
    local decimal_value = tonumber(hex_value, 16)
    if not decimal_value then
        error("Error: Invalid HEX conversion.")
    end
    local binary = ""
    for i = 39, 0, -1 do
        binary = binary .. ((decimal_value & (1 << i)) ~= 0 and "1" or "0")
    end
    if #binary ~= 40 then
        error("Unexpected UID length after conversion to binary.")
    end
    return binary
end

local function even_parity(bits)
    return (bits:gsub("0", ""):len() % 2 == 0) and "0" or "1"
end

local function encode_uid(uid)
    local uid_bin = hex_to_bin(uid)
    local start_bits = '1' .. string.rep('1', 8)
    local data_with_parity = ''
    for i = 1, 40, 4 do
        local nibble = uid_bin:sub(i, i + 3)
        local parity_bit = even_parity(nibble)
        data_with_parity = data_with_parity .. nibble .. parity_bit
    end
    local col_parity_bits = ''
    for i = 1, 4 do
        local col_bits = ''
        for j = i, #data_with_parity, 5 do
            col_bits = col_bits .. data_with_parity:sub(j, j)
        end
        col_parity_bits = col_parity_bits .. even_parity(col_bits)
    end
    local stop_bit = '0'
    local full_bin = start_bits .. data_with_parity .. col_parity_bits .. stop_bit
    return string.format("%X", tonumber(full_bin, 2))
end

local function get_uid_from_user()
    while true do
        print(dash)
        io.write(ac.cyan .. '(1)' .. ac.reset .. ' Manual entry UID |' .. ac.cyan .. ' (2)' .. ac.reset .. ' Read via Proxmark3 ')

        local choice
        repeat
            choice = io.read()
            if choice ~= "1" and choice ~= "2" then
                io.write(ac.yellow .. "Invalid choice. Please enter (1) or (2)  " .. ac.reset)
            end
        until choice == "1" or choice == "2"
        if choice == "1" then
            local format
            repeat
                io.write("Choose format HEX or DEC (engraved ID) "..ac.cyan.."(h/d) "..ac.reset)
                format = io.read():lower()
                if format ~= "h" and format ~= "d" then
                    print(ac.yellow .. "Invalid choice. Choose format HEX or DEC" .. ac.reset)
                end
            until format == "h" or format == "d"
            while true do
                io.write("Enter 10-character UID: "..ac.green)
                local uid = io.read():upper()
                if format == "h" and uid:match("^[A-F0-9]+$") and #uid == 10 then
                    return uid
                elseif format == "d" and uid:match("^%d%d%d%d%d%d%d%d%d%d$") then
                    return string.format("%010X", tonumber(uid))
                else
                    print(ac.yellow .. "Invalid UID format. Enter exactly 10 characters in selected format." .. ac.reset)
                end
            end
        elseif choice == "2" then
            io.write("Place original FOB on coil for reading and press" ..ac.cyan.." Enter..." .. ac.reset)
            io.read()
            while true do
                reset_log_file()
                p:console('lf em 410x read')
                local log_content = read_log_file(logfile)
                local uid = extract_uid(log_content)
                if uid and #uid == 10 then
                    print("Readed EM4102 ID: " ..ac.green.. uid ..ac.reset)
                    return uid
                else
                    io.write(ac.yellow .. "Error reading UID. Please adjust FOB position and press Enter..." .. ac.reset)
                    io.read()
                end
            end
        end
    end
end

local function verify_written_data(blocks, block0_value, uid_count)
    reset_log_file()
    p:console('lf t55 detect')
    for i = 0, #blocks do
        p:console('lf t55 read -b ' .. i)
    end
    local log_content = read_log_file(logfile)
    local verified = true
    local pattern_block0 = "%[%s*%+%]%s*00%s*|%s*([A-F0-9]+)"
    local found_block0 = log_content:match(pattern_block0)
    if not found_block0 or found_block0:upper() ~= block0_value:upper() then
        print("Error in block 0 ...expected: " .. ac.green .. block0_value .. ac.reset .. " ...found: " .. ac.green .. (found_block0 or "N/A") .. ac.reset)
        verified = false
    end
    for i = 1, #blocks do
        local expected_block = blocks[i]
        local pattern = "%[%s*%+%]%s*" .. string.format("%02X", i) .. " |%s*([A-F0-9]+)"
        local found_block = log_content:match(pattern)
        if not found_block or found_block:upper() ~= expected_block:upper() then
            print("Error in block " .. i .. " ...expected: " .. ac.green .. expected_block .. ac.reset .. " ...found: " .. ac.green .. (found_block or "N/A") .. ac.reset)
            verified = false
        end
    end
    return verified
end

local function write(blocks, block0_value)
    p:console('lf t55xx write -b 0 -d ' .. block0_value)
    for i = 1, #blocks do
        p:console('lf t55xx write -b ' .. i .. ' -d ' .. blocks[i])
    end
end

local function main(args)
    for o, a in getopt.getopt(args, 'h') do
        if o == 'h' then
            return help()
        end
    end
    local blocks = {}
    local uid_count = 0
    for i = 1, 3 do
        local uid = get_uid_from_user()
        local encoded_hex = encode_uid(uid)
        blocks[#blocks + 1] = encoded_hex:sub(1, 8)
        blocks[#blocks + 1] = encoded_hex:sub(9, 16)
        uid_count = uid_count + 1
        if i < 3 then
            local next_choice
            repeat
                io.write(ac.reset.."Do you want to add another UID? "..ac.cyan.."(y/n)  "..ac.reset)
                next_choice = io.read():lower()
                if next_choice ~= "y" and next_choice ~= "n" then
                    print(ac.yellow .. "Invalid choice. Please enter (y) for yes or (n) for no." .. ac.reset)
                end
            until next_choice == "y" or next_choice == "n"
            if next_choice == "y" then
                print(dash)
                print(ac.yellow .. (i == 1 and "::: Second UID :::" or "::: Third UID :::") .. ac.reset)
            elseif next_choice == "n" then
                break
            end
        end
    end
    local block0_value = (uid_count == 1) and "00148040" or (uid_count == 2) and "00148080" or "001480C0"
    io.write("Place the " .. ac.cyan .. "T5577" .. ac.reset .. " tag on the coil for writing and press " .. ac.cyan .. "Enter..." .. ac.reset)
    io.read()
    write(blocks, block0_value)
    print(dash)
    timer(3)
    local verified = verify_written_data(blocks, block0_value, uid_count)
    while not verified do
        print("Verification failed." .. ac.reset .. " Please adjust the " .. ac.cyan .. "T5577" .. ac.reset .. " position and try again.")
        io.write("Press " .. ac.cyan .. "Enter" .. ac.reset .. " to retry...")
        io.read()
        write(blocks, block0_value)
        timer(3)
        verified = verify_written_data(blocks, block0_value, uid_count)
    end
    print(ac.green .. "Successfully written " .. ac.reset .. uid_count .. ac.green .. " EM4102 UID(s) to T5577" .. ac.reset)
end

main(args)
