Add St25dvRfAreaSS (Register) for the ST25DV per-area RF security registers (RFA1SS..RFA4SS): pwd_ctrl (which RF password gates the area) and rw_protection (read/write protection level). Bound via tag.rf_area_ss(area) / tag.set_rf_area_ss(area, value). Modelled from the model's own tested protection logic (single source of truth) and cross-checked against _read_allowed / _write_allowed. 3 new tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
415 lines
16 KiB
Python
415 lines
16 KiB
Python
"""ST25DV04K / ST25DV16K / ST25DV64K — ST dynamic NFC/RFID tags (RF side).
|
|
|
|
Sourced from ST datasheet DS10925 Rev 11. ISO/IEC 15693 + I2C dynamic tag,
|
|
NFC Forum Type 5. This models the RF interface: standard + extended 15693 block
|
|
commands, the ST custom command set (config registers, dynamic registers,
|
|
64-bit password sessions, mailbox / fast-transfer mode), and up to four
|
|
password-protectable memory areas (ENDA1-3 / RFA1-4SS).
|
|
|
|
The I2C side is not simulated; dynamic registers that reflect I2C state
|
|
(FIELD_ON, VCC_ON, etc.) are modelled only as far as RF reads need them.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import struct
|
|
|
|
from pm3py.sim.frame import RFFrame
|
|
from pm3py.trace.decode_iso15 import decode_15693_st
|
|
from ..type5 import NfcType5Tag
|
|
from ..base import FLAG_ADDRESS, FLAG_INVENTORY
|
|
from pm3py.transponders.bitfield import BitField, Register
|
|
|
|
ST_MFG = 0x02
|
|
|
|
# RF_Ai_SS read/write protection level (bits 3:2). Labels describe areas 2-4; on Area 1 read
|
|
# stays open except at level 3 (an ST25DV quirk — see St25dvRfAreaSS).
|
|
_ST25DV_RW = {
|
|
0: "open (read+write)",
|
|
1: "read open / write needs password",
|
|
2: "read+write need password",
|
|
3: "read needs password / write locked",
|
|
}
|
|
_ST25DV_PWD = {0: "none", 1: "RF_PWD_1", 2: "RF_PWD_2", 3: "RF_PWD_3"}
|
|
|
|
|
|
class St25dvRfAreaSS(Register):
|
|
"""ST25DV RF_Ai_SS — one of the four RF-area security registers (RFA1SS..RFA4SS). Selects
|
|
which RF password gates the area and its read/write protection level. On **Area 1**, read
|
|
stays open unless the level is 3; on Areas 2-4 level >= 2 protects read too."""
|
|
|
|
_width_bits = 8
|
|
_label = "ST25DV RF_Ai_SS"
|
|
|
|
pwd_ctrl = BitField(1, 0, values=_ST25DV_PWD, doc="which RF password protects this area")
|
|
rw_protection = BitField(3, 2, values=_ST25DV_RW, doc="RF read/write protection level")
|
|
|
|
# Custom command codes (subset modelled)
|
|
CMD_READ_CFG = 0xA0
|
|
CMD_WRITE_CFG = 0xA1
|
|
CMD_MANAGE_GPO = 0xA9
|
|
CMD_WRITE_MSG = 0xAA
|
|
CMD_READ_MSG_LEN = 0xAB
|
|
CMD_READ_MSG = 0xAC
|
|
CMD_READ_DYN_CFG = 0xAD
|
|
CMD_WRITE_DYN_CFG = 0xAE
|
|
CMD_WRITE_PASSWORD = 0xB1
|
|
CMD_PRESENT_PASSWORD = 0xB3
|
|
CMD_FAST_READ_SINGLE = 0xC0
|
|
CMD_EXT_GET_SYSINFO = 0x3B
|
|
|
|
# Static config register pointers
|
|
REG_RFA1SS = 0x04
|
|
REG_ENDA1 = 0x05
|
|
REG_RFA2SS = 0x06
|
|
REG_ENDA2 = 0x07
|
|
REG_RFA3SS = 0x08
|
|
REG_ENDA3 = 0x09
|
|
REG_RFA4SS = 0x0A
|
|
REG_MB_MODE = 0x0D
|
|
REG_LOCK_CFG = 0x0F
|
|
|
|
# Dynamic register pointers
|
|
DYN_GPO_CTRL = 0x00
|
|
DYN_EH_CTRL = 0x02
|
|
DYN_MB_CTRL = 0x0D
|
|
DYN_MB_LEN = 0x0E
|
|
|
|
|
|
class ST25DV(NfcType5Tag):
|
|
"""Base for ST25DVxxK. Concrete ICs set block count / product code / IC ref."""
|
|
|
|
_uid_prefix = b"\xE0\x02" # product code appended per variant
|
|
_product_code: int = 0x24
|
|
_ic_ref: int = 0x24
|
|
_has_memsize_in_sysinfo: bool = True # only 04K returns memsize in 0x2B
|
|
|
|
def __init__(self, uid: bytes | str | None = None, ndef_message: bytes = b"",
|
|
passwords: dict[int, bytes] | None = None):
|
|
super().__init__(uid=uid, ndef_message=ndef_message,
|
|
block_size=4, num_blocks=self._num_blocks_cls,
|
|
ic_reference=self._ic_ref)
|
|
self._last_block = self._num_blocks - 1
|
|
# 64-bit passwords: 0 = config, 1-3 = user areas.
|
|
self._passwords = passwords or {i: bytes(8) for i in range(4)}
|
|
self._sessions: set[int] = set()
|
|
# Static config registers (pointer → value). Factory: single area.
|
|
endmax = self._last_block // 8
|
|
self._cfg = {
|
|
REG_RFA1SS: 0x00, REG_ENDA1: endmax,
|
|
REG_RFA2SS: 0x00, REG_ENDA2: endmax,
|
|
REG_RFA3SS: 0x00, REG_ENDA3: endmax,
|
|
REG_RFA4SS: 0x00, REG_MB_MODE: 0x00, REG_LOCK_CFG: 0x00,
|
|
}
|
|
# Dynamic registers
|
|
self._dyn = {DYN_GPO_CTRL: 0x88, DYN_EH_CTRL: 0x04, DYN_MB_CTRL: 0x00}
|
|
self._mailbox = bytearray(256)
|
|
self._mb_len = 0
|
|
|
|
def _write_cc(self) -> None:
|
|
"""Type 5 CC — use the 8-byte extended form when size/8 > 255."""
|
|
data_bytes = (self._num_blocks - 1) * self._block_size
|
|
mlen = data_bytes // 8
|
|
if mlen <= 0xFF:
|
|
self._memory[0:4] = bytes([0xE1, 0x40, mlen, 0x01])
|
|
else:
|
|
self._memory[0:8] = (bytes([0xE1, 0x40, 0x00, 0x01, 0x00, 0x00])
|
|
+ struct.pack(">H", mlen))
|
|
# NDEF data then starts after the 2-block CC.
|
|
self._ndef_data_offset = 2 * self._block_size
|
|
self._ndef_capacity = (self._num_blocks - 2) * self._block_size - 3
|
|
|
|
def decode_trace(self, direction: int, payload: bytes) -> str | None:
|
|
return decode_15693_st(direction, payload) or super().decode_trace(direction, payload)
|
|
|
|
async def power_on(self) -> None:
|
|
await super().power_on()
|
|
self._sessions.clear()
|
|
self._dyn[DYN_MB_CTRL] = 0x00 # mailbox disabled at boot
|
|
|
|
# ----- dispatch -----
|
|
|
|
async def handle_frame(self, frame: RFFrame) -> RFFrame | None:
|
|
if len(frame.data) < 2:
|
|
return None
|
|
flags, cmd = frame.data[0], frame.data[1]
|
|
addressed = bool(flags & FLAG_ADDRESS) and not (flags & FLAG_INVENTORY)
|
|
if cmd == CMD_EXT_GET_SYSINFO:
|
|
return self._handle_ext_sysinfo(frame, addressed)
|
|
if cmd >= 0xA0:
|
|
return self._handle_st_custom(cmd, frame, addressed)
|
|
return await super().handle_frame(frame)
|
|
|
|
def _params_offset(self, addressed: bool) -> int:
|
|
return 3 + (8 if addressed else 0) # flags,cmd,mfg [+uid]
|
|
|
|
def _handle_st_custom(self, cmd: int, frame: RFFrame, addressed: bool) -> RFFrame | None:
|
|
data = frame.data
|
|
if len(data) < 3 or data[2] != ST_MFG:
|
|
return None
|
|
if addressed and (len(data) < 11 or bytes(reversed(data[3:11])) != self._uid):
|
|
return None
|
|
po = self._params_offset(addressed)
|
|
|
|
if cmd == CMD_PRESENT_PASSWORD:
|
|
return self._handle_present_password(data, po)
|
|
if cmd == CMD_WRITE_PASSWORD:
|
|
return self._handle_write_password(data, po)
|
|
if cmd == CMD_READ_CFG:
|
|
return self._handle_read_cfg(data, po)
|
|
if cmd == CMD_WRITE_CFG:
|
|
return self._handle_write_cfg(data, po)
|
|
if cmd == CMD_READ_DYN_CFG:
|
|
return self._handle_read_dyn(data, po)
|
|
if cmd == CMD_WRITE_DYN_CFG:
|
|
return self._handle_write_dyn(data, po)
|
|
if cmd == CMD_MANAGE_GPO:
|
|
return self._make_response(b"")
|
|
if cmd == CMD_WRITE_MSG:
|
|
return self._handle_write_msg(data, po)
|
|
if cmd == CMD_READ_MSG_LEN:
|
|
return self._handle_read_msg_len()
|
|
if cmd == CMD_READ_MSG:
|
|
return self._handle_read_msg(data, po)
|
|
if cmd == CMD_FAST_READ_SINGLE:
|
|
return self._handle_fast_read_single(data, po)
|
|
return self._make_error(0x01)
|
|
|
|
# ----- system info (variant-dependent) -----
|
|
|
|
def _handle_system_info(self, frame: RFFrame) -> RFFrame | None:
|
|
resp = bytearray([0x00])
|
|
if self._has_memsize_in_sysinfo:
|
|
info_flags = 0x0F
|
|
resp.append(info_flags)
|
|
resp += self._uid[::-1]
|
|
resp += bytes([self._dsfid, self._afi])
|
|
resp += bytes([self._last_block & 0xFF, self._block_size - 1])
|
|
resp += bytes([self._ic_reference])
|
|
else:
|
|
info_flags = 0x0B # DSFID + AFI + ICref, no memsize
|
|
resp.append(info_flags)
|
|
resp += self._uid[::-1]
|
|
resp += bytes([self._dsfid, self._afi])
|
|
resp += bytes([self._ic_reference])
|
|
return RFFrame.from_bytes(bytes(resp))
|
|
|
|
def _handle_ext_sysinfo(self, frame: RFFrame, addressed: bool) -> RFFrame | None:
|
|
# Response always carries the full memory size (2-byte block count-1).
|
|
resp = bytearray([0x00, 0x0F])
|
|
resp += self._uid[::-1]
|
|
resp += bytes([self._dsfid, self._afi])
|
|
resp += struct.pack("<H", self._last_block) # number of blocks - 1
|
|
resp += bytes([self._block_size - 1])
|
|
resp += bytes([self._ic_reference])
|
|
return RFFrame.from_bytes(bytes(resp))
|
|
|
|
# ----- passwords -----
|
|
|
|
def _handle_present_password(self, data: bytes, po: int) -> RFFrame | None:
|
|
if len(data) < po + 9:
|
|
return self._make_error(0x0F)
|
|
num = data[po]
|
|
if num not in self._passwords:
|
|
return self._make_error(0x10)
|
|
supplied = bytes(data[po + 1:po + 9])
|
|
if supplied != self._passwords[num]:
|
|
self._sessions.discard(num)
|
|
return self._make_error(0x0F)
|
|
self._sessions = {num} # opening a session closes the previous
|
|
return self._make_response(b"")
|
|
|
|
def _handle_write_password(self, data: bytes, po: int) -> RFFrame | None:
|
|
if len(data) < po + 9:
|
|
return self._make_error(0x0F)
|
|
num = data[po]
|
|
if num not in self._passwords:
|
|
return self._make_error(0x10)
|
|
if num not in self._sessions:
|
|
return self._make_error(0x12)
|
|
self._passwords[num] = bytes(data[po + 1:po + 9])
|
|
return self._make_response(b"")
|
|
|
|
# ----- config registers -----
|
|
|
|
def _read_static_reg(self, ptr: int) -> int | None:
|
|
if ptr in self._cfg:
|
|
return self._cfg[ptr]
|
|
return None
|
|
|
|
def _handle_read_cfg(self, data: bytes, po: int) -> RFFrame | None:
|
|
if len(data) < po + 1:
|
|
return self._make_error(0x0F)
|
|
val = self._read_static_reg(data[po])
|
|
if val is None:
|
|
return self._make_error(0x10)
|
|
return self._make_response(bytes([val]))
|
|
|
|
def _handle_write_cfg(self, data: bytes, po: int) -> RFFrame | None:
|
|
if 0 not in self._sessions or (self._cfg[REG_LOCK_CFG] & 0x01):
|
|
return self._make_error(0x12)
|
|
if len(data) < po + 2:
|
|
return self._make_error(0x0F)
|
|
ptr, val = data[po], data[po + 1]
|
|
if ptr not in self._cfg:
|
|
return self._make_error(0x10)
|
|
self._cfg[ptr] = val
|
|
return self._make_response(b"")
|
|
|
|
def _handle_read_dyn(self, data: bytes, po: int) -> RFFrame | None:
|
|
if len(data) < po + 1:
|
|
return self._make_error(0x0F)
|
|
ptr = data[po]
|
|
return self._make_response(bytes([self._dyn.get(ptr, 0x00)]))
|
|
|
|
def _handle_write_dyn(self, data: bytes, po: int) -> RFFrame | None:
|
|
if len(data) < po + 2:
|
|
return self._make_error(0x0F)
|
|
ptr, val = data[po], data[po + 1]
|
|
self._dyn[ptr] = val
|
|
return self._make_response(b"")
|
|
|
|
# ----- mailbox / fast transfer -----
|
|
|
|
def _mb_enabled(self) -> bool:
|
|
return bool(self._dyn.get(DYN_MB_CTRL, 0) & 0x01)
|
|
|
|
def _handle_write_msg(self, data: bytes, po: int) -> RFFrame | None:
|
|
if not self._mb_enabled():
|
|
return self._make_error(0x0F)
|
|
if len(data) < po + 1:
|
|
return self._make_error(0x0F)
|
|
length = data[po] + 1
|
|
msg = bytes(data[po + 1:po + 1 + length])
|
|
self._mailbox[0:len(msg)] = msg
|
|
self._mb_len = len(msg)
|
|
return self._make_response(b"")
|
|
|
|
def _handle_read_msg_len(self) -> RFFrame | None:
|
|
if not self._mb_enabled():
|
|
return self._make_error(0x0F)
|
|
return self._make_response(bytes([max(self._mb_len - 1, 0)]))
|
|
|
|
def _handle_read_msg(self, data: bytes, po: int) -> RFFrame | None:
|
|
if not self._mb_enabled():
|
|
return self._make_error(0x0F)
|
|
if len(data) < po + 2:
|
|
return self._make_error(0x0F)
|
|
ptr, nb = data[po], data[po + 1] + 1
|
|
return self._make_response(bytes(self._mailbox[ptr:ptr + nb]))
|
|
|
|
def _handle_fast_read_single(self, data: bytes, po: int) -> RFFrame | None:
|
|
if len(data) < po + 1:
|
|
return self._make_error(0x0F)
|
|
block = data[po]
|
|
if block >= self._num_blocks:
|
|
return self._make_error(0x10)
|
|
if not self._read_allowed(block):
|
|
return self._make_error(0x15)
|
|
off = block * self._block_size
|
|
return self._make_response(bytes(self._memory[off:off + self._block_size]))
|
|
|
|
# ----- area protection -----
|
|
|
|
def _area_index(self, block: int) -> int:
|
|
e1 = 8 * self._cfg[REG_ENDA1] + 7
|
|
e2 = 8 * self._cfg[REG_ENDA2] + 7
|
|
e3 = 8 * self._cfg[REG_ENDA3] + 7
|
|
if block <= e1:
|
|
return 1
|
|
if block <= e2:
|
|
return 2
|
|
if block <= e3:
|
|
return 3
|
|
return 4
|
|
|
|
_RFASS_REG = {1: REG_RFA1SS, 2: REG_RFA2SS, 3: REG_RFA3SS, 4: REG_RFA4SS}
|
|
|
|
def _rfaiss(self, area: int) -> int:
|
|
return self._cfg[self._RFASS_REG[area]]
|
|
|
|
def rf_area_ss(self, area: int) -> St25dvRfAreaSS:
|
|
"""RF_Ai_SS security register for memory area ``area`` (1-4) as a self-describing
|
|
:class:`St25dvRfAreaSS`."""
|
|
return St25dvRfAreaSS(self._cfg[self._RFASS_REG[area]])
|
|
|
|
def set_rf_area_ss(self, area: int, value: "St25dvRfAreaSS | int") -> None:
|
|
"""Program the RF_Ai_SS register for memory area ``area`` (1-4)."""
|
|
self._cfg[self._RFASS_REG[area]] = int(value) & 0xFF
|
|
|
|
def _session_for_area(self, area: int) -> int:
|
|
pwd_ctrl = self._rfaiss(area) & 0x03
|
|
return pwd_ctrl # 0 = none, 1-3 = RF_PWD_1..3
|
|
|
|
def _read_allowed(self, block: int) -> bool:
|
|
area = self._area_index(block)
|
|
rw = (self._rfaiss(area) >> 2) & 0x03
|
|
if area == 1:
|
|
need = (rw == 0x03) # A1: read forbidden-without-session only at 0b11
|
|
else:
|
|
need = (rw >= 0x02)
|
|
if not need:
|
|
return True
|
|
return self._session_for_area(area) in self._sessions
|
|
|
|
def _write_allowed(self, block: int) -> bool:
|
|
area = self._area_index(block)
|
|
rw = (self._rfaiss(area) >> 2) & 0x03
|
|
if rw == 0x03:
|
|
return False
|
|
if rw == 0x00:
|
|
return True
|
|
return self._session_for_area(area) in self._sessions
|
|
|
|
def _handle_read_single(self, frame: RFFrame) -> RFFrame | None:
|
|
block = self._extract_block_number(frame)
|
|
if block is None:
|
|
return None
|
|
if block < self._num_blocks and not self._read_allowed(block):
|
|
return self._make_error(0x15)
|
|
return super()._handle_read_single(frame)
|
|
|
|
def _handle_write_single(self, frame: RFFrame) -> RFFrame | None:
|
|
block = self._extract_block_number(frame)
|
|
if block is not None and block < self._num_blocks and not self._write_allowed(block):
|
|
return self._make_error(0x12)
|
|
return super()._handle_write_single(frame)
|
|
|
|
def _handle_extended_read_single(self, frame: RFFrame) -> RFFrame | None:
|
|
block = self._extract_block_number_ext(frame)
|
|
if block is not None and block < self._num_blocks and not self._read_allowed(block):
|
|
return self._make_error(0x15)
|
|
return super()._handle_extended_read_single(frame)
|
|
|
|
def _handle_extended_write_single(self, frame: RFFrame) -> RFFrame | None:
|
|
block = self._extract_block_number_ext(frame)
|
|
if block is not None and block < self._num_blocks and not self._write_allowed(block):
|
|
return self._make_error(0x12)
|
|
return super()._handle_extended_write_single(frame)
|
|
|
|
|
|
class ST25DV04K(ST25DV):
|
|
"""ST25DV04K — 512 bytes user (128 blocks). GetSystemInfo carries memsize."""
|
|
_uid_prefix = b"\xE0\x02\x24"
|
|
_num_blocks_cls = 128
|
|
_product_code = 0x24
|
|
_ic_ref = 0x24
|
|
_has_memsize_in_sysinfo = True
|
|
|
|
|
|
class ST25DV16K(ST25DV):
|
|
"""ST25DV16K — 2048 bytes user (512 blocks). Use ExtGetSystemInfo for memsize."""
|
|
_uid_prefix = b"\xE0\x02\x26"
|
|
_num_blocks_cls = 512
|
|
_product_code = 0x26
|
|
_ic_ref = 0x26
|
|
_has_memsize_in_sysinfo = False
|
|
|
|
|
|
class ST25DV64K(ST25DV):
|
|
"""ST25DV64K — 8192 bytes user (2048 blocks). Use ExtGetSystemInfo for memsize."""
|
|
_uid_prefix = b"\xE0\x02\x26"
|
|
_num_blocks_cls = 2048
|
|
_product_code = 0x26
|
|
_ic_ref = 0x26
|
|
_has_memsize_in_sysinfo = False
|