diff --git a/pm3py/core/protocol.py b/pm3py/core/protocol.py index 9c0f2f4..5c335ee 100644 --- a/pm3py/core/protocol.py +++ b/pm3py/core/protocol.py @@ -222,6 +222,7 @@ class Cmd(IntEnum): HF_SNIFF_STREAM = 0x0337 HF_SNIFF_STATUS = 0x0338 + HF_ISO14443A_SIM_TRACE = 0x0339 # FW->Host: live 14a sim trace (fire-and-forget) # Sim table commands (firmware response table) SIM_TABLE_UPLOAD = 0x0900 diff --git a/pm3py/sim/sim_session.py b/pm3py/sim/sim_session.py index c200471..643e5bd 100644 --- a/pm3py/sim/sim_session.py +++ b/pm3py/sim/sim_session.py @@ -11,6 +11,7 @@ import serial from .frame import RFFrame from .table_compiler import ResponseTable, TableCompiler from pm3py.trace.format import TraceFormatter +from pm3py.trace.decode_iso14a import decode_14443a from .transponder import Transponder from ..core.protocol import Cmd from ..core.transport import encode_ng_frame, decode_response_frame, RESP_PREAMBLE_MAGIC, RESP_PREAMBLE_SIZE, RESP_POSTAMBLE_SIZE @@ -20,6 +21,20 @@ CMD_SIM_TABLE_UPLOAD = 0x0950 CMD_SIM_TABLE_CLEAR = 0x0951 CMD_SIM_TABLE_UPDATE = 0x0952 CMD_HF_ISO15693_SIM_TRACE = 0x0336 +CMD_HF_ISO14443A_SIM_TRACE = 0x0339 + +# 14a sim flag bit (pm3_cmd.h FLAG_SIM_TRACE): stream live reader<->tag trace. +FLAG_SIM_TRACE = 0x2000 + +# Sim-trace command id -> protocol tag used in trace entries (0 = ISO 15693, +# 1 = ISO 14443-A). The firmware ring format is identical across protocols: +# byte 0 = dir (low nibble) | crc_fail (0x80) | 0x03 ADC report (15693 only), +# followed by the raw frame bytes. The active protocol's decoder lives in the +# session formatter, so this only needs the protocol tag. +_TRACE_CMDS = { + CMD_HF_ISO15693_SIM_TRACE: 0, + CMD_HF_ISO14443A_SIM_TRACE: 1, +} # Max NG payload per frame MAX_PAYLOAD = 512 @@ -98,33 +113,94 @@ class SimSession: """Clear the firmware response table.""" await self._t.send_ng(CMD_SIM_TABLE_CLEAR, b"") - async def start_14a(self, tag, compile: bool = True) -> None: - """Compile table, upload, start 14443-A sim with WTX relay loop.""" - self._tag_model = tag - if compile: - if hasattr(tag, '_keys_a'): - table = TableCompiler.compile_mifare(tag) - else: - table = TableCompiler.compile_14a(tag) - await self.upload_table(table) + def start_14a(self, tag, tagtype: int = 7, trace: bool = False, + on_frame=None, quiet: bool = False, exit_after: int = 0) -> None: + """Start an ISO 14443-A (Layer 3) sim, optionally streaming a live trace. - # Start sim (fire-and-forget — response comes when sim ends) + Synchronous, mirroring :meth:`start_15693`: the firmware runs + anticollision and the tagtype's built-in command set autonomously; this + sends the sim-start frame over raw serial and, when tracing, spawns the + reader thread that decodes each reader↔tag frame. + + Args: + tagtype: firmware emulation type (see ``hf.iso14a.sim``; 7 = MFU + EV1/NTAG215, 1 = MIFARE Classic 1k, 2 = Ultralight, ...). + trace: if True, print live reader↔tag communication to the console. + on_frame: optional callback(frame: dict) invoked per decoded frame + (see the ``entries`` property for the dict shape). Passing it + auto-enables trace streaming. + quiet: if True, don't print frames (callback + ``entries`` still fill). + exit_after: stop the sim after N reader reads (0 = run until stopped). + + Note: this starts anticollision + protocol handling from the tagtype's + firmware model. Loading the tag's memory contents into emulator RAM (so + reads return model data) is a separate step and is not performed here; + the trace still shows every reader↔tag frame regardless. + + 14a-4 (ISO-DEP / WTX relay) reuses the same firmware trace but routes it + through the async relay loop — see the plan's I/O model decision. + """ + self._tag_model = tag + self._on_frame = on_frame + self._quiet = quiet + self._print_frames = trace and not quiet + stream = trace or (on_frame is not None) + + # Resolve the raw serial port + if self._port: + ser = self._port + elif self._t and self._t._writer: + ser = self._t._writer.transport.serial + else: + raise RuntimeError("No serial port available") + + tag._serial = ser + + # Fold the UID length into flags per FLAG_SET_UID_IN_DATA (pm3_cmd.h). uid = tag._uid - flag_val = 0x0001 # FLAG_INTERACTIVE if len(uid) == 4: - flag_val |= 0x0010 + uid_flag = 0x0010 # FLAG_4B_UID_IN_DATA elif len(uid) == 7: - flag_val |= 0x0020 + uid_flag = 0x0020 # FLAG_7B_UID_IN_DATA elif len(uid) == 10: - flag_val |= 0x0030 - atqa = tag.atqa if hasattr(tag, 'atqa') else b"\x04\x00" - sak = tag.sak if hasattr(tag, 'sak') else 0x08 - payload = atqa + bytes([sak]) + uid - await self._t.send_ng_no_response(Cmd.HF_ISO14443A_SIMULATE) - # TODO: use send_mix for 14a sim (MIX frame format) + uid_flag = 0x0030 # FLAG_10B_UID_IN_DATA + else: + raise ValueError("UID must be 4, 7, or 10 bytes") + flag_val = uid_flag + if stream: + flag_val |= FLAG_SIM_TRACE + + # HF_ISO14443A_SIMULATE NG payload — same layout as hf.iso14a.sim(): + # struct { u8 tagtype; u16 flags; u8 uid[10]; u8 exitAfter; + # u8 rats[20]; u8 ulauth_1a1_len; u8 ulauth_1a2_len; + # u8 ulauth_1a1[16]; u8 ulauth_1a2[16]; bool mirror; } PACKED + payload = struct.pack( + " None: @@ -281,12 +357,13 @@ class SimSession: resp = decode_response_frame(bytes(buf[:frame_len])) buf = buf[frame_len:] - if resp["cmd"] == CMD_HF_ISO15693_SIM_TRACE: + if resp["cmd"] in _TRACE_CMDS: + protocol = _TRACE_CMDS[resp["cmd"]] data = resp["data"] if len(data) >= 2: direction = data[0] & 0x0F - if direction == 0x03 and len(data) >= 3: - # ADC field strength report + if protocol == 0 and direction == 0x03 and len(data) >= 3: + # ADC field strength report (ISO 15693 only) adc_mv = (data[1] << 8) | data[2] if self.on_field_strength: self.on_field_strength(adc_mv) @@ -294,7 +371,7 @@ class SimSession: crc_fail = bool(data[0] & 0x80) payload = data[1:] entry = { - "protocol": 0, # ISO 15693 + "protocol": protocol, "direction": direction, "timestamp": None, "duration": None, diff --git a/tests/test_sim_trace_14a.py b/tests/test_sim_trace_14a.py new file mode 100644 index 0000000..f754be2 --- /dev/null +++ b/tests/test_sim_trace_14a.py @@ -0,0 +1,117 @@ +"""ISO 14443-A sim live trace: firmware CMD_HF_ISO14443A_SIM_TRACE -> entries. + +Mirrors the SimSession portion of test_trace_scripting.py but for the 14a sim +trace path (protocol tag 1, decode_14443a). Uses synthetic response frames fed +through SimSession._trace_reader with a mocked serial port — no hardware. +""" +import struct +from unittest.mock import MagicMock + +from pm3py.core.protocol import RESP_PREAMBLE_MAGIC +from pm3py.core.transport import RESP_POSTAMBLE_NOCRC +from pm3py.sim.sim_session import SimSession, CMD_HF_ISO14443A_SIM_TRACE +from pm3py.trace.decode_iso14a import decode_14443a +from pm3py.trace.format import TraceFormatter + +REQA = bytes.fromhex("26") # reader -> tag, short frame +ATQA = bytes.fromhex("0400") # tag -> reader, no CRC + + +def _fmt(): + return TraceFormatter(mode="sim", decoder=decode_14443a, crc_len=2, is_tty=False) + + +def _trace_frame(direction, payload): + """Build one CMD_HF_ISO14443A_SIM_TRACE response frame (USB, no CRC).""" + data = bytes([direction]) + payload + header = struct.pack("