diff --git a/pm3py/cli/rawcli/catalog.py b/pm3py/cli/rawcli/catalog.py index 75ecc38..dbeb4e2 100644 --- a/pm3py/cli/rawcli/catalog.py +++ b/pm3py/cli/rawcli/catalog.py @@ -98,14 +98,51 @@ TYPE2 = _catalog( _c("HALT", [], lambda: bytes([0x50, 0x00]), "halt the tag (0x50 00)"), ) -# ---- MIFARE Classic (block ops; auth is the reader's job before these) ---- +# ---- MIFARE Classic --------------------------------------------------------------------------- +# Classic reads/writes are gated by a crypto1 auth per sector, so — unlike NTAG — a bare 0x30 does +# nothing. These run via hf.mfc, which does the auth + block op in firmware. build() still exposes +# the wire opcode (0x30/0xA0) for hex/binary completion. Key defaults to the transport key +# FFFFFFFFFFFF / key A; override as READ(, <12-hex key>, A|B). +_MFC_KEYTYPE = {"A": 0, "B": 1, "0": 0, "1": 1} +# common transport / default keys tried by CHK when you don't know the sector key +_MFC_KEYS = ["FFFFFFFFFFFF", "A0A1A2A3A4A5", "D3F7D3F7D3F7", "000000000000", + "B0B1B2B3B4B5", "4D3A99C351DD", "1A982C7E459A", "AABBCCDDEEFF"] + + +def _kt(keytype) -> int: + return _MFC_KEYTYPE.get(str(keytype).upper(), 0) + + +def _mfc_read(dev, block, key="FFFFFFFFFFFF", keytype="A"): + r = dev.hf.mfc.rdbl(_int(block), key=key, key_type=_kt(keytype)) + if isinstance(r, dict) and r.get("success"): + return f"block {_int(block)} = {r['data']} (key {str(keytype).upper()})" + err = r.get("error") if isinstance(r, dict) else "?" + return f"READ block {_int(block)}: auth/read failed with key {str(keytype).upper()} {key} (status {err})" + + +def _mfc_write(dev, block, data, key="FFFFFFFFFFFF", keytype="A"): + r = dev.hf.mfc.wrbl(_int(block), _hex(data).ljust(16, b"\x00")[:16], key=key, key_type=_kt(keytype)) + ok = isinstance(r, dict) and r.get("success") + return f"WRITE block {_int(block)} <- {_hex(data).hex()}: {'ok' if ok else 'failed'} (key {str(keytype).upper()})" + + +def _mfc_chk(dev, block, keytype="A"): + r = dev.hf.mfc.chk(_int(block), _MFC_KEYS, key_type=_kt(keytype)) + if isinstance(r, dict) and r.get("found"): + return f"block {_int(block)} key {str(keytype).upper()} = {r['key']}" + return f"CHK block {_int(block)} key {str(keytype).upper()}: no key from the default list works" + + MFC = _catalog( "MIFARE Classic", "hf14a", - _c("READ", ["block"], lambda block: bytes([0x30, _int(block)]), - "read a 16-byte block (needs prior auth) (0x30)"), - _c("WRITE", ["block", "data"], - lambda block, data: [bytes([0xA0, _int(block)]), _hex(data).ljust(16, b"\x00")[:16]], - "write a 16-byte block, two-phase (0xA0) — needs prior auth"), + _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("CHK", ["block", "keytype"], run=_mfc_chk, + help="try the default key list against a block — reports the working key A/B"), _c("HALT", [], lambda: bytes([0x50, 0x00]), "halt the card (0x50 00)"), ) diff --git a/tests/test_rawcli.py b/tests/test_rawcli.py index d6c04c7..4dad4c7 100644 --- a/tests/test_rawcli.py +++ b/tests/test_rawcli.py @@ -11,7 +11,7 @@ from pm3py.cli.rawcli.trace_view import render_exchange from pm3py.cli.rawcli.parser import parse_token, parse_bytes, parse_line from pm3py.cli.rawcli.entry import byte_space from pm3py.cli.rawcli.identify import identify, clear, name_14a, format_summary -from pm3py.cli.rawcli.catalog import TYPE2, ISO15, T5577, LF_READONLY, catalog_for, catalog_for as _cf +from pm3py.cli.rawcli.catalog import TYPE2, ISO15, T5577, LF_READONLY, MFC, catalog_for, catalog_for as _cf from pm3py.cli.rawcli.tlv import parse_tlv, format_tlv from pm3py.cli.rawcli.completer import RawCompleter from pm3py.cli.rawcli.app import dispatch @@ -359,6 +359,23 @@ class TestCatalog: # two-phase write builds a frame list: command frame + 16-byte data frame assert TYPE2.get("COMPAT_WRITE").build("4", "0102") == [b"\xA0\x04", b"\x01\x02" + b"\x00" * 14] + def test_mfc_authenticated_ops(self): + # Classic READ/WRITE run via hf.mfc (crypto1 auth in firmware); default key FFFFFFFFFFFF/A + dev = MagicMock() + dev.hf.mfc.rdbl.return_value = {"success": True, "block": 4, "data": "aa" * 16} + dev.hf.mfc.wrbl.return_value = {"success": True, "block": 4} + dev.hf.mfc.chk.return_value = {"found": True, "key": "A0A1A2A3A4A5"} + assert MFC.get("READ").opcode() == 0x30 and MFC.get("WRITE").opcode() == 0xA0 # hint opcode + assert MFC.get("READ").run is not None # executes, not raw exchange + MFC.get("READ").run(dev, "4") + dev.hf.mfc.rdbl.assert_called_with(4, key="FFFFFFFFFFFF", key_type=0) # default key A + MFC.get("READ").run(dev, "4", "A0A1A2A3A4A5", "B") + dev.hf.mfc.rdbl.assert_called_with(4, key="A0A1A2A3A4A5", key_type=1) # override key + B + assert "ok" in MFC.get("WRITE").run(dev, "4", "00" * 16) + assert "A0A1A2A3A4A5" in MFC.get("CHK").run(dev, "3") + dev.hf.mfc.rdbl.return_value = {"success": False, "error": 1} + assert "failed" in MFC.get("READ").run(dev, "4") # clear failure line + 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"