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>
639 lines
26 KiB
Python
639 lines
26 KiB
Python
"""Tests for ICODE 3 (SL2S3003) 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_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.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
|
|
|
def test_inherits_slix2(self):
|
|
from pm3py.transponders.hf.iso15693.nxp.icode3 import Icode3Tag
|
|
from pm3py.transponders.hf.iso15693.nxp.icode_slix2 import IcodeSlix2Tag
|
|
assert issubclass(Icode3Tag, IcodeSlix2Tag)
|
|
|
|
def test_default_76_blocks(self):
|
|
from pm3py.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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.transponders.hf.iso15693.nxp.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
|