- NfcType2Tag._build_mfu_dump(): assemble the firmware mfu_dump_t (56-byte prefix version/pages/signature + page data) so the sim serves real memory (UID/CC/NDEF/ PWD/PACK). Firmware is data-driven for tagtype 2/7, so an NTAG213 dump presents correctly under tagtype 7. - NfcType2Tag.sync(): push the image via CMD_HF_MIFARE_EML_MEMSET (same primitive as the client's hf mfu esetblk), chunked. Updates a live sim with the fw host-poll. - start_14a: load the dump before HF_ISO14443A_SIMULATE so the reader sees real page data instead of an empty buffer (works on current fw; no reflash for this). - fix: set_ndef/clear_ndef cleared memory to the end, wiping the lock/config/PWD/ PACK pages on NTAG parts. Bounded to the CC-declared NDEF area now. - tests: mfu_dump layout, config-page preservation, EML_MEMSET framing/chunking. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.4 KiB
Python
102 lines
3.4 KiB
Python
"""14a sim memory sync: mfu_dump_t builder, EML_MEMSET push, config-page safety.
|
|
|
|
Covers the P0 fix — loading a Type 2 (NTAG/Ultralight) model's memory into the
|
|
firmware emulator so the reader sees real page data (UID/CC/NDEF/PWD/PACK).
|
|
"""
|
|
import struct
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
from pm3py.sim import NTAG213, NTAG216, ndef_uri
|
|
from pm3py.transponders.hf.iso14443a.ndef import (
|
|
MFU_DUMP_PREFIX_LENGTH, CMD_HF_MIFARE_EML_MEMSET, PAGE_SIZE,
|
|
)
|
|
|
|
|
|
def test_mfu_dump_layout():
|
|
t = NTAG213()
|
|
t.set_ndef(ndef_uri("https://dngr.us/rick"))
|
|
img = t._build_mfu_dump()
|
|
|
|
assert len(img) == MFU_DUMP_PREFIX_LENGTH + t._total_pages * PAGE_SIZE
|
|
assert img[0:8] == t._version_bytes # version[8]
|
|
assert img[11] == t._total_pages - 1 # pages = last page index
|
|
assert img[12:44] == bytes(t._signature)[:32].ljust(32, b"\x00") # signature[32]
|
|
# page data begins at offset 56 and mirrors _memory
|
|
assert img[MFU_DUMP_PREFIX_LENGTH:] == bytes(t._memory)
|
|
# NDEF TLV present at page 4
|
|
assert img[MFU_DUMP_PREFIX_LENGTH + 4 * PAGE_SIZE] == 0x03
|
|
|
|
|
|
def test_set_ndef_preserves_config_pages():
|
|
"""set_ndef must not wipe the lock/config/PWD/PACK pages above the NDEF area."""
|
|
t = NTAG213()
|
|
assert t.get_page(0x2B) == b"\xff\xff\xff\xff" # PWD default
|
|
assert t.get_page(0x29)[3] == 0xFF # CFG0 AUTH0
|
|
|
|
t.set_ndef(ndef_uri("https://dngr.us/rick"))
|
|
|
|
assert t.get_page(0x2B) == b"\xff\xff\xff\xff" # PWD survived
|
|
assert t.get_page(0x29)[3] == 0xFF # AUTH0 survived
|
|
assert t.get_page(4)[0] == 0x03 # NDEF TLV written
|
|
|
|
|
|
def test_clear_ndef_preserves_config_pages():
|
|
t = NTAG213()
|
|
t.set_ndef(ndef_uri("https://dngr.us/rick"))
|
|
t.clear_ndef()
|
|
assert t.get_page(0x2B) == b"\xff\xff\xff\xff" # PWD still intact
|
|
assert t.get_page(4) == b"\x00\x00\x00\x00" # NDEF area cleared
|
|
|
|
|
|
def _capture_eml_writes(tag):
|
|
"""Run tag.sync() with a mock serial; return list of (cmd, payload)."""
|
|
captured = []
|
|
|
|
def fake_encode(cmd, payload=b""):
|
|
captured.append((cmd, payload))
|
|
return b""
|
|
|
|
tag._serial = MagicMock()
|
|
with patch("pm3py.core.transport.encode_ng_frame", side_effect=fake_encode):
|
|
tag.sync()
|
|
return captured
|
|
|
|
|
|
def test_sync_pushes_full_image_via_eml_memset():
|
|
t = NTAG213()
|
|
t.set_ndef(ndef_uri("https://dngr.us/rick"))
|
|
image = t._build_mfu_dump()
|
|
|
|
frames = _capture_eml_writes(t)
|
|
assert frames, "sync() wrote no frames"
|
|
|
|
reassembled = bytearray()
|
|
for cmd, payload in frames:
|
|
assert cmd == CMD_HF_MIFARE_EML_MEMSET
|
|
blockno, blockcnt, blockwidth = struct.unpack_from("<HBB", payload, 0)
|
|
assert blockwidth == PAGE_SIZE
|
|
data = payload[4:]
|
|
assert len(data) == blockcnt * blockwidth
|
|
assert blockno * blockwidth == len(reassembled) # contiguous, in order
|
|
reassembled += data
|
|
|
|
assert bytes(reassembled) == image
|
|
|
|
|
|
def test_sync_chunks_large_tag():
|
|
"""NTAG216 (~231 pages) exceeds one 512-byte frame -> multiple EML_MEMSET."""
|
|
t = NTAG216()
|
|
frames = _capture_eml_writes(t)
|
|
assert len(frames) >= 2
|
|
# every chunk stays within the NG payload cap
|
|
for _cmd, payload in frames:
|
|
assert len(payload) <= 512
|
|
|
|
|
|
def test_sync_requires_live_session():
|
|
import pytest
|
|
t = NTAG213()
|
|
t._serial = None
|
|
with pytest.raises(RuntimeError, match="live session"):
|
|
t.sync()
|