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>
This commit is contained in:
michael
2026-07-16 08:47:22 -07:00
parent fd16fa8f90
commit 3b4ec4541b
4 changed files with 202 additions and 0 deletions

View File

@@ -35,6 +35,40 @@ def test_mf_wrbl():
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)