"""LF demod tests — hardware-free. Every case builds a synthetic Proxmark-style envelope from a transponder *encoder*, runs it through the demod, and asserts the credential comes back. Because the encode side is the shipped model, a green test means the demod is the true inverse of what the tag emits. """ from unittest.mock import MagicMock from pm3py.lf import dsp, demod_samples, read_config, identify_lf from pm3py.lf import protocols from pm3py.transponders.lf.em.em4100 import EM4100Tag from pm3py.transponders.lf.atmel.t5577 import T5577Config, CONFIG_EM4100 # --- synthetic envelope builders (inverse of the demod) --------------------------------------- def _manchester_env(frame_bits, rf=64, high=200, low=60, repeats=4, start_offset=0, noise=0): """Manchester-encode a bit list into an RF/n ASK envelope (data 1 -> half-bits 1,0).""" half = [] for d in frame_bits * repeats: half += [1, 0] if d else [0, 1] sph = rf // 2 env = [] for hb in half: env += [high if hb else low] * sph if noise: # deterministic ± ripple, no RNG (Math.random is unavailable and tests must be stable) env = [max(0, min(255, v + (noise if (i % 3 == 0) else -noise))) for i, v in enumerate(env)] return bytes(env[start_offset:]) def _em4100_env(tag_id, **kw): return _manchester_env(EM4100Tag._encode(tag_id), **kw) def _t55xx_config_env(word, rf=64, **kw): bits = [(word >> (31 - i)) & 1 for i in range(32)] return _manchester_env(bits, rf=rf, **kw) # --- DSP primitives --------------------------------------------------------------------------- class TestDSP: def test_threshold_midpoint(self): assert 100 < dsp.threshold([60] * 50 + [200] * 50) < 160 def test_flat_capture_detected(self): assert dsp.is_flat([128] * 200) assert not dsp.is_flat([60] * 100 + [200] * 100) def test_clock_detects_halfbit(self): env = _em4100_env(0x0102030405, rf=64) half = dsp.detect_clock(dsp.binarize(env)) assert half is not None and abs(half - 32) < 4 # RF/64 -> 32-sample half-bit def test_manchester_roundtrip(self): # (1,0)->1, (0,1)->0 at phase 0 assert dsp.manchester_decode([1, 0, 0, 1, 1, 0]) == [1, 0, 1] # --- EM4100 ----------------------------------------------------------------------------------- class TestEM4100: def test_roundtrip(self): r = protocols.decode_em4100(_em4100_env(0x0102030405)) assert r and r["protocol"] == "EM4100" assert r["tag_id"] == 0x0102030405 and r["id_hex"] == "0102030405" def test_various_ids_and_rates(self): for tid in (0x0000000001, 0xAB12CD34EF, 0xFFFFFFFFFF): for rf in (32, 64): r = protocols.decode_em4100(_em4100_env(tid, rf=rf)) assert r and r["tag_id"] == tid, f"{tid:010X} @ RF/{rf}" def test_starts_mid_frame(self): # capture that begins partway through a frame still locks (frame repeats) r = protocols.decode_em4100(_em4100_env(0x1234567890, start_offset=777)) assert r and r["tag_id"] == 0x1234567890 def test_survives_noise(self): r = protocols.decode_em4100(_em4100_env(0x00DEADBEEF, noise=25)) assert r and r["tag_id"] == 0x00DEADBEEF def test_flat_is_none(self): assert protocols.decode_em4100(bytes([128] * 4000)) is None def test_customer_and_card_fields(self): r = protocols.decode_em4100(_em4100_env(0xAB01020304)) assert r["customer_id"] == 0xAB assert r["card_number"] == 0x01020304 # --- T55xx config ----------------------------------------------------------------------------- class TestT55xxConfig: def test_em4100_config_roundtrip(self): r = protocols.decode_t55xx_config(_t55xx_config_env(CONFIG_EM4100)) assert r and r["protocol"] == "T5577" assert r["config"] == CONFIG_EM4100 assert r["modulation"] == "ASK" assert r["emulating"] == "EM4100 / EM4102" def test_preset_labels(self): assert protocols.emulation_name(T5577Config._PRESETS["hid"]) == "HID Prox" assert protocols.emulation_name(T5577Config._PRESETS["indala"]) == "Indala" assert protocols.emulation_name(T5577Config._PRESETS["fdxb"]) == "FDX-B" def test_unknown_config_family_label(self): # an ASK config that isn't a known preset -> family label, not a crash label = protocols.emulation_name(0x00148000) assert "ASK" in label or "EM" in label # --- device orchestration (mocked) ------------------------------------------------------------ def _mock_lf_device(config_env=b"", emitted_env=b""): dev = MagicMock() dev.lf.t55.readbl.return_value = {"status": 0} dev.lf.read.return_value = {"status": 0, "samples": len(emitted_env)} # identify_lf downloads config first, then the emitted stream dev.lf.download_samples.side_effect = [config_env, emitted_env] return dev class TestIdentifyLF: def test_t5577_emulating_em4100(self): dev = _mock_lf_device(config_env=_t55xx_config_env(CONFIG_EM4100), emitted_env=_em4100_env(0x0102030405)) lines = [] r = identify_lf(dev, emit=lines.append) assert r["found"] and r["field"] == "lf" assert r["chip"] == "T5577" assert r["emulating"] == "EM4100 / EM4102" assert r["emitted"]["id_hex"] == "0102030405" assert r["label"] == "T5577 (EM4100 / EM4102)" assert any("T55xx block 0" in l for l in lines) def test_native_em4100_no_config(self): # a real EM4100 chip: nothing answers the T55xx read, but it emits its ID dev = _mock_lf_device(config_env=bytes([128] * 4000), emitted_env=_em4100_env(0x1122334455)) r = identify_lf(dev) assert r["found"] and r["chip"] is None assert r["label"] == "EM4100 1122334455" def test_nothing_on_antenna(self): dev = _mock_lf_device(config_env=bytes([128] * 4000), emitted_env=bytes([128] * 4000)) assert identify_lf(dev) is None def test_no_device(self): assert identify_lf(None) is None