Big transponder support update

This commit is contained in:
michael
2026-07-05 09:53:52 -07:00
parent 950628ef4e
commit dc626faa60
42 changed files with 5859 additions and 6 deletions

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

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

View 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

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

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