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:
@@ -94,11 +94,12 @@ class RawCompleter(Completer):
|
||||
else:
|
||||
base, body = "hex", low
|
||||
for page, role in landmark_pages(self._session.transponder):
|
||||
# a raw byte is fixed-width (2 hex / 8 binary digits) — match the full-width form only
|
||||
if base == "bin":
|
||||
val, forms = f"{page:08b}", (f"{page:08b}", f"{page:b}")
|
||||
val = f"{page:08b}"
|
||||
else:
|
||||
val, forms = f"{page:02X}", (f"{page:02x}", f"{page:x}")
|
||||
if body and not any(f.startswith(body) for f in forms):
|
||||
val = f"{page:02X}"
|
||||
if body and not val.lower().startswith(body):
|
||||
continue
|
||||
yield Completion(val, start_position=-len(partial),
|
||||
display=f"{val} {role}", display_meta=role)
|
||||
|
||||
@@ -12,28 +12,24 @@ avoid an import-order circular, with a test guarding against drift.
|
||||
from __future__ import annotations
|
||||
|
||||
# Page layout per NTAG21x / Ultralight EV1 IC (user range inclusive; keys are identify's names).
|
||||
# The static Type-2 header (pages 0-3) and PWD/PACK/config structure are shared; the family only
|
||||
# differs in page 3 (CC on NTAG, OTP on Ultralight), the ASCII mirror (NTAG only), and the CFG1
|
||||
# NFC-counter bits (NTAG 213/215/216 only). "lock" is the *dynamic* lock page (absent on the
|
||||
# smallest parts); "cnt" flags the CFG1 NFC-counter bits.
|
||||
_LAYOUTS = {
|
||||
"NTAG210": {"user": (0x04, 0x0F), "cfg0": 0x10, "cfg1": 0x11, "pwd": 0x12, "pack": 0x13},
|
||||
"NTAG212": {"user": (0x04, 0x23), "lock": 0x24, "cfg0": 0x25, "cfg1": 0x26, "pwd": 0x27, "pack": 0x28},
|
||||
"NTAG213": {"user": (0x04, 0x27), "lock": 0x28, "cfg0": 0x29, "cfg1": 0x2A, "pwd": 0x2B, "pack": 0x2C},
|
||||
"NTAG215": {"user": (0x04, 0x81), "lock": 0x82, "cfg0": 0x83, "cfg1": 0x84, "pwd": 0x85, "pack": 0x86},
|
||||
"NTAG216": {"user": (0x04, 0xE1), "lock": 0xE2, "cfg0": 0xE3, "cfg1": 0xE4, "pwd": 0xE5, "pack": 0xE6},
|
||||
"MIFARE Ultralight EV1 (MF0UL11)": {"user": (0x04, 0x0F), "cfg0": 0x10, "cfg1": 0x11, "pwd": 0x12, "pack": 0x13},
|
||||
"MIFARE Ultralight EV1 (MF0UL21)": {"user": (0x04, 0x23), "lock": 0x24, "cfg0": 0x25, "cfg1": 0x26, "pwd": 0x27, "pack": 0x28},
|
||||
"NTAG210": {"user": (0x04, 0x0F), "cfg0": 0x10, "cfg1": 0x11, "pwd": 0x12, "pack": 0x13, "fam": "ntag"},
|
||||
"NTAG212": {"user": (0x04, 0x23), "lock": 0x24, "cfg0": 0x25, "cfg1": 0x26, "pwd": 0x27, "pack": 0x28, "fam": "ntag"},
|
||||
"NTAG213": {"user": (0x04, 0x27), "lock": 0x28, "cfg0": 0x29, "cfg1": 0x2A, "pwd": 0x2B, "pack": 0x2C, "fam": "ntag", "cnt": True},
|
||||
"NTAG215": {"user": (0x04, 0x81), "lock": 0x82, "cfg0": 0x83, "cfg1": 0x84, "pwd": 0x85, "pack": 0x86, "fam": "ntag", "cnt": True},
|
||||
"NTAG216": {"user": (0x04, 0xE1), "lock": 0xE2, "cfg0": 0xE3, "cfg1": 0xE4, "pwd": 0xE5, "pack": 0xE6, "fam": "ntag", "cnt": True},
|
||||
"MIFARE Ultralight EV1 (MF0UL11)": {"user": (0x04, 0x0F), "cfg0": 0x10, "cfg1": 0x11, "pwd": 0x12, "pack": 0x13, "fam": "ul"},
|
||||
"MIFARE Ultralight EV1 (MF0UL21)": {"user": (0x04, 0x23), "lock": 0x24, "cfg0": 0x25, "cfg1": 0x26, "pwd": 0x27, "pack": 0x28, "fam": "ul"},
|
||||
}
|
||||
|
||||
_ROLES = {
|
||||
"cfg0": "CFG0 — MIRROR / MIRROR_PAGE / AUTH0",
|
||||
"cfg1": "CFG1 — ACCESS (PROT / CFGLCK / AUTHLIM)",
|
||||
"pwd": "PWD (32-bit password)",
|
||||
"pack": "PACK (password acknowledge)",
|
||||
"lock": "dynamic lock bytes",
|
||||
}
|
||||
|
||||
# The pages guaranteed on every Type 2 tag (down to the smallest, NTAG210): UID/CC and user
|
||||
# memory 0x04-0x0F. Used when only the NTAG/Ultralight *family* is known — GET_VERSION couldn't
|
||||
# The pages guaranteed on every Type 2 tag (down to the smallest, NTAG210): the static header and
|
||||
# user memory 0x04-0x0F. Used when only the NTAG/Ultralight *family* is known — GET_VERSION couldn't
|
||||
# complete on a flaky link, or an original Ultralight has no GET_VERSION. Config pages differ by
|
||||
# model, so they're left unnamed rather than guessed.
|
||||
# model, so they're left unnamed rather than guessed; page 3 could be CC or OTP.
|
||||
_GENERIC_TYPE2 = {"user": (0x04, 0x0F)}
|
||||
|
||||
|
||||
@@ -51,6 +47,29 @@ def _resolve_layout(transponder: str | None) -> dict | None:
|
||||
return None
|
||||
|
||||
|
||||
def _page3_role(layout: dict) -> str:
|
||||
fam = layout.get("fam")
|
||||
if fam == "ul":
|
||||
return "OTP (one-time programmable)"
|
||||
if fam == "ntag":
|
||||
return "Capability Container (CC)"
|
||||
return "Capability Container (CC) / OTP" # generic: family unknown
|
||||
|
||||
|
||||
def _cfg0_role(layout: dict) -> str:
|
||||
# CFG0: AUTH0 (first protected page) + strong-modulation; NTAG adds the ASCII mirror
|
||||
if layout.get("fam") == "ntag":
|
||||
return "CFG0 — AUTH0 (first protected page), MIRROR (mode/page/byte), STRG_MOD_EN"
|
||||
return "CFG0 — AUTH0 (first protected page), STRG_MOD_EN"
|
||||
|
||||
|
||||
def _cfg1_role(layout: dict) -> str:
|
||||
# CFG1 ACCESS byte: read/write protection, config lock, auth-retry limit; NTAG counters add
|
||||
# the NFC-counter enable/protect bits
|
||||
bits = "PROT, CFGLCK, " + ("NFC_CNT_EN, NFC_CNT_PWD_PROT, " if layout.get("cnt") else "") + "AUTHLIM"
|
||||
return f"CFG1 — ACCESS ({bits})"
|
||||
|
||||
|
||||
def _t5577_block_role(block: int | None) -> str | None:
|
||||
"""Role of a T5577 block: 0 = config, 7 = password, 1-6 = data (the emulated tag content)."""
|
||||
if block is None:
|
||||
@@ -65,39 +84,47 @@ def _t5577_block_role(block: int | None) -> str | None:
|
||||
|
||||
|
||||
def page_role(transponder: str | None, page: int | None) -> str | None:
|
||||
"""The role of ``page``/``block`` on ``transponder`` (user memory / CC / AUTH0 / PWD /
|
||||
T5577 config / password / …), or None."""
|
||||
"""The role of ``page``/``block`` on ``transponder`` — the full Type 2 header (UID / static
|
||||
lock / CC-or-OTP), user memory, dynamic lock, CFG0/CFG1 (family-accurate fields), PWD, PACK —
|
||||
or a T5577 block role, or None when unknown."""
|
||||
if transponder and "T5577" in transponder.upper():
|
||||
return _t5577_block_role(page)
|
||||
layout = _resolve_layout(transponder)
|
||||
if layout is None or page is None:
|
||||
return None
|
||||
if 0 <= page <= 2:
|
||||
if 0 <= page <= 1:
|
||||
return "UID / serial number"
|
||||
if page == 2:
|
||||
return "static lock bytes + internal (locks pages 03-0F)"
|
||||
if page == 3:
|
||||
return "Capability Container (CC)"
|
||||
return _page3_role(layout)
|
||||
lo, hi = layout["user"]
|
||||
if lo <= page <= hi:
|
||||
return "user memory"
|
||||
for key, role in _ROLES.items():
|
||||
if page == layout.get(key):
|
||||
return role
|
||||
if page == layout.get("lock"):
|
||||
return "dynamic lock bytes"
|
||||
if page == layout.get("cfg0"):
|
||||
return _cfg0_role(layout)
|
||||
if page == layout.get("cfg1"):
|
||||
return _cfg1_role(layout)
|
||||
if page == layout.get("pwd"):
|
||||
return "PWD (32-bit password)"
|
||||
if page == layout.get("pack"):
|
||||
return "PACK (password acknowledge) + RFUI"
|
||||
return None
|
||||
|
||||
|
||||
def landmark_pages(transponder: str | None) -> list[tuple[int, str]]:
|
||||
"""Notable pages/blocks with their datasheet role, for argument autocomplete inside a
|
||||
``READ(<page>)`` / ``WRITE(<block>)`` call — so the completion menu lists the memory map
|
||||
(``0x03 → Capability Container``, ``0xE5 → PWD``, …). Ordered by page. Empty when the tag
|
||||
has no known layout."""
|
||||
"""Notable pages/blocks with their datasheet role, for the completion menu's memory map. Every
|
||||
named region of the identified tag — UID, static lock, CC/OTP, user, dynamic lock, CFG0/CFG1,
|
||||
PWD, PACK. Ordered by page; empty when the tag has no known layout."""
|
||||
if transponder and "T5577" in transponder.upper():
|
||||
return [(b, _t5577_block_role(b)) for b in range(8)]
|
||||
layout = _resolve_layout(transponder)
|
||||
if layout is None:
|
||||
return []
|
||||
out = {0: "UID / serial number", 3: "Capability Container (CC)",
|
||||
layout["user"][0]: "user memory"}
|
||||
pages = {0, 2, 3, layout["user"][0]}
|
||||
for key in ("lock", "cfg0", "cfg1", "pwd", "pack"):
|
||||
if key in layout:
|
||||
out[layout[key]] = _ROLES.get(key, key)
|
||||
return sorted(out.items())
|
||||
pages.add(layout[key])
|
||||
return [(p, page_role(transponder, p)) for p in sorted(pages) if page_role(transponder, p)]
|
||||
|
||||
Reference in New Issue
Block a user