Files
pm3py/pm3py/core/hf_mfu.py
michael 75bf0e19cb feat(hf): rename mfc -> mf (MIFARE Classic), add mfu (Ultralight/NTAG)
hf.mfc renamed to hf.mf to match the C client's 'hf mf'. New hf.mfu (HFMFUCommands) provides rdbl/wrbl/dump over the dedicated CMD_HF_MIFAREU_READBL/WRITEBL using the firmware mful_readblock_t / mful_writeblock_t structs; a READ returns 4 pages (16 bytes) and the firmware replies them directly. keytype selects auth (0=none, 1=UL-C, 2=EV1/NTAG pwd, 3=UL-AES). Wire formats verified against firmware structs; needs a bench pass for the auth/write-length paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 00:40:07 -07:00

65 lines
2.6 KiB
Python

"""MIFARE Ultralight / NTAG commands: hf.mfu.*
ISO14443-3A tags with 4-byte pages. A READ (0x30) returns 4 consecutive
pages (16 bytes). keytype selects the auth scheme:
0 = none, 1 = UL-C (16-byte 3DES key), 2 = UL-EV1/NTAG (4-byte pwd),
3 = UL-AES (16-byte key).
"""
import struct
from .protocol import Cmd
from .transport import PM3Transport
def _key_bytes(key) -> bytes:
if key is None:
return b""
if isinstance(key, str):
return bytes.fromhex(key)
return bytes(key)
class HFMFUCommands:
"""MIFARE Ultralight / NTAG commands."""
def __init__(self, transport: PM3Transport):
self._t = transport
async def rdbl(self, block: int, key=None, keytype: int = 0) -> dict:
"""Read starting at page ``block``; returns 16 bytes (4 pages)."""
kb = _key_bytes(key)
# mful_readblock_t { bool use_schann; u8 block_no; u8 num_of_blocks;
# u8 keytype; u8 keylen; u8 key[16] }
payload = struct.pack("<BBBBB16s", 0, block & 0xFF, 1, keytype & 0xFF,
len(kb), kb.ljust(16, b"\x00")[:16])
resp = await self._t.send_ng(Cmd.HF_MIFAREU_READBL, payload, timeout=5.0)
if resp.status != 0:
return {"success": False, "block": block, "error": resp.status}
return {"success": True, "block": block,
"data": resp.data.hex(), "raw": bytes(resp.data)}
async def wrbl(self, block: int, data, key=None, keytype: int = 0) -> dict:
"""Write 4 bytes to page ``block``."""
if isinstance(data, str):
data = bytes.fromhex(data)
kb = _key_bytes(key)
# mful_writeblock_t { bool use_schann; u8 block_no; u8 keytype;
# u8 keylen; u8 key[16]; u8 data[16] }
payload = struct.pack("<BBBB16s16s", 0, block & 0xFF, keytype & 0xFF,
len(kb), kb.ljust(16, b"\x00")[:16],
bytes(data).ljust(16, b"\x00")[:16])
resp = await self._t.send_ng(Cmd.HF_MIFAREU_WRITEBL, payload, timeout=5.0)
return {"success": resp.status == 0, "block": block}
async def dump(self, start: int = 0, pages: int = 16) -> dict:
"""Read ``pages`` pages from ``start`` (each READ returns 4 pages)."""
out = bytearray()
page = start
while page < start + pages:
r = await self.rdbl(page)
if not r.get("success"):
break
out += r["raw"][:16]
page += 4
return {"success": len(out) > 0, "pages": len(out) // 4,
"data": out.hex(), "raw": bytes(out)}