From a24f0f803fbd8a1c1fdc9b9a614024b315797f6d Mon Sep 17 00:00:00 2001 From: michael Date: Sun, 5 Jul 2026 12:58:37 -0700 Subject: [PATCH] feat(sim): reject wrong-protocol tags in start_14a/start_15693; export tags; uid getter - start_14a now raises TypeError for a non-Tag14443A transponder, start_15693 likewise for a non-Tag15693 (local imports to avoid the base<->sim circular import). Prevents silently starting a sim with an incompatible tag model. - Re-export the full 14a/14b/15693 tag lineup (NTAG21x, Ultralight family, NTAG I2C, ST25*, OPTIGA, ISO14443B, TI parts) from pm3py.sim for `from pm3py.sim import NTAG213`-style use. - Add a read-only `uid` property on the Transponder base (mirrors set_uid()). - Tests for both guards. Co-Authored-By: Claude Opus 4.8 --- pm3py/sim/__init__.py | 44 +++++++++++++++++++++++++++++++++++++ pm3py/sim/sim_session.py | 20 +++++++++++++++++ pm3py/sim/transponder.py | 5 +++++ tests/test_sim_trace_14a.py | 22 +++++++++++++++++++ 4 files changed, 91 insertions(+) diff --git a/pm3py/sim/__init__.py b/pm3py/sim/__init__.py index 0ba922f..a3b964f 100644 --- a/pm3py/sim/__init__.py +++ b/pm3py/sim/__init__.py @@ -23,6 +23,20 @@ from pm3py.transponders.hf.iso14443a.nxp.crypto1 import Crypto1 from pm3py.transponders.hf.iso14443a.nxp.mifare_classic import MifareClassicTag, MifareClassicReader from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag +from pm3py.transponders.hf.iso14443a.nxp.type2 import NxpType2Tag +from pm3py.transponders.hf.iso14443a.nxp.ntag21x import ( + Ntag21x, NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, +) +from pm3py.transponders.hf.iso14443a.nxp.ultralight import ( + MifareUltralight, MifareUltralightC, MifareUltralightEV1, MF0UL11, MF0UL21, + UL_C_DEFAULT_KEY, +) +from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import NtagI2C, NT3H2111, NT3H2211 +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 ( + ST25TA, ST25TA512B, ST25TA02KB, ST25TA16K, ST25TA64K, +) from pm3py.transponders.hf.iso15693.base import Tag15693, Reader15693, State15693 from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag @@ -34,6 +48,20 @@ from pm3py.transponders.hf.iso15693.nxp.ntag5_platform import Ntag5PlatformTag from pm3py.transponders.hf.iso15693.nxp.ntag5_switch import Ntag5SwitchTag from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag from pm3py.transponders.hf.iso15693.nxp.ntag5_boost import Ntag5BoostTag +from pm3py.transponders.hf.iso15693.st.st25tv import ST25TV, ST25TV512C, ST25TV02KC +from pm3py.transponders.hf.iso15693.st.st25dv import ST25DV, ST25DV04K, ST25DV16K, ST25DV64K +from pm3py.transponders.hf.iso15693.infineon.myd_vicinity import ( + MydVicinity, SRF55V02P, SRF55V10P, +) +from pm3py.transponders.hf.iso14443b.base import Tag14443B, StateB, Tag14443B_4, StateB4 +from pm3py.transponders.hf.iso14443b.st.st25tb import ( + ST25TB, ST25TB512AC, ST25TB512AT, ST25TB02K, ST25TB04K, +) +from pm3py.transponders.hf.iso14443b.ti.rf430cl330h import RF430CL330H +from pm3py.transponders.hf.iso15693.ti.tagit import TagItHFIPlus +from pm3py.transponders.hf.iso15693.ti.rf430frl import ( + RF430FRL15xH, RF430FRL152H, RF430FRL153H, RF430FRL154H, +) from pm3py.transponders.lf.base import TagLF, ReaderLF, Modulation from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader @@ -55,6 +83,22 @@ __all__ = [ "HIDProxTag", "HIDReader", "T5577Tag", "T5577Reader", "NfcType2Tag", "NfcType4Tag", "NfcType5Tag", + "NxpType2Tag", + "Ntag21x", "NTAG210", "NTAG212", "NTAG213", "NTAG215", "NTAG216", + "MifareUltralight", "MifareUltralightC", "MifareUltralightEV1", + "MF0UL11", "MF0UL21", "UL_C_DEFAULT_KEY", + "NtagI2C", "NT3H2111", "NT3H2211", + "ST25TN", "ST25TN512", "ST25TN01K", + "ST25TA", "ST25TA512B", "ST25TA02KB", "ST25TA16K", "ST25TA64K", + "OptigaAuthenticateNBT", + "ST25TV", "ST25TV512C", "ST25TV02KC", + "ST25DV", "ST25DV04K", "ST25DV16K", "ST25DV64K", + "MydVicinity", "SRF55V02P", "SRF55V10P", + "Tag14443B", "StateB", "Tag14443B_4", "StateB4", + "ST25TB", "ST25TB512AC", "ST25TB512AT", "ST25TB02K", "ST25TB04K", + "RF430CL330H", + "TagItHFIPlus", + "RF430FRL15xH", "RF430FRL152H", "RF430FRL153H", "RF430FRL154H", "ndef_text", "ndef_uri", "ndef_mime", "NxpIcodeTag", "IcodeSlixTag", "IcodeSlix2Tag", "Icode3Tag", "IcodeDnaTag", diff --git a/pm3py/sim/sim_session.py b/pm3py/sim/sim_session.py index 643e5bd..e4dab34 100644 --- a/pm3py/sim/sim_session.py +++ b/pm3py/sim/sim_session.py @@ -140,6 +140,16 @@ class SimSession: 14a-4 (ISO-DEP / WTX relay) reuses the same firmware trace but routes it through the async relay loop — see the plan's I/O model decision. """ + # Local import: pm3py.transponders.hf.iso14443a.base pulls in pm3py.sim, + # so a module-level import here would be circular. + from pm3py.transponders.hf.iso14443a.base import Tag14443A + if not isinstance(tag, Tag14443A): + raise TypeError( + f"start_14a requires an ISO 14443-A transponder (a Tag14443A " + f"subclass), got {type(tag).__name__}. Use start_15693() for " + f"ISO 15693 tags." + ) + self._tag_model = tag self._on_frame = on_frame self._quiet = quiet @@ -217,6 +227,16 @@ class SimSession: dict shape). Passing it auto-enables trace streaming. quiet: if True, don't print frames (callback + `entries` still fill). """ + # Local import: pm3py.transponders.hf.iso15693.base pulls in pm3py.sim, + # so a module-level import here would be circular. + from pm3py.transponders.hf.iso15693.base import Tag15693 + if not isinstance(tag, Tag15693): + raise TypeError( + f"start_15693 requires an ISO 15693 transponder (a Tag15693 " + f"subclass), got {type(tag).__name__}. Use start_14a() for " + f"ISO 14443-A tags." + ) + self._tag_model = tag self._on_frame = on_frame self._quiet = quiet diff --git a/pm3py/sim/transponder.py b/pm3py/sim/transponder.py index b3700df..3efdd84 100644 --- a/pm3py/sim/transponder.py +++ b/pm3py/sim/transponder.py @@ -128,3 +128,8 @@ class Transponder(abc.ABC): for r in self.regions.values(): if r.eml_offset >= 0: self._eml_push_region(r) + + @property + def uid(self) -> bytes: + """Current tag UID.""" + return self._parse_uid(self._uid) \ No newline at end of file diff --git a/tests/test_sim_trace_14a.py b/tests/test_sim_trace_14a.py index f754be2..2edee22 100644 --- a/tests/test_sim_trace_14a.py +++ b/tests/test_sim_trace_14a.py @@ -115,3 +115,25 @@ def test_14a_entries_accumulate_and_clear(): assert [e["direction"] for e in s.entries] == [0, 1] s.clear() assert s.entries == [] + + +def test_start_14a_rejects_non_14443a_tag(): + """A 15693 (or any non-Tag14443A) transponder must be refused up front.""" + import pytest + from pm3py.sim import Icode3Tag + + s = SimSession() + s._port = MagicMock() # would otherwise fail on port resolution + with pytest.raises(TypeError, match="ISO 14443-A"): + s.start_14a(Icode3Tag(uid=None)) + + +def test_start_15693_rejects_non_15693_tag(): + """Symmetric guard: a 14443-A tag must be refused by start_15693.""" + import pytest + from pm3py.sim import NTAG213 + + s = SimSession() + s._port = MagicMock() + with pytest.raises(TypeError, match="ISO 15693"): + s.start_15693(NTAG213())