feat(rawcli): LF T5577 identify + clear stale result per identify
- identify clears the previous field/protocol/transponder up front, so consecutive identifies (esp. when swapping tags) never show stale data. - LF identify: probe a T5577 by reading config block 0 (lf.t55.readbl(0)). A valid config confirms the T5577 and, decoded via T5577Config, names what it's emulating — "T5577 (EM4100 / EM4102)", "T5577 (HID Prox)", etc. (exact-word map, else modulation family). Tightly gated (status ok, non-trivial config, known modulation, sane max_block) so no-tag reads don't false-positive; lf.search() couldn't do this without demod. - format_summary shows the config word for LF (no UID). 4 tests (T5577 emulation report, no-T5577, stale-clear). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,10 @@ def identify(session, emit=None) -> dict:
|
||||
each annotated trace line (default: no trace). Returns a summary dict."""
|
||||
emit = emit or (lambda _line: None)
|
||||
dev = session.device
|
||||
# clear the previous result up front — a new identify must never show stale field/transponder
|
||||
session.field = None
|
||||
session.transponder = None
|
||||
session.protocol = "hf14a"
|
||||
if dev is None:
|
||||
return {"found": False, "error": "no device connected"}
|
||||
|
||||
@@ -174,11 +178,67 @@ def identify(session, emit=None) -> dict:
|
||||
return {"found": True, "field": "hf", "protocol": "15693",
|
||||
"transponder": "ISO15693", "uid": uid15}
|
||||
|
||||
# LF: lf.search() only captures samples — it can't confirm a tag without demod, so it would
|
||||
# always false-positive. Reliable LF identify (T55xx probe / EM4100 demod) is a follow-up.
|
||||
# LF: probe for a T5577 by reading its config block (block 0). A valid config word confirms
|
||||
# a T5577 and — decoded — tells us what it's emulating. lf.search() can't do this (no demod).
|
||||
lf = _identify_lf(dev, session, emit)
|
||||
if lf:
|
||||
return lf
|
||||
|
||||
return {"found": False}
|
||||
|
||||
|
||||
# Known T5577 config words → the tag it's set up to emulate (Proxmark3 config-block table).
|
||||
_T5577_EMULATION = {
|
||||
0x00148040: "EM4100 / EM4102",
|
||||
0x00107060: "HID Prox",
|
||||
0x00081040: "Indala",
|
||||
0x903F0082: "FDX-B",
|
||||
0x00088040: "Viking",
|
||||
0x000880E8: "raw / default",
|
||||
}
|
||||
|
||||
|
||||
def _infer_emulation(cfg) -> str:
|
||||
"""From a decoded T5577 config, name the emulated tag (exact word, else modulation family)."""
|
||||
exact = _T5577_EMULATION.get(int(cfg))
|
||||
if exact:
|
||||
return exact
|
||||
mod = cfg.modulation
|
||||
if not isinstance(mod, str):
|
||||
return "unknown"
|
||||
family = ("EM/ASK" if mod == "ASK" else "HID/AWID" if mod.startswith("FSK")
|
||||
else "Indala/PSK" if mod.startswith("PSK") else "FDX-B/Nedap" if "biphase" in mod
|
||||
else mod)
|
||||
return f"{family} · {mod} RF/{cfg.data_bit_rate}"
|
||||
|
||||
|
||||
def _identify_lf(dev, session, emit):
|
||||
"""Read T55xx block 0; if it decodes to a plausible config, report the T5577 and what it
|
||||
emulates. Tightly gated so no-tag reads don't false-positive."""
|
||||
r = _try(lambda: dev.lf.t55.readbl(0), None)
|
||||
if not isinstance(r, dict) or r.get("status") != 0:
|
||||
return None
|
||||
raw = r.get("raw") or b""
|
||||
if len(raw) < 4:
|
||||
return None
|
||||
config = int.from_bytes(raw[:4], "big")
|
||||
if config in (0, 0xFFFFFFFF):
|
||||
return None
|
||||
try:
|
||||
from pm3py.sim import T5577Config
|
||||
cfg = T5577Config(config)
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(cfg.modulation, str) or not (1 <= cfg.max_block <= 8):
|
||||
return None # implausible → probably no T5577
|
||||
emit(f" T55xx block 0 = 0x{config:08X} "
|
||||
f"({cfg.modulation}, RF/{cfg.data_bit_rate}, {cfg.max_block} blocks)")
|
||||
session.field, session.protocol = "lf", "lf"
|
||||
session.transponder = f"T5577 ({_infer_emulation(cfg)})"
|
||||
return {"found": True, "field": "lf", "protocol": "lf",
|
||||
"transponder": session.transponder, "config": f"{config:08X}"}
|
||||
|
||||
|
||||
def _probe_version(dev, fmt, emit) -> tuple[str | None, bytes | None]:
|
||||
"""Send a real GET_VERSION (0x60 +CRC), trace the exchange, decode the exact model."""
|
||||
cmd = b"\x60"
|
||||
@@ -208,6 +268,12 @@ def clear(session) -> None:
|
||||
def format_summary(result: dict) -> str:
|
||||
if not result.get("found"):
|
||||
return f"no tag found{': ' + result['error'] if result.get('error') else ''}"
|
||||
uid = result.get("uid")
|
||||
uid_hex = uid.hex().upper() if isinstance(uid, (bytes, bytearray)) else (uid or "?")
|
||||
return f"{result['transponder']} ({result['field'].upper()} / {result['protocol']}) UID {uid_hex}"
|
||||
if result.get("uid"):
|
||||
uid = result["uid"]
|
||||
detail = f"UID {uid.hex().upper() if isinstance(uid, (bytes, bytearray)) else uid}"
|
||||
elif result.get("config"):
|
||||
detail = f"config 0x{result['config']}"
|
||||
else:
|
||||
detail = ""
|
||||
head = f"{result['transponder']} ({result['field'].upper()} / {result['protocol']})"
|
||||
return f"{head} {detail}".rstrip()
|
||||
|
||||
@@ -182,6 +182,7 @@ def _mock_device(scan14=None, scan15=None, version=None):
|
||||
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.lf.t55.readbl.return_value = {"status": 1, "raw": b""} # no T55xx by default
|
||||
dev.hf.iso14a.raw.return_value = {"raw": (version + b"\x99\x88") if version else None}
|
||||
return dev
|
||||
|
||||
@@ -228,6 +229,32 @@ class TestIdentify:
|
||||
s = RawSession(device=dev)
|
||||
assert identify(s)["found"] is False and s.transponder is None
|
||||
|
||||
def test_lf_t5577_reports_emulation(self):
|
||||
# T55xx block 0 = EM4100 config -> "T5577 (EM4100 / EM4102)"
|
||||
dev = _mock_device()
|
||||
dev.lf.t55.readbl.return_value = {"status": 0, "raw": bytes.fromhex("00148040")}
|
||||
s = RawSession(device=dev)
|
||||
r = identify(s)
|
||||
assert r["found"] and s.field == "lf" and s.protocol == "lf"
|
||||
assert s.transponder == "T5577 (EM4100 / EM4102)"
|
||||
assert r["config"] == "00148040"
|
||||
|
||||
def test_lf_no_t5577_not_found(self):
|
||||
dev = _mock_device()
|
||||
dev.lf.t55.readbl.return_value = {"status": 1, "raw": b""} # no T55xx response
|
||||
assert identify(RawSession(device=dev))["found"] is False
|
||||
|
||||
def test_identify_clears_stale_result(self):
|
||||
# a second identify that finds nothing must clear the previous tag from the session
|
||||
dev = _mock_device(scan14={"found": True, "sak": 0x08, "atqa": "0400", "uid": "b978423d"})
|
||||
s = RawSession(device=dev)
|
||||
identify(s)
|
||||
assert s.transponder == "MIFARE Classic 1K"
|
||||
dev.hf.iso14a.scan.return_value = {"found": False} # tag removed
|
||||
dev.lf.t55.readbl.return_value = {"status": 1, "raw": b""}
|
||||
identify(s)
|
||||
assert s.transponder is None and s.field is None # no stale data
|
||||
|
||||
def test_sak_names_from_an10833(self):
|
||||
assert name_14a({"sak": 0x18}) == "MIFARE Classic 4K"
|
||||
assert name_14a({"sak": 0x20}).startswith("ISO14443-4")
|
||||
|
||||
Reference in New Issue
Block a user