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>
This commit is contained in:
michael
2026-07-05 00:40:07 -07:00
parent 137ec1e721
commit 75bf0e19cb
5 changed files with 106 additions and 6 deletions

View File

@@ -16,7 +16,7 @@ def test_full_api_surface():
assert hasattr(pm3.lf, "t55")
assert hasattr(pm3.hf, "iso14a")
assert hasattr(pm3.hf, "iso15")
assert hasattr(pm3.hf, "mfc")
assert hasattr(pm3.hf, "mf")
assert hasattr(pm3, "send_ng")
assert hasattr(pm3, "send_mix")
assert hasattr(pm3, "firmware")
@@ -45,9 +45,9 @@ def test_mf_rdbl_returns_hex():
block = bytes(range(16))
mock_transport.send_ng.return_value = PM3Response(
cmd=Cmd.HF_MIFARE_READBL, status=0, reason=0, ng=True, data=block)
pm3.hf.mfc._t = mock_transport
pm3.hf.mf._t = mock_transport
result = asyncio.get_event_loop().run_until_complete(
pm3.hf.mfc.rdbl(block=4, key="FFFFFFFFFFFF"))
pm3.hf.mf.rdbl(block=4, key="FFFFFFFFFFFF"))
assert result["data"] == "000102030405060708090a0b0c0d0e0f"
assert result["raw"] == block