feat(registers): NTAG I2C plus config/session/PT_I2C registers
Add self-describing registers for the NTAG I2C plus (NT3H2111/2211),
decoded bit-exact from the NT3H2111 datasheet (Tables 10-14):
- NtagI2CConfig config registers E8h/E9h (NC_REG: pass-through, FD_ON/
FD_OFF, SRAM mirror, transfer dir; LAST_NDEF_BLOCK;
SRAM_MIRROR_BLOCK; watchdog; I2C_CLOCK_STR; REG_LOCK)
- NtagI2CSession session registers ECh/EDh — same NC_REG copy plus the
NS_REG status byte and NEG_AUTH_REACHED
- NtagI2CPtI2C PT_I2C (E7h): 2K_PROT / SRAM_PROT / I2C_PROT
Config and session share one _NtagI2CRegBlock base (byte 6 differs:
REG_LOCK vs NS_REG). Bound via tag.config / tag.session / tag.pt_i2c;
the fresh-tag decode matches the shipped defaults. 7 new tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -57,10 +57,11 @@ pm3py/
|
|||||||
replay.py # Trace replay
|
replay.py # Trace replay
|
||||||
access_control/ # Wiegand, OSDP
|
access_control/ # Wiegand, OSDP
|
||||||
transponders/ # tag/transponder models (extracted from sim/)
|
transponders/ # tag/transponder models (extracted from sim/)
|
||||||
bitfield.py # BitField descriptor + Register base — self-describing config
|
bitfield.py # BitField descriptor + Register base — self-describing config/
|
||||||
# registers (T5577Config, Ntag21xConfig, UL EV1/C, MFC access
|
# frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC
|
||||||
# bits). Fields are documented attrs -> shell completion + repr
|
# access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C). Fields are
|
||||||
# decode table. Layouts cross-checked vs the firmware submodule.
|
# documented attrs -> shell completion + repr decode table.
|
||||||
|
# Layouts cross-checked vs the firmware submodule + NXP datasheets.
|
||||||
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
|
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
|
||||||
# nxp/type2,ntag21x,ultralight,ntag_i2c
|
# nxp/type2,ntag21x,ultralight,ntag_i2c
|
||||||
# (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus)
|
# (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus)
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ from pm3py.transponders.hf.iso14443a.nxp.ultralight import (
|
|||||||
MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21,
|
MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21,
|
||||||
UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig,
|
UL_C_DEFAULT_KEY, UltralightEV1Config, UltralightCConfig,
|
||||||
)
|
)
|
||||||
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import NtagI2C, NT3H2111, NT3H2211
|
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import (
|
||||||
|
NtagI2C, NT3H2111, NT3H2211, NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C,
|
||||||
|
)
|
||||||
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K
|
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN, ST25TN512, ST25TN01K
|
||||||
from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT
|
from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT
|
||||||
from pm3py.transponders.hf.iso14443a.st.st25ta import (
|
from pm3py.transponders.hf.iso14443a.st.st25ta import (
|
||||||
@@ -93,6 +95,7 @@ __all__ = [
|
|||||||
"MF0UL11", "MF0UL21", "UL_C_DEFAULT_KEY",
|
"MF0UL11", "MF0UL21", "UL_C_DEFAULT_KEY",
|
||||||
"UltralightEV1Config", "UltralightCConfig",
|
"UltralightEV1Config", "UltralightCConfig",
|
||||||
"NtagI2C", "NT3H2111", "NT3H2211",
|
"NtagI2C", "NT3H2111", "NT3H2211",
|
||||||
|
"NtagI2CConfig", "NtagI2CSession", "NtagI2CPtI2C",
|
||||||
"ST25TN", "ST25TN512", "ST25TN01K",
|
"ST25TN", "ST25TN512", "ST25TN01K",
|
||||||
"ST25TA", "ST25TA512B", "ST25TA02KB", "ST25TA16K", "ST25TA64K",
|
"ST25TA", "ST25TA512B", "ST25TA02KB", "ST25TA16K", "ST25TA64K",
|
||||||
"OptigaAuthenticateNBT",
|
"OptigaAuthenticateNBT",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from pm3py.sim.frame import RFFrame
|
from pm3py.sim.frame import RFFrame
|
||||||
|
from pm3py.transponders.bitfield import BitField, Register
|
||||||
from .type2 import (
|
from .type2 import (
|
||||||
NxpType2Tag, _ack, _nak, NAK_ARG, PWD_AUTH,
|
NxpType2Tag, _ack, _nak, NAK_ARG, PWD_AUTH,
|
||||||
SECTOR_SELECT, READ_CMD, WRITE_CMD, GET_VERSION, FAST_READ, READ_SIG,
|
SECTOR_SELECT, READ_CMD, WRITE_CMD, GET_VERSION, FAST_READ, READ_SIG,
|
||||||
@@ -27,6 +28,83 @@ FAST_WRITE = 0xA6
|
|||||||
NFC_PROT = 0x80
|
NFC_PROT = 0x80
|
||||||
NFC_DIS_SEC1 = 0x20
|
NFC_DIS_SEC1 = 0x20
|
||||||
|
|
||||||
|
# NC_REG field-detect event maps (datasheet Table 13)
|
||||||
|
_FD_OFF = {0: "field off", 1: "field off / HALT",
|
||||||
|
2: "field off / last NDEF read", 3: "field off / last data (pass-through)"}
|
||||||
|
_FD_ON = {0: "field on", 1: "first SoC", 2: "tag selected", 3: "data ready (pass-through)"}
|
||||||
|
_I2C_PROT = {0: "full I2C access", 1: "protected area read-only",
|
||||||
|
2: "protected area no access", 3: "protected area no access"}
|
||||||
|
|
||||||
|
|
||||||
|
class _NtagI2CRegBlock(Register):
|
||||||
|
"""Shared decode of an NTAG I2C plus register block (config pages E8h/E9h or the session
|
||||||
|
copy at ECh/EDh) as one 64-bit word. Byte 6 differs (REG_LOCK vs NS_REG), so the concrete
|
||||||
|
:class:`NtagI2CConfig` / :class:`NtagI2CSession` add it."""
|
||||||
|
|
||||||
|
_width_bits = 64
|
||||||
|
|
||||||
|
# --- NC_REG (byte 0) ---
|
||||||
|
nfcs_i2c_rst = BitField(63, doc="enable NFC-silence + I2C soft-reset via repeated start")
|
||||||
|
pthru_on = BitField(62, doc="pass-through mode: SRAM mapped to end of Sector 0")
|
||||||
|
fd_off = BitField(61, 60, values=_FD_OFF, doc="event that releases the field-detect pin")
|
||||||
|
fd_on = BitField(59, 58, values=_FD_ON, doc="event that pulls the field-detect pin low")
|
||||||
|
sram_mirror_on = BitField(57, doc="mirror the 64-byte SRAM into user memory")
|
||||||
|
transfer_dir = BitField(56, values={0: "I2C->NFC", 1: "NFC->I2C"},
|
||||||
|
doc="pass-through data-flow direction (must be 1 when not in pass-through)")
|
||||||
|
# --- byte 1..5 ---
|
||||||
|
last_ndef_block = BitField(55, 48, doc="I2C block of the last NDEF byte (drives NDEF_DATA_READ)")
|
||||||
|
sram_mirror_block = BitField(47, 40, doc="I2C block where the SRAM mirror starts (default F8h)")
|
||||||
|
wdt_ls = BitField(39, 32, doc="watchdog timer, least-significant byte")
|
||||||
|
wdt_ms = BitField(31, 24, doc="watchdog timer, most-significant byte")
|
||||||
|
i2c_clock_str = BitField(16, doc="enable I2C clock stretching")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def watchdog(self) -> int:
|
||||||
|
"""16-bit watchdog time (WDT_MS:WDT_LS)."""
|
||||||
|
return (self.wdt_ms << 8) | self.wdt_ls
|
||||||
|
|
||||||
|
|
||||||
|
class NtagI2CConfig(_NtagI2CRegBlock):
|
||||||
|
"""NTAG I2C plus configuration registers (pages E8h/E9h) — the power-on default behaviour.
|
||||||
|
Changes take effect after the next POR; use :class:`NtagI2CSession` to change the live
|
||||||
|
session. ``REG_LOCK`` here permanently freezes these registers per interface."""
|
||||||
|
|
||||||
|
_label = "NTAG I2C config registers (E8/E9)"
|
||||||
|
|
||||||
|
reg_lock_i2c = BitField(9, doc="permanently lock these registers from I2C writes (irreversible)")
|
||||||
|
reg_lock_nfc = BitField(8, doc="permanently lock these registers from NFC writes (irreversible)")
|
||||||
|
|
||||||
|
|
||||||
|
class NtagI2CSession(_NtagI2CRegBlock):
|
||||||
|
"""NTAG I2C plus session registers (pages ECh/EDh) — the live copy for the current RF
|
||||||
|
session (NFC-read-only; POR reloads them from the config registers). Byte 6 is the NS_REG
|
||||||
|
status; ``I2C_CLOCK_STR`` byte also carries the ``neg_auth_reached`` status bit."""
|
||||||
|
|
||||||
|
_label = "NTAG I2C session registers (EC/ED)"
|
||||||
|
|
||||||
|
neg_auth_reached = BitField(17, doc="status: negative-PWD_AUTH limit reached (auth locked)")
|
||||||
|
# NS_REG status (byte 6)
|
||||||
|
ndef_data_read = BitField(15, doc="status: all bytes up to LAST_NDEF_BLOCK have been read")
|
||||||
|
i2c_locked = BitField(14, doc="status: memory arbitration locked to the I2C interface")
|
||||||
|
rf_locked = BitField(13, doc="status: memory arbitration locked to the NFC interface")
|
||||||
|
sram_i2c_ready = BitField(12, doc="status: SRAM data ready for the I2C interface to read")
|
||||||
|
sram_rf_ready = BitField(11, doc="status: SRAM data ready for the NFC interface to read")
|
||||||
|
eeprom_wr_err = BitField(10, doc="status: EEPROM write/erase HV error (write 0 via I2C to clear)")
|
||||||
|
eeprom_wr_busy = BitField(9, doc="status: EEPROM write in progress")
|
||||||
|
rf_field_present = BitField(8, doc="status: an RF field is present")
|
||||||
|
|
||||||
|
|
||||||
|
class NtagI2CPtI2C(Register):
|
||||||
|
"""NTAG I2C plus I2C-protection register (PT_I2C, page E7h byte 0) — restricts access to
|
||||||
|
the password-protected area from the I2C side."""
|
||||||
|
|
||||||
|
_width_bits = 8
|
||||||
|
_label = "NTAG I2C PT_I2C (E7)"
|
||||||
|
|
||||||
|
prot_2k = BitField(3, doc="require password auth to reach Sector 1 from I2C (2k)")
|
||||||
|
sram_prot = BitField(2, doc="require password auth for pass-through / SRAM mirror")
|
||||||
|
i2c_prot = BitField(1, 0, values=_I2C_PROT, doc="I2C access level to the protected area")
|
||||||
|
|
||||||
|
|
||||||
class NtagI2C(NxpType2Tag):
|
class NtagI2C(NxpType2Tag):
|
||||||
"""NTAG I2C plus base. Concrete ICs: NT3H2111 (1k), NT3H2211 (2k)."""
|
"""NTAG I2C plus base. Concrete ICs: NT3H2111 (1k), NT3H2211 (2k)."""
|
||||||
@@ -65,6 +143,42 @@ class NtagI2C(NxpType2Tag):
|
|||||||
self._set_page(0xED, bytes([0x08, 0x01, 0x00, 0x00])) # WDT_MS/I2C_CLK_STR/NS_REG/RFU
|
self._set_page(0xED, bytes([0x08, 0x01, 0x00, 0x00])) # WDT_MS/I2C_CLK_STR/NS_REG/RFU
|
||||||
self._set_page(0xE7, bytes([0x00, 0x00, 0x00, 0x00])) # PT_I2C
|
self._set_page(0xE7, bytes([0x00, 0x00, 0x00, 0x00])) # PT_I2C
|
||||||
|
|
||||||
|
# ----- config / session / protection registers -----
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> NtagI2CConfig:
|
||||||
|
"""The configuration registers (pages E8h/E9h) — power-on defaults. Assign back to
|
||||||
|
program them (``tag.config = cfg``); effect applies after the next POR."""
|
||||||
|
raw = self._get_page(0xE8) + self._get_page(0xE9)
|
||||||
|
return NtagI2CConfig(int.from_bytes(raw, "big"))
|
||||||
|
|
||||||
|
@config.setter
|
||||||
|
def config(self, value: "NtagI2CConfig | int") -> None:
|
||||||
|
raw = int(value).to_bytes(8, "big")
|
||||||
|
self._set_page(0xE8, raw[0:4])
|
||||||
|
self._set_page(0xE9, raw[4:8])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def session(self) -> NtagI2CSession:
|
||||||
|
"""The session registers (pages ECh/EDh) — the live copy for this RF session."""
|
||||||
|
raw = self._get_page(0xEC) + self._get_page(0xED)
|
||||||
|
return NtagI2CSession(int.from_bytes(raw, "big"))
|
||||||
|
|
||||||
|
@session.setter
|
||||||
|
def session(self, value: "NtagI2CSession | int") -> None:
|
||||||
|
raw = int(value).to_bytes(8, "big")
|
||||||
|
self._set_page(0xEC, raw[0:4])
|
||||||
|
self._set_page(0xED, raw[4:8])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pt_i2c(self) -> NtagI2CPtI2C:
|
||||||
|
"""The I2C-protection register (PT_I2C, page E7h byte 0)."""
|
||||||
|
return NtagI2CPtI2C(self._get_page(0xE7)[0])
|
||||||
|
|
||||||
|
@pt_i2c.setter
|
||||||
|
def pt_i2c(self, value: "NtagI2CPtI2C | int") -> None:
|
||||||
|
self._set_page(0xE7, bytes([int(value) & 0xFF, 0, 0, 0]))
|
||||||
|
|
||||||
# ----- sector / page validity -----
|
# ----- sector / page validity -----
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from pm3py.sim import (
|
|||||||
Ntag21xConfig, NTAG213, MF0UL21, MifareUltralightC,
|
Ntag21xConfig, NTAG213, MF0UL21, MifareUltralightC,
|
||||||
MifareAccessConditions, MifareClassicTag,
|
MifareAccessConditions, MifareClassicTag,
|
||||||
EM4100Code, EM4100Tag,
|
EM4100Code, EM4100Tag,
|
||||||
|
NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -299,3 +300,78 @@ class TestEM4100Code:
|
|||||||
assert "EM4100 64-bit code" in text
|
assert "EM4100 64-bit code" in text
|
||||||
assert "0102030405" in text
|
assert "0102030405" in text
|
||||||
assert "valid" in text and "yes" in text
|
assert "valid" in text and "yes" in text
|
||||||
|
|
||||||
|
|
||||||
|
class TestNtagI2CRegisters:
|
||||||
|
def test_fresh_config_defaults(self):
|
||||||
|
# NT3H2111 ships E8=01 00 F8 48, E9=08 01 00 00 (datasheet defaults)
|
||||||
|
cfg = NT3H2111().config
|
||||||
|
assert cfg.transfer_dir == "NFC->I2C" # NC_REG bit0 = 1
|
||||||
|
assert cfg.pthru_on is False
|
||||||
|
assert cfg.sram_mirror_on is False
|
||||||
|
assert cfg.sram_mirror_block == 0xF8
|
||||||
|
assert cfg.watchdog == 0x0848
|
||||||
|
assert cfg.i2c_clock_str is True
|
||||||
|
assert cfg.reg_lock_i2c is False and cfg.reg_lock_nfc is False
|
||||||
|
|
||||||
|
def test_nc_reg_fields_roundtrip(self):
|
||||||
|
tag = NT3H2111()
|
||||||
|
cfg = tag.config
|
||||||
|
cfg.pthru_on = True
|
||||||
|
cfg.sram_mirror_on = True
|
||||||
|
cfg.fd_on = "tag selected" # 0b10
|
||||||
|
cfg.fd_off = "field off / last NDEF read" # 0b10
|
||||||
|
cfg.last_ndef_block = 0x2A
|
||||||
|
tag.config = cfg # program E8/E9
|
||||||
|
back = tag.config
|
||||||
|
assert back.pthru_on is True and back.sram_mirror_on is True
|
||||||
|
assert back.fd_on == "tag selected"
|
||||||
|
assert back.fd_off == "field off / last NDEF read"
|
||||||
|
assert back.last_ndef_block == 0x2A
|
||||||
|
|
||||||
|
def test_reg_lock(self):
|
||||||
|
cfg = NtagI2CConfig(0)
|
||||||
|
cfg.reg_lock_nfc = True
|
||||||
|
# REG_LOCK is byte 6 (bits 15:8); bit0 = NFC lock -> byte6 == 0x01
|
||||||
|
assert bytes(cfg)[6] == 0x01
|
||||||
|
assert cfg.reg_lock_nfc is True and cfg.reg_lock_i2c is False
|
||||||
|
|
||||||
|
def test_session_ns_reg_decode(self):
|
||||||
|
# NS_REG is byte 6 of the EC/ED block; set NDEF_DATA_READ + RF_FIELD_PRESENT + busy
|
||||||
|
sess = NtagI2CSession(0)
|
||||||
|
assert sess.rf_field_present is False
|
||||||
|
raw = int(NtagI2CSession(0))
|
||||||
|
# byte6 bit7 (ndef_data_read)=bit15, bit0 (rf_field_present)=bit8, bit1 (busy)=bit9
|
||||||
|
sess = NtagI2CSession((1 << 15) | (1 << 8) | (1 << 9))
|
||||||
|
assert sess.ndef_data_read is True
|
||||||
|
assert sess.rf_field_present is True
|
||||||
|
assert sess.eeprom_wr_busy is True
|
||||||
|
assert sess.rf_locked is False
|
||||||
|
|
||||||
|
def test_session_binding(self):
|
||||||
|
tag = NT3H2111()
|
||||||
|
sess = tag.session
|
||||||
|
# session copy shares NC_REG defaults (transfer_dir=1)
|
||||||
|
assert sess.transfer_dir == "NFC->I2C"
|
||||||
|
assert sess.rf_field_present is False # NS_REG starts clear
|
||||||
|
|
||||||
|
def test_pt_i2c(self):
|
||||||
|
pt = NtagI2CPtI2C(0)
|
||||||
|
pt.sram_prot = True
|
||||||
|
pt.i2c_prot = "protected area read-only" # 0b01, unambiguous
|
||||||
|
assert pt.sram_prot is True
|
||||||
|
assert pt.i2c_prot == "protected area read-only"
|
||||||
|
assert int(pt) == (1 << 2) | (1 << 0)
|
||||||
|
# datasheet 1Xb == "no access to protected area" for both codes 2 and 3
|
||||||
|
assert NtagI2CPtI2C(2).i2c_prot == "protected area no access"
|
||||||
|
assert NtagI2CPtI2C(3).i2c_prot == "protected area no access"
|
||||||
|
# tag binding
|
||||||
|
tag = NT3H2211()
|
||||||
|
tag.pt_i2c = pt
|
||||||
|
assert tag.pt_i2c.i2c_prot == "protected area read-only"
|
||||||
|
|
||||||
|
def test_repr_is_decode_table(self):
|
||||||
|
text = repr(NT3H2111().config)
|
||||||
|
assert "NTAG I2C config registers" in text
|
||||||
|
assert "transfer_dir" in text and "NFC->I2C" in text
|
||||||
|
assert "sram_mirror_block" in text
|
||||||
|
|||||||
Reference in New Issue
Block a user