feat(rawcli): deep, traced identify — exact model via GET_VERSION
`identify` now probes and shows its work instead of a coarse SAK guess: - Streams every probe exchange to the trace (proxmark-style annotated): the reconstructed 14a activation (WUPA/ATQA/anticollision CL1+CL2/ SELECT/SAK) plus a REAL GET_VERSION (0x60 +CRC) sent over the persistent connection. - Names the exact IC from GET_VERSION: an NTAG216 now reports "NTAG216", not "MIFARE UL/NTAG". Exact map mirrors the transponder models' _version_bytes (static, to dodge an import-order circular; a test guards against drift). Unknown chips fall back to product family + an honest memory-size *range* (AN10833 storage-byte encoding), not a bogus size. - SAK fallback table + GET_VERSION product-nibble map aligned to NXP AN10833 (MIFARE type identification procedure). 11 tests (exact/structural decode, map-model sync, traced probe, SAK names). GET_VERSION is a real device exchange — the user's hardware test. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -146,11 +146,15 @@ class TestDispatch:
|
||||
assert "whole number of bytes" in capsys.readouterr().out
|
||||
|
||||
|
||||
def _mock_device(scan14=None, scan15=None):
|
||||
NTAG216_VERSION = bytes.fromhex("0004040201001303")
|
||||
|
||||
|
||||
def _mock_device(scan14=None, scan15=None, version=None):
|
||||
dev = MagicMock()
|
||||
dev.hf.iso14a.scan.return_value = scan14 or {"found": False}
|
||||
dev.hf.iso15.scan.return_value = scan15 or {"found": False}
|
||||
dev.lf.search.return_value = None
|
||||
dev.hf.iso14a.raw.return_value = {"raw": (version + b"\x99\x88") if version else None}
|
||||
return dev
|
||||
|
||||
|
||||
@@ -160,13 +164,22 @@ class TestIdentify:
|
||||
r = identify(s)
|
||||
assert r["found"] is False and s.field is None
|
||||
|
||||
def test_14a_sets_field_and_name(self):
|
||||
def test_14a_sak_only_name(self):
|
||||
# SAK != 0 -> named from AN10833 Table 4, no GET_VERSION
|
||||
s = RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": 0x08, "uid": b"\x01\x02\x03\x04"}))
|
||||
scan14={"found": True, "sak": 0x08, "atqa": "0400", "uid": "01020304"}))
|
||||
r = identify(s)
|
||||
assert r["found"] and s.field == "hf" and s.protocol == "hf14a"
|
||||
assert s.transponder == "MIFARE Classic 1K"
|
||||
assert r["uid"] == b"\x01\x02\x03\x04"
|
||||
|
||||
def test_14a_getversion_exact(self):
|
||||
s = RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": 0x00, "atqa": "4400", "uid": "04a1b2c3d4e5f6"},
|
||||
version=NTAG216_VERSION))
|
||||
r = identify(s)
|
||||
assert s.transponder == "NTAG216" # exact, not "MIFARE UL/NTAG"
|
||||
assert r["version"] == NTAG216_VERSION.hex()
|
||||
s.device.hf.iso14a.raw.assert_called_with(b"\x60", flags=0x20) # real GET_VERSION+CRC
|
||||
|
||||
def test_15693_fallback(self):
|
||||
s = RawSession(device=_mock_device(scan15={"found": True, "uid": b"\xE0\x04"}))
|
||||
@@ -174,24 +187,52 @@ class TestIdentify:
|
||||
assert r["found"] and s.field == "hf" and s.protocol == "hf15"
|
||||
assert s.transponder == "ISO15693"
|
||||
|
||||
def test_name_unknown_sak(self):
|
||||
def test_sak_names_from_an10833(self):
|
||||
assert name_14a({"sak": 0x18}) == "MIFARE Classic 4K"
|
||||
assert name_14a({"sak": 0x20}).startswith("ISO14443-4")
|
||||
assert name_14a({"sak": 0x99}) == "ISO14443-A (SAK 99)"
|
||||
|
||||
def test_clear_forgets_tag(self):
|
||||
s = RawSession(device=_mock_device(scan14={"found": True, "sak": 0x08}))
|
||||
s = RawSession(device=_mock_device(scan14={"found": True, "sak": 0x08, "uid": "01020304"}))
|
||||
identify(s)
|
||||
clear(s)
|
||||
assert s.field is None and s.transponder is None and s.protocol == "hf14a"
|
||||
|
||||
|
||||
class TestVersionId:
|
||||
def test_exact_models(self):
|
||||
from pm3py.cli.rawcli.identify import decode_version
|
||||
assert decode_version(bytes.fromhex("0004040201001303")) == "NTAG216"
|
||||
assert decode_version(bytes.fromhex("0004040201000f03")) == "NTAG213"
|
||||
assert "Ultralight EV1" in decode_version(bytes.fromhex("0004030101000b03"))
|
||||
|
||||
def test_structural_range_for_unknown(self):
|
||||
from pm3py.cli.rawcli.identify import decode_version
|
||||
# unknown NTAG-family version -> product + honest size range (AN10833)
|
||||
out = decode_version(bytes.fromhex("0004040201001503"))
|
||||
assert out.startswith("NTAG (") and "B)" in out
|
||||
|
||||
def test_static_map_matches_models(self):
|
||||
# guard against drift between the static version map and the transponder models
|
||||
from pm3py.cli.rawcli.identify import _VERSION_MODELS
|
||||
from pm3py.sim import (NTAG210, NTAG212, NTAG213, NTAG215, NTAG216,
|
||||
MF0UL11, MF0UL21, NT3H2111, NT3H2211)
|
||||
model_vbytes = {c._version_bytes.hex() for c in
|
||||
(NTAG210, NTAG212, NTAG213, NTAG215, NTAG216,
|
||||
MF0UL11, MF0UL21, NT3H2111, NT3H2211)}
|
||||
assert set(_VERSION_MODELS) == model_vbytes
|
||||
|
||||
|
||||
class TestControlCommands:
|
||||
def test_identify_command(self, capsys):
|
||||
s = RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": 0x00, "uid": b"\x04\x11\x22\x33"}))
|
||||
scan14={"found": True, "sak": 0x00, "atqa": "4400", "uid": "04a1b2c3d4e5f6"},
|
||||
version=NTAG216_VERSION))
|
||||
dispatch(s, "identify")
|
||||
out = capsys.readouterr().out
|
||||
assert "MIFARE UL/NTAG" in out and "04112233" in out
|
||||
assert s.breadcrumb() == "[raw / hf / MIFARE UL/NTAG / 0x]"
|
||||
out = _plain(capsys.readouterr().out)
|
||||
assert "NTAG216" in out # exact model in the summary
|
||||
assert "WUPA" in out and "GET_VERSION" in out # probe exchanges streamed to the trace
|
||||
assert s.breadcrumb() == "[raw / hf / NTAG216 / 0x]"
|
||||
|
||||
def test_transponder_when_none(self, capsys):
|
||||
dispatch(RawSession(), "transponder")
|
||||
@@ -231,7 +272,7 @@ class TestCatalog:
|
||||
class TestFunctionCalls:
|
||||
def _id(self, sak=0x00):
|
||||
return RawSession(device=_mock_device(
|
||||
scan14={"found": True, "sak": sak, "uid": b"\x04\x01\x02\x03"}))
|
||||
scan14={"found": True, "sak": sak, "atqa": "4400", "uid": "04010203"}))
|
||||
|
||||
def test_call_read_sends_correct_bytes(self, capsys):
|
||||
s = self._id()
|
||||
|
||||
Reference in New Issue
Block a user