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

@@ -108,3 +108,35 @@ def test_mf_sniff_routes_to_14a():
assert t.send_mix.call_args.args[0] == Cmd.HF_ISO14443A_SNIFF
assert t.send_mix.call_args.kwargs["arg0"] == 0
t.send_ng.assert_not_called()
def test_mfu_rdbl():
import struct as _struct
from pm3py.core.hf_mfu import HFMFUCommands
t = AsyncMock()
mfu = HFMFUCommands(t)
# firmware replies 16 bytes (4 pages) directly
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFAREU_READBL, status=0,
reason=0, ng=True, data=bytes(range(16)))
r = asyncio.get_event_loop().run_until_complete(mfu.rdbl(block=4))
assert r["success"] is True
assert r["data"] == "000102030405060708090a0b0c0d0e0f"
# request packs mful_readblock_t {use_schann,block_no,num,keytype,keylen,key[16]} = 21 bytes
payload = t.send_ng.call_args.args[1]
assert len(payload) == 21
assert payload[1] == 4 # block_no
assert t.send_ng.call_args.args[0] == Cmd.HF_MIFAREU_READBL
def test_mfu_wrbl():
from pm3py.core.hf_mfu import HFMFUCommands
t = AsyncMock()
mfu = HFMFUCommands(t)
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFAREU_WRITEBL, status=0,
reason=0, ng=True, data=b"")
r = asyncio.get_event_loop().run_until_complete(mfu.wrbl(block=5, data="aabbccdd"))
assert r["success"] is True
payload = t.send_ng.call_args.args[1]
assert len(payload) == 36 # {use_schann,block,keytype,keylen,key[16],data[16]}
assert payload[1] == 5
assert payload[20:24] == bytes.fromhex("aabbccdd")