"""Tests for the ST25TV02KC / ST25TV512C (ST NFC Forum Type 5) models.""" import asyncio import pytest from pm3py.sim.frame import RFFrame from pm3py.transponders.hf.iso15693.st.st25tv import ( ST25TV512C, ST25TV02KC, PWD_A2, PWD_CFG, PWD_PRIV, NUID, ) def run(coro): return asyncio.new_event_loop().run_until_complete(coro) def _cover(rnd: bytes, pwd: bytes) -> bytes: ks = (rnd * ((len(pwd) // 2) + 1))[:len(pwd)] return bytes(a ^ b for a, b in zip(pwd, ks)) def _get_random(tag) -> bytes: return run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xB4, 0x02])))).data[1:3] def _present(tag, pwd_id, pwd) -> RFFrame: rnd = _get_random(tag) frame = bytes([0x02, 0xB3, 0x02, pwd_id]) + _cover(rnd, pwd) return run(tag.handle_frame(RFFrame.from_bytes(frame))) class TestIdentity: def test_uid_prefix(self): assert ST25TV512C()._uid[0:3] == b"\xE0\x02\x08" @pytest.mark.parametrize("cls,blocks", [(ST25TV512C, 16), (ST25TV02KC, 80)]) def test_block_count(self, cls, blocks): assert cls()._num_blocks == blocks def test_inventory(self): tag = ST25TV512C() run(tag.power_on()) r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00])))) assert r.data[0] == 0x00 assert bytes(reversed(r.data[2:10])) == tag._uid def test_system_info(self): tag = ST25TV512C() run(tag.power_on()) r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x2B])))) assert r.data[1] == 0x0F # info flags assert r.data[-1] == 0x08 # IC ref assert r.data[-2] == 0x03 # block size - 1 assert r.data[-3] == 0x0F # END_MEM (16 blocks - 1) def test_cc_readable(self): tag = ST25TV512C() run(tag.power_on()) r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x20, 0x00])))) assert r.data[0] == 0x00 and r.data[1] == 0xE1 class TestPasswords: def test_get_random(self): tag = ST25TV512C() run(tag.power_on()) r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xB4, 0x02])))) assert r.data[0] == 0x00 and len(r.data) == 3 def test_area_read_protection(self): tag = ST25TV02KC(end_a1=0x0F, passwords={0: bytes(4), 1: bytes(4), 2: b"\xAA\xBB\xCC\xDD", 3: bytes(4)}) tag._rwprot[PWD_A2] = 0x02 # A2 read+write need a session run(tag.power_on()) # A1 (block 5) open, A2 (block 20) protected assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x20, 5])))).data[0] == 0x00 r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x20, 20])))) assert r.data[0] == 0x01 and r.data[1] == 0x15 # block read-protected # authenticate A2 → read works assert _present(tag, PWD_A2, b"\xAA\xBB\xCC\xDD").data[0] == 0x00 assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0x20, 20])))).data[0] == 0x00 def test_wrong_password_errors(self): tag = ST25TV02KC(passwords={0: bytes(4), 1: bytes(4), 2: b"\x01\x02\x03\x04", 3: bytes(4)}) run(tag.power_on()) rnd = _get_random(tag) frame = bytes([0x02, 0xB3, 0x02, PWD_A2]) + _cover(rnd, b"\x00\x00\x00\x00") assert run(tag.handle_frame(RFFrame.from_bytes(frame))).data[0] == 0x01 class TestPrivacy: def test_kill(self): tag = ST25TV02KC() run(tag.power_on()) rnd = _get_random(tag) uid_lsb = tag._uid[::-1] frame = bytes([0x22, 0xA6, 0x02]) + uid_lsb + bytes([PWD_CFG]) + _cover(rnd, bytes(4)) assert run(tag.handle_frame(RFFrame.from_bytes(frame))).data[0] == 0x00 assert tag.killed assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00])))) is None def test_discreet_toggle_masks_uid(self): tag = ST25TV02KC() run(tag.power_on()) rnd = _get_random(tag) uid_lsb = tag._uid[::-1] frame = bytes([0x22, 0xBA, 0x02]) + uid_lsb + bytes([PWD_PRIV]) + _cover(rnd, bytes(4)) assert run(tag.handle_frame(RFFrame.from_bytes(frame))).data[0] == 0x00 assert tag.ds_state == "DISCREET" r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x26, 0x01, 0x00])))) assert bytes(reversed(r.data[2:10])) == NUID def test_leave_discreet_unaddressed(self): tag = ST25TV02KC() run(tag.power_on()) # enter rnd = _get_random(tag) uid_lsb = tag._uid[::-1] run(tag.handle_frame(RFFrame.from_bytes( bytes([0x22, 0xBA, 0x02]) + uid_lsb + bytes([PWD_PRIV]) + _cover(rnd, bytes(4))))) assert tag.ds_state == "DISCREET" # leave: non-addressed toggle rnd = _get_random(tag) run(tag.handle_frame(RFFrame.from_bytes( bytes([0x02, 0xBA, 0x02, PWD_PRIV]) + _cover(rnd, bytes(4))))) assert tag.ds_state == "READY" class TestConfig: def test_read_config_uid(self): tag = ST25TV512C() run(tag.power_on()) # ReadConfiguration FID=0xFE PID=0x01 → UID r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xA0, 0x02, 0xFE, 0x01])))) assert r.data[0] == 0x00 and bytes(reversed(r.data[1:9])) == tag._uid def test_write_config_needs_session(self): tag = ST25TV512C() run(tag.power_on()) # WriteConfiguration without CFG session → error r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xA1, 0x02, 0x00, 0x01, 0x08])))) assert r.data[0] == 0x01 # present PWD_CFG, then write END_A1 assert _present(tag, PWD_CFG, bytes(4)).data[0] == 0x00 r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x02, 0xA1, 0x02, 0x00, 0x01, 0x08])))) assert r.data[0] == 0x00 assert tag._end_a1 == 0x08