146 lines
5.7 KiB
Python
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
|