feat(rawcli): complete + family-accurate NTAG21x / Ultralight EV1 memory map
The page hinting was coarse and partly wrong: pages 0-2 were all "UID", page 3 was always
"CC" (it's OTP on Ultralight), config fields were abbreviated, and the NTAG210/212 (no counter)
and Ultralight (no ASCII mirror) differences were ignored. Now every named region is covered
and the fields track the actual IC:
00-01 UID / serial number
02 static lock bytes + internal (locks pages 03-0F)
03 Capability Container (CC) [NTAG] / OTP (one-time programmable) [Ultralight]
04..N user memory
<lock> dynamic lock bytes (present on 212/213/215/216 and MF0UL21; absent on 210/UL11)
CFG0 AUTH0, MIRROR (mode/page/byte on NTAG only), STRG_MOD_EN
CFG1 ACCESS: PROT, CFGLCK, [NFC_CNT_EN, NFC_CNT_PWD_PROT on 213/215/216], AUTHLIM
PWD 32-bit password
PACK password acknowledge + RFUI
_LAYOUTS gains a family tag ("ntag"/"ul") and a counter flag; page_role/landmark_pages derive
page 3 (CC vs OTP) and the CFG0/CFG1 field lists from them. The generic fallback (unknown model)
now also names the static-lock and CC/OTP header pages. Raw-byte completion matches the
fixed-width byte form only (so partial "2" no longer wrongly hits page 02).
Hardware-verified on NTAG213: both READ( and raw "30 " list the full 9-region map. Tests cover
the family differences (counter bits, mirror, OTP, dynamic-lock presence) and layout/model drift
for the Ultralight parts too. 1330 green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -634,23 +634,43 @@ class TestMemoryHint:
|
||||
def test_page_role(self):
|
||||
from pm3py.cli.rawcli.memory import page_role
|
||||
assert page_role("NTAG213", 0) == "UID / serial number"
|
||||
assert page_role("NTAG213", 1) == "UID / serial number"
|
||||
assert "static lock" in page_role("NTAG213", 2) # page 2 is lock/internal, not UID
|
||||
assert page_role("NTAG213", 3) == "Capability Container (CC)"
|
||||
assert page_role("NTAG213", 4) == "user memory"
|
||||
assert page_role("NTAG213", 0x28) == "dynamic lock bytes"
|
||||
assert "AUTH0" in page_role("NTAG213", 0x29) # CFG0
|
||||
assert page_role("NTAG213", 0x2A).startswith("CFG1")
|
||||
assert "PWD" in page_role("NTAG213", 0x2B)
|
||||
assert "PACK" in page_role("NTAG213", 0x2C)
|
||||
assert page_role("NTAG216", 0x04) == "user memory" # different IC, different pages
|
||||
assert "AUTH0" in page_role("NTAG216", 0xE3)
|
||||
|
||||
def test_page_role_family_differences(self):
|
||||
from pm3py.cli.rawcli.memory import page_role
|
||||
# NTAG 213/215/216 have the NFC counter bits in CFG1; 210/212 don't
|
||||
assert "NFC_CNT_EN" in page_role("NTAG213", 0x2A)
|
||||
assert "NFC_CNT_EN" not in page_role("NTAG210", 0x11) # CFG1, no counter
|
||||
assert "NFC_CNT_EN" not in page_role("NTAG212", 0x26)
|
||||
# NTAG has the ASCII mirror in CFG0; Ultralight EV1 does not
|
||||
assert "MIRROR" in page_role("NTAG213", 0x29)
|
||||
assert "MIRROR" not in page_role("MIFARE Ultralight EV1 (MF0UL11)", 0x10)
|
||||
# page 3 is CC on NTAG, OTP on Ultralight
|
||||
assert page_role("MIFARE Ultralight EV1 (MF0UL11)", 3) == "OTP (one-time programmable)"
|
||||
assert page_role("MIFARE Ultralight EV1 (MF0UL21)", 3).startswith("OTP")
|
||||
# UL21 has a dynamic lock page (like NTAG212); UL11 doesn't (like NTAG210)
|
||||
assert page_role("MIFARE Ultralight EV1 (MF0UL21)", 0x24) == "dynamic lock bytes"
|
||||
|
||||
def test_generic_type2_fallback(self):
|
||||
from pm3py.cli.rawcli.memory import landmark_pages, page_role
|
||||
# only the family is known (flaky/absent GET_VERSION) -> universal pages still resolve
|
||||
# only the family is known (flaky/absent GET_VERSION) -> the shared header still resolves
|
||||
for name in ("MIFARE Ultralight / NTAG", "NTAG (144 B)"):
|
||||
assert page_role(name, 4) == "user memory"
|
||||
assert page_role(name, 0) == "UID / serial number"
|
||||
assert page_role(name, 3) == "Capability Container (CC)"
|
||||
assert "static lock" in page_role(name, 2)
|
||||
assert page_role(name, 3) == "Capability Container (CC) / OTP" # family unknown
|
||||
assert page_role(name, 0x20) is None # model-specific -> not guessed
|
||||
assert [p for p, _ in landmark_pages(name)] == [0, 3, 4] # no config landmarks
|
||||
assert [p for p, _ in landmark_pages(name)] == [0, 2, 3, 4] # header, no config
|
||||
assert page_role("MIFARE Classic 1K", 4) is None # not Type 2 -> nothing
|
||||
|
||||
def test_t5577_block_roles(self):
|
||||
@@ -668,6 +688,18 @@ class TestMemoryHint:
|
||||
L = _LAYOUTS[name]
|
||||
assert (L["cfg0"], L["cfg1"], L["pwd"], L["pack"]) == \
|
||||
(cls._cfg0_page, cls._cfg1_page, cls._pwd_page, cls._pack_page)
|
||||
assert L["fam"] == "ntag"
|
||||
assert L.get("cnt", False) == cls._has_nfc_counter # CFG1 NFC-counter bits
|
||||
|
||||
def test_ultralight_layouts_match_models(self):
|
||||
from pm3py.cli.rawcli.memory import _LAYOUTS
|
||||
from pm3py.sim import MF0UL11, MF0UL21
|
||||
for cls, name in [(MF0UL11, "MIFARE Ultralight EV1 (MF0UL11)"),
|
||||
(MF0UL21, "MIFARE Ultralight EV1 (MF0UL21)")]:
|
||||
L = _LAYOUTS[name]
|
||||
assert (L["cfg0"], L["cfg1"], L["pwd"], L["pack"]) == \
|
||||
(cls._cfg0_page, cls._cfg1_page, cls._pwd_page, cls._pack_page)
|
||||
assert L["fam"] == "ul" # page 3 is OTP, no ASCII mirror
|
||||
|
||||
|
||||
class TestKeyBindings:
|
||||
|
||||
Reference in New Issue
Block a user