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 log_file_path = dir .. "Paxton_log.txt" local nam = "" local pm3 = require('pm3') p = pm3.pm3() local command = core.console command('clear') author = ' Author: jareckib - 30.01.2025' tutorial = ' Based on Equipter tutorial - Downgrade Paxton to EM4102' version = ' version v1.20' desc = [[ The script automates the copying of Paxton fobs read - write. It also allows manual input of data for blocks 4-7. The third option is reading data stored in the log file and create new fob. Additionally, the script calculates the ID for downgrading Paxton to EM4102. ]] usage = [[ script run paxton_clone ]] arguments = [[ script run paxton_clone -h : this help ]] local debug = true local function dbg(args) if not DEBUG then return end if type(args) == 'table' then local i = 1 while args[i] do dbg(args[i]) i = i+1 end else print('###', args) end end local function help() print() print(author) print(tutorial) print(version) print(desc) print(ac.cyan..' Usage'..ac.reset) print(usage) print(ac.cyan..' Arguments'..ac.reset) print(arguments) 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 error(" Could not open the file") end local content = file:read("*all") file:close() return content end local function parse_blocks(result) local blocks = {} for line in result:gmatch("[^\r\n]+") do local block_num, block_data = line:match("%[%=%]%s+%d/0x0([4-7])%s+%|%s+([0-9A-F ]+)") if block_num and block_data then block_num = tonumber(block_num) block_data = block_data:gsub("%s+", "") blocks[block_num] = block_data end end return blocks end local function hex_to_bin(hex_string) local bin_string = "" local hex_to_bin_map = { ['0'] = "0000", ['1'] = "0001", ['2'] = "0010", ['3'] = "0011", ['4'] = "0100", ['5'] = "0101", ['6'] = "0110", ['7'] = "0111", ['8'] = "1000", ['9'] = "1001", ['A'] = "1010", ['B'] = "1011", ['C'] = "1100", ['D'] = "1101", ['E'] = "1110", ['F'] = "1111" } for i = 1, #hex_string do bin_string = bin_string .. hex_to_bin_map[hex_string:sub(i, i)] end return bin_string end local function remove_last_two_bits(binary_str) return binary_str:sub(1, #binary_str - 2) end local function split_into_5bit_chunks(binary_str) local chunks = {} for i = 1, #binary_str, 5 do table.insert(chunks, binary_str:sub(i, i + 4)) end return chunks end local function remove_parity_bit(chunks) local no_parity_chunks = {} for _, chunk in ipairs(chunks) do if #chunk == 5 then table.insert(no_parity_chunks, chunk:sub(2)) end end return no_parity_chunks end local function convert_to_hex(chunks) local hex_values = {} for _, chunk in ipairs(chunks) do if #chunk > 0 then table.insert(hex_values, string.format("%X", tonumber(chunk, 2))) end end return hex_values end local function convert_to_decimal(chunks) local decimal_values = {} for _, chunk in ipairs(chunks) do table.insert(decimal_values, tonumber(chunk, 2)) end return decimal_values end local function find_until_before_f(hex_values) local result = {} for _, value in ipairs(hex_values) do if value == 'F' then break end table.insert(result, value) end return result end local function process_block(block) local binary_str = hex_to_bin(block) binary_str = remove_last_two_bits(binary_str) local chunks = split_into_5bit_chunks(binary_str) local no_parity_chunks = remove_parity_bit(chunks) return no_parity_chunks end local function calculate_id_net(blocks) local all_hex_values = {} for _, block in ipairs(blocks) do local hex_values = convert_to_hex(process_block(block)) for _, hex in ipairs(hex_values) do table.insert(all_hex_values, hex) end end local selected_hex_values = find_until_before_f(all_hex_values) if #selected_hex_values == 0 then error(ac.red..' Error: '..ac.reset..'No valid data found in blocks 4 and 5') end local combined_hex = table.concat(selected_hex_values) if not combined_hex:match("^%x+$") then error(ac.red..' Error: '..ac.reset..'Invalid data in blocks 4 and 5') end local decimal_id = tonumber(combined_hex) local stripped_hex_id = string.format("%X", decimal_id) local padded_hex_id = string.format("%010X", decimal_id) return decimal_id, padded_hex_id end local function calculate_id_switch(blocks) local all_decimal_values = {} for _, block in ipairs(blocks) do local decimal_values = convert_to_decimal(process_block(block)) for _, dec in ipairs(decimal_values) do table.insert(all_decimal_values, dec) end end if #all_decimal_values < 15 then error(ac.red..' Error:'..ac.reset..' Not enough data after processing blocks 4, 5, 6, and 7') end local id_positions = {9, 11, 13, 15, 2, 4, 6, 8} local id_numbers = {} for _, pos in ipairs(id_positions) do table.insert(id_numbers, all_decimal_values[pos]) end local decimal_id = tonumber(table.concat(id_numbers)) local padded_hex_id = string.format("%010X", decimal_id) return decimal_id, padded_hex_id end local function name_exists_in_log(name) local file = io.open(log_file_path, "r") if not file then return false end local pattern = "^Name:%s*" .. name .. "%s*$" for line in file:lines() do if line:match(pattern) then file:close() return true end end file:close() return false end local function log_result(blocks, em410_id, name) local log_file = io.open(log_file_path, "a") if log_file then log_file:write("Name: " .. name .. "\n") log_file:write("Date: ", os.date("%Y-%m-%d %H:%M:%S"), "\n") for i = 4, 7 do log_file:write(string.format("Block %d: %s\n", i, blocks[i] or "nil")) end log_file:write(string.format('EM4102 ID: %s\n', em410_id or "nil")) log_file:write('--------------------------\n') log_file:close() print(' Log saved as: pm3/.proxmark3/logs/' ..ac.yellow..' Paxton_log.txt'..ac.reset) else print(" Failed to open log file for writing.") end end local function verify_written_data(original_blocks) p:console('lf hitag read --ht2 -k BDF5E846') local result = read_log_file(logfile) local verified_blocks = parse_blocks(result) local success = true for i = 4, 7 do if original_blocks[i] ~= verified_blocks[i] then print(' Verification failed.. Block '..ac.green.. i ..ac.reset.. ' inconsistent.') success = false end end if success then print(ac.green..' Verification successful. Data was written correctly.' .. ac.reset) else print(ac.yellow.. ' Adjust the position of the Paxton fob on the coil.' .. ac.reset) end end local function handle_cloning(decimal_id, padded_hex_id, blocks, was_option_3) while true do io.write(" Create Paxton choose " .. ac.cyan .. "1" .. ac.reset .. " or EM4102 choose " .. ac.cyan .. "2 " .. ac.reset) local choice = io.read() if choice == "1" then io.write(" Place the" .. ac.cyan .. " Paxton " .. ac.reset .. "Fob on the coil to write.." .. ac.green .. " ENTER " .. ac.reset .. "to continue..") io.read() print(dash) p:console("lf hitag wrbl --ht2 -p 4 -d " .. blocks[4] .. " -k BDF5E846") p:console("lf hitag wrbl --ht2 -p 5 -d " .. blocks[5] .. " -k BDF5E846") p:console("lf hitag wrbl --ht2 -p 6 -d " .. blocks[6] .. " -k BDF5E846") p:console("lf hitag wrbl --ht2 -p 7 -d " .. blocks[7] .. " -k BDF5E846") reset_log_file() --timer(5) verify_written_data(blocks) elseif choice == "2" then io.write(" Place the" .. ac.cyan .. " T5577 " .. ac.reset .. "tag on the coil and press" .. ac.green .. " ENTER " .. ac.reset .. "to continue..") io.read() p:console("lf em 410x clone --id " .. padded_hex_id) print(' Cloned EM4102 to T5577 with ID ' ..ac.green.. padded_hex_id ..ac.reset) else print(ac.yellow .. " Invalid choice." .. ac.reset .. " Please enter " .. ac.cyan .. "1" .. ac.reset .. " or " .. ac.cyan .. "2" .. ac.reset) goto ask_again end while true do print(dash) io.write(" Make next RFID Fob"..ac.cyan.." (y/n) "..ac.reset) local another = io.read() if another:lower() == "n" then if was_option_3 then print(" No writing to Paxton_log.txt - Name: " ..ac.green.. nam .. ac.reset.. " exist") return end print() print(ac.green .. " Saving Paxton_log file..." .. ac.reset) while true do io.write(" Enter a name for database (cannot be empty/duplicate): "..ac.yellow) name = io.read() io.write(ac.reset..'') if name == nil or name:match("^%s*$") then print(ac.red .. ' ERROR:'..ac.reset..' Name cannot be empty.') else if name_exists_in_log(name) then print(ac.yellow .. ' Name exists!!! '..ac.reset.. 'Please choose a different name.') else break end end end log_result(blocks, padded_hex_id, name) print(ac.green .. " Log saved successfully!" .. ac.reset) reset_log_file() return elseif another:lower() == "y" then goto ask_again else print(ac.yellow.." Invalid response."..ac.reset.." Please enter"..ac.cyan.." y"..ac.reset.." or"..ac.cyan.." n"..ac.reset) end end ::ask_again:: end end local function is_valid_hex(input) return #input == 8 and input:match("^[0-9A-Fa-f]+$") end local function main(args) while true do for o, a in getopt.getopt(args, 'h') do if o == 'h' then return help() end end command('clear') print(dash) print(ac.green .. ' Select option: ' .. ac.reset) print(ac.cyan .. ' 1' .. ac.reset .. ' - Read Paxton blocks 4-7 to make a copy') print(ac.cyan .. ' 2' .. ac.reset .. ' - Manually input data for Paxton blocks 4-7') print(ac.cyan .. " 3" .. ac.reset .. " - Search in Paxton_log by name and use the data") print(dash) while true do io.write(' Your choice '..ac.cyan..'(1/2/3): ' .. ac.reset) input_option = io.read() if input_option == "1" or input_option == "2" or input_option == "3" then break else print(ac.yellow .. ' Invalid choice.' .. ac.reset .. ' Please enter ' .. ac.cyan .. '1' .. ac.reset .. ' or ' .. ac.cyan .. '2' .. ac.reset..' or'..ac.cyan..' 3'..ac.reset) end end local was_option_3 = false if input_option == "1" then local show_place_message = true while true do if show_place_message then io.write(' Place the' .. ac.cyan .. ' Paxton' .. ac.reset .. ' Fob on the coil to read..' .. ac.green .. 'ENTER' .. ac.reset .. ' to continue..') end io.read() print(dash) p:console('lf hitag read --ht2 -k BDF5E846') if not logfile then error(" No files in this directory") end local result = read_log_file(logfile) local blocks = parse_blocks(result) local empty_block = false for i = 4, 7 do if not blocks[i] then empty_block = true break end end if empty_block then io.write(ac.yellow .. ' Adjust the Fob position on the coil.' .. ac.reset .. ' Press' .. ac.green .. ' ENTER' .. ac.reset .. ' to continue..') show_place_message = false else print(' Readed blocks:') print() for i = 4, 7 do if blocks[i] then print(string.format(" Block %d: %s%s%s", i, ac.yellow, blocks[i], ac.reset)) end end local decimal_id, padded_hex_id if blocks[5] and (blocks[5]:sub(4, 4) == 'F' or blocks[5]:sub(4, 4) == 'f') then print(dash) print(' Identified Paxton ' .. ac.cyan .. 'Net2' .. ac.reset) decimal_id, padded_hex_id = calculate_id_net({blocks[4], blocks[5]}) else print(dash) print(' Identified Paxton ' .. ac.cyan .. 'Switch2' .. ac.reset) decimal_id, padded_hex_id = calculate_id_switch({blocks[4], blocks[5], blocks[6], blocks[7]}) end print(string.format(" ID for EM4102 is: %s", ac.green .. padded_hex_id .. ac.reset)) print(dash) handle_cloning(decimal_id, padded_hex_id, blocks, was_option_3) break end end elseif input_option == "2" then local blocks = {} for i = 4, 7 do while true do io.write(ac.reset..' Enter data for block ' .. i .. ': ' .. ac.yellow) local input = io.read() input = input:upper() if is_valid_hex(input) then blocks[i] = input break else print(ac.yellow .. ' Invalid input.' .. ac.reset .. ' Each block must be 4 bytes (8 hex characters).') end end end local decimal_id, padded_hex_id if blocks[5] and (blocks[5]:sub(4, 4) == 'F' or blocks[5]:sub(4, 4) == 'f') then print(ac.reset.. dash) print(' Identified Paxton ' .. ac.cyan .. 'Net2' .. ac.reset) decimal_id, padded_hex_id = calculate_id_net({blocks[4], blocks[5]}) else print(ac.reset.. dash) print(' Identified Paxton ' .. ac.cyan .. 'Switch2' .. ac.reset) decimal_id, padded_hex_id = calculate_id_switch({blocks[4], blocks[5], blocks[6], blocks[7]}) end print(dash) print(string.format(" ID for EM4102 is: %s", ac.green .. padded_hex_id .. ac.reset)) print(dash) if not padded_hex_id then print(ac.red..' ERROR: '..ac.reset.. 'Invalid block data provided') return end handle_cloning(decimal_id, padded_hex_id, blocks, was_option_3) break elseif input_option == "3" then was_option_3 = true local retries = 3 while retries > 0 do io.write(' Enter the name to search ('..retries..' attempts) : '..ac.yellow) local user_input = io.read() io.write(ac.reset..'') if user_input == nil or user_input:match("^%s*$") then print(ac.yellow..' Error: '..ac.reset.. 'Empty name !!!') end local name_clean = "^Name:%s*" .. user_input:gsub("%s", "%%s") .. "%s*$" local file = io.open(log_file_path, "r") if not file then print(ac.red .. ' Error:'..ac.reset.. 'Could not open log file.') return end local lines = {} for line in file:lines() do table.insert(lines, line) end file:close() local found = false for i = 1, #lines do if lines[i]:match(name_clean) then nam = user_input local blocks = { [4] = lines[i + 2]:match("Block 4: (.+)"), [5] = lines[i + 3]:match("Block 5: (.+)"), [6] = lines[i + 4]:match("Block 6: (.+)"), [7] = lines[i + 5]:match("Block 7: (.+)") } local em4102_id = lines[i + 6]:match("EM4102 ID: (.+)") print(dash) print(' I found the data under the name: '..ac.yellow ..nam.. ac.reset) for j = 4, 7 do print(string.format(" Block %d: %s%s%s", j, ac.yellow, blocks[j] or "N/A", ac.reset)) end print(" EM4102 ID: " .. ac.green .. (em4102_id or "N/A") .. ac.reset) print(dash) local decimal_id, padded_hex_id = em4102_id, em4102_id handle_cloning(decimal_id, padded_hex_id, blocks, was_option_3, nam) found = true break end end if not found then retries = retries - 1 else break end end if retries == 0 then print(ac.yellow .. " Name not found after 3 attempts." .. ac.reset) end end print(dash) print(' Exiting script Lua...') return end end main(args)