Files
pm3py/tests/test_sim_nxp_icode.py
michael 668170457e feat: migrate sim framework from worktree to master
Migrates all 43 sim modules and 23 test files (~7.8k LOC, 686 tests)
from .worktrees/sim-framework/ into pm3py/sim/. Import fixes:
- 4 files: ..protocol/..transport → ..core.protocol/..core.transport
- trace_fmt.py: pm3py.hf_15 → pm3py.trace.ndef
- test_sim_pm3medium.py: flat imports → core.*
- test_sim_trace_fmt.py: flat imports → core.*
- Added sim Cmd entries to core/protocol.py (EML_SETMEM, SIM_TABLE_*)

751 tests passing (686 sim + 65 core).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 20:04:55 -07:00

1017 lines
40 KiB
Python

"""Tests for NFC Type 5, NXP ICODE platform, and ICODE SLIX2."""
import asyncio
import struct
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.medium import SoftwareMedium
from pm3py.sim.type5 import NfcType5Tag
from pm3py.sim.nxp_icode import NxpIcodeTag
from pm3py.sim.icode_slix import IcodeSlixTag
from pm3py.sim.icode_slix2 import (
IcodeSlix2Tag, PWD_EAS_AFI, PWD_READ, PWD_WRITE, PWD_PRIVACY, PWD_DESTROY,
)
from pm3py.sim.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,
)
from pm3py.sim.iso15693 import Reader15693
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
# ---------------------------------------------------------------------------
# NFC Type 5 Tag
# ---------------------------------------------------------------------------
class TestNfcType5Tag:
def test_cc_in_block_0(self):
"""Block 0 contains capability container with MBREAD flag."""
tag = NfcType5Tag(uid=bytes(8), ndef_message=b"\xD1\x01\x04\x54\x02enHi",
block_size=4, num_blocks=28)
run(tag.power_on())
read_cmd = bytes([0x02, 0x20, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
cc = resp.data[1:5]
assert cc[0] == 0xE1 # NDEF magic
assert cc[1] == 0x40 # version 1.0, read/write
assert cc[3] & 0x01 # MBREAD supported — required for phone NDEF reads
def test_ndef_tlv_in_block_1(self):
"""NDEF TLV starts in block 1."""
msg = b"\xD1\x01\x04\x54\x02enHi"
tag = NfcType5Tag(uid=bytes(8), ndef_message=msg, block_size=4, num_blocks=28)
run(tag.power_on())
read_cmd = bytes([0x02, 0x20, 0x01])
resp = run(tag.handle_frame(RFFrame.from_bytes(read_cmd)))
data = resp.data[1:5]
assert data[0] == 0x03 # NDEF TLV type
assert data[1] == len(msg)
def test_inherits_15693_inventory(self):
"""Type 5 tags respond to ISO 15693 inventory."""
medium = SoftwareMedium()
tag = NfcType5Tag(uid=b"\xE0\x04\x01\x02\x03\x04\x05\x06",
ndef_message=b"")
run(medium.attach(tag))
reader = Reader15693(medium)
tags = run(reader.inventory())
assert len(tags) == 1
# ---------------------------------------------------------------------------
# NXP ICODE platform (shared features)
# ---------------------------------------------------------------------------
class TestNxpIcodeTag:
def test_inherits_type5(self):
tag = NxpIcodeTag(uid=bytes(8))
run(tag.power_on())
# Should respond to inventory
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is not None
def test_get_random_number(self):
"""NXP GET_RANDOM_NUMBER (0xB2) returns 2 random bytes."""
tag = NxpIcodeTag(uid=b"\xE0\x04" + bytes(6))
run(tag.power_on())
cmd = bytes([0x02, 0xB2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert len(resp.data) >= 3 # flags + 2 random bytes
def test_get_nxp_system_info(self):
"""GET NXP SYSTEM INFO (0xAB) returns PP + feature data on SLIX2."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
cmd = bytes([0x02, 0xAB, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success flags
assert len(resp.data) >= 8 # flags + PP + PP_cond + lock + feature_flags
def test_read_signature(self):
"""READ SIGNATURE (0xBD) returns 32-byte ECC signature on SLIX2."""
sig = bytes(range(32))
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), signature=sig)
run(tag.power_on())
cmd = bytes([0x02, 0xBD, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
assert resp.data[1:] == sig
def test_read_signature_default_zeros(self):
"""READ SIGNATURE returns 32 zero bytes when no signature configured on SLIX2."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
cmd = bytes([0x02, 0xBD, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert len(resp.data) == 33 # flags + 32 bytes
assert resp.data[1:] == bytes(32)
def test_wrong_mfg_code_ignored(self):
"""Commands with wrong manufacturer code return None."""
tag = NxpIcodeTag(uid=bytes(8))
run(tag.power_on())
cmd = bytes([0x02, 0xB2, 0x05]) # mfg=0x05, not NXP
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# Should fall through (not an NXP custom command match)
# Depending on base class behavior, may return None or error
# The key is it shouldn't return a valid GET_RANDOM response
def test_slix_rejects_get_nxp_system_info(self):
"""SLIX (SL2S2002) does NOT support GET NXP SYSTEM INFO (0xAB)."""
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, 0xAB, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None
def test_slix_rejects_read_signature(self):
"""SLIX (SL2S2002) does NOT support READ SIGNATURE (0xBD)."""
tag = IcodeSlixTag(uid=b"\xE0\x04\x01\x10" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, 0xBD, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None
# ---------------------------------------------------------------------------
# ICODE SLIX2
# ---------------------------------------------------------------------------
class TestIcodeSlix2Tag:
def test_inherits_nxp_icode(self):
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
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_privacy_mode_hides_uid(self):
"""In privacy mode, tag doesn't respond to inventory."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag.enter_privacy_mode()
assert tag.privacy_mode
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is None # hidden
def test_privacy_mode_allows_get_random(self):
"""In privacy mode, GET_RANDOM still works (needed for password XOR)."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag.enter_privacy_mode()
cmd = bytes([0x02, 0xB2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert len(resp.data) >= 3
def test_privacy_mode_unlock_with_xor(self):
"""SET_PASSWORD with correct XOR'd privacy password disables privacy mode."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag.enter_privacy_mode()
# First get random for XOR
cmd = bytes([0x02, 0xB2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
r = resp.data[1:3]
# XOR the password
pwd = struct.pack("<I", 0xDEADBEEF)
xored = bytes([pwd[0] ^ r[0], pwd[1] ^ r[1], pwd[2] ^ r[0], pwd[3] ^ r[1]])
cmd = bytes([0x02, 0xB3, 0x04, 0x04]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
assert not tag.privacy_mode
def test_enable_privacy_command(self):
"""ENABLE_PRIVACY (0xBA) enters privacy mode with XOR'd password."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
assert not tag.privacy_mode
# Get random
cmd = bytes([0x02, 0xB2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
r = resp.data[1:3]
# XOR the password
pwd = struct.pack("<I", 0xDEADBEEF)
xored = bytes([pwd[0] ^ r[0], pwd[1] ^ r[1], pwd[2] ^ r[0], pwd[3] ^ r[1]])
cmd = bytes([0x02, 0xBA, 0x04]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert tag.privacy_mode
def test_enable_privacy_wrong_password(self):
"""ENABLE_PRIVACY with wrong password fails."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
# Wrong password (not XOR'd correctly)
cmd = bytes([0x02, 0xBA, 0x04, 0x00, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01 # error flag
assert not tag.privacy_mode
def test_wrong_password(self):
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag.enter_privacy_mode()
cmd = bytes([0x02, 0xB3, 0x04, 0x04]) + struct.pack("<I", 0x00000000)
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None or (resp.data[0] & 0x01) # error
assert tag.privacy_mode # still private
def test_eas_flag(self):
"""EAS (Electronic Article Surveillance) state management."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
assert not tag.eas_enabled
tag.set_eas(True)
assert tag.eas_enabled
tag.set_eas(False)
assert not tag.eas_enabled
def test_eas_afi_password(self):
"""EAS/AFI password (pwd_id 0x10) is supported."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0x11223344)
assert PWD_EAS_AFI in tag._passwords
assert tag._passwords[PWD_EAS_AFI] == 0x11223344
def test_password_protection_read(self):
"""Read password controls read access to protected blocks."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), read_password=0xAABBCCDD,
protection_pointer=4) # blocks 4+ protected
run(tag.power_on())
# Read unprotected block 0 — should work
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x20, 0x00]))))
assert resp is not None and resp.data[0] == 0x00
# Read protected block 4 — should fail without password
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x20, 0x04]))))
assert resp is not None and resp.data[0] & 0x01 # error flag
def test_get_nxp_system_info_with_protection(self):
"""GET NXP SYSTEM INFO reflects protection pointer."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), protection_pointer=10)
run(tag.power_on())
cmd = bytes([0x02, 0xAB, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
assert resp.data[1] == 10 # PP = 10
assert resp.data[2] & 0x01 # read protection condition set
def test_protect_page(self):
"""PROTECT PAGE (0xB6) sets protection pointer after auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
write_password=0x12345678)
run(tag.power_on())
# Authenticate with write password first
cmd = bytes([0x02, 0xB2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
r = resp.data[1:3]
pwd = struct.pack("<I", 0x12345678)
xored = bytes([pwd[0] ^ r[0], pwd[1] ^ r[1], pwd[2] ^ r[0], pwd[3] ^ r[1]])
cmd = bytes([0x02, 0xB3, 0x04, 0x02]) + xored # pwd_id=0x02 (write)
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00 # auth success
# Now PROTECT PAGE
cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01]) # PP=8, cond=read_prot
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert tag._protection_pointer == 8
def test_protect_page_requires_auth(self):
"""PROTECT PAGE fails without write password auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
write_password=0x12345678)
run(tag.power_on())
cmd = bytes([0x02, 0xB6, 0x04, 0x08, 0x01])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01 # error — not authenticated
def test_read_signature_inherited(self):
"""SLIX2 inherits READ_SIGNATURE from NxpIcodeTag."""
sig = b"\xAA" * 32
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5), signature=sig)
run(tag.power_on())
cmd = bytes([0x02, 0xBD, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert resp.data[1:] == sig
# ---------------------------------------------------------------------------
# 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}"
# ---------------------------------------------------------------------------
# WRITE PASSWORD (0xB4)
# ---------------------------------------------------------------------------
class TestWritePassword:
def test_write_password_changes_value(self):
"""WRITE PASSWORD (0xB4) changes password after auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111)
run(tag.power_on())
# Auth with current read password
_authenticate(tag, PWD_READ, 0x11111111)
# Write new password (XOR'd with fresh random)
new_xored = _xor_password(tag, 0x22222222)
cmd = bytes([0x02, 0xB4, 0x04, PWD_READ]) + new_xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
# Old password should fail, new should work
# Need fresh random for each attempt
xored_old = _xor_password(tag, 0x11111111)
cmd = bytes([0x02, 0xB3, 0x04, PWD_READ]) + xored_old
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01 # error — old password rejected
xored_new = _xor_password(tag, 0x22222222)
cmd = bytes([0x02, 0xB3, 0x04, PWD_READ]) + xored_new
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00 # new password works
def test_write_password_requires_auth(self):
"""WRITE PASSWORD fails without prior SET_PASSWORD."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111)
run(tag.power_on())
new_xored = _xor_password(tag, 0x22222222)
cmd = bytes([0x02, 0xB4, 0x04, PWD_READ]) + new_xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01 # error
def test_write_password_wrong_pwd_id(self):
"""WRITE PASSWORD for non-existent password fails."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111)
run(tag.power_on())
_authenticate(tag, PWD_READ, 0x11111111)
# Try to write a password that doesn't exist (destroy)
new_xored = _xor_password(tag, 0x33333333)
cmd = bytes([0x02, 0xB4, 0x04, PWD_DESTROY]) + new_xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01 # error — not authed for destroy
# ---------------------------------------------------------------------------
# LOCK PASSWORD (0xB5)
# ---------------------------------------------------------------------------
class TestLockPassword:
def test_lock_password_prevents_write(self):
"""LOCK PASSWORD (0xB5) permanently prevents WRITE PASSWORD."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111)
run(tag.power_on())
# Auth and lock the read password
_authenticate(tag, PWD_READ, 0x11111111)
cmd = bytes([0x02, 0xB5, 0x04, PWD_READ])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
# Re-auth (still works — password itself unchanged)
_authenticate(tag, PWD_READ, 0x11111111)
# WRITE PASSWORD should now fail even though authenticated
new_xored = _xor_password(tag, 0x22222222)
cmd = bytes([0x02, 0xB4, 0x04, PWD_READ]) + new_xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01 # error — locked
def test_lock_password_requires_auth(self):
"""LOCK PASSWORD fails without prior auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111)
run(tag.power_on())
cmd = bytes([0x02, 0xB5, 0x04, PWD_READ])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01 # error
def test_lock_password_irreversible(self):
"""Once locked, password stays locked even after re-auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
write_password=0xAAAAAAAA)
run(tag.power_on())
_authenticate(tag, PWD_WRITE, 0xAAAAAAAA)
cmd = bytes([0x02, 0xB5, 0x04, PWD_WRITE])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Re-auth and try lock again — should still be locked (idempotent ok)
_authenticate(tag, PWD_WRITE, 0xAAAAAAAA)
assert PWD_WRITE in tag._locked_passwords
# ---------------------------------------------------------------------------
# LOCK PAGE PROTECTION (0xB7)
# ---------------------------------------------------------------------------
class TestLockPageProtection:
def test_lock_page_protection(self):
"""LOCK PAGE PROTECTION (0xB7) permanently locks PP config."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
write_password=0x12345678,
protection_pointer=8)
run(tag.power_on())
_authenticate(tag, PWD_WRITE, 0x12345678)
cmd = bytes([0x02, 0xB7, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
# PROTECT PAGE should now fail
_authenticate(tag, PWD_WRITE, 0x12345678)
cmd = bytes([0x02, 0xB6, 0x04, 0x04, 0x01])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01 # error — locked
def test_lock_page_protection_requires_auth(self):
"""LOCK PAGE PROTECTION fails without write auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
write_password=0x12345678)
run(tag.power_on())
cmd = bytes([0x02, 0xB7, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# DESTROY (0xB9)
# ---------------------------------------------------------------------------
class TestDestroy:
def test_destroy_kills_tag(self):
"""DESTROY (0xB9) permanently kills tag — no more responses."""
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 is not None
assert resp.data[0] == 0x00 # success
# Tag should not respond to anything now
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is None
def test_destroy_wrong_password(self):
"""DESTROY with wrong password fails, tag still alive."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
destroy_password=0xDEAD1234)
run(tag.power_on())
xored = _xor_password(tag, 0x00000000) # wrong
cmd = bytes([0x02, 0xB9, 0x04]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01 # error
# Tag still alive
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is not None
def test_destroy_no_password_configured(self):
"""DESTROY fails if no destroy password configured."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
xored = _xor_password(tag, 0x00000000)
cmd = bytes([0x02, 0xB9, 0x04]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# 64-BIT PASSWORD PROTECTION (0xBB)
# ---------------------------------------------------------------------------
class TestPasswordProtection64Bit:
def test_64bit_requires_both_passwords(self):
"""64-BIT PASSWORD PROTECTION (0xBB) needs both read + write auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111,
write_password=0x22222222,
protection_pointer=4)
run(tag.power_on())
# Auth both
_authenticate(tag, PWD_READ, 0x11111111)
_authenticate(tag, PWD_WRITE, 0x22222222)
cmd = bytes([0x02, 0xBB, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00 # success
assert tag._extended_protection
def test_64bit_fails_without_read_auth(self):
"""64-BIT PASSWORD PROTECTION fails with only write auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111,
write_password=0x22222222)
run(tag.power_on())
_authenticate(tag, PWD_WRITE, 0x22222222)
cmd = bytes([0x02, 0xBB, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
def test_64bit_fails_without_write_auth(self):
"""64-BIT PASSWORD PROTECTION fails with only read auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
read_password=0x11111111,
write_password=0x22222222)
run(tag.power_on())
_authenticate(tag, PWD_READ, 0x11111111)
cmd = bytes([0x02, 0xBB, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# SET EAS (0xA2) / RESET EAS (0xA3)
# ---------------------------------------------------------------------------
class TestSetResetEas:
def test_set_eas_with_auth(self):
"""SET EAS (0xA2) enables EAS after EAS/AFI password auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
assert not tag.eas_enabled
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert tag.eas_enabled
def test_set_eas_requires_auth_when_protected(self):
"""SET EAS fails without auth when password protection is enabled."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
tag._eas_password_protected = True
cmd = bytes([0x02, CMD_SET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
def test_set_eas_without_password(self):
"""SET EAS succeeds without auth when no password protection enabled."""
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 is not None
assert resp.data[0] == 0x00
assert tag.eas_enabled
def test_reset_eas(self):
"""RESET EAS (0xA3) disables EAS."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
tag.set_eas(True)
assert tag.eas_enabled
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, CMD_RESET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert not tag.eas_enabled
# ---------------------------------------------------------------------------
# LOCK EAS (0xA4)
# ---------------------------------------------------------------------------
class TestLockEas:
def test_lock_eas_prevents_changes(self):
"""LOCK EAS (0xA4) permanently locks EAS state."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
tag.set_eas(True)
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, CMD_LOCK_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# SET EAS / RESET EAS should now fail
_authenticate(tag, PWD_EAS_AFI, 0xAABBCCDD)
cmd = bytes([0x02, CMD_RESET_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
assert tag.eas_enabled # still enabled — locked
def test_lock_eas_requires_auth(self):
"""LOCK EAS fails without EAS/AFI auth when protected."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
tag._eas_password_protected = True
cmd = bytes([0x02, CMD_LOCK_EAS, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# EAS ALARM (0xA5)
# ---------------------------------------------------------------------------
class TestEasAlarm:
def test_eas_alarm_when_enabled(self):
"""EAS ALARM (0xA5) returns EAS sequence when EAS is enabled."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
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 # flags + 32-byte EAS sequence
def test_eas_alarm_when_disabled(self):
"""EAS ALARM returns no response when EAS is disabled."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
assert not tag.eas_enabled
cmd = bytes([0x02, CMD_EAS_ALARM, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None
def test_eas_alarm_custom_id(self):
"""EAS ALARM includes EAS ID when set."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
tag.set_eas(True)
tag._eas_id = 0x1234
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
# ---------------------------------------------------------------------------
# PASSWORD PROTECT EAS/AFI (0xA6)
# ---------------------------------------------------------------------------
class TestPasswordProtectEasAfi:
def test_enable_eas_afi_protection(self):
"""PASSWORD PROTECT EAS/AFI (0xA6) enables password protection."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
assert not tag._eas_password_protected
_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 is not None
assert resp.data[0] == 0x00
assert tag._eas_password_protected
def test_eas_afi_protection_requires_auth(self):
"""PASSWORD PROTECT EAS/AFI fails without auth."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
cmd = bytes([0x02, CMD_PASSWORD_PROTECT_EAS_AFI, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# WRITE EAS ID (0xA7)
# ---------------------------------------------------------------------------
class TestWriteEasId:
def test_write_eas_id(self):
"""WRITE EAS ID (0xA7) sets 16-bit EAS identifier."""
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 is not None
assert resp.data[0] == 0x00
assert tag._eas_id == 0x1234
def test_write_eas_id_requires_auth_when_protected(self):
"""WRITE EAS ID fails without auth when protected."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
eas_afi_password=0xAABBCCDD)
run(tag.power_on())
tag._eas_password_protected = True
cmd = bytes([0x02, CMD_WRITE_EAS_ID, 0x04, 0x34, 0x12])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# STAY QUIET PERSISTENT (0xBC)
# ---------------------------------------------------------------------------
class TestStayQuietPersistent:
@staticmethod
def _sqp_cmd(uid: bytes) -> bytes:
"""Build addressed STAY QUIET PERSISTENT command."""
# flags=0x22 (addressed + high data rate), cmd=0xBC, UID reversed, mfg=0x04
return bytes([0x22, 0xBC]) + uid[::-1] + bytes([0x04])
def test_stay_quiet_persistent(self):
"""STAY QUIET PERSISTENT (0xBC) silences tag persistently."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
cmd = self._sqp_cmd(tag._uid)
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# Tag should not respond to inventory
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is None
def test_stay_quiet_persistent_survives_power_cycle(self):
"""STAY QUIET PERSISTENT survives power-off/on."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5))
run(tag.power_on())
cmd = self._sqp_cmd(tag._uid)
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Simulate power cycle
run(tag.power_off())
run(tag.power_on())
# Still quiet
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is None
def test_stay_quiet_persistent_reset_by_password(self):
"""SET_PASSWORD still works even in persistent quiet (addressed)."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
privacy_password=0xDEADBEEF)
run(tag.power_on())
cmd = self._sqp_cmd(tag._uid)
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# SET_PASSWORD should still work (unaddressed custom cmds still handled)
xored = _xor_password(tag, 0xDEADBEEF)
cmd = bytes([0x02, 0xB3, 0x04, PWD_PRIVACY]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# ---------------------------------------------------------------------------
# INVENTORY READ (0xA0) / FAST INVENTORY READ (0xA1)
# ---------------------------------------------------------------------------
class TestInventoryRead:
def test_inventory_read_returns_data(self):
"""INVENTORY READ (0xA0) returns UID + block data."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02\x01\x03\x04\x05\x06",
block_size=4, num_blocks=28)
run(tag.power_on())
# Write known data to block 0
write_cmd = bytes([0x02, 0x21, 0x00, 0xAA, 0xBB, 0xCC, 0xDD])
run(tag.handle_frame(RFFrame.from_bytes(write_cmd)))
# INVENTORY READ: flags(1) + cmd(1) + mfg(1) + mask_len(1) + first_block(1) + num_blocks(1)
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 # success flags
# Response contains DSFID + UID + block data
assert len(resp.data) > 10
def test_fast_inventory_read(self):
"""FAST INVENTORY READ (0xA1) same behavior as INVENTORY READ."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02\x01\x03\x04\x05\x06",
block_size=4, num_blocks=28)
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
def test_inventory_read_specific_blocks(self):
"""INVENTORY READ reads specific block range."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02\x01\x03\x04\x05\x06",
block_size=4, num_blocks=28)
run(tag.power_on())
# Write known data
write_cmd = bytes([0x02, 0x21, 0x02, 0x11, 0x22, 0x33, 0x44])
run(tag.handle_frame(RFFrame.from_bytes(write_cmd)))
# Read block 2, 1 block
cmd = bytes([0x06, CMD_INVENTORY_READ, 0x04, 0x00, 0x02, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
# Response should contain the written data
assert b"\x11\x22\x33\x44" in resp.data
# ---------------------------------------------------------------------------
# Block 79 counter (increment-only write semantics)
# ---------------------------------------------------------------------------
class TestBlock79Counter:
def test_counter_increments(self):
"""WRITE to block 79 increments the 16-bit counter, not overwrites."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
block_size=4, num_blocks=80)
run(tag.power_on())
# Write increment value of 1
cmd = bytes([0x02, 0x21, 79, 0x01, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# Read back — counter should be 1
cmd = bytes([0x02, 0x20, 79])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = struct.unpack_from("<H", resp.data, 1)[0]
assert counter == 1
def test_counter_accumulates(self):
"""Multiple writes to block 79 accumulate."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
block_size=4, num_blocks=80)
run(tag.power_on())
for _ in range(5):
cmd = bytes([0x02, 0x21, 79, 0x01, 0x00, 0x00, 0x00])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x20, 79])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = struct.unpack_from("<H", resp.data, 1)[0]
assert counter == 5
def test_counter_saturates_at_max(self):
"""Counter saturates at 0xFFFF, does not wrap."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
block_size=4, num_blocks=80)
run(tag.power_on())
# Set counter close to max via direct memory
offset = 79 * 4
tag._memory[offset:offset + 2] = struct.pack("<H", 0xFFFE)
# Increment by 1 → 0xFFFF
cmd = bytes([0x02, 0x21, 79, 0x01, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Increment again — should stay at 0xFFFF
cmd = bytes([0x02, 0x21, 79, 0x01, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
cmd = bytes([0x02, 0x20, 79])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = struct.unpack_from("<H", resp.data, 1)[0]
assert counter == 0xFFFF
def test_normal_blocks_still_overwrite(self):
"""Non-counter blocks still use normal overwrite semantics."""
tag = IcodeSlix2Tag(uid=b"\xE0\x04\x02" + bytes(5),
block_size=4, num_blocks=80)
run(tag.power_on())
# Write to block 0 twice — should overwrite, not increment
cmd = bytes([0x02, 0x21, 0x00, 0xAA, 0xBB, 0xCC, 0xDD])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x21, 0x00, 0x11, 0x22, 0x33, 0x44])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x20, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[1:5] == bytes([0x11, 0x22, 0x33, 0x44])