Big transponder support update
This commit is contained in:
Binary file not shown.
85
tests/test_sim_myd_vicinity.py
Normal file
85
tests/test_sim_myd_vicinity.py
Normal file
@@ -0,0 +1,85 @@
|
||||
"""Tests for the Infineon my-d vicinity (SRF55V02P / SRF55V10P)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso15693.infineon.myd_vicinity import SRF55V02P, SRF55V10P
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def cmd(tag, payload: bytes):
|
||||
return run(tag.handle_frame(RFFrame.from_bytes(payload)))
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
@pytest.mark.parametrize("cls,prefix,blocks", [
|
||||
(SRF55V02P, b"\xE0\x05\x40", 56),
|
||||
(SRF55V10P, b"\xE0\x05\x00", 248),
|
||||
])
|
||||
def test_uid_and_blocks(self, cls, prefix, blocks):
|
||||
tag = cls()
|
||||
assert tag._uid[0:3] == prefix
|
||||
assert tag._num_blocks == blocks
|
||||
|
||||
def test_infineon_mfg_code(self):
|
||||
assert SRF55V02P()._uid[1] == 0x05
|
||||
|
||||
def test_no_get_system_info(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x2B]))
|
||||
assert r.data[0] == 0x01 and r.data[1] == 0x01 # command not supported
|
||||
|
||||
def test_inventory(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x26, 0x01, 0x00]))
|
||||
assert r.data[0] == 0x00
|
||||
assert bytes(reversed(r.data[2:10])) == tag._uid
|
||||
|
||||
|
||||
class TestBlockMode:
|
||||
def test_iso_read_write(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x21, 0x05, 0xCA, 0xFE, 0xBA, 0xBE]))
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x05])).data[1:5] == b"\xCA\xFE\xBA\xBE"
|
||||
|
||||
|
||||
class TestPageMode:
|
||||
def test_custom_read_page_reflects_blocks(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
# block 5 lives in page 2 (bytes 4-7 of the 8-byte page)
|
||||
cmd(tag, bytes([0x02, 0x21, 0x05, 0xCA, 0xFE, 0xBA, 0xBE]))
|
||||
r = cmd(tag, bytes([0x02, 0xA0, 0x05, 0x10, 0x02, 0x00]))
|
||||
assert len(r.data) == 9
|
||||
assert r.data[5:9] == b"\xCA\xFE\xBA\xBE"
|
||||
|
||||
def test_custom_write_page(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0xA0, 0x05, 0x30, 0x03, 0x00]) + bytes(range(8)))
|
||||
r = cmd(tag, bytes([0x02, 0xA0, 0x05, 0x10, 0x03, 0x00]))
|
||||
assert r.data[1:9] == bytes(range(8))
|
||||
|
||||
def test_write_and_reread(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0xA0, 0x05, 0xB0, 0x04, 0x00]) + bytes([9] * 8))
|
||||
assert r.data[0] == 0x00 and r.data[1:9] == bytes([9] * 8)
|
||||
|
||||
|
||||
class TestValueCounter:
|
||||
def test_decrement_only(self):
|
||||
tag = SRF55V02P()
|
||||
run(tag.power_on())
|
||||
# set page 6 counter0 to 16
|
||||
cmd(tag, bytes([0x02, 0xA0, 0x05, 0x30, 0x06, 0x00]) + bytes([0x10, 0, 0, 0, 0, 0, 0, 0]))
|
||||
# decrement to 5 → ok
|
||||
assert cmd(tag, bytes([0x02, 0xA0, 0x05, 0x00, 0x06, 0x00]) + bytes([5, 0, 0, 0])).data[0] == 0x00
|
||||
# increment to 9 → rejected
|
||||
assert cmd(tag, bytes([0x02, 0xA0, 0x05, 0x00, 0x06, 0x00]) + bytes([9, 0, 0, 0])).data[0] == 0x01
|
||||
220
tests/test_sim_ntag21x.py
Normal file
220
tests/test_sim_ntag21x.py
Normal file
@@ -0,0 +1,220 @@
|
||||
"""Tests for the NTAG21x models (NTAG210/212/213/215/216)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.nxp.ntag21x import (
|
||||
NTAG210, NTAG212, NTAG213, NTAG215, NTAG216, Ntag21x,
|
||||
)
|
||||
|
||||
UID = b"\x04\x01\x02\x03\x04\x05\x06"
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def select(tag):
|
||||
"""Bring a 7-byte-UID tag through REQA + 2-cascade SELECT to ACTIVE."""
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = tag._uid
|
||||
ct = b"\x88" + uid[0:3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)]))))
|
||||
cl2 = uid[3:7]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2 + bytes([_compute_bcc(cl2)]))))
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
|
||||
def activate(cls, **kw):
|
||||
tag = cls(uid=UID, **kw)
|
||||
run(tag.power_on())
|
||||
select(tag)
|
||||
return tag
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Identity / GET_VERSION
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestVersion:
|
||||
CASES = {
|
||||
NTAG210: b"\x00\x04\x04\x01\x01\x00\x0B\x03",
|
||||
NTAG212: b"\x00\x04\x04\x01\x01\x00\x0E\x03",
|
||||
NTAG213: b"\x00\x04\x04\x02\x01\x00\x0F\x03",
|
||||
NTAG215: b"\x00\x04\x04\x02\x01\x00\x11\x03",
|
||||
NTAG216: b"\x00\x04\x04\x02\x01\x00\x13\x03",
|
||||
}
|
||||
|
||||
@pytest.mark.parametrize("cls,version", CASES.items())
|
||||
def test_get_version(self, cls, version):
|
||||
tag = activate(cls)
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x60")))
|
||||
assert resp.data == version
|
||||
|
||||
@pytest.mark.parametrize("cls,pages", [
|
||||
(NTAG210, 20), (NTAG212, 41), (NTAG213, 45), (NTAG215, 135), (NTAG216, 231),
|
||||
])
|
||||
def test_page_count(self, cls, pages):
|
||||
assert cls(uid=UID)._total_pages == pages
|
||||
|
||||
def test_atqa_sak(self):
|
||||
tag = NTAG213(uid=UID)
|
||||
run(tag.power_on())
|
||||
assert run(tag.handle_frame(RFFrame.from_hex("26"))).data == b"\x44\x00"
|
||||
select(tag) # SAK 0x00 verified via ACTIVE reach with 2 cascades
|
||||
|
||||
def test_cc_defaults(self):
|
||||
assert NTAG213(uid=UID)._get_page(3) == b"\xE1\x10\x12\x00"
|
||||
assert NTAG215(uid=UID)._get_page(3) == b"\xE1\x10\x3E\x00"
|
||||
assert NTAG216(uid=UID)._get_page(3) == b"\xE1\x10\x6D\x00"
|
||||
assert NTAG210(uid=UID)._get_page(3) == b"\xE1\x10\x06\x00"
|
||||
assert NTAG212(uid=UID)._get_page(3) == b"\xE1\x10\x10\x00"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# READ / FAST_READ / WRITE
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReadWrite:
|
||||
def test_read_returns_16_bytes(self):
|
||||
tag = activate(NTAG213)
|
||||
assert len(run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x00"))).data) == 16
|
||||
|
||||
def test_read_rolls_over(self):
|
||||
tag = activate(NTAG213) # 45 pages
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x2C"))) # last page
|
||||
assert len(resp.data) == 16 # wraps to page 0
|
||||
|
||||
def test_read_out_of_range_naks(self):
|
||||
tag = activate(NTAG213)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\xFF"))).data == b"\x00"
|
||||
|
||||
def test_fast_read(self):
|
||||
tag = activate(NTAG216)
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x3A\x04\x07")))
|
||||
assert len(resp.data) == 16 # pages 4..7 inclusive
|
||||
|
||||
def test_fast_read_bad_range_naks(self):
|
||||
tag = activate(NTAG213)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x3A\x05\x04"))).data == b"\x00"
|
||||
|
||||
def test_write_and_readback(self):
|
||||
tag = activate(NTAG215)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x08\xDE\xAD\xBE\xEF"))).data == b"\x0A"
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x08")))
|
||||
assert resp.data[0:4] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_write_uid_page_naks(self):
|
||||
tag = activate(NTAG213)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x00\x01\x02\x03\x04"))).data == b"\x00"
|
||||
|
||||
def test_compat_write_two_phase(self):
|
||||
tag = activate(NTAG213)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA0\x06"))).data == b"\x0A"
|
||||
data = b"\x11\x22\x33\x44" + bytes(12)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(data))).data == b"\x0A"
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x06")))
|
||||
assert resp.data[0:4] == b"\x11\x22\x33\x44"
|
||||
|
||||
def test_otp_or_on_write(self):
|
||||
tag = activate(NTAG213)
|
||||
# NTAG page 3 is CC; OR semantics — writes set bits, never clear.
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x03\x00\x00\x00\x0F")))
|
||||
cc = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4]
|
||||
assert cc == b"\xE1\x10\x12\x0F" # original OR new bits
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# READ_SIG
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestReadSig:
|
||||
def test_read_sig_32_bytes(self):
|
||||
sig = bytes(range(32))
|
||||
tag = NTAG216(uid=UID, signature=sig)
|
||||
run(tag.power_on()); select(tag)
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x3C\x00")))
|
||||
assert resp.data == sig
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# PWD_AUTH + AUTH0 protection
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestPwdAuth:
|
||||
def test_pwd_auth_returns_pack(self):
|
||||
tag = activate(NTAG213, password=b"\x11\x22\x33\x44", pack=b"\xAA\xBB")
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x11\x22\x33\x44")))
|
||||
assert resp.data == b"\xAA\xBB"
|
||||
|
||||
def test_wrong_pwd_naks(self):
|
||||
tag = activate(NTAG213, password=b"\x11\x22\x33\x44")
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x00\x00\x00\x00"))).data == b"\x04"
|
||||
|
||||
def test_read_protection_gates_on_auth0_and_prot(self):
|
||||
tag = activate(NTAG213, password=b"\x11\x22\x33\x44", pack=b"\xAA\xBB",
|
||||
auth0=0x10, prot=True)
|
||||
# page above AUTH0 blocked pre-auth
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x20"))).data == b"\x00"
|
||||
# page below AUTH0 open
|
||||
assert len(run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x08"))).data) == 16
|
||||
# authenticate, then protected read works
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x11\x22\x33\x44")))
|
||||
assert len(run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x20"))).data) == 16
|
||||
|
||||
def test_write_protection_without_prot(self):
|
||||
# prot=False → write protected above AUTH0, but reads still open.
|
||||
tag = activate(NTAG213, password=b"\x11\x22\x33\x44", auth0=0x10, prot=False)
|
||||
assert len(run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x20"))).data) == 16 # read open
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x20\x01\x02\x03\x04"))).data == b"\x00"
|
||||
|
||||
def test_pwd_pack_read_back_as_zero(self):
|
||||
tag = activate(NTAG213, password=b"\x11\x22\x33\x44", pack=b"\xAA\xBB")
|
||||
pwd_page = NTAG213._pwd_page
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([0x30, pwd_page]))))
|
||||
assert resp.data[0:4] == b"\x00\x00\x00\x00"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# NFC counter
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestNfcCounter:
|
||||
def test_counter_bumps_once_per_power_cycle(self):
|
||||
tag = activate(NTAG216, nfc_cnt_en=True)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x00")))
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))) # 2nd read, same cycle
|
||||
cnt = run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x02"))).data
|
||||
assert int.from_bytes(cnt, "little") == 1
|
||||
run(tag.power_on()); select(tag)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x00")))
|
||||
cnt = run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x02"))).data
|
||||
assert int.from_bytes(cnt, "little") == 2
|
||||
|
||||
def test_counter_disabled_naks(self):
|
||||
tag = activate(NTAG213, nfc_cnt_en=False)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x02"))).data == b"\x00"
|
||||
|
||||
def test_ntag210_212_have_no_counter(self):
|
||||
for cls in (NTAG210, NTAG212):
|
||||
tag = activate(cls)
|
||||
# READ_CNT is not a command for these ICs → no response
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x02"))) is None
|
||||
assert cls._has_nfc_counter is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Trace decode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestDecode:
|
||||
def test_decode_commands(self):
|
||||
tag = NTAG213(uid=UID)
|
||||
assert tag.decode_trace(0, b"\x30\x04") == "READ p4"
|
||||
assert tag.decode_trace(0, b"\x3A\x04\x10") == "FAST_READ p4-16"
|
||||
assert tag.decode_trace(0, b"\x60") == "GET_VERSION"
|
||||
assert tag.decode_trace(0, b"\x1B\x11\x22\x33\x44") == "PWD_AUTH 11223344"
|
||||
assert tag.decode_trace(0, b"\x39\x02") == "READ_CNT #2"
|
||||
assert tag.decode_trace(1, b"\x0A") == "ACK"
|
||||
assert tag.decode_trace(1, b"\x00") == "NAK 0x0"
|
||||
128
tests/test_sim_ntag_i2c.py
Normal file
128
tests/test_sim_ntag_i2c.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Tests for the NTAG I2C plus models (NT3H2111 1k, NT3H2211 2k)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.nxp.ntag_i2c import NT3H2111, NT3H2211
|
||||
|
||||
UID = b"\x04\x01\x02\x03\x04\x05\x06"
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def select(tag):
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = tag._uid
|
||||
ct = b"\x88" + uid[0:3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)]))))
|
||||
cl2 = uid[3:7]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2 + bytes([_compute_bcc(cl2)]))))
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
|
||||
def activate(cls, **kw):
|
||||
tag = cls(uid=UID, **kw)
|
||||
run(tag.power_on())
|
||||
select(tag)
|
||||
return tag
|
||||
|
||||
|
||||
def sector_select(tag, sector):
|
||||
r1 = run(tag.handle_frame(RFFrame.from_bytes(b"\xC2\xFF")))
|
||||
r2 = run(tag.handle_frame(RFFrame.from_bytes(bytes([sector, 0, 0, 0]))))
|
||||
return r1, r2
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
@pytest.mark.parametrize("cls,version", [
|
||||
(NT3H2111, b"\x00\x04\x04\x05\x02\x02\x13\x03"),
|
||||
(NT3H2211, b"\x00\x04\x04\x05\x02\x02\x15\x03"),
|
||||
])
|
||||
def test_get_version(self, cls, version):
|
||||
tag = activate(cls)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))).data == version
|
||||
|
||||
def test_cc(self):
|
||||
assert activate(NT3H2111)._get_page(3) == b"\xE1\x10\x6D\x00"
|
||||
assert activate(NT3H2211)._get_page(3) == b"\xE1\x10\xEA\x00"
|
||||
|
||||
|
||||
class TestReadWrite:
|
||||
def test_read_no_rollover_into_invalid(self):
|
||||
tag = activate(NT3H2111)
|
||||
# Page EDh is the last readable session-register page; EEh/EFh/F0h are
|
||||
# invalid, so those bytes come back as 00h rather than wrapping to page 0.
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\xED")))
|
||||
assert len(resp.data) == 16
|
||||
assert resp.data[4:16] == bytes(12)
|
||||
|
||||
def test_read_invalid_start_naks(self):
|
||||
tag = activate(NT3H2111)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\xEA"))).data == b"\x00"
|
||||
|
||||
def test_write_readback(self):
|
||||
tag = activate(NT3H2111)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x10\x01\x02\x03\x04"))).data == b"\x0A"
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x10"))).data[0:4] == b"\x01\x02\x03\x04"
|
||||
|
||||
def test_pwd_pack_masked(self):
|
||||
tag = activate(NT3H2111, password=b"\x11\x22\x33\x44", pack=b"\xAA\xBB")
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\xE5"))).data[0:4] == bytes(4)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\xE6"))).data[0:4] == bytes(4)
|
||||
|
||||
|
||||
class TestSectorSelect:
|
||||
def test_packet1_acks(self):
|
||||
tag = activate(NT3H2211)
|
||||
r1, _ = sector_select(tag, 1)
|
||||
assert r1.data == b"\x0A"
|
||||
|
||||
def test_success_is_silent(self):
|
||||
tag = activate(NT3H2211)
|
||||
_, r2 = sector_select(tag, 1)
|
||||
assert r2 is None # passive ACK (>1 ms silence)
|
||||
assert tag._current_sector == 1
|
||||
|
||||
def test_invalid_sector_naks(self):
|
||||
tag = activate(NT3H2211)
|
||||
_, r2 = sector_select(tag, 2) # sector 2 is invalid
|
||||
assert r2.data == b"\x00"
|
||||
|
||||
def test_sector1_only_on_2k(self):
|
||||
tag1k = activate(NT3H2111)
|
||||
_, r2 = sector_select(tag1k, 1)
|
||||
assert r2.data == b"\x00" # 1k has no sector 1
|
||||
|
||||
def test_sector1_user_memory(self):
|
||||
tag = activate(NT3H2211)
|
||||
sector_select(tag, 1)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x10\xDE\xAD\xBE\xEF")))
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x10"))).data[0:4] == b"\xDE\xAD\xBE\xEF"
|
||||
# switching back to sector 0 keeps the CC visible
|
||||
sector_select(tag, 0)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4] == b"\xE1\x10\xEA\x00"
|
||||
|
||||
def test_sector3_mirrors_session_regs(self):
|
||||
tag = activate(NT3H2211)
|
||||
sector_select(tag, 3)
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\xF8")))
|
||||
assert len(resp.data) == 16
|
||||
|
||||
|
||||
class TestFastWrite:
|
||||
def test_fast_write_sram(self):
|
||||
tag = activate(NT3H2111)
|
||||
payload = bytes(range(64))
|
||||
frame = b"\xA6\xF0\xFF" + payload
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(frame))).data == b"\x0A"
|
||||
assert bytes(tag._sram) == payload
|
||||
|
||||
|
||||
class TestDecode:
|
||||
def test_sector_select_decode(self):
|
||||
tag = NT3H2111(uid=UID)
|
||||
assert tag.decode_trace(0, b"\xC2\xFF") == "SECTOR_SELECT p1"
|
||||
assert tag.decode_trace(0, b"\x3A\x00\x0F") == "FAST_READ p0-15"
|
||||
181
tests/test_sim_optiga_nbt.py
Normal file
181
tests/test_sim_optiga_nbt.py
Normal file
@@ -0,0 +1,181 @@
|
||||
"""Tests for the Infineon OPTIGA Authenticate NBT (ISO 14443-A, NFC Type 4)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.infineon.optiga_nbt import OptigaAuthenticateNBT
|
||||
|
||||
UID = b"\x05\x84\x89\x02\x95\x43\x00"
|
||||
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.atqa = run(tag.handle_frame(RFFrame.from_hex("26"))).data
|
||||
uid = tag._uid
|
||||
ct = b"\x88" + uid[0:3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)]))))
|
||||
r = run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + uid[3:7] + bytes([_compute_bcc(uid[3:7])]))))
|
||||
self.sak = r.data
|
||||
self.ats = run(tag.handle_frame(RFFrame.from_bytes(b"\xE0\x50"))).data
|
||||
|
||||
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 TestActivation:
|
||||
def test_atqa_sak(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
assert s.atqa == b"\x44\x00" # ATQA 0x0044 (7-byte UID)
|
||||
assert s.sak[0] & 0x20 # SAK ISO-DEP bit
|
||||
|
||||
def test_ats_historical_nbt2000(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
assert s.ats == b"\x0C\x78\x77\x70\x02NBT2000"
|
||||
|
||||
def test_infineon_uid_prefix(self):
|
||||
assert OptigaAuthenticateNBT()._uid[0] == 0x05
|
||||
|
||||
|
||||
class TestFileSystem:
|
||||
def test_cc_contents(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
assert s.select_file("E103") == b"\x90\x00"
|
||||
cc = s.apdu("00B0000020")
|
||||
assert cc[0:3] == b"\x00\x2F\x20" # CCLEN, mapping ver 2.0
|
||||
assert cc[3:5] == b"\x01\x00" # MLe 0x0100
|
||||
assert cc[5:7] == b"\x00\xFF" # MLc 0x00FF
|
||||
assert cc[7:15] == b"\x04\x06\xE1\x04\x10\x00\x00\x00" # NDEF TLV
|
||||
assert cc[15:23] == b"\x05\x06\xE1\xA1\x04\x00\x00\x00" # proprietary TLV
|
||||
|
||||
def test_ndef_read(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID, ndef_message=NDEF))
|
||||
s.select_app()
|
||||
s.select_file("E104")
|
||||
assert s.apdu("00B0000002")[0:2] == len(NDEF).to_bytes(2, "big")
|
||||
assert s.apdu("00B00002%02X" % len(NDEF))[:-2] == NDEF
|
||||
|
||||
def test_proprietary_files_present(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
for fid in ("E1A1", "E1A2", "E1A3", "E1A4", "E1AF"):
|
||||
assert s.select_file(fid) == b"\x90\x00"
|
||||
|
||||
def test_cc_write_never(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
s.select_file("E103")
|
||||
# EF.CC write policy is NEVER → 6982 (offset 0x20 is outside the fixed 02..0E range)
|
||||
assert s.apdu("00D6002001FF") == b"\x69\x82"
|
||||
|
||||
def test_cc_fixed_region_protected(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
s.select_file("E103")
|
||||
assert s.apdu("00D6000201FF") == b"\x69\x85" # offset 02 in fixed region
|
||||
|
||||
def test_read_binary_6cxx(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
s.select_file("E103") # 64-byte CC
|
||||
r = s.apdu("00B00000FF") # Le 255 > file size
|
||||
assert r[0] == 0x6C and r[1] == 64
|
||||
|
||||
|
||||
class TestAuthenticate:
|
||||
def test_ecdsa_signature(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
sig = s.apdu("0088000004DEADBEEF00")
|
||||
assert sig[-2:] == b"\x90\x00"
|
||||
assert sig[0] == 0x30 # DER SEQUENCE
|
||||
# verify the signature against the tag's public key
|
||||
from Crypto.Signature import DSS
|
||||
from Crypto.Hash import SHA256
|
||||
h = SHA256.new(bytes.fromhex("DEADBEEF"))
|
||||
DSS.new(s.tag._ecc_key.public_key(), "fips-186-3", encoding="der").verify(h, sig[:-2])
|
||||
|
||||
def test_empty_challenge_rejected(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
assert s.apdu("0088000000") == b"\x67\x00"
|
||||
|
||||
|
||||
class TestPasswordManagement:
|
||||
def test_create_and_fap_gating(self):
|
||||
tag = OptigaAuthenticateNBT(uid=UID)
|
||||
s = Session(tag)
|
||||
s.select_app()
|
||||
assert s.apdu("00E10000090111223344AABB0003") == b"\x90\x00" # create pwid 1
|
||||
tag.set_fap(b"\xE1\x04", nfc_read=0x81, nfc_write=0x40) # NDEF read pw-protected
|
||||
# read without verify → 6982
|
||||
s.select_file("E104")
|
||||
assert s.apdu("00B0000002") == b"\x69\x82"
|
||||
# select with read password (tag 52) then read
|
||||
assert s.apdu("00A4000C08E1045204 11223344".replace(" ", ""))[-2:] == b"\x90\x00"
|
||||
assert s.apdu("00B0000002")[-2:] == b"\x90\x00"
|
||||
|
||||
def test_create_password_validation(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
# password response 0000 is RFU/rejected
|
||||
assert s.apdu("00E1000009011122334400000003") == b"\x6A\x80"
|
||||
|
||||
def test_change_password(self):
|
||||
tag = OptigaAuthenticateNBT(uid=UID)
|
||||
s = Session(tag)
|
||||
s.select_app()
|
||||
s.apdu("00E10000090111223344AABB0003")
|
||||
# CHANGE (P2 b7=1 → 0x41 for pwid 1): new password
|
||||
assert s.apdu("0024004104AABBCCDD") == b"\x90\x00"
|
||||
assert tag._passwords[1].value == b"\xAA\xBB\xCC\xDD"
|
||||
|
||||
def test_delete_password(self):
|
||||
tag = OptigaAuthenticateNBT(uid=UID)
|
||||
s = Session(tag)
|
||||
s.select_app()
|
||||
s.apdu("00E10000090111223344AABB0003")
|
||||
assert s.apdu("00E4000100") == b"\x90\x00" # delete pwid 1
|
||||
assert 1 not in tag._passwords
|
||||
|
||||
|
||||
class TestLifecycle:
|
||||
def test_finalize_personalization(self):
|
||||
tag = OptigaAuthenticateNBT(uid=UID)
|
||||
s = Session(tag)
|
||||
s.select_app()
|
||||
assert tag.lifecycle == "PERSONALIZATION"
|
||||
assert s.apdu("00E2000003BF6300") == b"\x90\x00"
|
||||
assert tag.lifecycle == "OPERATIONAL"
|
||||
# PERSONALIZE DATA rejected in OPERATIONAL
|
||||
assert s.apdu("00E2000003BF6300") == b"\x69\x85"
|
||||
|
||||
def test_get_data_applet_version(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
assert s.apdu("0030DF3A00")[-2:] == b"\x90\x00"
|
||||
|
||||
|
||||
class TestRobustness:
|
||||
def test_short_apdu_no_crash(self):
|
||||
s = Session(OptigaAuthenticateNBT(uid=UID))
|
||||
s.select_app()
|
||||
assert s.apdu("00B0") == b"\x67\x00" # truncated, no IndexError
|
||||
assert s.apdu("00A4") == b"\x67\x00"
|
||||
143
tests/test_sim_rf430cl330h.py
Normal file
143
tests/test_sim_rf430cl330h.py
Normal file
@@ -0,0 +1,143 @@
|
||||
"""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
|
||||
53
tests/test_sim_rf430frl.py
Normal file
53
tests/test_sim_rf430frl.py
Normal file
@@ -0,0 +1,53 @@
|
||||
"""Tests for the TI RF430FRL15xH ISO 15693 sensor transponder (identity + base)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso15693.ti.rf430frl import (
|
||||
RF430FRL152H, RF430FRL153H, RF430FRL154H,
|
||||
)
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def cmd(tag, payload: bytes):
|
||||
return run(tag.handle_frame(RFFrame.from_bytes(payload)))
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
@pytest.mark.parametrize("cls,dev_id", [
|
||||
(RF430FRL152H, b"\xE7\x81"),
|
||||
(RF430FRL153H, b"\xFB\x81"),
|
||||
(RF430FRL154H, b"\xFC\x81"),
|
||||
])
|
||||
def test_device_id_and_uid_prefix(self, cls, dev_id):
|
||||
tag = cls()
|
||||
assert tag.device_id == dev_id
|
||||
assert tag._uid[0:3] == b"\xE0\x07\xA2" # E0 + TI mfg 07 + UID5
|
||||
|
||||
def test_ti_manufacturer_code(self):
|
||||
assert RF430FRL152H()._uid[1] == 0x07
|
||||
|
||||
|
||||
class TestIso15693:
|
||||
def test_inventory(self):
|
||||
tag = RF430FRL152H()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x26, 0x01, 0x00]))
|
||||
assert r.data[0] == 0x00
|
||||
assert bytes(reversed(r.data[2:10])) == tag._uid
|
||||
|
||||
def test_read_write_block(self):
|
||||
tag = RF430FRL152H()
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x21, 0x04, 0xAA, 0xBB, 0xCC, 0xDD]))
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x04])).data[1:5] == b"\xAA\xBB\xCC\xDD"
|
||||
|
||||
def test_get_system_info(self):
|
||||
tag = RF430FRL152H()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x2B]))
|
||||
assert r.data[1] == 0x0F
|
||||
assert bytes(reversed(r.data[2:10])) == tag._uid
|
||||
115
tests/test_sim_st25dv.py
Normal file
115
tests/test_sim_st25dv.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""Tests for the ST25DV04K/16K/64K (ISO 15693 + I2C dynamic tag, RF side)."""
|
||||
import asyncio
|
||||
import struct
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso15693.st.st25dv import (
|
||||
ST25DV04K, ST25DV16K, ST25DV64K, REG_ENDA1, REG_RFA2SS, DYN_MB_CTRL,
|
||||
)
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def cmd(tag, payload: bytes):
|
||||
return run(tag.handle_frame(RFFrame.from_bytes(payload)))
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
@pytest.mark.parametrize("cls,prefix,blocks", [
|
||||
(ST25DV04K, b"\xE0\x02\x24", 128),
|
||||
(ST25DV16K, b"\xE0\x02\x26", 512),
|
||||
(ST25DV64K, b"\xE0\x02\x26", 2048),
|
||||
])
|
||||
def test_uid_and_blocks(self, cls, prefix, blocks):
|
||||
tag = cls()
|
||||
assert tag._uid[0:3] == prefix
|
||||
assert tag._num_blocks == blocks
|
||||
|
||||
def test_04k_sysinfo_has_memsize(self):
|
||||
tag = ST25DV04K()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x2B]))
|
||||
assert r.data[1] == 0x0F
|
||||
assert r.data[-1] == 0x24 # IC ref
|
||||
assert r.data[-3] == 0x7F # 128 blocks - 1
|
||||
|
||||
def test_16k_sysinfo_omits_memsize(self):
|
||||
tag = ST25DV16K()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x2B]))
|
||||
assert r.data[1] == 0x0B # DSFID+AFI+ICref, no memsize
|
||||
assert r.data[-1] == 0x26
|
||||
|
||||
def test_ext_sysinfo_gives_memsize(self):
|
||||
tag = ST25DV64K()
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x3B, 0x02, 0x0F]))
|
||||
assert struct.unpack_from("<H", r.data, 12)[0] == 2047
|
||||
|
||||
|
||||
class TestBlocks:
|
||||
def test_read_write(self):
|
||||
tag = ST25DV04K()
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x21, 0x08, 0xDE, 0xAD, 0xBE, 0xEF]))
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x08])).data[1:5] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_extended_read_write_high_block(self):
|
||||
tag = ST25DV16K()
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x31]) + struct.pack("<H", 300) + b"\x11\x22\x33\x44")
|
||||
r = cmd(tag, bytes([0x02, 0x30]) + struct.pack("<H", 300))
|
||||
assert r.data[1:5] == b"\x11\x22\x33\x44"
|
||||
|
||||
def test_fast_read_single(self):
|
||||
tag = ST25DV04K()
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x21, 0x08, 1, 2, 3, 4]))
|
||||
r = cmd(tag, bytes([0x02, 0xC0, 0x02, 0x08]))
|
||||
assert r.data[1:5] == b"\x01\x02\x03\x04"
|
||||
|
||||
|
||||
class TestPasswordConfig:
|
||||
def test_present_password_and_write_config(self):
|
||||
tag = ST25DV04K()
|
||||
run(tag.power_on())
|
||||
# write config denied without config session
|
||||
assert cmd(tag, bytes([0x02, 0xA1, 0x02, REG_ENDA1, 0x03])).data[0] == 0x01
|
||||
assert cmd(tag, bytes([0x02, 0xB3, 0x02, 0x00]) + bytes(8)).data[0] == 0x00
|
||||
assert cmd(tag, bytes([0x02, 0xA1, 0x02, REG_ENDA1, 0x03])).data[0] == 0x00
|
||||
assert cmd(tag, bytes([0x02, 0xA0, 0x02, REG_ENDA1])).data[1] == 0x03
|
||||
|
||||
def test_wrong_password(self):
|
||||
tag = ST25DV04K(passwords={0: b"\x01" * 8, 1: bytes(8), 2: bytes(8), 3: bytes(8)})
|
||||
run(tag.power_on())
|
||||
assert cmd(tag, bytes([0x02, 0xB3, 0x02, 0x00]) + bytes(8)).data[0] == 0x01
|
||||
|
||||
def test_area_read_protection(self):
|
||||
pw1 = b"\x01\x02\x03\x04\x05\x06\x07\x08"
|
||||
tag = ST25DV04K(passwords={0: bytes(8), 1: pw1, 2: bytes(8), 3: bytes(8)})
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0xB3, 0x02, 0x00]) + bytes(8)) # config session
|
||||
cmd(tag, bytes([0x02, 0xA1, 0x02, REG_ENDA1, 0x00])) # Area1 = blocks 0-7
|
||||
cmd(tag, bytes([0x02, 0xA1, 0x02, REG_RFA2SS, 0x09])) # A2 rwprot=10, pwd_ctrl=1
|
||||
r = cmd(tag, bytes([0x02, 0x20, 20])) # block in Area2
|
||||
assert r.data[0] == 0x01 and r.data[1] == 0x15
|
||||
assert cmd(tag, bytes([0x02, 0xB3, 0x02, 0x01]) + pw1).data[0] == 0x00
|
||||
assert cmd(tag, bytes([0x02, 0x20, 20])).data[0] == 0x00
|
||||
|
||||
|
||||
class TestMailbox:
|
||||
def test_mailbox_round_trip(self):
|
||||
tag = ST25DV04K()
|
||||
run(tag.power_on())
|
||||
# mailbox disabled → error
|
||||
assert cmd(tag, bytes([0x02, 0xAB, 0x02])).data[0] == 0x01
|
||||
# enable via dynamic register MB_CTRL_Dyn b0
|
||||
cmd(tag, bytes([0x02, 0xAE, 0x02, DYN_MB_CTRL, 0x01]))
|
||||
msg = b"hello"
|
||||
cmd(tag, bytes([0x02, 0xAA, 0x02, len(msg) - 1]) + msg)
|
||||
assert cmd(tag, bytes([0x02, 0xAB, 0x02])).data[1] == len(msg) - 1
|
||||
r = cmd(tag, bytes([0x02, 0xAC, 0x02, 0x00, len(msg) - 1]))
|
||||
assert r.data[1:1 + len(msg)] == msg
|
||||
153
tests/test_sim_st25ta.py
Normal file
153
tests/test_sim_st25ta.py
Normal file
@@ -0,0 +1,153 @@
|
||||
"""Tests for the ST25TA family (ST NFC Forum Type 4)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.st.st25ta import (
|
||||
ST25TA512B, ST25TA02KB, ST25TA16K, ST25TA64K,
|
||||
)
|
||||
|
||||
NDEF_MSG = b"\xD1\x01\x04\x54\x02enHi" # 9 bytes
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
class Session:
|
||||
"""Drives a Type 4 tag through activation + APDU exchange."""
|
||||
|
||||
def __init__(self, tag):
|
||||
self.tag = tag
|
||||
self.blk = 0
|
||||
run(tag.power_on())
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = tag._uid
|
||||
ct = b"\x88" + uid[0:3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)]))))
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + uid[3:7] + bytes([_compute_bcc(uid[3:7])]))))
|
||||
self.ats = run(tag.handle_frame(RFFrame.from_bytes(b"\xE0\x50"))).data
|
||||
|
||||
def apdu(self, hexstr: str) -> bytes:
|
||||
pcb = 0x02 | self.blk
|
||||
self.blk ^= 1
|
||||
resp = run(self.tag.handle_frame(RFFrame.from_bytes(bytes([pcb]) + bytes.fromhex(hexstr))))
|
||||
return resp.data[1:]
|
||||
|
||||
def select_app(self):
|
||||
return self.apdu("00A4040007D276000085010100")
|
||||
|
||||
def select_file(self, fid: str):
|
||||
return self.apdu("00A4000C02" + fid)
|
||||
|
||||
def read(self, off: int, le: int) -> bytes:
|
||||
return self.apdu(f"00B0{off:04X}{le:02X}")
|
||||
|
||||
|
||||
class TestActivation:
|
||||
@pytest.mark.parametrize("cls,ats", [
|
||||
(ST25TA512B, b"\x05\x75\x80\x60\x02"),
|
||||
(ST25TA02KB, b"\x05\x75\x80\x60\x02"),
|
||||
(ST25TA16K, b"\x05\x78\x80\x90\x02"),
|
||||
])
|
||||
def test_ats(self, cls, ats):
|
||||
s = Session(cls())
|
||||
assert s.ats == ats
|
||||
|
||||
def test_uid_prefix(self):
|
||||
assert ST25TA02KB()._uid[0] == 0x02
|
||||
assert ST25TA02KB()._uid[1] == 0xE3
|
||||
|
||||
|
||||
class TestFileSystem:
|
||||
def test_cc_read(self):
|
||||
s = Session(ST25TA02KB())
|
||||
assert s.select_app() == b"\x90\x00"
|
||||
assert s.select_file("E103") == b"\x90\x00"
|
||||
cc = s.read(0, 15)
|
||||
assert cc[-2:] == b"\x90\x00"
|
||||
assert cc[0:4] == b"\x00\x0F\x20\x00"
|
||||
|
||||
def test_cc_per_variant(self):
|
||||
assert ST25TA512B()._cc_file[3:5] == b"\x00\x40"
|
||||
assert ST25TA02KB()._cc_file[3:5] == b"\x00\xFF"
|
||||
assert ST25TA16K()._cc_file[3:5] == b"\x00\xF6"
|
||||
assert ST25TA64K()._cc_file[11:13] == b"\x20\x00"
|
||||
|
||||
def test_ndef_read(self):
|
||||
s = Session(ST25TA02KB(ndef_message=NDEF_MSG))
|
||||
s.select_app()
|
||||
assert s.select_file("0001") == b"\x90\x00"
|
||||
assert s.read(0, 2)[0:2] == len(NDEF_MSG).to_bytes(2, "big")
|
||||
assert s.read(2, len(NDEF_MSG))[:-2] == NDEF_MSG
|
||||
|
||||
def test_ndef_update(self):
|
||||
s = Session(ST25TA02KB())
|
||||
s.select_app()
|
||||
s.select_file("0001")
|
||||
new = b"\x00\x03\xD0\x00\x00"
|
||||
assert s.apdu("00D60000%02X%s" % (len(new), new.hex())) == b"\x90\x00"
|
||||
assert s.read(0, len(new))[:-2] == new
|
||||
|
||||
def test_system_file_uid_and_icref(self):
|
||||
s = Session(ST25TA02KB())
|
||||
s.select_app()
|
||||
assert s.select_file("E101") == b"\x90\x00"
|
||||
assert s.read(8, 7)[:-2] == s.tag._uid
|
||||
assert s.read(0x11, 1)[0] == 0xE2
|
||||
|
||||
def test_select_unknown_file(self):
|
||||
s = Session(ST25TA02KB())
|
||||
s.select_app()
|
||||
assert s.select_file("E104") == b"\x6A\x82"
|
||||
|
||||
|
||||
class TestPassword:
|
||||
def test_read_protection(self):
|
||||
s = Session(ST25TA02KB(read_password=b"\x11" * 16))
|
||||
s.select_app()
|
||||
s.select_file("0001")
|
||||
assert s.read(0, 2) == b"\x69\x82"
|
||||
assert s.apdu("0020000110" + ("11" * 16)) == b"\x90\x00"
|
||||
assert s.read(0, 2)[-2:] == b"\x90\x00"
|
||||
|
||||
def test_wrong_password_retry_counter(self):
|
||||
s = Session(ST25TA02KB(read_password=b"\x11" * 16))
|
||||
s.select_app()
|
||||
s.select_file("0001")
|
||||
assert s.apdu("0020000110" + ("00" * 16)) == b"\x63\xC2"
|
||||
assert s.apdu("0020000110" + ("00" * 16)) == b"\x63\xC1"
|
||||
assert s.apdu("0020000110" + ("00" * 16)) == b"\x63\xC0"
|
||||
# blocked after 3 failures
|
||||
assert s.apdu("0020000110" + ("00" * 16)) == b"\x69\x85"
|
||||
|
||||
def test_write_protection(self):
|
||||
s = Session(ST25TA02KB(write_password=b"\x22" * 16))
|
||||
s.select_app()
|
||||
s.select_file("0001")
|
||||
assert s.apdu("00D6000003000000") == b"\x69\x82"
|
||||
assert s.apdu("0020000210" + ("22" * 16)) == b"\x90\x00"
|
||||
assert s.apdu("00D6000003000000") == b"\x90\x00"
|
||||
|
||||
def test_verify_query(self):
|
||||
s = Session(ST25TA02KB(read_password=b"\x11" * 16))
|
||||
s.select_app()
|
||||
s.select_file("0001")
|
||||
# Lc=0 query → password required
|
||||
assert s.apdu("00200001") == b"\x63\x00"
|
||||
|
||||
|
||||
class TestCounter:
|
||||
def test_event_counter_bumps_once_per_session(self):
|
||||
tag = ST25TA02KB()
|
||||
tag.enable_counter(on_write=False)
|
||||
s = Session(tag)
|
||||
s.select_app()
|
||||
s.select_file("0001")
|
||||
s.read(0, 2)
|
||||
s.read(0, 2)
|
||||
assert tag.event_counter == 1
|
||||
|
||||
def test_16k_has_no_counter(self):
|
||||
assert ST25TA16K()._has_counter is False
|
||||
141
tests/test_sim_st25tb.py
Normal file
141
tests/test_sim_st25tb.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""Tests for the ISO 14443 Type B stack + ST25TB family (SRIX lineage)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443b.st.st25tb import (
|
||||
ST25TB512AC, ST25TB512AT, ST25TB02K, ST25TB04K,
|
||||
)
|
||||
from pm3py.trace.decode_iso14b import decode_14443b, crc14b
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def frame(tag, payload: bytes):
|
||||
return run(tag.handle_frame(RFFrame.from_bytes(payload)))
|
||||
|
||||
|
||||
def selected(cls, uid=None):
|
||||
tag = cls(uid=uid)
|
||||
run(tag.power_on())
|
||||
cid = frame(tag, b"\x06\x00").data[0]
|
||||
frame(tag, bytes([0x0E, cid]))
|
||||
assert tag.state == "SELECTED"
|
||||
return tag
|
||||
|
||||
|
||||
class TestAnticollision:
|
||||
def test_initiate_moves_to_inventory(self):
|
||||
tag = ST25TB02K()
|
||||
run(tag.power_on())
|
||||
assert tag.state == "READY"
|
||||
r = frame(tag, b"\x06\x00")
|
||||
assert len(r.data) == 1
|
||||
assert tag.state == "INVENTORY"
|
||||
|
||||
def test_memory_commands_need_select(self):
|
||||
tag = ST25TB02K()
|
||||
run(tag.power_on())
|
||||
frame(tag, b"\x06\x00") # INVENTORY, not yet selected
|
||||
assert frame(tag, b"\x08\x07") is None
|
||||
|
||||
def test_select_and_get_uid(self):
|
||||
tag = selected(ST25TB02K, uid=bytes([0xD0, 0x02, 0x3F, 1, 2, 3, 4, 5]))
|
||||
r = frame(tag, b"\x0B")
|
||||
assert r.data == tag._uid[::-1] # LSByte first
|
||||
|
||||
def test_wrong_chip_id_not_selected(self):
|
||||
tag = ST25TB02K()
|
||||
run(tag.power_on())
|
||||
cid = frame(tag, b"\x06\x00").data[0]
|
||||
assert frame(tag, bytes([0x0E, (cid + 1) & 0xFF])) is None
|
||||
assert tag.state != "SELECTED"
|
||||
|
||||
def test_completion_deactivates(self):
|
||||
tag = selected(ST25TB02K)
|
||||
assert frame(tag, b"\x0F") is None
|
||||
assert tag.state == "DEACTIVATED"
|
||||
|
||||
def test_reset_to_inventory(self):
|
||||
tag = selected(ST25TB02K)
|
||||
frame(tag, b"\x0C")
|
||||
assert tag.state == "INVENTORY"
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
@pytest.mark.parametrize("cls,pc,blocks", [
|
||||
(ST25TB512AC, 0x1B, 16),
|
||||
(ST25TB512AT, 0x33, 16),
|
||||
(ST25TB02K, 0x3F, 64),
|
||||
(ST25TB04K, 0x1F, 128),
|
||||
])
|
||||
def test_uid_and_size(self, cls, pc, blocks):
|
||||
tag = cls()
|
||||
assert tag._uid[0:3] == bytes([0xD0, 0x02, pc])
|
||||
assert tag._num_blocks == blocks
|
||||
|
||||
|
||||
class TestMemory:
|
||||
def test_write_read_user_block(self):
|
||||
tag = selected(ST25TB04K)
|
||||
frame(tag, b"\x09\x10\xDE\xAD\xBE\xEF") # no response expected
|
||||
assert frame(tag, b"\x08\x10").data == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_write_block_has_no_response(self):
|
||||
tag = selected(ST25TB02K)
|
||||
assert frame(tag, b"\x09\x10\x01\x02\x03\x04") is None
|
||||
|
||||
def test_system_block_255(self):
|
||||
tag = selected(ST25TB02K)
|
||||
assert len(frame(tag, b"\x08\xFF").data) == 4
|
||||
|
||||
def test_invalid_block_no_response(self):
|
||||
tag = selected(ST25TB02K) # 64 blocks
|
||||
assert frame(tag, b"\x08\x64") is None # block 100 invalid
|
||||
|
||||
|
||||
class TestCounters:
|
||||
def test_factory_values(self):
|
||||
tag = selected(ST25TB02K)
|
||||
assert frame(tag, b"\x08\x05").data == (0xFFFFFFFE).to_bytes(4, "little")
|
||||
assert frame(tag, b"\x08\x06").data == (0xFFFFFFFF).to_bytes(4, "little")
|
||||
|
||||
def test_decrement_only(self):
|
||||
tag = selected(ST25TB02K)
|
||||
frame(tag, bytes([0x09, 0x05]) + (0x1000).to_bytes(4, "little"))
|
||||
assert int.from_bytes(frame(tag, b"\x08\x05").data, "little") == 0x1000
|
||||
# higher value rejected
|
||||
frame(tag, bytes([0x09, 0x05]) + (0x2000).to_bytes(4, "little"))
|
||||
assert int.from_bytes(frame(tag, b"\x08\x05").data, "little") == 0x1000
|
||||
|
||||
|
||||
class TestOTP:
|
||||
def test_otp_bit_clear_only(self):
|
||||
tag = selected(ST25TB02K)
|
||||
assert frame(tag, b"\x08\x00").data == b"\xFF\xFF\xFF\xFF" # ships all-1s
|
||||
frame(tag, b"\x09\x00\x0F\x00\x00\x00")
|
||||
assert frame(tag, b"\x08\x00").data == b"\x0F\x00\x00\x00"
|
||||
# cannot set bits back
|
||||
frame(tag, b"\x09\x00\xFF\xFF\xFF\xFF")
|
||||
assert frame(tag, b"\x08\x00").data == b"\x0F\x00\x00\x00"
|
||||
|
||||
def test_at_variant_blocks_are_plain_eeprom(self):
|
||||
tag = selected(ST25TB512AT)
|
||||
frame(tag, b"\x09\x00\x0F\xF0\x00\x00")
|
||||
assert frame(tag, b"\x08\x00").data == b"\x0F\xF0\x00\x00"
|
||||
|
||||
|
||||
class TestDecode:
|
||||
def test_request_decode(self):
|
||||
assert decode_14443b(0, b"\x06\x00") == "INITIATE"
|
||||
assert decode_14443b(0, b"\x06\x04") == "PCALL16"
|
||||
assert decode_14443b(0, b"\x26") == "SLOT_MARKER 2"
|
||||
assert decode_14443b(0, b"\x0E\x5A") == "SELECT chip=0x5A"
|
||||
assert decode_14443b(0, b"\x08\x07") == "READ_BLOCK #7"
|
||||
assert decode_14443b(0, b"\x09\x07") == "WRITE_BLOCK #7"
|
||||
|
||||
def test_crc_b(self):
|
||||
# ST25TB datasheet reference vector.
|
||||
assert crc14b(bytes.fromhex("0A123456")) == bytes.fromhex("2CF6")
|
||||
92
tests/test_sim_st25tn.py
Normal file
92
tests/test_sim_st25tn.py
Normal file
@@ -0,0 +1,92 @@
|
||||
"""Tests for the ST25TN512 / ST25TN01K (ST NFC Forum Type 2) models."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.st.st25tn import ST25TN512, ST25TN01K
|
||||
|
||||
UID = b"\x02\x11\x22\x33\x44\x55\x66"
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def select(tag):
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = tag._uid
|
||||
ct = b"\x88" + uid[0:3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)]))))
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + uid[3:7] + bytes([_compute_bcc(uid[3:7])]))))
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
|
||||
def activate(cls, **kw):
|
||||
tag = cls(uid=UID, **kw)
|
||||
run(tag.power_on())
|
||||
select(tag)
|
||||
return tag
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
def test_st_uid_prefix_autogen(self):
|
||||
assert ST25TN512()._uid[0] == 0x02 # ST manufacturer code
|
||||
|
||||
def test_atqa_sak(self):
|
||||
tag = ST25TN512(uid=UID)
|
||||
run(tag.power_on())
|
||||
assert run(tag.handle_frame(RFFrame.from_hex("26"))).data == b"\x44\x00"
|
||||
select(tag) # reaches ACTIVE with SAK 0x00
|
||||
|
||||
def test_cc_defaults(self):
|
||||
assert ST25TN512(uid=UID)._get_page(3) == b"\xE1\x10\x08\x00"
|
||||
assert ST25TN01K(uid=UID)._get_page(3) == b"\xE1\x10\x14\x00"
|
||||
|
||||
def test_read_returns_uid(self):
|
||||
tag = activate(ST25TN512)
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x00")))
|
||||
assert resp.data[0:3] == UID[0:3]
|
||||
|
||||
def test_product_id_block(self):
|
||||
tag = activate(ST25TN512)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x2D"))).data[0:4] == b"\x91\x90\x13\x05"
|
||||
tag = activate(ST25TN01K)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x2D"))).data[0:4] == b"\x90\x90\x13\x05"
|
||||
|
||||
|
||||
class TestCommands:
|
||||
def test_no_fast_read_version_pwd(self):
|
||||
tag = activate(ST25TN512)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x3A\x00\x03"))) is None
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))) is None
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x00\x00\x00\x00"))) is None
|
||||
|
||||
def test_write_readback(self):
|
||||
tag = activate(ST25TN512)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x05\xDE\xAD\xBE\xEF"))).data == b"\x0A"
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x05"))).data[0:4] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_product_id_read_only(self):
|
||||
tag = activate(ST25TN512)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x2D\x01\x02\x03\x04"))).data == b"\x00"
|
||||
|
||||
def test_kill_pwd_reads_zero(self):
|
||||
tag = activate(ST25TN512)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x2F"))).data[0:4] == bytes(4)
|
||||
|
||||
|
||||
class TestKill:
|
||||
def test_kill_with_correct_password(self):
|
||||
tag = activate(ST25TN512, kill_password=b"\x11\x22\x33\x44")
|
||||
# wrong password → NAK
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x30\x00\x00\x00\x00"))).data == b"\x00"
|
||||
# correct → ACK, killed on next boot
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x30\x11\x22\x33\x44"))).data == b"\x0A"
|
||||
run(tag.power_on())
|
||||
assert tag.killed
|
||||
assert run(tag.handle_frame(RFFrame.from_hex("26"))) is None
|
||||
|
||||
def test_kill_disabled(self):
|
||||
tag = activate(ST25TN512, kill_password=b"\x00\x00\x00\x00", kill_enabled=False)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x30\x00\x00\x00\x00"))).data == b"\x00"
|
||||
145
tests/test_sim_st25tv.py
Normal file
145
tests/test_sim_st25tv.py
Normal file
@@ -0,0 +1,145 @@
|
||||
"""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
|
||||
120
tests/test_sim_tagit.py
Normal file
120
tests/test_sim_tagit.py
Normal file
@@ -0,0 +1,120 @@
|
||||
"""Tests for the TI Tag-it HF-I Plus (ISO 15693)."""
|
||||
import asyncio
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso15693.ti.tagit import TagItHFIPlus
|
||||
|
||||
UID = bytes([0xE0, 0x07, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66])
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def cmd(tag, payload: bytes):
|
||||
return run(tag.handle_frame(RFFrame.from_bytes(payload)))
|
||||
|
||||
|
||||
class TestIdentity:
|
||||
def test_ti_mfg_prefix(self):
|
||||
assert TagItHFIPlus()._uid[0:2] == b"\xE0\x07"
|
||||
|
||||
def test_64_blocks(self):
|
||||
assert TagItHFIPlus()._num_blocks == 64
|
||||
assert TagItHFIPlus()._block_size == 4
|
||||
|
||||
def test_inventory(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x26, 0x01, 0x00]))
|
||||
assert r.data[0] == 0x00
|
||||
assert bytes(reversed(r.data[2:10])) == UID
|
||||
|
||||
def test_system_info(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x2B]))
|
||||
assert r.data[-3] == 0x3F # 64 blocks - 1
|
||||
assert r.data[-2] == 0x03 # block size - 1
|
||||
|
||||
|
||||
class TestBlocks:
|
||||
def test_read_write(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x21, 0x05, 0xDE, 0xAD, 0xBE, 0xEF]))
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x05])).data[1:5] == b"\xDE\xAD\xBE\xEF"
|
||||
|
||||
def test_read_multiple(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x21, 0x05, 1, 2, 3, 4]))
|
||||
cmd(tag, bytes([0x02, 0x21, 0x06, 5, 6, 7, 8]))
|
||||
r = cmd(tag, bytes([0x02, 0x23, 0x05, 0x01])) # read 2 blocks from 5
|
||||
assert r.data[1:9] == bytes([1, 2, 3, 4, 5, 6, 7, 8])
|
||||
|
||||
|
||||
class TestCustomCommands:
|
||||
def test_write_2_blocks(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0xA2, 0x07, 0x08]) + bytes(range(8)))
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x08])).data[1:5] == bytes(range(4))
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x09])).data[1:5] == bytes(range(4, 8))
|
||||
|
||||
def test_lock_2_blocks_then_write_fails(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0xA3, 0x07, 0x0A])) # lock blocks 10,11
|
||||
r = cmd(tag, bytes([0x02, 0x21, 0x0A, 0, 0, 0, 0]))
|
||||
assert r.data[0] == 0x01 and r.data[1] == 0x12
|
||||
|
||||
def test_write_multiple_not_supported(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
r = cmd(tag, bytes([0x02, 0x24, 0x00, 0x01]) + bytes(8))
|
||||
assert r.data[0] == 0x01 and r.data[1] == 0x01
|
||||
|
||||
|
||||
class TestAddressedMode:
|
||||
"""Regression for the review finding: custom commands were dead in addressed
|
||||
mode, and SELECT/LOCK/READ_MULTIPLE bypassed the addressed-UID filter."""
|
||||
|
||||
def test_addressed_write_2_blocks(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
uid_lsb = UID[::-1]
|
||||
frame = bytes([0x22, 0xA2, 0x07]) + uid_lsb + bytes([0x08]) + bytes(range(8))
|
||||
assert cmd(tag, frame).data[0] == 0x00
|
||||
assert cmd(tag, bytes([0x02, 0x20, 0x08])).data[1:5] == bytes(range(4))
|
||||
|
||||
def test_addressed_wrong_uid_dropped(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
assert cmd(tag, bytes([0x22, 0x20, 0x00]) + bytes(8)) is None # read
|
||||
assert cmd(tag, bytes([0x22, 0x25]) + bytes(8)) is None # select
|
||||
|
||||
def test_select_uid_gated(self):
|
||||
from pm3py.transponders.hf.iso15693.base import State15693
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
assert cmd(tag, bytes([0x22, 0x25]) + bytes(8)) is None
|
||||
assert tag._state != State15693.SELECTED
|
||||
assert cmd(tag, bytes([0x22, 0x25]) + UID[::-1]).data[0] == 0x00
|
||||
assert tag._state == State15693.SELECTED
|
||||
|
||||
|
||||
class TestLocking:
|
||||
def test_lock_block_then_write_fails(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
assert cmd(tag, bytes([0x02, 0x22, 0x05])).data[0] == 0x00 # lock block 5
|
||||
r = cmd(tag, bytes([0x02, 0x21, 0x05, 0, 0, 0, 0]))
|
||||
assert r.data[0] == 0x01 and r.data[1] == 0x12
|
||||
|
||||
def test_double_lock_errors(self):
|
||||
tag = TagItHFIPlus(uid=UID)
|
||||
run(tag.power_on())
|
||||
cmd(tag, bytes([0x02, 0x22, 0x05]))
|
||||
assert cmd(tag, bytes([0x02, 0x22, 0x05])).data[1] == 0x11 # already locked
|
||||
173
tests/test_sim_ultralight.py
Normal file
173
tests/test_sim_ultralight.py
Normal file
@@ -0,0 +1,173 @@
|
||||
"""Tests for the MIFARE Ultralight family (original, Ultralight C, EV1)."""
|
||||
import asyncio
|
||||
import os
|
||||
import pytest
|
||||
|
||||
from pm3py.sim.frame import RFFrame
|
||||
from pm3py.transponders.hf.iso14443a.base import _compute_bcc
|
||||
from pm3py.transponders.hf.iso14443a.nxp.ultralight import (
|
||||
MifareUltralight, MifareUltralightC, MF0UL11, MF0UL21, UL_C_DEFAULT_KEY,
|
||||
)
|
||||
|
||||
UID = b"\x04\x01\x02\x03\x04\x05\x06"
|
||||
|
||||
|
||||
def run(coro):
|
||||
return asyncio.new_event_loop().run_until_complete(coro)
|
||||
|
||||
|
||||
def select(tag):
|
||||
run(tag.handle_frame(RFFrame.from_hex("26")))
|
||||
uid = tag._uid
|
||||
ct = b"\x88" + uid[0:3]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct + bytes([_compute_bcc(ct)]))))
|
||||
cl2 = uid[3:7]
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2 + bytes([_compute_bcc(cl2)]))))
|
||||
assert tag.state == "ACTIVE"
|
||||
|
||||
|
||||
def activate(cls, **kw):
|
||||
tag = cls(uid=UID, **kw)
|
||||
run(tag.power_on())
|
||||
select(tag)
|
||||
return tag
|
||||
|
||||
|
||||
def _rotl1(b):
|
||||
return b[1:] + b[:1]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Original MIFARE Ultralight (MF0ICU1)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUltralight:
|
||||
def test_16_pages(self):
|
||||
assert MifareUltralight(uid=UID)._total_pages == 16
|
||||
|
||||
def test_no_get_version(self):
|
||||
tag = activate(MifareUltralight)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))) is None
|
||||
|
||||
def test_no_fast_read_or_pwd_auth(self):
|
||||
tag = activate(MifareUltralight)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x3A\x00\x03"))) is None
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x00\x00\x00\x00"))) is None
|
||||
|
||||
def test_read_write(self):
|
||||
tag = activate(MifareUltralight)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x04\xCA\xFE\xBA\xBE"))).data == b"\x0A"
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data[0:4] == b"\xCA\xFE\xBA\xBE"
|
||||
|
||||
def test_otp_is_or_write(self):
|
||||
tag = activate(MifareUltralight) # page 3 = OTP, starts at 0
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x03\x01\x00\x00\x00")))
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x03\x02\x00\x00\x00")))
|
||||
otp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4]
|
||||
assert otp == b"\x03\x00\x00\x00" # OR of 0x01 and 0x02
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MIFARE Ultralight C (MF0ICU2) — 3DES
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUltralightC:
|
||||
def test_48_pages_no_get_version(self):
|
||||
assert MifareUltralightC(uid=UID)._total_pages == 48
|
||||
tag = activate(MifareUltralightC)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))) is None
|
||||
|
||||
def test_default_key_installed(self):
|
||||
assert MifareUltralightC(uid=UID).key == UL_C_DEFAULT_KEY
|
||||
|
||||
def _do_auth(self, tag, key=UL_C_DEFAULT_KEY):
|
||||
from Crypto.Cipher import DES3
|
||||
r = run(tag.handle_frame(RFFrame.from_bytes(b"\x1A\x00")))
|
||||
assert r.data[0] == 0xAF and len(r.data) == 9
|
||||
ek_rndb = r.data[1:9]
|
||||
rndb = DES3.new(key, DES3.MODE_CBC, iv=bytes(8)).decrypt(ek_rndb)
|
||||
rnda = os.urandom(8)
|
||||
token = DES3.new(key, DES3.MODE_CBC, iv=ek_rndb).encrypt(rnda + _rotl1(rndb))
|
||||
r = run(tag.handle_frame(RFFrame.from_bytes(b"\xAF" + token)))
|
||||
if r.data[0] != 0x00:
|
||||
return None
|
||||
got = DES3.new(key, DES3.MODE_CBC, iv=token[8:16]).decrypt(r.data[1:9])
|
||||
return got == _rotl1(rnda)
|
||||
|
||||
def test_3des_auth_round_trip(self):
|
||||
tag = activate(MifareUltralightC, auth0=0x04)
|
||||
assert self._do_auth(tag) is True
|
||||
assert tag._auth_state == "AUTHENTICATED"
|
||||
|
||||
def test_auth_gates_protected_read(self):
|
||||
tag = activate(MifareUltralightC, auth0=0x04, auth1_read_protect=True)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data == b"\x00"
|
||||
assert self._do_auth(tag) is True
|
||||
assert len(run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data) == 16
|
||||
|
||||
def test_wrong_key_fails(self):
|
||||
tag = activate(MifareUltralightC, auth0=0x04)
|
||||
wrong = bytes(range(16))
|
||||
assert self._do_auth(tag, key=wrong) is None
|
||||
assert tag._auth_state != "AUTHENTICATED"
|
||||
|
||||
def test_key_not_readable(self):
|
||||
tag = activate(MifareUltralightC, auth0=0x30) # protection off
|
||||
resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x2C")))
|
||||
assert resp.data[0:4] == b"\x00\x00\x00\x00"
|
||||
|
||||
def test_counter_increments(self):
|
||||
tag = activate(MifareUltralightC)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x29\x03\x00\x00\x00")))
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x29\x02\x00\x00\x00")))
|
||||
cnt = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x29"))).data[0:2]
|
||||
assert int.from_bytes(cnt, "little") == 5
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# MIFARE Ultralight EV1 (MF0UL11 / MF0UL21)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestUltralightEV1:
|
||||
@pytest.mark.parametrize("cls,version,pages", [
|
||||
(MF0UL11, b"\x00\x04\x03\x01\x01\x00\x0B\x03", 20),
|
||||
(MF0UL21, b"\x00\x04\x03\x01\x01\x00\x0E\x03", 41),
|
||||
])
|
||||
def test_version_and_pages(self, cls, version, pages):
|
||||
tag = activate(cls)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x60"))).data == version
|
||||
assert cls(uid=UID)._total_pages == pages
|
||||
|
||||
def test_page3_is_otp_not_cc(self):
|
||||
# EV1 ships with OTP (zeros) at page 3, no CC.
|
||||
tag = activate(MF0UL11)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4] == b"\x00\x00\x00\x00"
|
||||
|
||||
def test_ndef_stamps_cc_into_otp(self):
|
||||
# Supplying an NDEF message NDEF-formats the tag: product CC into page 3.
|
||||
tag = activate(MF0UL21, ndef_message=b"\xD1\x01\x04\x54\x02enHi")
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))).data[0:4] == b"\xE1\x10\x10\x00"
|
||||
# NDEF TLV present at page 4
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))).data[0] == 0x03
|
||||
|
||||
def test_three_counters(self):
|
||||
tag = activate(MF0UL21)
|
||||
for n in range(3):
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(bytes([0x39, n])))).data == b"\x00\x00\x00"
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA5\x01\x0A\x00\x00\x00"))) # counter 1 += 10
|
||||
assert int.from_bytes(run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x01"))).data, "little") == 10
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x39\x00"))).data == b"\x00\x00\x00" # unchanged
|
||||
|
||||
def test_counter_overflow_naks(self):
|
||||
tag = activate(MF0UL11)
|
||||
run(tag.handle_frame(RFFrame.from_bytes(b"\xA5\x00\xFF\xFF\xFF\x00"))) # to 0xFFFFFF
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\xA5\x00\x01\x00\x00\x00"))).data == b"\x04"
|
||||
|
||||
def test_check_tearing(self):
|
||||
tag = activate(MF0UL11)
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x3E\x00"))).data == b"\xBD"
|
||||
|
||||
def test_pwd_auth(self):
|
||||
tag = activate(MF0UL21, password=b"\xDE\xAD\xBE\xEF", pack=b"\x12\x34")
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\xDE\xAD\xBE\xEF"))).data == b"\x12\x34"
|
||||
assert run(tag.handle_frame(RFFrame.from_bytes(b"\x1B\x00\x00\x00\x00"))).data == b"\x04"
|
||||
Reference in New Issue
Block a user