144 lines
5.1 KiB
Python
144 lines
5.1 KiB
Python
"""Tests for the TI RF430CL330H (ISO 14443-B, NFC Forum Type 4)."""
|
|
import asyncio
|
|
import pytest
|
|
|
|
from pm3py.sim.frame import RFFrame
|
|
from pm3py.transponders.hf.iso14443b.ti.rf430cl330h import RF430CL330H
|
|
|
|
PUPI = bytes([0x11, 0x22, 0x33, 0x44])
|
|
NDEF = b"\xD1\x01\x04\x54\x02enHi" # 9 bytes
|
|
|
|
|
|
def run(coro):
|
|
return asyncio.new_event_loop().run_until_complete(coro)
|
|
|
|
|
|
class Session:
|
|
def __init__(self, tag):
|
|
self.tag = tag
|
|
self.blk = 0
|
|
run(tag.power_on())
|
|
self.atqb = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00])))).data
|
|
run(tag.handle_frame(RFFrame.from_bytes(bytes([0x1D]) + tag.pupi + bytes(4))))
|
|
|
|
def apdu(self, hexstr: str) -> bytes:
|
|
pcb = 0x02 | self.blk
|
|
self.blk ^= 1
|
|
return run(self.tag.handle_frame(RFFrame.from_bytes(bytes([pcb]) + bytes.fromhex(hexstr)))).data[1:]
|
|
|
|
def select_app(self):
|
|
return self.apdu("00A4040007D276000085010100")
|
|
|
|
def select_file(self, fid):
|
|
return self.apdu("00A4000C02" + fid)
|
|
|
|
|
|
class TestTypeBActivation:
|
|
def test_reqb_returns_atqb(self):
|
|
tag = RF430CL330H(pupi=PUPI)
|
|
run(tag.power_on())
|
|
r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00]))))
|
|
assert r.data[0] == 0x50
|
|
assert r.data[1:5] == PUPI
|
|
assert len(r.data) == 12
|
|
assert tag.state == "READY"
|
|
|
|
def test_attrib_activates(self):
|
|
tag = RF430CL330H(pupi=PUPI)
|
|
run(tag.power_on())
|
|
run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00]))))
|
|
r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x1D]) + PUPI + bytes(4))))
|
|
assert r is not None
|
|
assert tag.state == "ACTIVE"
|
|
|
|
def test_attrib_wrong_pupi_ignored(self):
|
|
tag = RF430CL330H(pupi=PUPI)
|
|
run(tag.power_on())
|
|
run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00]))))
|
|
assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x1D]) + bytes(4) + bytes(4)))) is None
|
|
|
|
def test_wupb_wakes_halted(self):
|
|
tag = RF430CL330H(pupi=PUPI)
|
|
run(tag.power_on())
|
|
run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00]))))
|
|
run(tag.handle_frame(RFFrame.from_bytes(bytes([0x50]) + PUPI))) # HLTB
|
|
assert tag.state == "HALT"
|
|
# REQB ignored while halted, WUPB (param bit3) wakes
|
|
assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00])))) is None
|
|
r = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x08]))))
|
|
assert r.data[0] == 0x50
|
|
|
|
def test_atqb_protocol_type_is_iso14443_4(self):
|
|
tag = RF430CL330H(pupi=PUPI)
|
|
run(tag.power_on())
|
|
atqb = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x05, 0x00, 0x00])))).data
|
|
# ProtInfo byte 1 low nibble = 1 → ISO 14443-4 compliant
|
|
assert atqb[10] & 0x0F == 0x01
|
|
|
|
|
|
class TestType4Ndef:
|
|
def test_cc_read(self):
|
|
s = Session(RF430CL330H(pupi=PUPI))
|
|
assert s.select_app() == b"\x90\x00"
|
|
assert s.select_file("E103") == b"\x90\x00"
|
|
cc = s.apdu("00B000000F")
|
|
assert cc[-2:] == b"\x90\x00"
|
|
assert cc[0:4] == b"\x00\x0F\x20\x00" # CCLEN, ver, MLe hi
|
|
assert cc[3:5] == b"\x00\xF9" # MLe = 0x00F9
|
|
assert cc[5:7] == b"\x00\xF6" # MLc = 0x00F6
|
|
assert cc[9:11] == b"\xE1\x04" # NDEF file id
|
|
|
|
def test_ndef_read(self):
|
|
s = Session(RF430CL330H(pupi=PUPI, ndef_message=NDEF))
|
|
s.select_app()
|
|
assert s.select_file("E104") == b"\x90\x00"
|
|
assert s.apdu("00B0000002")[0:2] == len(NDEF).to_bytes(2, "big")
|
|
assert s.apdu("00B00002%02X" % len(NDEF))[:-2] == NDEF
|
|
|
|
def test_ndef_update(self):
|
|
s = Session(RF430CL330H(pupi=PUPI))
|
|
s.select_app()
|
|
s.select_file("E104")
|
|
new = b"\x00\x03\xD0\x00\x00"
|
|
assert s.apdu("00D60000%02X%s" % (len(new), new.hex())) == b"\x90\x00"
|
|
assert s.apdu("00B00000%02X" % len(new))[:-2] == new
|
|
|
|
def test_select_unknown_file(self):
|
|
s = Session(RF430CL330H(pupi=PUPI))
|
|
s.select_app()
|
|
assert s.select_file("E105") == b"\x6A\x82"
|
|
|
|
def test_read_before_app_select(self):
|
|
s = Session(RF430CL330H(pupi=PUPI))
|
|
assert s.select_file("E103") == b"\x6A\x82"
|
|
|
|
def test_set_ndef_api(self):
|
|
tag = RF430CL330H(pupi=PUPI)
|
|
tag.set_ndef(b"\xD1\x01\x01\x54\x00")
|
|
s = Session(tag)
|
|
s.select_app()
|
|
s.select_file("E104")
|
|
assert s.apdu("00B0000002")[0:2] == (5).to_bytes(2, "big")
|
|
|
|
|
|
class TestRobustness:
|
|
"""Regression for the review findings: short APDUs crashed, and UPDATE
|
|
BINARY past the file end silently grew the NDEF buffer."""
|
|
|
|
def test_short_apdu_no_crash(self):
|
|
s = Session(RF430CL330H(pupi=PUPI))
|
|
s.select_app()
|
|
assert s.apdu("00B0") == b"\x67\x00"
|
|
assert s.apdu("00A4") == b"\x67\x00"
|
|
|
|
def test_update_past_end_rejected(self):
|
|
s = Session(RF430CL330H(pupi=PUPI))
|
|
s.select_app()
|
|
s.select_file("E104")
|
|
assert s.apdu("00D6FFFF0141") == b"\x6A\x86"
|
|
|
|
|
|
class TestVolatileNote:
|
|
def test_default_pupi_is_4_bytes(self):
|
|
assert len(RF430CL330H().pupi) == 4
|