Files
pm3py/tests/test_sim_icode3.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

639 lines
25 KiB
Python

"""Tests for ICODE 3 (SL2S3003) transponder model."""
import asyncio
import struct
import pytest
from pm3py.sim.frame import RFFrame
from pm3py.sim.nxp_icode import (
CMD_SET_EAS, CMD_READ_CONFIG, CMD_WRITE_CONFIG, CMD_READ_TT,
)
def run(coro):
return asyncio.get_event_loop().run_until_complete(coro)
def _xor_password(tag, pwd_value: int) -> 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):
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_READ = 0x01
PWD_WRITE = 0x02
PWD_PRIVACY = 0x04
PWD_DESTROY = 0x08
PWD_EAS_AFI = 0x10
PWD_CONFIG = 0x20
# ---------------------------------------------------------------------------
# Basics
# ---------------------------------------------------------------------------
class TestIcode3Basics:
def test_import(self):
from pm3py.sim.icode3 import Icode3Tag
def test_inherits_slix2(self):
from pm3py.sim.icode3 import Icode3Tag
from pm3py.sim.icode_slix2 import IcodeSlix2Tag
assert issubclass(Icode3Tag, IcodeSlix2Tag)
def test_default_76_blocks(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
assert tag._num_blocks == 76
def test_responds_to_inventory(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + 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
# ---------------------------------------------------------------------------
# 6th password (config, pwd_id 0x20)
# ---------------------------------------------------------------------------
class TestIcode3ConfigPassword:
def test_config_password_auth(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
config_password=0xDEADBEEF)
run(tag.power_on())
_authenticate(tag, PWD_CONFIG, 0xDEADBEEF)
def test_config_password_wrong(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
config_password=0xDEADBEEF)
run(tag.power_on())
xored = _xor_password(tag, 0x00000000)
cmd = bytes([0x02, 0xB3, 0x04, PWD_CONFIG]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# Inherited SLIX2 features still work
# ---------------------------------------------------------------------------
class TestIcode3InheritsSlix2:
def test_eas(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + 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_privacy(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
privacy_password=0xAAAAAAAA)
run(tag.power_on())
tag.enter_privacy_mode()
assert tag.privacy_mode
def test_destroy(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
destroy_password=0xBBBBBBBB)
run(tag.power_on())
xored = _xor_password(tag, 0xBBBBBBBB)
cmd = bytes([0x02, 0xB9, 0x04]) + xored
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00])))) is None
# ---------------------------------------------------------------------------
# 24-bit counter (block 75)
# ---------------------------------------------------------------------------
class TestIcode3Counter:
def test_counter_increments_24bit(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Write increment of 1 to block 75
cmd = bytes([0x02, 0x21, 75, 0x01, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
# Read back
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = int.from_bytes(resp.data[1:4], 'little')
assert counter == 1
def test_counter_accumulates(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
for _ in range(10):
cmd = bytes([0x02, 0x21, 75, 0x01, 0x00, 0x00, 0x00])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = int.from_bytes(resp.data[1:4], 'little')
assert counter == 10
def test_counter_saturates_at_24bit(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Set counter near max
offset = 75 * 4
tag._memory[offset:offset + 3] = (0xFFFFFE).to_bytes(3, 'little')
cmd = bytes([0x02, 0x21, 75, 0x01, 0x00, 0x00, 0x00])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# Now at 0xFFFFFF
cmd = bytes([0x02, 0x21, 75, 0x01, 0x00, 0x00, 0x00])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# Should saturate
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = int.from_bytes(resp.data[1:4], 'little')
assert counter == 0xFFFFFF
def test_normal_blocks_still_overwrite(self):
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
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])
# ---------------------------------------------------------------------------
# READ CONFIG (0xC0) / WRITE CONFIG (0xC1)
# ---------------------------------------------------------------------------
class TestIcode3Config:
def test_read_config(self):
"""READ CONFIG returns config memory blocks."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Read config block 16 (DSFID), 1 block
cmd = bytes([0x02, CMD_READ_CONFIG, 0x04, 16, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert len(resp.data) >= 5 # flags + 4 bytes
def test_write_config(self):
"""WRITE CONFIG writes to config memory."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Write to config block 18 (EAS ID)
cmd = bytes([0x02, CMD_WRITE_CONFIG, 0x04, 18, 0x34, 0x12, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# Read it back
cmd = bytes([0x02, CMD_READ_CONFIG, 0x04, 18, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[1:3] == bytes([0x34, 0x12])
def test_read_config_passwords_masked(self):
"""READ CONFIG masks password blocks with 0x00."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
read_password=0xDEADBEEF)
run(tag.power_on())
# Read config block 42 (READ_PWD) — should be masked
cmd = bytes([0x02, CMD_READ_CONFIG, 0x04, 42, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert resp.data[1:5] == bytes(4) # masked with 0x00
def test_write_config_requires_auth_when_protected(self):
"""WRITE CONFIG fails without config password when protected."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4),
config_password=0xAABBCCDD)
run(tag.power_on())
tag._config_password_protected = True
cmd = bytes([0x02, CMD_WRITE_CONFIG, 0x04, 18, 0x34, 0x12, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] & 0x01
def test_read_config_multiple_blocks(self):
"""READ CONFIG can read multiple blocks."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Read 3 blocks starting at block 16
cmd = bytes([0x02, CMD_READ_CONFIG, 0x04, 16, 0x02])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
assert len(resp.data) >= 13 # flags + 3 * 4 bytes
# ---------------------------------------------------------------------------
# READ TT (0xC4) — TagTamper
# ---------------------------------------------------------------------------
class TestIcode3TagTamper:
def test_read_tt_closed(self):
"""READ TT returns closed status by default."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=True)
run(tag.power_on())
cmd = bytes([0x02, CMD_READ_TT, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# actual_status=0x43('C'), stored_status=0x43('C')
assert resp.data[1] == 0x43 # 'C' = Closed
assert resp.data[2] == 0x43
def test_read_tt_open(self):
"""READ TT returns open status when tampered."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=True)
run(tag.power_on())
tag._tt_status_actual = 0x4F # 'O' = Open
cmd = bytes([0x02, CMD_READ_TT, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[1] == 0x4F # 'O'
def test_read_tt_not_supported_without_flag(self):
"""READ TT returns error when tag_tamper=False."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), tag_tamper=False)
run(tag.power_on())
cmd = bytes([0x02, CMD_READ_TT, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None or resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# GET NXP SYSTEM INFO feature flags
# ---------------------------------------------------------------------------
class TestIcode3SystemInfo:
def test_feature_flags(self):
"""GET NXP SYSTEM INFO returns ICODE 3 feature flags."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
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
# Feature flags should indicate counter, privacy, destroy, etc.
feature_flags = struct.unpack_from("<I", resp.data, 4)[0]
assert feature_flags & (1 << 1) # COUNTER
assert feature_flags & (1 << 8) # ORIGINALITY SIG
assert feature_flags & (1 << 10) # P QUIET
assert feature_flags & (1 << 12) # PRIVACY
assert feature_flags & (1 << 13) # DESTROY
# ---------------------------------------------------------------------------
# NFC ASCII mirror
# ---------------------------------------------------------------------------
class TestIcode3NfcMirror:
def test_uid_mirror(self):
"""UID mirror overlays 16 ASCII hex bytes into user memory on read."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on())
# Enable UID mirror at block 10, byte 0
tag.set_nfc_mirror(sel=1, block=10, byte_offset=0)
# Read block 10 — should contain ASCII hex of UID
cmd = bytes([0x02, 0x20, 10])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# UID E0040102... MSB first → "E0040102" as ASCII = 0x45 0x30 0x30 0x34
data = resp.data[1:5]
assert data == b"E004"
def test_uid_counter_mirror(self):
"""UID + counter mirror: 16 bytes UID + 'x' + 6 bytes counter."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on())
# Set counter to 0x000042 (66 decimal)
offset = 75 * 4
tag._memory[offset:offset + 3] = (0x42).to_bytes(3, 'little')
tag.set_nfc_mirror(sel=2, block=10, byte_offset=0)
# Read blocks 10-15 to get full mirror
mirror_data = bytearray()
for blk in range(10, 16):
cmd = bytes([0x02, 0x20, blk])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
mirror_data.extend(resp.data[1:5])
# First 16 bytes = UID hex: "E004012003040506"
assert mirror_data[:16] == b"E004012003040506"
# Separator
assert mirror_data[16:17] == b"x"
# Counter 6 bytes hex MSB first: 0x000042 → "000042"
assert mirror_data[17:23] == b"000042"
def test_mirror_disabled(self):
"""With mirror disabled (sel=0), reads return physical memory."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on())
tag.set_nfc_mirror(sel=0, block=10, byte_offset=0)
# Write data to block 10
cmd = bytes([0x02, 0x21, 10, 0xAA, 0xBB, 0xCC, 0xDD])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# Read back — should be physical data
cmd = bytes([0x02, 0x20, 10])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[1:5] == bytes([0xAA, 0xBB, 0xCC, 0xDD])
def test_mirror_with_byte_offset(self):
"""Mirror starting at byte 2 within a block."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on())
# Write known data first
cmd = bytes([0x02, 0x21, 10, 0xFF, 0xFF, 0xFF, 0xFF])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
# UID mirror at block 10, byte 2
tag.set_nfc_mirror(sel=1, block=10, byte_offset=2)
cmd = bytes([0x02, 0x20, 10])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
data = resp.data[1:5]
# Bytes 0-1 = physical (0xFF, 0xFF), bytes 2-3 = mirror start ("E0")
assert data[0:2] == bytes([0xFF, 0xFF])
assert data[2:4] == b"E0"
def test_mirror_outside_block_returns_physical(self):
"""Blocks before mirror region return physical memory."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on())
tag.set_nfc_mirror(sel=1, block=20, byte_offset=0)
cmd = bytes([0x02, 0x21, 5, 0x11, 0x22, 0x33, 0x44])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x20, 5])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[1:5] == bytes([0x11, 0x22, 0x33, 0x44])
# ---------------------------------------------------------------------------
# Privacy mode 2 + PICK RANDOM ID
# ---------------------------------------------------------------------------
class TestIcode3PrivacyMode2:
def test_privacy_mode2_inventory_zeroed_uid(self):
"""In privacy mode 2, inventory returns zeroed UID."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag._privacy_mode_sel = 2
tag.enter_privacy_mode()
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
assert resp is not None
# UID should be E0 04 00 00 00 00 00 00 (reversed in response)
uid_in_resp = resp.data[2:10]
uid_msb = bytes(reversed(uid_in_resp))
assert uid_msb == b"\xE0\x04\x00\x00\x00\x00\x00\x00"
def test_privacy_mode2_allows_reads(self):
"""Privacy mode 2 allows READ SINGLE BLOCK."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF)
run(tag.power_on())
# Write data first
cmd = bytes([0x02, 0x21, 0, 0xAA, 0xBB, 0xCC, 0xDD])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
tag._privacy_mode_sel = 2
tag.enter_privacy_mode()
# Read should work in privacy mode 2
cmd = bytes([0x02, 0x20, 0])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
def test_privacy_mode1_blocks_reads(self):
"""Privacy mode 1 blocks everything except GET_RANDOM/SET_PASSWORD."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag._privacy_mode_sel = 1
tag.enter_privacy_mode()
# Read should fail in privacy mode 1
cmd = bytes([0x02, 0x20, 0])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None
def test_pick_random_id(self):
"""PICK RANDOM ID (0xC2) generates random UID for inventory."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06",
privacy_password=0xDEADBEEF)
run(tag.power_on())
tag._privacy_mode_sel = 2
tag.enter_privacy_mode()
# PICK RANDOM ID
cmd = bytes([0x02, 0xC2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is not None
assert resp.data[0] == 0x00
# Now inventory should return the random ID, not the zeroed one
inv = bytes([0x26, 0x01, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(inv)))
uid_in_resp = bytes(reversed(resp.data[2:10]))
# Random ID format: E0 04 00 00 CID1 CID0 RID1 RID0
assert uid_in_resp[0:2] == b"\xE0\x04"
assert uid_in_resp[2:4] == b"\x00\x00"
# Last 4 bytes should be non-zero (random + CID)
# (statistically near-impossible to be all zero)
def test_pick_random_id_fails_outside_privacy(self):
"""PICK RANDOM ID fails when not in privacy mode."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20\x03\x04\x05\x06")
run(tag.power_on())
cmd = bytes([0x02, 0xC2, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp is None or resp.data[0] & 0x01
# ---------------------------------------------------------------------------
# 48-byte originality signature (SL2S3003 8.5.3.20)
# ---------------------------------------------------------------------------
class TestIcode3Signature48:
def test_default_32_byte_signature(self):
"""Default READ SIGNATURE returns 32 bytes (1 flag + 32 sig = 33)."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
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 len(resp.data) == 33 # 1 flag + 32 signature
def test_48_byte_signature_mode(self):
"""With 48-byte mode enabled, READ SIGNATURE returns 49 bytes."""
from pm3py.sim.icode3 import Icode3Tag
sig48 = bytes(range(48))
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48)
run(tag.power_on())
tag.set_signature_mode_48(True)
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 len(resp.data) == 49 # 1 flag + 48 signature
assert resp.data[1:] == sig48
def test_48_byte_mode_disabled_truncates(self):
"""With 48-byte mode disabled, READ SIGNATURE returns first 32 bytes."""
from pm3py.sim.icode3 import Icode3Tag
sig48 = bytes(range(48))
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4), signature=sig48)
run(tag.power_on())
# Mode defaults to 32-byte
cmd = bytes([0x02, 0xBD, 0x04])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert len(resp.data) == 33 # 1 flag + 32 signature
# ---------------------------------------------------------------------------
# 24-bit counter preset vs increment (SL2S3003 8.5.3.25)
# ---------------------------------------------------------------------------
class TestIcode3CounterPreset:
def test_value_0x000001_increments(self):
"""Writing 0x000001 to block 75 increments counter by 1."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Counter starts at 0, write 0x000001 → increment by 1 → 1
cmd = bytes([0x02, 0x21, 75, 0x01, 0x00, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = int.from_bytes(resp.data[1:4], 'little')
assert counter == 1
def test_value_not_0x000001_presets(self):
"""Writing != 0x000001 to block 75 presets (overwrites) counter."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Set counter to 100 first
offset = 75 * 4
tag._memory[offset:offset + 3] = (100).to_bytes(3, 'little')
# Write 0x000100 (256) → should preset to 256, NOT add to 100
cmd = bytes([0x02, 0x21, 75, 0x00, 0x01, 0x00, 0x00])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[0] == 0x00
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = int.from_bytes(resp.data[1:4], 'little')
assert counter == 256 # preset, not 356
def test_0x000001_twice_increments_twice(self):
"""Writing 0x000001 twice increments counter from 0 → 1 → 2."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
cmd = bytes([0x02, 0x21, 75, 0x01, 0x00, 0x00, 0x00])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
counter = int.from_bytes(resp.data[1:4], 'little')
assert counter == 2
def test_prot_byte_written(self):
"""PROT byte (data[3]) is written to memory alongside counter."""
from pm3py.sim.icode3 import Icode3Tag
tag = Icode3Tag(uid=b"\xE0\x04\x01\x20" + bytes(4))
run(tag.power_on())
# Preset counter to 0x000005 with PROT=0xAB
cmd = bytes([0x02, 0x21, 75, 0x05, 0x00, 0x00, 0xAB])
run(tag.handle_frame(RFFrame.from_bytes(cmd)))
cmd = bytes([0x02, 0x20, 75])
resp = run(tag.handle_frame(RFFrame.from_bytes(cmd)))
assert resp.data[4] == 0xAB # PROT byte at offset 3 in block