diff --git a/client/luascripts/Paxton_clone.lua b/client/luascripts/Paxton_clone.lua new file mode 100644 index 000000000..7c62671fc --- /dev/null +++ b/client/luascripts/Paxton_clone.lua @@ -0,0 +1,427 @@ +-------------------------------------------------------------------- +-- Author - jareckib - 30.01.2025 +-- Based on Equipter's tutorial - Downgrade Paxton to EM4102 +-- version v 1.17 +--------------------------------------------------------------------- +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 command = core.console + +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 handle_cloning(decimal_id, padded_hex_id, blocks, was_option_3) + while true do + print(" Create Paxton choose " .. ac.cyan .. "1" .. ac.reset .. " or EM4102 choose " .. ac.cyan .. "2" .. ac.reset) + print(dash) + io.write(" Your choice "..ac.cyan.."(1/2): "..ac.reset) + local choice = io.read() + if choice == "1" then + print(dash) + print(" Place the" .. ac.cyan .. " Paxton " .. ac.reset .. "Fob on the coil to write.." .. ac.green .. " ENTER " .. ac.reset .. "to continue..") + io.read() + print(dash) + command("lf hitag wrbl --ht2 -p 4 -d " .. blocks[4] .. " -k BDF5E846") + command("lf hitag wrbl --ht2 -p 5 -d " .. blocks[5] .. " -k BDF5E846") + command("lf hitag wrbl --ht2 -p 6 -d " .. blocks[6] .. " -k BDF5E846") + command("lf hitag wrbl --ht2 -p 7 -d " .. blocks[7] .. " -k BDF5E846") + elseif choice == "2" then + print(dash) + print(" Place the" .. ac.cyan .. " T5577 " .. ac.reset .. "tag on the coil and press" .. ac.green .. " ENTER " .. ac.reset .. "to continue..") + io.read() + print(dash) + command("lf em 410x clone --id " .. padded_hex_id) + 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.." > "..ac.yellow) + local another = io.read() + io.write(ac.reset..'') + 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) + local file = io.open(logfile, "w+") + file:write("") + file:close() + 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() + while true do + command('clear') + print() + print(dash) + local input_option + 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 + print(' Place the' .. ac.cyan .. ' Paxton' .. ac.reset .. ' Fob on the coil to read..' .. ac.green .. 'ENTER' .. ac.reset .. ' to continue..') + print(dash) + end + io.read() + command('lf hitag read --ht2 -k BDF5E846') + command('clear') + 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 + print(dash) + print(ac.yellow .. ' Adjust the Fob position on the coil.' .. ac.reset .. ' Press' .. ac.green .. ' ENTER' .. ac.reset .. ' to continue..') + print(dash) + show_place_message = false + else + print(dash) + 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(dash) + 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() \ No newline at end of file