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:
@@ -388,6 +388,35 @@ class TestCatalog:
|
||||
dev.hf.mf.rdbl.return_value = {"success": False, "error": 1}
|
||||
assert "failed" in MFC.get("READ").run(dev, "4") # clear failure line
|
||||
|
||||
def test_mfc_datasheet_command_set(self):
|
||||
# the full MIFARE Classic wire command set is present with datasheet opcodes
|
||||
opcodes = {n: MFC.get(n).opcode() for n in MFC.names()}
|
||||
assert opcodes["AUTH_A"] == 0x60 and opcodes["AUTH_B"] == 0x61
|
||||
assert opcodes["INCREMENT"] == 0xC1 and opcodes["DECREMENT"] == 0xC0
|
||||
assert opcodes["RESTORE"] == 0xC2 and opcodes["TRANSFER"] == 0xB0
|
||||
assert opcodes["PERSONALIZE_UID"] == 0x40 and opcodes["SET_MOD_TYPE"] == 0x43
|
||||
for op, name in [(0x60, "AUTH_A"), (0xC1, "INCREMENT"), (0xB0, "TRANSFER"), (0x40, "PERSONALIZE_UID")]:
|
||||
assert MFC.by_opcode(op).name == name # raw byte maps back to the command
|
||||
# every page/block command's first param is a block -> gets the memory map
|
||||
for n in ("AUTH_A", "AUTH_B", "READ", "WRITE", "INCREMENT", "DECREMENT", "RESTORE", "TRANSFER"):
|
||||
assert MFC.get(n).params[0] == "block"
|
||||
|
||||
def test_mfc_value_ops_run(self):
|
||||
dev = MagicMock()
|
||||
dev.hf.mf.rdbl.return_value = {"success": True, "block": 4, "data": "aa" * 16}
|
||||
dev.hf.mf.value.return_value = {"success": True, "block": 4}
|
||||
MFC.get("AUTH_A").run(dev, "3")
|
||||
dev.hf.mf.rdbl.assert_called_with(3, key="FFFFFFFFFFFF", key_type=0) # AUTH_A -> key A
|
||||
MFC.get("AUTH_B").run(dev, "3", "A0A1A2A3A4A5")
|
||||
dev.hf.mf.rdbl.assert_called_with(3, key="A0A1A2A3A4A5", key_type=1) # AUTH_B -> key B
|
||||
MFC.get("INCREMENT").run(dev, "4", "10")
|
||||
dev.hf.mf.value.assert_called_with(4, 0, value=10, key="FFFFFFFFFFFF", key_type=0)
|
||||
MFC.get("DECREMENT").run(dev, "4", "3", "A0A1A2A3A4A5", "B")
|
||||
dev.hf.mf.value.assert_called_with(4, 1, value=3, key="A0A1A2A3A4A5", key_type=1)
|
||||
MFC.get("TRANSFER").run(dev, "4", "8") # copy block 4 -> 8 (restore+transfer)
|
||||
dev.hf.mf.value.assert_called_with(4, 2, transfer_block=8, key="FFFFFFFFFFFF", key_type=0)
|
||||
assert "not yet wired" in MFC.get("PERSONALIZE_UID").run(dev, "2") # EV1 opcode-only
|
||||
|
||||
def test_iso15_builds(self):
|
||||
assert ISO15.get("READ_BLOCK").build("4") == b"\x02\x20\x04"
|
||||
assert ISO15.get("INVENTORY").build() == b"\x26\x01\x00"
|
||||
@@ -586,6 +615,22 @@ class TestCompleter:
|
||||
gv = next(c for c in cs if "GET_VERSION" in self._disp(c))
|
||||
assert gv.text == "01100000" # raw binary byte, not the command name
|
||||
|
||||
def test_mfc_opcode_completion(self):
|
||||
# the new MIFARE Classic datasheet opcodes surface by hex AND binary value, inserting the byte
|
||||
s = RawSession()
|
||||
s.protocol, s.transponder = "hf14a", "MIFARE Classic 1K"
|
||||
for word, name, byte in [("c1", "INCREMENT", "C1"), ("60", "AUTH_A", "60"), ("b0", "TRANSFER", "B0")]:
|
||||
cs = self._complete(s, word)
|
||||
hit = next(c for c in cs if name in self._disp(c))
|
||||
assert hit.text == byte # inserts the raw opcode byte
|
||||
s.entry_mode = "bin"
|
||||
cs = self._complete(s, "11000001") # 0xC1 in binary
|
||||
assert any("INCREMENT" in self._disp(c) for c in cs)
|
||||
s.entry_mode = "hex"
|
||||
# the block argument gets the memory map for these commands too
|
||||
assert any("manufacturer" in (c.display_meta_text or "")
|
||||
for c in self._complete(s, "INCREMENT("))
|
||||
|
||||
def test_raw_page_byte_completion(self):
|
||||
# after a page-command opcode in raw entry, the page byte gets the memory map (raw bytes)
|
||||
s = RawSession()
|
||||
|
||||
Reference in New Issue
Block a user