"""Tests for NDEF Type 2 and Type 4 tag models.""" import asyncio import pytest from pm3py.sim.frame import RFFrame from pm3py.sim.medium import SoftwareMedium from pm3py.sim.iso14443a import Reader14443A, _compute_bcc from pm3py.sim.ndef import NfcType2Tag, NfcType4Tag def run(coro): return asyncio.get_event_loop().run_until_complete(coro) # --------------------------------------------------------------------------- # NFC Type 2 Tag (extends Tag14443A_3) # --------------------------------------------------------------------------- class TestNfcType2Tag: def test_atqa_sak(self): tag = NfcType2Tag(uid=b"\x04\x01\x02\x03\x04\x05\x06", ndef_message=b"") run(tag.power_on()) resp = run(tag.handle_frame(RFFrame.from_hex("26"))) assert resp.data == b"\x44\x00" # NTAG ATQA # SELECT (7-byte UID needs cascade) uid = b"\x04\x01\x02\x03\x04\x05\x06" ct_uid = b"\x88" + uid[0:3] bcc1 = _compute_bcc(ct_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1])))) cl2_uid = uid[3:7] bcc2 = _compute_bcc(cl2_uid) resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2])))) assert resp.data[0] & 0x20 == 0 # SAK bit 5 = 0 (no ISO-DEP) def test_read_returns_4_pages(self): """READ command returns 16 bytes (4 pages of 4 bytes).""" tag = NfcType2Tag(uid=b"\x04\x01\x02\x03\x04\x05\x06", ndef_message=b"\xD1\x01\x04\x54\x02enHi") run(tag.power_on()) self._select(tag) # READ page 0 resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x00"))) assert resp is not None assert len(resp.data) == 16 # 4 pages def test_cc_block_at_page_3(self): """Page 3 is the capability container.""" tag = NfcType2Tag(uid=b"\x04\x01\x02\x03\x04\x05\x06", ndef_message=b"\xD1\x01\x04\x54\x02enHi", total_pages=45) # NTAG213-like run(tag.power_on()) self._select(tag) # READ page 3 (CC is in pages 3) resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x03"))) cc = resp.data[0:4] assert cc[0] == 0xE1 # NDEF magic assert cc[1] == 0x10 # version 1.0 assert cc[2] > 0 # size (in 8-byte units) assert cc[3] == 0x00 # read/write access def test_ndef_data_in_pages_4_plus(self): """NDEF message stored starting at page 4 as TLV.""" msg = b"\xD1\x01\x04\x54\x02enHi" tag = NfcType2Tag(uid=b"\x04\x01\x02\x03\x04\x05\x06", ndef_message=msg) run(tag.power_on()) self._select(tag) # READ page 4 resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))) # TLV: type=0x03 (NDEF), length, message... assert resp.data[0] == 0x03 # NDEF TLV type assert resp.data[1] == len(msg) def test_write_page(self): """WRITE command writes 4 bytes to a page.""" tag = NfcType2Tag(uid=b"\x04\x01\x02\x03\x04\x05\x06", ndef_message=b"") run(tag.power_on()) self._select(tag) # WRITE page 4 resp = run(tag.handle_frame(RFFrame.from_bytes(b"\xA2\x04\xDE\xAD\xBE\xEF"))) assert resp is not None # ACK # Read back resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x30\x04"))) assert resp.data[0:4] == b"\xDE\xAD\xBE\xEF" def _select(self, tag): uid = tag._uid run(tag.handle_frame(RFFrame.from_hex("26"))) ct_uid = b"\x88" + uid[0:3] bcc1 = _compute_bcc(ct_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + ct_uid + bytes([bcc1])))) cl2_uid = uid[3:7] bcc2 = _compute_bcc(cl2_uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x95\x70" + cl2_uid + bytes([bcc2])))) # --------------------------------------------------------------------------- # NFC Type 4 Tag (extends Tag14443A_4) # --------------------------------------------------------------------------- class TestNfcType4Tag: def test_sak_indicates_isodep(self): tag = NfcType4Tag(uid=b"\x04\x01\x02\x03", ndef_message=b"") run(tag.power_on()) run(tag.handle_frame(RFFrame.from_hex("26"))) uid = b"\x04\x01\x02\x03" bcc = _compute_bcc(uid) resp = run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc])))) assert resp.data[0] & 0x20 != 0 # SAK bit 5 = 1 def test_rats_returns_ats(self): tag = NfcType4Tag(uid=b"\x04\x01\x02\x03", ndef_message=b"") run(tag.power_on()) self._select_and_rats(tag) assert tag.state == "PROTOCOL" def test_select_ndef_application(self): """SELECT NDEF application by AID D2760000850101.""" tag = NfcType4Tag(uid=b"\x04\x01\x02\x03", ndef_message=b"\xD1\x01\x04\x54\x02enHi") run(tag.power_on()) self._select_and_rats(tag) # SELECT AID via I-block select_apdu = b"\x00\xA4\x04\x00\x07\xD2\x76\x00\x00\x85\x01\x01" iblock = b"\x02" + select_apdu resp = run(tag.handle_frame(RFFrame.from_bytes(iblock))) assert resp is not None # Should get 9000 (success) assert resp.data[-2:] == b"\x90\x00" or resp.data[1:3] == b"\x90\x00" def test_select_cc_file(self): """SELECT CC file (E103) then READ BINARY.""" tag = NfcType4Tag(uid=b"\x04\x01\x02\x03", ndef_message=b"\xD1\x01\x04\x54\x02enHi") run(tag.power_on()) self._select_and_rats(tag) # Select NDEF app self._send_apdu(tag, b"\x00\xA4\x04\x00\x07\xD2\x76\x00\x00\x85\x01\x01") # Select CC file (E103) resp = self._send_apdu(tag, b"\x00\xA4\x00\x0C\x02\xE1\x03") assert resp is not None # READ BINARY offset=0, length=15 resp = self._send_apdu(tag, b"\x00\xB0\x00\x00\x0F") assert resp is not None # CC file starts with CCLEN(2) + version + ... payload = resp[:-2] # strip SW assert len(payload) >= 7 def test_read_ndef_file(self): """SELECT NDEF file (E104) then READ BINARY to get NDEF message.""" msg = b"\xD1\x01\x04\x54\x02enHi" tag = NfcType4Tag(uid=b"\x04\x01\x02\x03", ndef_message=msg) run(tag.power_on()) self._select_and_rats(tag) # Select NDEF app self._send_apdu(tag, b"\x00\xA4\x04\x00\x07\xD2\x76\x00\x00\x85\x01\x01") # Select NDEF file (E104) self._send_apdu(tag, b"\x00\xA4\x00\x0C\x02\xE1\x04") # READ BINARY — first 2 bytes are NDEF length resp = self._send_apdu(tag, b"\x00\xB0\x00\x00\x20") payload = resp[:-2] nlen = (payload[0] << 8) | payload[1] assert nlen == len(msg) assert payload[2:2 + nlen] == msg def _select_and_rats(self, tag): run(tag.handle_frame(RFFrame.from_hex("26"))) uid = tag._uid[:4] bcc = _compute_bcc(uid) run(tag.handle_frame(RFFrame.from_bytes(b"\x93\x70" + uid + bytes([bcc])))) run(tag.handle_frame(RFFrame.from_hex("E050"))) def _send_apdu(self, tag, apdu: bytes) -> bytes: """Send APDU via I-block, return response payload (including SW).""" pcb = 0x02 resp = run(tag.handle_frame(RFFrame.from_bytes(bytes([pcb]) + apdu))) if resp is None: return b"" return resp.data[1:] # strip PCB