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:
@@ -134,15 +134,89 @@ def _mfc_chk(dev, block, keytype="A"):
|
||||
return f"CHK block {_int(block)} key {str(keytype).upper()}: no key from the default list works"
|
||||
|
||||
|
||||
def _mfc_auth(dev, block, key, keytype):
|
||||
# a successful authenticated read == the key authenticates that sector (standalone crypto1 auth
|
||||
# isn't a firmware single-shot; auth is otherwise implicit in READ/WRITE)
|
||||
r = dev.hf.mf.rdbl(_int(block), key=key, key_type=_kt(keytype))
|
||||
ok = isinstance(r, dict) and r.get("success")
|
||||
return f"AUTH_{str(keytype).upper()} block {_int(block)} key {key}: {'ok' if ok else 'auth failed'}"
|
||||
|
||||
|
||||
def _mfc_auth_a(dev, block, key="FFFFFFFFFFFF"):
|
||||
return _mfc_auth(dev, block, key, "A")
|
||||
|
||||
|
||||
def _mfc_auth_b(dev, block, key="FFFFFFFFFFFF"):
|
||||
return _mfc_auth(dev, block, key, "B")
|
||||
|
||||
|
||||
def _mfc_value(dev, block, action, value, verb, key, keytype):
|
||||
r = dev.hf.mf.value(_int(block), action, value=value, key=key, key_type=_kt(keytype))
|
||||
ok = isinstance(r, dict) and r.get("success")
|
||||
return f"{verb} block {_int(block)}: {'ok' if ok else 'failed'} (key {str(keytype).upper()})"
|
||||
|
||||
|
||||
def _mfc_incr(dev, block, value="1", key="FFFFFFFFFFFF", keytype="A"):
|
||||
return _mfc_value(dev, block, 0, _int(value), f"INCREMENT by {_int(value)}", key, keytype)
|
||||
|
||||
|
||||
def _mfc_decr(dev, block, value="1", key="FFFFFFFFFFFF", keytype="A"):
|
||||
return _mfc_value(dev, block, 1, _int(value), f"DECREMENT by {_int(value)}", key, keytype)
|
||||
|
||||
|
||||
def _mfc_restore(dev, block, key="FFFFFFFFFFFF", keytype="A"):
|
||||
return _mfc_value(dev, block, 2, 0, "RESTORE (in place)", key, keytype)
|
||||
|
||||
|
||||
def _mfc_transfer(dev, block, dest, key="FFFFFFFFFFFF", keytype="A"):
|
||||
# datasheet TRANSFER commits the transfer buffer; the firmware fuses it with the value op, so
|
||||
# expose it as a value-block copy: restore <block> then transfer to <dest>
|
||||
r = dev.hf.mf.value(_int(block), 2, transfer_block=_int(dest), key=key, key_type=_kt(keytype))
|
||||
ok = isinstance(r, dict) and r.get("success")
|
||||
return f"TRANSFER block {_int(block)} -> {_int(dest)}: {'ok' if ok else 'failed'} (key {str(keytype).upper()})"
|
||||
|
||||
|
||||
def _mfc_personalize(dev, option="0"):
|
||||
return "PERSONALIZE_UID (0x40) — MIFARE Classic EV1 command; opcode shown for completion, execution not yet wired"
|
||||
|
||||
|
||||
def _mfc_setmod(dev, option="0"):
|
||||
return "SET_MOD_TYPE (0x43) — MIFARE Classic EV1 command; opcode shown for completion, execution not yet wired"
|
||||
|
||||
|
||||
# The MIFARE Classic datasheet wire command set (MF1S50/EV1). All ops after activation are crypto1-
|
||||
# gated, so they run via hf.mf (auth + op in firmware); build() exposes the wire opcode for the
|
||||
# hex/binary completion and the block map (params[0]=="block"). CHK is a pm3py key-finder (not
|
||||
# datasheet) kept because you need a key before anything else works.
|
||||
MFC = _catalog(
|
||||
"MIFARE Classic", "hf14a",
|
||||
_c("AUTH_A", ["block", "key"], build=lambda block, key="0": bytes([0x60, _int(block)]),
|
||||
run=_mfc_auth_a, help="authenticate a sector with Key A — reports if the key works (0x60)"),
|
||||
_c("AUTH_B", ["block", "key"], build=lambda block, key="0": bytes([0x61, _int(block)]),
|
||||
run=_mfc_auth_b, help="authenticate a sector with Key B — reports if the key works (0x61)"),
|
||||
_c("READ", ["block", "key", "keytype"], build=lambda block, key="0", keytype="0": bytes([0x30, _int(block)]),
|
||||
run=_mfc_read, help="auth + read a 16-byte block (key def FFFFFFFFFFFF/A) (0x30)"),
|
||||
_c("WRITE", ["block", "data", "key", "keytype"],
|
||||
build=lambda block, data, key="0", keytype="0": [bytes([0xA0, _int(block)]), _hex(data).ljust(16, b"\x00")[:16]],
|
||||
run=_mfc_write, help="auth + write a 16-byte block (0xA0)"),
|
||||
_c("INCREMENT", ["block", "value", "key", "keytype"],
|
||||
build=lambda block, value="0", key="0", keytype="0": bytes([0xC1, _int(block)]),
|
||||
run=_mfc_incr, help="increment a value block by <value> and commit (0xC1 + transfer)"),
|
||||
_c("DECREMENT", ["block", "value", "key", "keytype"],
|
||||
build=lambda block, value="0", key="0", keytype="0": bytes([0xC0, _int(block)]),
|
||||
run=_mfc_decr, help="decrement a value block by <value> and commit (0xC0 + transfer)"),
|
||||
_c("RESTORE", ["block", "key", "keytype"],
|
||||
build=lambda block, key="0", keytype="0": bytes([0xC2, _int(block)]),
|
||||
run=_mfc_restore, help="restore/refresh a value block in place (0xC2 + transfer)"),
|
||||
_c("TRANSFER", ["block", "dest", "key", "keytype"],
|
||||
build=lambda block, dest="0", key="0", keytype="0": bytes([0xB0, _int(block)]),
|
||||
run=_mfc_transfer, help="copy a value block <block> -> <dest> (0xC2 restore + 0xB0 transfer)"),
|
||||
_c("CHK", ["block", "keytype"], run=_mfc_chk,
|
||||
help="try the default key list against a block — reports the working key A/B"),
|
||||
_c("PERSONALIZE_UID", ["option"], build=lambda option="0": bytes([0x40, _int(option)]),
|
||||
run=_mfc_personalize, help="EV1: UID/anticollision personalization (0x40)"),
|
||||
_c("SET_MOD_TYPE", ["option"], build=lambda option="0": bytes([0x43, _int(option)]),
|
||||
run=_mfc_setmod, help="EV1: set modulation type (0x43)"),
|
||||
_c("HALT", [], lambda: bytes([0x50, 0x00]), "halt the card (0x50 00)"),
|
||||
)
|
||||
|
||||
|
||||
@@ -7,6 +7,16 @@ MF_KEY_A = 0
|
||||
MF_KEY_B = 1
|
||||
MFBLOCK_SIZE = 16
|
||||
|
||||
# value-block actions (firmware MifareValue datain[9])
|
||||
MF_VALUE_INCREMENT = 0
|
||||
MF_VALUE_DECREMENT = 1
|
||||
MF_VALUE_RESTORE = 2
|
||||
|
||||
|
||||
def _sector_of(block: int) -> int:
|
||||
"""Sector number for a block: sectors 0-31 are 4 blocks, 32-39 are 16 blocks."""
|
||||
return block // 4 if block < 128 else 32 + (block - 128) // 16
|
||||
|
||||
|
||||
class HFMFCommands:
|
||||
"""MIFARE Classic commands."""
|
||||
@@ -60,6 +70,45 @@ class HFMFCommands:
|
||||
success = resp.oldarg[0] > 0 if resp.oldarg else resp.status == 0
|
||||
return {"success": success, "block": block}
|
||||
|
||||
async def value(self, block: int, action: int, value: int = 0,
|
||||
transfer_block: int | None = None,
|
||||
key: str | bytes = "FFFFFFFFFFFF", key_type: int = MF_KEY_A,
|
||||
transfer_key: str | bytes = "FFFFFFFFFFFF",
|
||||
transfer_key_type: int | None = None) -> dict:
|
||||
"""MIFARE value-block operation (increment / decrement / restore + transfer/commit).
|
||||
|
||||
``action``: 0=increment, 1=decrement, 2=restore (see ``MF_VALUE_*``). ``value`` is the
|
||||
signed increment/decrement operand. The op writes the internal transfer buffer and commits
|
||||
it to ``transfer_block`` — or back to ``block`` in place when None. A transfer that crosses
|
||||
into another sector nested-auths there with ``transfer_key`` / ``transfer_key_type``.
|
||||
Wire opcodes 0xC1/0xC0/0xC2 (op) then 0xB0 (transfer) are issued by the firmware.
|
||||
"""
|
||||
key_bytes = self._parse_key(key)
|
||||
transfer_key_bytes = self._parse_key(transfer_key)
|
||||
if transfer_key_type is None:
|
||||
transfer_key_type = key_type
|
||||
# firmware: transferBlk==0 means "transfer in place" (commit to `block`)
|
||||
tblk = 0 if transfer_block is None else transfer_block
|
||||
commit_block = tblk if tblk != 0 else block
|
||||
|
||||
# cmddata[34]: key[0:6], action[9], transferBlk[10], value-block[11:27] (operand at 11:15),
|
||||
# transferKey[27:33], needNestedAuth[33] (mirrors client CmdHF14AMfValue)
|
||||
cmddata = bytearray(34)
|
||||
cmddata[0:6] = key_bytes
|
||||
cmddata[9] = action & 0xFF
|
||||
cmddata[10] = tblk & 0xFF
|
||||
struct.pack_into("<i", cmddata, 11, value)
|
||||
cmddata[27:33] = transfer_key_bytes
|
||||
if _sector_of(commit_block) != _sector_of(block):
|
||||
cmddata[33] = 1
|
||||
|
||||
resp = await self._t.send_mix(
|
||||
Cmd.HF_MIFARE_VALUE, arg0=block, arg1=key_type, arg2=transfer_key_type,
|
||||
payload=bytes(cmddata), timeout=5.0,
|
||||
)
|
||||
success = resp.oldarg[0] > 0 if resp.oldarg else resp.status == 0
|
||||
return {"success": success, "block": commit_block}
|
||||
|
||||
async def rdsc(self, sector: int, key: str | bytes = "FFFFFFFFFFFF",
|
||||
key_type: int = MF_KEY_A) -> dict:
|
||||
"""Read an entire MIFARE Classic sector."""
|
||||
|
||||
Reference in New Issue
Block a user