Files
pm3py/tests/test_hf_mf.py
michael 3b4ec4541b feat(rawcli): full MIFARE Classic datasheet command set + value blocks
Completes the MIFARE Classic catalog with its datasheet wire commands (the standing rule: every
transponder exposes its full datasheet command set, like NTAG21x), each carrying its wire opcode
so it autocompletes by name and by the hex/binary opcode value, with the block memory map:

  AUTH_A 0x60 / AUTH_B 0x61   auth a sector (test whether a key works; auth is otherwise implicit)
  READ 0x30 / WRITE 0xA0      (existing)
  INCREMENT 0xC1 / DECREMENT 0xC0 / RESTORE 0xC2 / TRANSFER 0xB0   value-block ops
  PERSONALIZE_UID 0x40 / SET_MOD_TYPE 0x43   EV1 (opcode + hint; execution not yet wired)
  CHK (key finder) / HALT 0x50   (existing)

core: hf.mf.value(block, action, value, transfer_block, key, key_type) wires CMD_HF_MIFARE_VALUE
(0x0627) — payload mirrors the stock client's CmdHF14AMfValue / firmware MifareValue (key[0:6],
action[9], transferBlk[10], operand[11:15], transfer-key[27:33], nested-auth flag[33]); the op is
committed to transfer_block, or in place when None; a cross-sector transfer sets the nested-auth
flag. INCREMENT/DECREMENT/RESTORE commit in place; TRANSFER copies a value block block->dest.

No completer/memory/app changes — the machinery is data-driven off the catalog.

Hardware-verified on a MIFARE Classic 1K: identify; c1/60/b0 (and binary) surface INCREMENT/AUTH_A
/TRANSFER with the block map; AUTH_A reports the right key ok and a wrong key failed; a value
round-trip INCREMENTed 100 -> 105 with the value-block format intact (block restored after). 1370
green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 08:47:22 -07:00

177 lines
7.8 KiB
Python

import struct
import asyncio
from unittest.mock import AsyncMock
from pm3py.core.protocol import Cmd
from pm3py.core.transport import PM3Response
from pm3py.core.hf_mfc import HFMFCommands
def test_mf_rdbl():
t = AsyncMock()
mf = HFMFCommands(t)
block_data = bytes(range(16))
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_READBL, status=0,
reason=0, ng=True, data=block_data)
result = asyncio.get_event_loop().run_until_complete(
mf.rdbl(block=0, key="FFFFFFFFFFFF"))
assert result["data"] == block_data.hex()
assert len(result["raw"]) == 16
def test_mf_wrbl():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_mix.return_value = PM3Response(cmd=Cmd.ACK, status=0, reason=0,
ng=False, data=b"", oldarg=[1, 0, 0])
result = asyncio.get_event_loop().run_until_complete(
mf.wrbl(block=4, key="FFFFFFFFFFFF", data="11" * 16))
assert result["success"] is True
# firmware: key at asBytes[0], block_data at asBytes[10]
kwargs = t.send_mix.call_args.kwargs
payload = kwargs["payload"]
assert len(payload) == 26
assert payload[:6] == bytes.fromhex("FFFFFFFFFFFF")
assert payload[6:10] == b"\x00" * 4
assert payload[10:26] == bytes.fromhex("11" * 16)
assert kwargs["arg0"] == 4
assert kwargs["arg1"] == 0
def test_mf_value_increment_in_place():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_mix.return_value = PM3Response(cmd=Cmd.ACK, status=0, reason=0,
ng=False, data=b"", oldarg=[1, 0, 0])
result = asyncio.get_event_loop().run_until_complete(
mf.value(block=5, action=0, value=10, key="FFFFFFFFFFFF"))
assert result == {"success": True, "block": 5} # in-place commit
args, kwargs = t.send_mix.call_args.args, t.send_mix.call_args.kwargs
assert args[0] == Cmd.HF_MIFARE_VALUE
assert kwargs["arg0"] == 5 and kwargs["arg1"] == 0 and kwargs["arg2"] == 0
p = kwargs["payload"]
assert len(p) == 34
assert p[:6] == bytes.fromhex("FFFFFFFFFFFF") # key
assert p[9] == 0 # action = increment
assert p[10] == 0 # transfer in place (commit to block)
assert struct.unpack_from("<i", p, 11)[0] == 10 # operand
assert p[33] == 0 # same sector -> no nested auth
def test_mf_value_transfer_cross_sector():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_mix.return_value = PM3Response(cmd=Cmd.ACK, status=0, reason=0,
ng=False, data=b"", oldarg=[1, 0, 0])
# block 4 (sector 1) restore -> block 8 (sector 2): cross-sector => nested-auth flag set
result = asyncio.get_event_loop().run_until_complete(
mf.value(block=4, action=2, transfer_block=8, key="A0A1A2A3A4A5"))
assert result == {"success": True, "block": 8}
p = t.send_mix.call_args.kwargs["payload"]
assert p[9] == 2 and p[10] == 8 # restore, transfer to block 8
assert p[33] == 1 # crosses sector -> nested auth
def test_mf_nested():
t = AsyncMock()
mf = HFMFCommands(t)
# reply: isOK=0, block=8, keytype=1, cuid, nt_a, ks_a, nt_b, ks_b
reply = struct.pack("<hBB", 0, 8, 1)
reply += bytes.fromhex("01020304") # cuid
reply += bytes.fromhex("11111111") # nt_a
reply += bytes.fromhex("22222222") # ks_a
reply += bytes.fromhex("33333333") # nt_b
reply += bytes.fromhex("44444444") # ks_b
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_NESTED, status=0,
reason=0, ng=True, data=reply)
result = asyncio.get_event_loop().run_until_complete(
mf.nested(block=0, key="FFFFFFFFFFFF", key_type=0,
target_block=8, target_key_type=1, calibrate=True))
# firmware struct order: block, keytype, target_block, target_keytype, calibrate, key[6]
payload = t.send_ng.call_args.args[1]
assert payload == struct.pack("<BBBBB", 0, 0, 8, 1, 1) + bytes.fromhex("FFFFFFFFFFFF")
assert result["success"] is True
assert result["block"] == 8
assert result["key_type"] == 1
assert result["cuid"] == "01020304"
assert result["ks_a"] == "22222222"
assert result["nt_b"] == "33333333"
def test_mf_cident():
t = AsyncMock()
mf = HFMFCommands(t)
# MAGIC_FLAG value returned as little-endian uint16
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_CIDENT, status=0,
reason=0, ng=True,
data=struct.pack("<H", 0x0001))
result = asyncio.get_event_loop().run_until_complete(mf.cident())
payload = t.send_ng.call_args.args[1]
assert payload == bytes([1, 0]) + bytes.fromhex("FFFFFFFFFFFF")
assert result["magic"] == 0x0001
assert result["is_magic"] is True
def test_mf_sim():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_ng.return_value = PM3Response(cmd=Cmd.HF_MIFARE_SIMULATE, status=0,
reason=0, ng=True, data=b"")
result = asyncio.get_event_loop().run_until_complete(
mf.sim(uid="11223344", size="1k", atqa="0400", sak=0x08))
assert result["status"] == 0
payload = t.send_ng.call_args.args[1]
# struct { u16 flags; u8 exitAfter; u8 uid[10]; u16 atqa; u8 sak }
assert len(payload) == 16
flags, exit_after = struct.unpack_from("<HB", payload, 0)
# 4B_UID_IN_DATA | FLAG_MF_1K | ATQA_IN_DATA | SAK_IN_DATA (interactive off by default)
assert flags == (0x0010 | 0x0040 | 0x0002 | 0x0004)
assert exit_after == 0
assert payload[3:7] == bytes.fromhex("11223344")
assert payload[7:13] == b"\x00" * 6
atqa_val, sak_val = struct.unpack_from("<HB", payload, 13)
assert atqa_val == 0x0004 # (0x00<<8)|0x04
assert sak_val == 0x08
def test_mf_sniff_routes_to_14a():
t = AsyncMock()
mf = HFMFCommands(t)
t.send_mix.return_value = PM3Response(cmd=Cmd.HF_ISO14443A_SNIFF, status=0,
reason=0, ng=False, data=b"")
result = asyncio.get_event_loop().run_until_complete(mf.sniff())
assert result["status"] == 0
# must target the ISO14443-A sniffer, not a (nonexistent) MF sniff handler
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")