diff --git a/pm3py/lf/capture.py b/pm3py/lf/capture.py index c047718..030d163 100644 --- a/pm3py/lf/capture.py +++ b/pm3py/lf/capture.py @@ -17,6 +17,7 @@ _SAMPLE_COUNT = 15000 _DECODERS = ( protocols.decode_em4100, protocols.decode_hid, + protocols.decode_awid, protocols.decode_fdxb, ) diff --git a/pm3py/lf/protocols.py b/pm3py/lf/protocols.py index 17a9824..898f04e 100644 --- a/pm3py/lf/protocols.py +++ b/pm3py/lf/protocols.py @@ -103,6 +103,58 @@ def decode_hid(samples) -> dict | None: return None +# AWID format length -> (facility offset/width, card offset/width) in the 66 de-parity'd bits +# (cmdlfawid.c index map). fmtLen is bits[0:8]; bit 8 is a Wiegand parity. +_AWID_FORMATS = { + 26: ((9, 8), (17, 16)), + 34: ((9, 8), (17, 24)), + 36: ((14, 11), (25, 18)), + 37: ((9, 13), (22, 18)), + 50: ((9, 16), (25, 32)), +} + + +def _strip_parity(bits, group: int = 4, want_odd: bool = True): + """Port of ``removeParity`` for AWID: each ``group`` bits are (group-1) data + 1 parity; + verify the parity and keep the data. Returns the compacted bits, or ``None`` on a parity + fault (the check that makes AWID trustworthy without a CRC).""" + out = [] + for i in range(0, len(bits) - group + 1, group): + g = bits[i:i + group] + if (sum(g) & 1) != (1 if want_odd else 0): + return None + out.extend(g[:group - 1]) + return out + + +def decode_awid(samples) -> dict | None: + """Demodulate an AWID envelope (FSK2a, no Manchester) to its Wiegand credential. + + ``detectAWID`` gives the 96 frame bits; the 88 after the preamble are 22 groups of 3 data + + 1 odd-parity bit, so stripping parity yields 66 bits whose first byte is the format length + (26/34/36/37/50). The 22 parity checks stand in for AWID's missing CRC — a bad frame fails + them — so this doesn't false-positive.""" + frame = fsk.awid_demod_fsk(samples) + if frame is None: + return None + data = _strip_parity(frame[8:8 + 88], group=4, want_odd=True) + if data is None or len(data) != 66: + return None + fmt_len = dsp.bits_to_int(data[0:8]) + spec = _AWID_FORMATS.get(fmt_len) + if spec is None: + return None + (fc_off, fc_w), (cn_off, cn_w) = spec + fc = dsp.bits_to_int(data[fc_off:fc_off + fc_w]) + cardnum = dsp.bits_to_int(data[cn_off:cn_off + cn_w]) + return { + "protocol": "AWID", + "format": f"AWID-{fmt_len}", + "facility_code": fc, + "card_number": cardnum, + } + + def _reflect(v: int, width: int) -> int: r = 0 for i in range(width): diff --git a/tests/test_lf_demod.py b/tests/test_lf_demod.py index 8ce19b1..ed4397b 100644 --- a/tests/test_lf_demod.py +++ b/tests/test_lf_demod.py @@ -164,6 +164,47 @@ class TestHID: assert r["protocol"] == "HID Prox" and r["card_number"] == 9999 +# --- AWID (FSK, no Manchester) ---------------------------------------------------------------- + +def _awid_env(fc, cn, fmt_len=26, repeats=4, **kw): + """A faithful AWID envelope: 66-bit Wiegand (fmtLen + fc + card) re-parity'd into 88 bits + (3 data + 1 odd parity per group), preambled, FSK2a-modulated (one symbol per bit).""" + (fc_off, fc_w), (cn_off, cn_w) = protocols._AWID_FORMATS[fmt_len] + data = [0] * 66 + for k in range(8): + data[k] = (fmt_len >> (7 - k)) & 1 + for k in range(fc_w): + data[fc_off + k] = (fc >> (fc_w - 1 - k)) & 1 + for k in range(cn_w): + data[cn_off + k] = (cn >> (cn_w - 1 - k)) & 1 + parity88 = [] # re-insert odd parity every 3 bits + for i in range(0, 66, 3): + g = data[i:i + 3] + parity88 += g + [1 - (sum(g) & 1)] # odd parity of the 3 data bits + frame = fsk.AWID_PREAMBLE + parity88 # 8 + 88 = 96 bits + return _fsk_env(frame, repeats=repeats, **kw) + + +class TestAWID: + def test_awid26_roundtrip(self): + r = protocols.decode_awid(_awid_env(12, 34567)) + assert r and r["protocol"] == "AWID" and r["format"] == "AWID-26" + assert r["facility_code"] == 12 and r["card_number"] == 34567 + + def test_awid_various(self): + for fc, cn in ((0, 1), (255, 65535), (200, 9999)): + r = protocols.decode_awid(_awid_env(fc, cn)) + assert r and r["facility_code"] == fc and r["card_number"] == cn, f"{fc}/{cn}" + + def test_parity_gate_rejects(self): + assert protocols.decode_awid(bytes([128] * 6000)) is None + assert protocols.decode_awid(_em4100_env(0x0102030405)) is None + + def test_demod_samples_picks_awid(self): + r = demod_samples(_awid_env(7, 4242)) + assert r["protocol"] == "AWID" and r["card_number"] == 4242 + + # --- FDX-B (biphase) -------------------------------------------------------------------------- def _fdxb_bits(country, national, animal=0):