feat(rawcli): MIFARE Classic block-level memory map
MFC gets the same memory-map hinting as NTAG, at the block level: block 0 manufacturer block — UID, BCC, SAK, ATQA, vendor data sector trailer Key A [0-5], access bits [6-8] + GPB [9], Key B [10-15] other blocks data block (sector N) Handles 1K (16 sectors), Mini (5 sectors) and 4K — including the eight 16-block sectors 32-39 (trailer = last block, e.g. 143 = sector 32, 255 = sector 39). landmark_pages lists block 0 + every sector trailer (the map skeleton; data blocks are uniform), so READ( / raw 30 surface it in the completion dropdown. Also routes "MIFARE Mini" to the MFC catalog (was falling through to Type 2). Tests cover the block roles, large-sector trailers, bounds, Mini routing, and the dropdown map. Path verified end-to-end with a mocked SAK-0x08 scan (identify -> catalog -> completer). Live hardware verify still needs an actual Classic card on the antenna. 1339 green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -194,7 +194,7 @@ def catalog_for(session) -> Catalog | None:
|
||||
if session.protocol == "hf14a":
|
||||
if not session.transponder:
|
||||
return None
|
||||
return MFC if "CLASSIC" in name else TYPE2
|
||||
return MFC if ("CLASSIC" in name or "MINI" in name) else TYPE2
|
||||
if session.protocol == "lf":
|
||||
if not session.transponder:
|
||||
return None
|
||||
|
||||
@@ -70,6 +70,56 @@ def _cfg1_role(layout: dict) -> str:
|
||||
return f"CFG1 — ACCESS ({bits})"
|
||||
|
||||
|
||||
# ---- MIFARE Classic block map --------------------------------------------------------------
|
||||
# 1K/Mini: 4-block sectors throughout. 4K: sectors 0-31 are 4 blocks, sectors 32-39 are 16 blocks.
|
||||
# The sector trailer (last block of each sector) holds Key A / access bits+GPB / Key B; block 0 is
|
||||
# the manufacturer block. Everything else is a data block.
|
||||
_MFC_MAX_BLOCK = {"mini": 19, "1k": 63, "4k": 255}
|
||||
|
||||
|
||||
def _mfc_size(transponder: str | None) -> str | None:
|
||||
"""'mini' / '1k' / '4k' for a MIFARE Classic name, else None."""
|
||||
if not transponder:
|
||||
return None
|
||||
up = transponder.upper()
|
||||
if "MINI" in up and "MIFARE" in up:
|
||||
return "mini"
|
||||
if "CLASSIC" in up:
|
||||
return "4k" if "4K" in up else "1k"
|
||||
return None
|
||||
|
||||
|
||||
def _mfc_sector(size: str, block: int) -> tuple[int, bool]:
|
||||
"""(sector number, is-trailer) for ``block`` on a Classic of ``size``."""
|
||||
if size == "4k" and block >= 128: # the eight 16-block sectors 32-39
|
||||
sector = 32 + (block - 128) // 16
|
||||
return sector, (block - 128) % 16 == 15
|
||||
sector = block // 4
|
||||
return sector, block % 4 == 3
|
||||
|
||||
|
||||
def _mfc_block_role(transponder: str | None, block: int | None) -> str | None:
|
||||
size = _mfc_size(transponder)
|
||||
if size is None or block is None or not (0 <= block <= _MFC_MAX_BLOCK[size]):
|
||||
return None
|
||||
if block == 0:
|
||||
return "manufacturer block — UID, BCC, SAK, ATQA, vendor data"
|
||||
sector, is_trailer = _mfc_sector(size, block)
|
||||
if is_trailer:
|
||||
return f"sector {sector} trailer — Key A [0-5], access bits [6-8] + GPB [9], Key B [10-15]"
|
||||
return f"data block (sector {sector})"
|
||||
|
||||
|
||||
def _mfc_landmarks(transponder: str | None) -> list[tuple[int, str]]:
|
||||
"""Block 0 + every sector trailer — the Classic memory-map skeleton (data blocks are uniform)."""
|
||||
size = _mfc_size(transponder)
|
||||
if size is None:
|
||||
return []
|
||||
blocks = [0] + [b for b in range(1, _MFC_MAX_BLOCK[size] + 1)
|
||||
if _mfc_sector(size, b)[1]]
|
||||
return [(b, _mfc_block_role(transponder, b)) for b in blocks]
|
||||
|
||||
|
||||
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:
|
||||
@@ -89,6 +139,8 @@ def page_role(transponder: str | None, page: int | None) -> str | None:
|
||||
or a T5577 block role, or None when unknown."""
|
||||
if transponder and "T5577" in transponder.upper():
|
||||
return _t5577_block_role(page)
|
||||
if _mfc_size(transponder):
|
||||
return _mfc_block_role(transponder, page)
|
||||
layout = _resolve_layout(transponder)
|
||||
if layout is None or page is None:
|
||||
return None
|
||||
@@ -120,6 +172,8 @@ def landmark_pages(transponder: str | None) -> list[tuple[int, str]]:
|
||||
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)]
|
||||
if _mfc_size(transponder):
|
||||
return _mfc_landmarks(transponder)
|
||||
layout = _resolve_layout(transponder)
|
||||
if layout is None:
|
||||
return []
|
||||
|
||||
Reference in New Issue
Block a user