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>
This commit is contained in:
638
tests/test_sim_icode3.py
Normal file
638
tests/test_sim_icode3.py
Normal file
@@ -0,0 +1,638 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user