diff --git a/pm3py/cli/rawcli/app.py b/pm3py/cli/rawcli/app.py index fe39993..ac78ce3 100644 --- a/pm3py/cli/rawcli/app.py +++ b/pm3py/cli/rawcli/app.py @@ -24,10 +24,10 @@ rawcli — raw command interface transponder show the identified transponder close drop the tag connection quit | exit | q leave rawcli - send raw bytes to the tag; the annotated exchange prints above + send raw bytes to the tag (CRC auto-appended on 14a); trace prints above Type hex as "30 04" or "3004"; Ctrl-/ toggles hex/binary entry (0x/0b). Function-style -transponder commands (READ(4), GET_DATA) land in the next phase.""" +commands (READ(4), GET_VERSION()) come from the identified tag — listed below.""" def _connect(port): @@ -43,6 +43,33 @@ def _connect(port): ISO14A_APPEND_CRC = 0x20 +def _needs_crc(payload: bytes) -> bool: + """Whether a 14a frame should get a CRC appended — everything except the frames ISO14443-A + sends without one: the 7-bit short frames REQA (0x26) / WUPA (0x52) and the ANTICOLLISION + frames (0x9x 0x20, which carry a BCC, not a CRC).""" + if not payload: + return False + b0 = payload[0] + if len(payload) == 1 and b0 in (0x26, 0x52): + return False + if len(payload) >= 2 and b0 in (0x93, 0x95, 0x97) and payload[1] == 0x20: + return False + return True + + +def _ensure_selected(device, protocol: str) -> None: + """Re-select the 14a tag before a command so a stale/dropped selection (or a prior bad frame) + can't leave the tag unresponsive. Silent — the re-select isn't traced. Note: this resets any + prior auth, so a PWD_AUTH must be issued together with the command it protects (a later + keep-session mode will address multi-step auth).""" + if device is None or protocol != "hf14a": + return + try: + device.hf.iso14a.scan() + except Exception: + pass + + def _raw_exchange(device, payload: bytes, protocol: str = "hf14a", crc: bool = False) -> bytes | None: """Issue one raw exchange over the identified protocol, returning the response bytes. ``crc`` appends the ISO14443-A CRC (catalog commands need it; manual raw hex is sent verbatim).""" @@ -100,7 +127,9 @@ def dispatch(state: RawSession, line: str, out=print) -> bool: if not cmd.payload: return True - response = _raw_exchange(state.device, cmd.payload, protocol=state.protocol) + _ensure_selected(state.device, state.protocol) + response = _raw_exchange(state.device, cmd.payload, protocol=state.protocol, + crc=state.protocol == "hf14a") # 14a frames need CRC; bare `60` works out(render_exchange(cmd.payload, response, protocol=state.protocol, is_tty=True)) return True @@ -121,6 +150,7 @@ def _handle_call(state: RawSession, cmd, out=print) -> None: return frames = built if isinstance(built, list) else [built] # multi-frame (e.g. two-phase write) crc = catalog.protocol == "hf14a" # 14a commands need CRC to be accepted + _ensure_selected(state.device, catalog.protocol) # re-select once for the command for frame in frames: response = _raw_exchange(state.device, frame, protocol=catalog.protocol, crc=crc) out(render_exchange(frame, response, protocol=catalog.protocol, is_tty=True)) diff --git a/pm3py/cli/rawcli/identify.py b/pm3py/cli/rawcli/identify.py index 6361151..379f5a4 100644 --- a/pm3py/cli/rawcli/identify.py +++ b/pm3py/cli/rawcli/identify.py @@ -186,8 +186,7 @@ def _probe_version(dev, fmt, emit) -> tuple[str | None, bytes | None]: if not raw: return None, None version = bytes(raw[:8]) - display = bytes(raw).rstrip(b"\x00") or version # drop the firmware buffer padding - emit(fmt.format(1, display).strip("\n")) + emit(fmt.format(1, bytes(raw)).strip("\n")) # raw() already trims to the received length model = decode_version(version) if model: emit(f" → {model}")