diff --git a/CLAUDE.md b/CLAUDE.md index 3d7917d..f5b5a25 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,10 +57,11 @@ pm3py/ replay.py # Trace replay access_control/ # Wiegand, OSDP transponders/ # tag/transponder models (extracted from sim/) - bitfield.py # BitField descriptor + Register base — self-describing config - # registers (T5577Config, Ntag21xConfig, UL EV1/C, MFC access - # bits). Fields are documented attrs -> shell completion + repr - # decode table. Layouts cross-checked vs the firmware submodule. + bitfield.py # BitField descriptor + Register base — self-describing config/ + # frame registers (T5577, EM4100, NTAG21x, Ultralight EV1/C, MFC + # access bits, NTAG I2C NC_REG/NS_REG/REG_LOCK/PT_I2C). Fields are + # 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 # nxp/type2,ntag21x,ultralight,ntag_i2c # (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus) diff --git a/pm3py/sim/__init__.py b/pm3py/sim/__init__.py index 678451b..48c917d 100644 --- a/pm3py/sim/__init__.py +++ b/pm3py/sim/__init__.py @@ -33,7 +33,9 @@ from pm3py.transponders.hf.iso14443a.nxp.ultralight import ( MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21, 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.infineon.optiga_nbt import OptigaAuthenticateNBT from pm3py.transponders.hf.iso14443a.st.st25ta import ( @@ -93,6 +95,7 @@ __all__ = [ "MF0UL11", "MF0UL21", "UL_C_DEFAULT_KEY", "UltralightEV1Config", "UltralightCConfig", "NtagI2C", "NT3H2111", "NT3H2211", + "NtagI2CConfig", "NtagI2CSession", "NtagI2CPtI2C", "ST25TN", "ST25TN512", "ST25TN01K", "ST25TA", "ST25TA512B", "ST25TA02KB", "ST25TA16K", "ST25TA64K", "OptigaAuthenticateNBT", diff --git a/pm3py/transponders/hf/iso14443a/nxp/ntag_i2c.py b/pm3py/transponders/hf/iso14443a/nxp/ntag_i2c.py index e9c569a..d68fba7 100644 --- a/pm3py/transponders/hf/iso14443a/nxp/ntag_i2c.py +++ b/pm3py/transponders/hf/iso14443a/nxp/ntag_i2c.py @@ -16,6 +16,7 @@ from __future__ import annotations import os from pm3py.sim.frame import RFFrame +from pm3py.transponders.bitfield import BitField, Register from .type2 import ( NxpType2Tag, _ack, _nak, NAK_ARG, PWD_AUTH, SECTOR_SELECT, READ_CMD, WRITE_CMD, GET_VERSION, FAST_READ, READ_SIG, @@ -27,6 +28,83 @@ FAST_WRITE = 0xA6 NFC_PROT = 0x80 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): """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(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 ----- @property diff --git a/tests/test_bitfield.py b/tests/test_bitfield.py index 4e435d5..f4f1f9a 100644 --- a/tests/test_bitfield.py +++ b/tests/test_bitfield.py @@ -7,6 +7,7 @@ from pm3py.sim import ( Ntag21xConfig, NTAG213, MF0UL21, MifareUltralightC, MifareAccessConditions, MifareClassicTag, EM4100Code, EM4100Tag, + NtagI2CConfig, NtagI2CSession, NtagI2CPtI2C, NT3H2111, NT3H2211, ) @@ -299,3 +300,78 @@ class TestEM4100Code: assert "EM4100 64-bit code" in text assert "0102030405" 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