99 lines
3.1 KiB
Python
99 lines
3.1 KiB
Python
"""ISO 14443 Type B (SRIX / ST25TB lineage) request/response decoders + CRC_B."""
|
|
from __future__ import annotations
|
|
|
|
# ST25TB / SRIX command opcodes
|
|
_14B_CMDS = {
|
|
0x08: "READ_BLOCK",
|
|
0x09: "WRITE_BLOCK",
|
|
0x0B: "GET_UID",
|
|
0x0C: "RESET_TO_INVENTORY",
|
|
0x0E: "SELECT",
|
|
0x0F: "COMPLETION",
|
|
}
|
|
|
|
|
|
def decode_14443b(direction: int, payload: bytes) -> str | None:
|
|
"""Decode an ISO 14443 Type B (ST25TB/SRIX) frame."""
|
|
if not payload:
|
|
return None
|
|
if direction == 0:
|
|
return _decode_request(payload)
|
|
return _decode_response(payload)
|
|
|
|
|
|
def _decode_request(payload: bytes) -> str | None:
|
|
b0 = payload[0]
|
|
|
|
# SRIX / ST25TB anticollision (opcode 0x06)
|
|
if b0 == 0x06 and len(payload) >= 2:
|
|
if payload[1] == 0x00:
|
|
return "INITIATE"
|
|
if payload[1] == 0x04:
|
|
return "PCALL16"
|
|
return f"ANTICOL 0x{payload[1]:02X}"
|
|
|
|
# SRIX slot marker: low nibble = 6, high nibble = slot 1..15
|
|
if (b0 & 0x0F) == 0x06 and (b0 >> 4) != 0:
|
|
return f"SLOT_MARKER {b0 >> 4}"
|
|
|
|
# Standard ISO 14443-3 Type B (REQB/WUPB/ATTRIB/HLTB)
|
|
if b0 == 0x05 and len(payload) >= 3:
|
|
return "WUPB" if (payload[2] & 0x08) else "REQB"
|
|
if (b0 & 0x0F) == 0x05 and (b0 >> 4) != 0:
|
|
return f"SLOT-MARKER {b0 >> 4}"
|
|
if b0 == 0x1D:
|
|
return "ATTRIB"
|
|
if b0 == 0x50 and len(payload) >= 5:
|
|
return "HLTB"
|
|
|
|
# SRIX / ST25TB memory + select opcodes (must precede the I-block fallback,
|
|
# since they share the PCB=00 bit pattern).
|
|
name = _14B_CMDS.get(b0)
|
|
if name is not None:
|
|
if b0 == 0x08 and len(payload) >= 2:
|
|
return f"READ_BLOCK #{payload[1]}"
|
|
if b0 == 0x09 and len(payload) >= 2:
|
|
return f"WRITE_BLOCK #{payload[1]}"
|
|
if b0 == 0x0E and len(payload) >= 2:
|
|
return f"SELECT chip=0x{payload[1]:02X}"
|
|
return name
|
|
|
|
# ISO-DEP I-block (PCB) — fallback for standard Type B Layer 4.
|
|
if b0 & 0xC0 == 0x00 and len(payload) > 1:
|
|
return f"I-BLOCK({b0 & 0x01})"
|
|
return None
|
|
|
|
|
|
def _decode_response(payload: bytes) -> str | None:
|
|
# ATQB: 50 | PUPI(4) | AppData(4) | ProtInfo(3) = 12 bytes
|
|
if len(payload) == 12 and payload[0] == 0x50:
|
|
return f"ATQB PUPI={payload[1:5].hex().upper()}"
|
|
if len(payload) == 1:
|
|
return f"CHIP_ID 0x{payload[0]:02X}"
|
|
if len(payload) == 4:
|
|
return f"BLOCK {payload.hex().upper()}"
|
|
if len(payload) == 8:
|
|
return f"UID {bytes(reversed(payload)).hex().upper()}"
|
|
# ISO-DEP I-block response
|
|
if payload[0] & 0xC0 == 0x00 and len(payload) > 1:
|
|
return f"I-BLOCK({payload[0] & 0x01})"
|
|
return None
|
|
|
|
|
|
def crc14b(data: bytes) -> bytes:
|
|
"""ISO/IEC 14443-3 Type B CRC (CRC_B), transmitted LSByte first.
|
|
|
|
Polynomial x^16 + x^12 + x^5 + 1, init 0xFFFF, result one's-complemented,
|
|
reflected. Matches the reference routine in the ST25TB datasheet appendix.
|
|
"""
|
|
crc = 0xFFFF
|
|
for b in data:
|
|
crc ^= b
|
|
for _ in range(8):
|
|
if crc & 0x0001:
|
|
crc = (crc >> 1) ^ 0x8408
|
|
else:
|
|
crc >>= 1
|
|
crc = (~crc) & 0xFFFF
|
|
return bytes([crc & 0xFF, (crc >> 8) & 0xFF])
|