Files
pm3py/tests/test_sim_st25tv.py
2026-07-05 09:53:52 -07:00

146 lines
5.7 KiB
Python

"""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