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 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-05 12:58:37 -07:00
parent 77ff1bb076
commit a24f0f803f
4 changed files with 91 additions and 0 deletions

View File

@@ -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.mifare_classic import MifareClassicTag, MifareClassicReader
from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader from pm3py.transponders.hf.iso14443a.nxp.desfire import DesfireTag, DesfireReader
from pm3py.transponders.hf.iso14443a.ndef import NfcType2Tag, NfcType4Tag 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.base import Tag15693, Reader15693, State15693
from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime from pm3py.transponders.hf.iso15693.type5 import NfcType5Tag, ndef_text, ndef_uri, ndef_mime
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag 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_switch import Ntag5SwitchTag
from pm3py.transponders.hf.iso15693.nxp.ntag5_link import Ntag5LinkTag 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.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.base import TagLF, ReaderLF, Modulation
from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader from pm3py.transponders.lf.em.em4100 import EM4100Tag, EM4100Reader
from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader from pm3py.transponders.lf.hid.hid import HIDProxTag, HIDReader
@@ -55,6 +83,22 @@ __all__ = [
"HIDProxTag", "HIDReader", "HIDProxTag", "HIDReader",
"T5577Tag", "T5577Reader", "T5577Tag", "T5577Reader",
"NfcType2Tag", "NfcType4Tag", "NfcType5Tag", "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", "ndef_text", "ndef_uri", "ndef_mime",
"NxpIcodeTag", "IcodeSlixTag", "IcodeSlix2Tag", "NxpIcodeTag", "IcodeSlixTag", "IcodeSlix2Tag",
"Icode3Tag", "IcodeDnaTag", "Icode3Tag", "IcodeDnaTag",

View File

@@ -140,6 +140,16 @@ class SimSession:
14a-4 (ISO-DEP / WTX relay) reuses the same firmware trace but routes it 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. 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._tag_model = tag
self._on_frame = on_frame self._on_frame = on_frame
self._quiet = quiet self._quiet = quiet
@@ -217,6 +227,16 @@ class SimSession:
dict shape). Passing it auto-enables trace streaming. dict shape). Passing it auto-enables trace streaming.
quiet: if True, don't print frames (callback + `entries` still fill). 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._tag_model = tag
self._on_frame = on_frame self._on_frame = on_frame
self._quiet = quiet self._quiet = quiet

View File

@@ -128,3 +128,8 @@ class Transponder(abc.ABC):
for r in self.regions.values(): for r in self.regions.values():
if r.eml_offset >= 0: if r.eml_offset >= 0:
self._eml_push_region(r) self._eml_push_region(r)
@property
def uid(self) -> bytes:
"""Current tag UID."""
return self._parse_uid(self._uid)

View File

@@ -115,3 +115,25 @@ def test_14a_entries_accumulate_and_clear():
assert [e["direction"] for e in s.entries] == [0, 1] assert [e["direction"] for e in s.entries] == [0, 1]
s.clear() s.clear()
assert s.entries == [] 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())