feat(lf): FSK stack + HID Prox demod (faithful lfdemod.c port)
New pm3py/lf/fsk.py — a function-for-function Python port of the Proxmark FSK stack (firmware/common/lfdemod.c): fsk_wave_demod (count samples between 0->1 transitions: short wave = fc/8, long = fc/10) + aggregate_bits (runs -> clock- resolution bits) = fskdemod, plus preamble_search, HIDdemodFSK, detectAWID hooks. Ported to track the C reference (and thus real-tag output), not reinvented. protocols.decode_hid: FSK2a + Manchester -> Wiegand. Format is told by frame structure, not parity alone — the 26-bit H10301 wire frame carries a 0x20 header at bit 37 + a sentinel 1 at bit 26 (per firmware add_HID_preamble); the 37-bit H10304 frame is header-less (37-bit Manchester count, no 0x20). Each candidate is still parity-validated by re-encoding through the shipped HIDProxTag, so a false alignment can't slip through. Added to the identify decoder chain after EM4100. Synthetic HID tests build the real add_HID_preamble framing (header+sentinel for 26-bit, bare 37-bit) and render fc/8/fc/10 square waves, so a green test means the demod inverts what a real HID card / PM3 sim emits. Full suite 1232 green. AWID (96-bit) framing decode + PSK/Indala + biphase/FDX-B queued next. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,9 +6,10 @@ shipped model, a green test means the demod is the true inverse of what the tag
|
||||
"""
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from pm3py.lf import dsp, demod_samples, read_config, identify_lf
|
||||
from pm3py.lf import dsp, fsk, demod_samples, read_config, identify_lf
|
||||
from pm3py.lf import protocols
|
||||
from pm3py.transponders.lf.em.em4100 import EM4100Tag
|
||||
from pm3py.transponders.lf.hid.hid import HIDProxTag
|
||||
from pm3py.transponders.lf.atmel.t5577 import T5577Config, CONFIG_EM4100
|
||||
|
||||
|
||||
@@ -38,6 +39,40 @@ def _t55xx_config_env(word, rf=64, **kw):
|
||||
return _manchester_env(bits, rf=rf, **kw)
|
||||
|
||||
|
||||
def _fsk_env(symbols, high=200, low=60, repeats=3):
|
||||
"""Render FSK2a final-bit symbols (0 -> fc/8 wave, 1 -> fc/10 wave) as a square-wave envelope,
|
||||
fcAll-style: (fc-halfFC) low then halfFC high per wave, ~clk/fc waves filling clk=50."""
|
||||
env = []
|
||||
for _ in range(repeats):
|
||||
for s in symbols:
|
||||
fc = 10 if s else 8
|
||||
half = fc >> 1
|
||||
for _w in range(round(50 / fc)):
|
||||
env += [low] * (fc - half) + [high] * half
|
||||
return bytes(env)
|
||||
|
||||
|
||||
def _bits(value, n):
|
||||
return [(value >> (n - 1 - i)) & 1 for i in range(n)]
|
||||
|
||||
|
||||
def _hid_env(fc, cn, fmt="H10301", repeats=5, **kw):
|
||||
"""A faithful HID Prox envelope (matches firmware add_HID_preamble): SOF + Manchester(frame),
|
||||
FSK2a modulated. 26-bit carries a 0x20 header (bit 37) + sentinel (bit 26); 37-bit is bare."""
|
||||
w_int = 0
|
||||
for b in HIDProxTag._encode_wiegand(fc, cn, fmt):
|
||||
w_int = (w_int << 1) | b
|
||||
if fmt == "H10301": # 26-bit: header + sentinel, 38-bit frame
|
||||
full = (0x20 << 32) | (1 << 26) | w_int
|
||||
frame = _bits(full, 38)
|
||||
else: # 37-bit: header-less
|
||||
frame = _bits(w_int, 37)
|
||||
manch = []
|
||||
for d in frame:
|
||||
manch += [1, 0] if d else [0, 1] # data 1 -> fc10,fc8 ; 0 -> fc8,fc10
|
||||
return _fsk_env(fsk.HID_PREAMBLE + manch, repeats=repeats, **kw)
|
||||
|
||||
|
||||
# --- DSP primitives ---------------------------------------------------------------------------
|
||||
|
||||
class TestDSP:
|
||||
@@ -90,6 +125,45 @@ class TestEM4100:
|
||||
assert r["card_number"] == 0x01020304
|
||||
|
||||
|
||||
# --- HID Prox (FSK) ---------------------------------------------------------------------------
|
||||
|
||||
class TestFSK:
|
||||
def test_fskdemod_recovers_preamble(self):
|
||||
# a bare SOF + a few symbols demodulates to the HID preamble pattern
|
||||
bits = fsk.fskdemod(_fsk_env(fsk.HID_PREAMBLE + [0, 1] * 20))
|
||||
found, start, _ = fsk.preamble_search(bits, fsk.HID_PREAMBLE)
|
||||
assert found
|
||||
|
||||
def test_noise_rejected(self):
|
||||
assert fsk.is_noise(bytes([128] * 5000))
|
||||
assert fsk.fskdemod(bytes([128] * 5000)) == []
|
||||
|
||||
|
||||
class TestHID:
|
||||
def test_h10301_roundtrip(self):
|
||||
r = protocols.decode_hid(_hid_env(123, 45678))
|
||||
assert r and r["protocol"] == "HID Prox" and r["format"] == "H10301"
|
||||
assert r["facility_code"] == 123 and r["card_number"] == 45678
|
||||
|
||||
def test_various_h10301(self):
|
||||
for fc, cn in ((0, 1), (255, 65535), (77, 12345)):
|
||||
r = protocols.decode_hid(_hid_env(fc, cn))
|
||||
assert r and r["facility_code"] == fc and r["card_number"] == cn, f"{fc}/{cn}"
|
||||
|
||||
def test_h10304_37bit(self):
|
||||
r = protocols.decode_hid(_hid_env(4321, 123456, fmt="H10304")) # CN < 2^19
|
||||
assert r and r["format"] == "H10304"
|
||||
assert r["facility_code"] == 4321 and r["card_number"] == 123456
|
||||
|
||||
def test_not_hid_returns_none(self):
|
||||
assert protocols.decode_hid(_em4100_env(0x0102030405)) is None
|
||||
assert protocols.decode_hid(bytes([128] * 6000)) is None
|
||||
|
||||
def test_demod_samples_picks_hid(self):
|
||||
r = demod_samples(_hid_env(200, 9999))
|
||||
assert r["protocol"] == "HID Prox" and r["card_number"] == 9999
|
||||
|
||||
|
||||
# --- T55xx config -----------------------------------------------------------------------------
|
||||
|
||||
class TestT55xxConfig:
|
||||
|
||||
Reference in New Issue
Block a user