Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
263 changes: 136 additions & 127 deletions script/pn532_cli_unit.py
Original file line number Diff line number Diff line change
Expand Up @@ -1310,132 +1310,6 @@ def get_block0(self, uid, args):
return
return str_to_bytes(block0)

@hf_mf.command("eSetUid")
class HfMfESetUid(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set UID of Mifare 1K emulator"
parser.add_argument(
"-s", "--slot", default=1, type=int, help="Emulator slot(1-8)"
)
parser.add_argument(
"-u",
type=str,
metavar="<hex>",
required=False,
help="UID to set (4 or 7 bytes)",
)
return parser

def on_exec(self, args: argparse.Namespace):
if args.u is None:
print("usage: hf mf eSetUid [-h] -u <hex>")
print("hf mf eSetUid: error: the following arguments are required: -u")
return
uid = bytes.fromhex(args.u)
if len(uid) not in [4, 7]:
print("UID length must be 4 or 7 bytes")
return
self.cmd.hf_mf_esetuid(args.slot - 1, uid)
print(f"Set Slot {args.slot} UID to {args.u} {CY}Success{C0}")

@hf_mf.command("eload")
class HfMfEload(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.description = "Load Mifare Dump to PN532Killer Slot"
parser.add_argument(
"-s", "--slot", default=1, type=int, help="Emulator slot(1-8)"
)
parser.add_argument(
"--bin",
type=str,
required=False,
help="MF 1k bin dump file",
)
parser.add_argument(
"--json",
type=str,
required=False,
help="MF 1k json dump file",
)
return parser

def on_exec(self, args: argparse.Namespace):
if not args.bin and not args.json:
print("Please choose either bin file or json file")
return
dump_map = {}
if args.bin:
# read bytes from bin, each block 16 bytes, map like "0":"11223344556677889900AABBCCDDEEFF"
with open(args.bin, "rb") as bin_file:
block_index = 0
while True:
block = bin_file.read(16)
if not block:
break
dump_map[str(block_index)] = block.hex().upper()
block_index += 1
elif args.json:
with open(args.json, "r") as json_file:
file_dump = json.load(json_file)
if "blocks" in file_dump:
dump_map = file_dump["blocks"]

# if dump_map key count is not 64, return
if len(dump_map) != 64:
print("Invalid dump file")
return
for block_index, block_data in dump_map.items():
if not is_hex(block_data, 32):
print(f"Invalid block {block_index}")
return
self.cmd.hf_mf_load(dump_map, args.slot)


@hf_mf.command("eread")
class HfMfEread(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get Mifare Classic dump from PN532Killer Slot"
parser.add_argument(
"-s", "--slot", default=1, type=int, help="Emulator slot(1-8)"
)
parser.add_argument("--file", action="store_true", help="Save to json file")
parser.add_argument("--bin", action="store_true", help="Save to bin file")
return parser

def on_exec(self, args: argparse.Namespace):
self.device_com.set_work_mode(2, 0x01, args.slot - 1)
dump_map = self.cmd.hf_mf_eread(args.slot)
# {"0": "11223344556677889900AABBCCDDEEFF", "1": "11223344556677889900AABBCCDDEEFF", ...}
if not dump_map:
print("Get dump failed")
return
file_name = "mf_dump_{args.slot}"
file_index = 0
if args.file:
while True:
if os.path.exists(f"{file_name}_{file_index}.json"):
file_index += 1
else:
file_name = f"{file_name}_{file_index}.json"
break
with open(file_name, "w") as json_file:
json.dump({"blocks": dump_map}, json_file)
if args.bin:
while True:
if os.path.exists(f"{file_name}_{file_index}.bin"):
file_index += 1
else:
file_name = f"{file_name}_{file_index}.bin"
break
with open(file_name, "wb") as bin_file:
for block_index, block_data in dump_map.items():
bin_file.write(bytes.fromhex(block_data))


@hf_mf.command("setuid")
class HfMfSetUid(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
Expand Down Expand Up @@ -1981,6 +1855,131 @@ def on_exec(self, args: argparse.Namespace):
else:
print(f"{CR}Not MiFare Classic{C0}")

@hf_mf.command("eSetUid")
class HfMfESetUid(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Set 4 bytes or 7 bytes UID of PN532Killer Mifare 1K emulator"
parser.add_argument(
"-s", "--slot", default=1, type=int, help="Emulator slot(1-8)"
)
parser.add_argument(
"-u",
type=str,
metavar="<hex>",
required=False,
help="UID to set (4 or 7 bytes)",
)
return parser

def on_exec(self, args: argparse.Namespace):
if args.u is None:
print("usage: hf mf eSetUid [-h] -u <hex>")
print("hf mf eSetUid: error: the following arguments are required: -u")
return
uid = bytes.fromhex(args.u)
if len(uid) not in [4, 7]:
print("UID length must be 4 or 7 bytes")
return
self.cmd.hf_mf_esetuid(args.slot - 1, uid)
print(f"Set Slot {args.slot} UID to {args.u} {CY}Success{C0}")

@hf_mf.command("eLoad")
class HfMfEload(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.formatter_class = argparse.RawDescriptionHelpFormatter
parser.description = "Load Mifare Classic Dump to PN532Killer Slot"
parser.add_argument(
"-s", "--slot", default=1, type=int, help="Emulator slot(1-8)"
)
parser.add_argument(
"--bin",
type=str,
required=False,
help="MF 1k bin dump file",
)
parser.add_argument(
"--json",
type=str,
required=False,
help="MF 1k json dump file",
)
return parser

def on_exec(self, args: argparse.Namespace):
if not args.bin and not args.json:
print("Please choose either bin file or json file")
return
dump_map = {}
if args.bin:
# read bytes from bin, each block 16 bytes, map like "0":"11223344556677889900AABBCCDDEEFF"
with open(args.bin, "rb") as bin_file:
block_index = 0
while True:
block = bin_file.read(16)
if not block:
break
dump_map[str(block_index)] = block.hex().upper()
block_index += 1
elif args.json:
with open(args.json, "r") as json_file:
file_dump = json.load(json_file)
if "blocks" in file_dump:
dump_map = file_dump["blocks"]

# if dump_map key count is not 64, return
if len(dump_map) != 64:
print("Invalid dump file")
return
for block_index, block_data in dump_map.items():
if not is_hex(block_data, 32):
print(f"Invalid block {block_index}")
return
self.cmd.hf_mf_load(dump_map, args.slot)


@hf_mf.command("eRead")
class HfMfEread(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Get Mifare Classic dump from PN532Killer Slot"
parser.add_argument(
"-s", "--slot", default=1, type=int, help="Emulator slot(1-8)"
)
parser.add_argument("--file", action="store_true", help="Save to json file")
parser.add_argument("--bin", action="store_true", help="Save to bin file")
return parser

def on_exec(self, args: argparse.Namespace):
self.device_com.set_work_mode(2, 0x01, args.slot - 1)
dump_map = self.cmd.hf_mf_eread(args.slot)
# {"0": "11223344556677889900AABBCCDDEEFF", "1": "11223344556677889900AABBCCDDEEFF", ...}
if not dump_map:
print("Get dump failed")
return
file_name = "mf_dump_{args.slot}"
file_index = 0
if args.file:
while True:
if os.path.exists(f"{file_name}_{file_index}.json"):
file_index += 1
else:
file_name = f"{file_name}_{file_index}.json"
break
with open(file_name, "w") as json_file:
json.dump({"blocks": dump_map}, json_file)
if args.bin:
while True:
if os.path.exists(f"{file_name}_{file_index}.bin"):
file_index += 1
else:
file_name = f"{file_name}_{file_index}.bin"
break
with open(file_name, "wb") as bin_file:
for block_index, block_data in dump_map.items():
bin_file.write(bytes.fromhex(block_data))

@hf_mfu.command("rdbl")
class HfMfRdbl(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
Expand Down Expand Up @@ -2254,7 +2253,7 @@ def on_exec(self, args: argparse.Namespace):
print("LF Tag no found")


@lf_em_410x.command("esetid")
@lf_em_410x.command("eSetid")
class LfEm410xESetId(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
Expand Down Expand Up @@ -2310,3 +2309,13 @@ def args_parser(self) -> ArgumentParserNoExit:
def on_exec(self, args: argparse.Namespace):
self.device_com.set_normal_mode()
self.cmd.ntag_emulator(url=args.uri)

@ntag.command("reader")
class NtagReader(DeviceRequiredUnit):
def args_parser(self) -> ArgumentParserNoExit:
parser = ArgumentParserNoExit()
parser.description = "Read NTAG data and open URI in browser"
return parser

def on_exec(self, args: argparse.Namespace):
self.cmd.ntag_reader()
56 changes: 56 additions & 0 deletions script/pn532_cmd.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pn532_enum import MfcKeyType, MfcValueBlockOperator
from time import sleep
from pn532_utils import CC, CB, CG, C0, CY, CR
from pn532_utils import NdefParser
import os
import subprocess
import ndef
Expand Down Expand Up @@ -1108,6 +1109,61 @@ def ntag_emulator(self, url: str):
self.device.set_normal_mode()
return resp

def ntag_reader(self):
import webbrowser
"""
Read NTAG and parse NDEF data. If the data contains a URI (e.g., link, tel, mailto), open it in the browser.
"""
try:
self.device.set_normal_mode()
self.stop_flag = False
input_thread = threading.Thread(target=self.wait_for_enter)
input_thread.start()

while not self.stop_flag:
resp = self.hf_14a_scan()
if resp is None:
print("No tag found. Waiting for tag...")
sleep(0.5)
continue

dump_bin_data = bytearray()
max_block = 4
block = 0
while block < max_block:
resp = self.mf0_read_one_block(block)
if block == 0 and resp and resp.parsed and len(resp.parsed) == 16:
max_block = resp.parsed[14] * 2 + 9
if resp and resp.parsed and len(resp.parsed) == 16:
dump_bin_data.extend(resp.parsed)
else:
print(f"Error reading block {block}: {resp}")
break
block += 4
print("NTAG Dump:")
for i in range(0, len(dump_bin_data), 16):
print(f"{CG}{' '.join(f'{byte:02X}' for byte in dump_bin_data[i:i + 16])} {''.join(chr(byte) if 32 <= byte <= 126 else '.' for byte in dump_bin_data[i:i + 16])}{C0}")
ndef_parser = NdefParser(dump_bin_data)
urls = ndef_parser.get_urls()
if urls:
print("NDEF URLs:")
for url in urls:
print(f"{CG}{url}{C0}")

for url in urls:
if "http" in url or "tel" in url or "mailto" in url:
webbrowser.open(url)

print("Remove card and place another, or press Enter to stop...")
sleep(1)

print("Stopped NTAG reading.")
return []
except Exception as e:
print("Error reading NTAG:", e)
self.stop_flag = True
return []

stop_flag = False
def wait_for_enter(self):
print("Press Enter to stop...")
Expand Down
1 change: 1 addition & 0 deletions script/pn532_enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class Pn532KillerCommand(enum.IntEnum):
"HfMfDump",
"HfMfWipe",
"NtagEmulate",
"NtagReader",
"HfMfuRdbl",
"HfMfuWrbl",
"HfMfuDump",
Expand Down
Loading