Deletes 24 shim files from sim/. All test imports now point directly to canonical transponders/ and trace/ paths. sim/ contains only infrastructure (15 files): frame, memory, transponder ABC, reader ABC, medium, sim_session, table_compiler, replay, relay, fuzzer, pm3medium, mcu_bridge, mcu_protocol, dual_session, __init__. 751 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
342 lines
14 KiB
Python
342 lines
14 KiB
Python
"""Tests for ICODE SLIX (SL2S2002) transponder model."""
|
||
import asyncio
|
||
import struct
|
||
import pytest
|
||
|
||
from pm3py.sim.frame import RFFrame
|
||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import (
|
||
CMD_SET_EAS, CMD_RESET_EAS, CMD_LOCK_EAS, CMD_EAS_ALARM,
|
||
CMD_PASSWORD_PROTECT_EAS_AFI, CMD_WRITE_EAS_ID,
|
||
CMD_INVENTORY_READ, CMD_FAST_INVENTORY_READ,
|
||
)
|
||
|
||
|
||
def run(coro):
|
||
return asyncio.get_event_loop().run_until_complete(coro)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Helper: GET_RANDOM + XOR password
|
||
# ---------------------------------------------------------------------------
|
||
|
||
def _xor_password(tag, pwd_value: int) -> bytes:
|
||
"""Get random from tag, return XOR'd password bytes."""
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xB2, 0x04]))))
|
||
r = resp.data[1:3]
|
||
pwd = struct.pack("<I", pwd_value)
|
||
return bytes([pwd[0] ^ r[0], pwd[1] ^ r[1], pwd[2] ^ r[0], pwd[3] ^ r[1]])
|
||
|
||
|
||
def _authenticate(tag, pwd_id: int, pwd_value: int):
|
||
"""GET_RANDOM + SET_PASSWORD for a given password ID."""
|
||
xored = _xor_password(tag, pwd_value)
|
||
cmd = bytes([0x02, 0xB3, 0x04, pwd_id]) + xored
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00, f"auth failed for pwd_id {pwd_id:#x}"
|
||
|
||
|
||
PWD_EAS_AFI = 0x10
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Basic SLIX properties
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestIcodeSlixBasics:
|
||
def test_import(self):
|
||
"""IcodeSlixTag can be imported."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
|
||
def test_inherits_nxp_icode(self):
|
||
"""SLIX inherits from NxpIcodeTag."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
from pm3py.transponders.hf.iso15693.nxp.nxp_icode import NxpIcodeTag
|
||
assert issubclass(IcodeSlixTag, NxpIcodeTag)
|
||
|
||
def test_default_28_blocks(self):
|
||
"""SLIX defaults to 28 blocks × 4 bytes."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
assert tag._num_blocks == 28
|
||
assert tag._block_size == 4
|
||
|
||
def test_responds_to_inventory(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
inv = bytes([0x26, 0x01, 0x00])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
|
||
assert resp is not None
|
||
|
||
def test_read_write_block(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
# Write block 5
|
||
cmd = bytes([0x02, 0x21, 0x05, 0xAA, 0xBB, 0xCC, 0xDD])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
# Read it back
|
||
cmd = bytes([0x02, 0x20, 0x05])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[1:5] == bytes([0xAA, 0xBB, 0xCC, 0xDD])
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Single password (EAS/AFI only)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestIcodeSlixPassword:
|
||
def test_set_password_eas_afi(self):
|
||
"""SET_PASSWORD with correct EAS/AFI password succeeds."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
|
||
|
||
def test_set_password_wrong(self):
|
||
"""SET_PASSWORD with wrong password fails."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
xored = _xor_password(tag, 0x00000000)
|
||
cmd = bytes([0x02, 0xB3, 0x04, PWD_EAS_AFI]) + xored
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] & 0x01
|
||
|
||
def test_write_password(self):
|
||
"""WRITE_PASSWORD changes the EAS/AFI password."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
|
||
new_xored = _xor_password(tag, 0x11223344)
|
||
cmd = bytes([0x02, 0xB4, 0x04, PWD_EAS_AFI]) + new_xored
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
# New password works
|
||
_authenticate(tag, PWD_EAS_AFI, 0x11223344)
|
||
|
||
def test_lock_password(self):
|
||
"""LOCK_PASSWORD prevents WRITE_PASSWORD."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
|
||
cmd = bytes([0x02, 0xB5, 0x04, PWD_EAS_AFI])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
# WRITE_PASSWORD now fails
|
||
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
|
||
new_xored = _xor_password(tag, 0x11223344)
|
||
cmd = bytes([0x02, 0xB4, 0x04, PWD_EAS_AFI]) + new_xored
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] & 0x01
|
||
|
||
def test_no_privacy_password(self):
|
||
"""SLIX has no privacy — only EAS/AFI password accepted."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
# Try to auth with privacy pwd_id — should fail
|
||
xored = _xor_password(tag, 0xAABBCCDD)
|
||
cmd = bytes([0x02, 0xB3, 0x04, 0x04]) + xored # pwd_id=0x04 (privacy)
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] & 0x01
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# EAS subsystem
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestIcodeSlixEas:
|
||
def test_set_eas(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
assert tag.eas_enabled
|
||
|
||
def test_reset_eas(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
tag.set_eas(True)
|
||
cmd = bytes([0x02, CMD_RESET_EAS, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
assert not tag.eas_enabled
|
||
|
||
def test_eas_alarm(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
tag.set_eas(True)
|
||
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is not None
|
||
assert resp.data[0] == 0x00
|
||
assert len(resp.data) >= 33
|
||
|
||
def test_eas_alarm_silent_when_disabled(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is None
|
||
|
||
def test_lock_eas(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
tag.set_eas(True)
|
||
cmd = bytes([0x02, CMD_LOCK_EAS, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
# Can't change now
|
||
cmd = bytes([0x02, CMD_RESET_EAS, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] & 0x01
|
||
|
||
def test_password_protect_eas_afi(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
|
||
cmd = bytes([0x02, CMD_PASSWORD_PROTECT_EAS_AFI, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
assert tag._eas_password_protected
|
||
|
||
def test_write_eas_id(self):
|
||
"""WRITE EAS ID is SLIX2-only (EAS Selective per AN11809)."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
|
||
eas_afi_password=0xAABBCCDD)
|
||
run(tag.power_on())
|
||
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
|
||
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
assert tag._eas_id == 0x1234
|
||
|
||
def test_slix_rejects_write_eas_id(self):
|
||
"""SLIX (SL2S2002) does not support WRITE EAS ID (0xA7)."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is None
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# INVENTORY READ
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestIcodeSlixInventoryRead:
|
||
def test_inventory_read(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06")
|
||
run(tag.power_on())
|
||
cmd = bytes([0x06, CMD_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is not None
|
||
assert resp.data[0] == 0x00
|
||
|
||
def test_fast_inventory_read(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10\x03\x04\x05\x06")
|
||
run(tag.power_on())
|
||
cmd = bytes([0x06, CMD_FAST_INVENTORY_READ, 0x04, 0x00, 0x00, 0x00])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is not None
|
||
assert resp.data[0] == 0x00
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# SLIX2 still works after refactoring
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestSlix2StillWorks:
|
||
"""Verify SLIX2 inherits from SLIX and retains all features."""
|
||
|
||
def test_slix2_inherits_slix(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||
assert issubclass(IcodeSlix2Tag, IcodeSlixTag)
|
||
|
||
def test_slix2_privacy_still_works(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
|
||
privacy_password=0xDEADBEEF)
|
||
run(tag.power_on())
|
||
tag.enter_privacy_mode()
|
||
assert tag.privacy_mode
|
||
|
||
def test_slix2_destroy_still_works(self):
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
|
||
destroy_password=0xDEAD1234)
|
||
run(tag.power_on())
|
||
xored = _xor_password(tag, 0xDEAD1234)
|
||
cmd = bytes([0x02, 0xB9, 0x04]) + xored
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
# Dead
|
||
inv = bytes([0x26, 0x01, 0x00])
|
||
assert run(tag.handle_frame(RFFrame.from_bytes(inv))) is None
|
||
|
||
def test_slix2_eas_inherited_from_slix(self):
|
||
"""SLIX2 EAS works via inheritance from SLIX."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
||
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp.data[0] == 0x00
|
||
assert tag.eas_enabled
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# SLIX should NOT have SLIX2-only features
|
||
# ---------------------------------------------------------------------------
|
||
|
||
class TestSlixLacksPrivacy:
|
||
def test_no_enable_privacy(self):
|
||
"""SLIX does not respond to ENABLE PRIVACY (0xBA)."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, 0xBA, 0x04, 0x00, 0x00, 0x00, 0x00])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
# Should be None (unhandled) or error
|
||
assert resp is None or resp.data[0] & 0x01
|
||
|
||
def test_no_destroy(self):
|
||
"""SLIX does not respond to DESTROY (0xB9)."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, 0xB9, 0x04, 0x00, 0x00, 0x00, 0x00])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is None or resp.data[0] & 0x01
|
||
|
||
def test_no_protect_page(self):
|
||
"""SLIX does not respond to PROTECT PAGE (0xB6)."""
|
||
from pm3py.transponders.hf.iso15693.nxp.icode_slix import IcodeSlixTag
|
||
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
|
||
run(tag.power_on())
|
||
cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01])
|
||
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
|
||
assert resp is None or resp.data[0] & 0x01
|