fix(lf): T55xx config via rotation-to-preset match; label the emitted credential
Hardware-validated. Two real bugs in the T55xx config decode, both fixed by requiring a bit-rotation of the recovered repeating word to match a known preset: - Rotation ambiguity: a block read recovers the 32-bit config at an arbitrary bit offset. A real capture came back 0x000A4020 = the EM4100 word 0x00148040 rotated by one; the old "looks sane" scorer accepted the rotation verbatim and mislabeled it FSK1/RF-6. Now every rotation is tried and only a preset match (EM4100/HID/Indala/FDX-B/Viking/default) is accepted -> resolves to 0x00148040 = EM4100. - False positives: garbage config captures (all-0, all-1, a rotation of the tag's own emission) no longer pass -> decode returns None and the reliable emitted-stream decode carries the result. identify_lf label now names the chip + the actual decoded credential: "T5577 (EM4100 00FFFFFFFF)" instead of the config's guess. On real hardware this is now stable 4/4: config 00148040, emitted EM4100 00FFFFFFFF. Tests: rotation recovery (0x000A4020 -> EM4100), no-false-positive on all-1s/random, updated label expectation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -154,8 +154,11 @@ def identify_lf(device, emit=None) -> dict | None:
|
||||
if emitted is not None:
|
||||
emit(f" emitted: {credential_label(emitted)}")
|
||||
|
||||
# label: prefer the physical chip + what it emulates; else the emitted credential
|
||||
if chip:
|
||||
# label: name the chip and what it's actually emitting (the reliable, decoded credential);
|
||||
# fall back to the config's emulation guess, or just the emitted credential.
|
||||
if chip and emitted:
|
||||
label = f"{chip} ({credential_label(emitted)})"
|
||||
elif chip:
|
||||
label = f"{chip} ({emulating})"
|
||||
else:
|
||||
label = credential_label(emitted)
|
||||
|
||||
@@ -240,61 +240,48 @@ def _fdxb_parse(frame) -> dict | None:
|
||||
|
||||
|
||||
def decode_t55xx_config(samples) -> dict | None:
|
||||
"""Recover a T55xx block-0 config word from a block-0 read response (ASK/Manchester modes).
|
||||
"""Recover a T55xx block-0 config from a block-0 read response (ASK/Manchester modes).
|
||||
|
||||
A block read streams the 32-bit word with no header, so alignment is found by the word
|
||||
repeating (block value at offset N equals offset N+32) and the candidate being a *sane*
|
||||
config (known modulation, valid master key, 1..7 data blocks). Best-effort by nature — the
|
||||
reader labels it as a probable config, not gospel. Returns ``None`` for FSK/PSK-mode tags
|
||||
(handled elsewhere) or when nothing repeats cleanly."""
|
||||
best = None
|
||||
A block read streams the 32-bit config with no header, so the demod recovers it at *some* bit
|
||||
rotation. We collect every 32-bit word that repeats one block-period later and accept one only
|
||||
when a bit-rotation of it exactly matches a known config preset (EM4100, HID, Indala, FDX-B,
|
||||
Viking, default). That both resolves the rotation ambiguity (a real capture came back as
|
||||
0x000A4020 = the EM4100 word 0x00148040 rotated by one) and rejects the spurious repeats
|
||||
(all-0, all-1, a rotation of the tag's own emission) that a bare "looks like a sane config"
|
||||
score waved through as false positives. Returns ``None`` when nothing rotates to a preset —
|
||||
the emitted-stream decode carries the result instead."""
|
||||
for bits in _manchester_bits(samples):
|
||||
clean = [b for b in bits if b is not None]
|
||||
cand = _find_repeating_word(clean, 32)
|
||||
if cand is None:
|
||||
continue
|
||||
cfg = T5577Config(cand)
|
||||
score = _config_score(cfg)
|
||||
if best is None or score > best[0]:
|
||||
best = (score, cand, cfg)
|
||||
if best is None or best[0] < 2: # need at least a known modulation + sane structure
|
||||
return None
|
||||
_, word, cfg = best
|
||||
return {
|
||||
"protocol": "T5577",
|
||||
"config": word,
|
||||
"config_hex": f"{word:08X}",
|
||||
"modulation": cfg.modulation if isinstance(cfg.modulation, str) else f"0x{cfg.modulation:02X}",
|
||||
"data_bit_rate": cfg.data_bit_rate,
|
||||
"max_block": cfg.max_block,
|
||||
"emulating": emulation_name(word, cfg),
|
||||
}
|
||||
for word in _repeating_words(clean, 32):
|
||||
for rot in range(32):
|
||||
rv = ((word << rot) | (word >> (32 - rot))) & 0xFFFFFFFF
|
||||
if rv in _PRESET_BY_WORD:
|
||||
cfg = T5577Config(rv)
|
||||
return {
|
||||
"protocol": "T5577",
|
||||
"config": rv,
|
||||
"config_hex": f"{rv:08X}",
|
||||
"modulation": cfg.modulation if isinstance(cfg.modulation, str)
|
||||
else f"0x{cfg.modulation:02X}",
|
||||
"data_bit_rate": cfg.data_bit_rate,
|
||||
"max_block": cfg.max_block,
|
||||
"emulating": emulation_name(rv, cfg),
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def _find_repeating_word(bits, width: int) -> int | None:
|
||||
"""Smallest-offset ``width``-bit window that equals the next ``width`` bits (one block
|
||||
period), scored by config sanity; returns the best repeating word's value or ``None``."""
|
||||
def _repeating_words(bits, width: int):
|
||||
"""Yield the distinct ``width``-bit windows that equal the next ``width`` bits (a block-read
|
||||
candidate repeating one block-period later)."""
|
||||
n = len(bits)
|
||||
best = None
|
||||
seen = set()
|
||||
for start in range(max(0, n - 2 * width)):
|
||||
w = bits[start:start + width]
|
||||
if w == bits[start + width:start + 2 * width]:
|
||||
val = dsp.bits_to_int(w)
|
||||
score = _config_score(T5577Config(val))
|
||||
if best is None or score > best[0]:
|
||||
best = (score, val)
|
||||
return best[1] if best else None
|
||||
|
||||
|
||||
def _config_score(cfg: T5577Config) -> int:
|
||||
score = 0
|
||||
if isinstance(cfg.modulation, str): # decoded to a known scheme
|
||||
score += 1
|
||||
if cfg.master_key in (0, 6, 9): # normal / extended-mode keys
|
||||
score += 1
|
||||
if 1 <= cfg.max_block <= 7:
|
||||
score += 1
|
||||
return score
|
||||
if val not in seen:
|
||||
seen.add(val)
|
||||
yield val
|
||||
|
||||
|
||||
def emulation_name(word: int, cfg: T5577Config | None = None) -> str:
|
||||
|
||||
Reference in New Issue
Block a user