diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..8684af9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# pm3py — Development Guide + +## What is this? + +A pure-Python async library that speaks the Proxmark3 NG wire protocol directly over USB serial. Returns structured dicts, not text. No dependency on the C client binary. + +## Quick reference + +```bash +# Run tests (no hardware needed, all mocked) +cd /home/work/pm3py +python -m pytest tests/ -v + +# Install for development +pip install -e . +``` + +## Architecture + +- `protocol.py` — Wire constants, CRC-16/A, Cmd enum, PM3Status enum. Source of truth for command IDs and frame formats. +- `transport.py` — Frame encode/decode (`encode_ng_frame`, `encode_mix_frame`, `decode_response_frame`), `PM3Transport` async serial class. USB skips CRC (uses magic `0x3361`), FPC UART uses CRC with byte-swapped wire format (`(low << 8) | high`). +- `client.py` — `Proxmark3` class (async-first, `Proxmark3.sync()` for REPL), `_SyncProxy`, `FirmwareInfo`, port auto-detection. +- `hw.py` — Hardware commands. LED API is platform-aware (Easy vs RDV4 have different color-to-pin mappings). PWM validation fetches capabilities on first use. +- `lf.py` — LF commands + `T55xxCommands` + `LFSearchResult`. Full tag demod (EM410x, HID, etc.) is NOT implemented — that requires the C client's DSP stack. +- `hf.py` — HF core (tune, search, sniff, dropfield). +- `hf_14a.py` — ISO 14443-A (scan, raw, sniff, sim). Uses MIX frames. +- `hf_15.py` — ISO 15693 (scan, rdbl, wrbl, sniff). Uses NG frames with ISO command payloads. +- `hf_mf.py` — MIFARE Classic (rdbl, wrbl, rdsc, chk, sniff, sim, nested, cident). Read uses NG, write uses MIX. + +## Wire protocol essentials + +- **NG frame (client→device):** magic `0x61334d50` + `length|0x8000` + cmd + payload + crc/nocrc +- **MIX frame:** Same but `ng` bit unset, payload starts with 3x uint64 args (24 bytes) +- **Response frame:** magic `0x62334d50` + `length|ng` + status + reason + cmd + payload + crc/nocrc +- **USB: no CRC** (postamble = `0x3361` cmd / `0x3362` resp). C client sets `send_with_crc_on_usb = false`. +- **FPC UART: CRC-16/A** with byte-swapped wire encoding + +## Key patterns + +- All command methods are `async`. The sync wrapper in `_SyncProxy` intercepts via `__getattr__` and calls `loop.run_until_complete()`. +- Command classes hold a `self._t` reference to `PM3Transport` (or mock in tests). +- Tests use `AsyncMock` for transport. Set `hw._is_rdv4 = False` to skip capabilities fetch in LED tests. +- `capabilities()` response parsed at known byte/bit offsets from the C struct (version=7 format). + +## Platform differences (PM3 Easy vs RDV4) + +| Color | Easy | RDV4 | +|--------|-----------|-----------| +| green | A (0x01) | B (0x02) | +| red | B (0x02) | C (0x04) | +| orange | C (0x04) | A (0x01) | +| blue | D (0x08) | *(none)* | +| red2 | *(none)* | D (0x08) | + +PWM-capable: Easy = A,B. RDV4 = A,D. + +## Adding new commands + +1. Find the `CMD_*` constant in `include/pm3_cmd.h` and add to `Cmd` enum in `protocol.py` +2. Check if the C client uses `SendCommandNG` (→ `send_ng`) or `SendCommandMIX` (→ `send_mix`) +3. Check the payload struct in `pm3_cmd.h` and use `struct.pack` to build it +4. Parse the response using `struct.unpack_from` on `resp.data` +5. Return a dict with human-readable keys +6. Write test with `AsyncMock` transport — no hardware needed diff --git a/README.md b/README.md new file mode 100644 index 0000000..03070c2 --- /dev/null +++ b/README.md @@ -0,0 +1,185 @@ +# pm3py + +Pure-Python library for talking to a [Proxmark3](https://github.com/RfidResearchGroup/proxmark3) over its NG wire protocol. Returns structured Python dicts instead of parsing terminal output. Async-first with a sync wrapper for REPL use. + +**No dependency on the C client.** Speaks the wire protocol directly over USB serial. + +## Install + +```bash +cd pm3py +pip install -e . +``` + +Requires Python 3.10+ and `pyserial-asyncio`. + +## Quick Start + +```python +from pm3py import Proxmark3 + +# Sync mode — auto-detects the device +pm3 = Proxmark3.sync() +print(pm3.firmware.version_string) +print(pm3.hw.ping()) +print(pm3.hw.version()) +pm3.close() +``` + +Or with async: + +```python +import asyncio +from pm3py import Proxmark3 + +async def main(): + async with Proxmark3() as pm3: + print(await pm3.hw.ping()) + print(await pm3.hf.a14.scan()) + +asyncio.run(main()) +``` + +## API + +All commands return Python dicts. The API mirrors the C client's command tree: + +### Hardware — `pm3.hw.*` + +```python +pm3.hw.ping() # verify connectivity +pm3.hw.version() # firmware version, chip ID +pm3.hw.status() # device status +pm3.hw.capabilities() # compiled features, platform (Easy vs RDV4) +pm3.hw.tune() # full antenna tuning (LF + HF voltages) +pm3.hw.led("green", on=True) # LED control with color names +pm3.hw.led("red", pulse=True, speed=300)# PWM effects (validates hardware support) +pm3.hw.led("all", on=False) # turn off +pm3.hw.dbg(level=2) # set debug level +pm3.hw.break_loop() # stop long-running operations +pm3.hw.reset() # reset device +``` + +LED colors are platform-aware — `"green"` maps to the correct physical LED on both PM3 Easy and RDV4. PWM operations (brightness, pulse, fade) validate hardware capability and raise `PM3Error` if the LED doesn't support it. + +### Low Frequency — `pm3.lf.*` + +```python +pm3.lf.tune(divisor=95) # LF antenna voltage at frequency +pm3.lf.read(samples=30000) # capture ADC samples +pm3.lf.config() # get sampling config +pm3.lf.config(divisor=88) # set config (134 kHz) +pm3.lf.search() # capture + return LFSearchResult +pm3.lf.sniff() # alias for read +pm3.lf.sim(gap=0, data=b"...") # simulate from buffer + +# T55xx tags +pm3.lf.t55.readbl(block=0) +pm3.lf.t55.writebl(block=1, data=0xDEADBEEF) +pm3.lf.t55.wakeup(password=0x12345678) +``` + +`lf.search()` returns an `LFSearchResult` with callable next steps: + +```python +result = pm3.lf.search() +print(result) # shows available actions +raw = result.download() # download raw ADC samples +result.tune() # check antenna voltage +result.read_t55xx(block=0) # try reading as T55xx +``` + +Full tag demodulation (EM410x, HID, AWID, etc.) requires the C client's DSP stack. + +### High Frequency — `pm3.hf.*` + +```python +pm3.hf.tune() # HF antenna voltage +pm3.hf.search() # try 14a then 15693 +pm3.hf.sniff() # sniff HF traffic +pm3.hf.dropfield() # turn off field +``` + +### ISO 14443-A — `pm3.hf.a14.*` + +```python +pm3.hf.a14.scan() # → {uid, atqa, sak, ats} +pm3.hf.a14.raw(data=b"\x50\x00") # raw APDU +pm3.hf.a14.sniff() +pm3.hf.a14.sim(uid=b"\x01\x02\x03\x04") +``` + +### ISO 15693 — `pm3.hf.iso15.*` + +```python +pm3.hf.iso15.scan() # → {uid, dsfid} +pm3.hf.iso15.rdbl(block=0) # read block +pm3.hf.iso15.wrbl(block=0, data="00112233") +pm3.hf.iso15.sniff() +``` + +### MIFARE Classic — `pm3.hf.mf.*` + +```python +pm3.hf.mf.rdbl(block=0) # read block (default key FF..FF) +pm3.hf.mf.rdbl(block=4, key="A0A1A2A3A4A5", key_type=1) # key B +pm3.hf.mf.wrbl(block=4, key="FFFFFFFFFFFF", data="00" * 16) +pm3.hf.mf.rdsc(sector=0) # read entire sector +pm3.hf.mf.chk(block=0, keys=["FFFFFFFFFFFF", "A0A1A2A3A4A5"]) +pm3.hf.mf.sniff() +pm3.hf.mf.sim(uid="01020304", size="1k") +pm3.hf.mf.nested(block=0, key="FFFFFFFFFFFF") +pm3.hf.mf.cident() # identify magic card type +``` + +### Raw Commands + +For anything not wrapped: + +```python +from pm3py import Cmd + +resp = pm3.send_ng(Cmd.STATUS) # NG frame +resp = pm3.send_mix(Cmd.HF_ISO14443A_READER, arg0=0x0103) # MIX frame +``` + +## Firmware Compatibility + +On connect, pm3py pings the device and fetches the firmware version. Results are in `pm3.firmware`: + +```python +pm3 = Proxmark3.sync() +print(pm3.firmware.compatible) # True if NG protocol works +print(pm3.firmware.version_string) # firmware version +print(pm3.firmware.chip_id) # e.g. "0x270B0A40" +print(pm3.firmware.warnings) # any issues detected +``` + +## Architecture + +``` +pm3py/ +├── protocol.py # Wire protocol constants, CRC-16/A, command IDs, error codes +├── transport.py # Frame encode/decode, async serial I/O, PM3Transport +├── client.py # Proxmark3 class, sync proxy, firmware probe +├── hw.py # hw.* commands +├── lf.py # lf.* commands + T55xx +├── hf.py # hf.* commands (tune, search, sniff) +├── hf_14a.py # hf.14a.* (ISO 14443-A) +├── hf_15.py # hf.15.* (ISO 15693) +└── hf_mf.py # hf.mf.* (MIFARE Classic) +``` + +- **Transport layer** handles serial I/O, CRC, NG/MIX frame encoding/decoding +- **Command modules** send structured commands and parse responses into dicts +- **USB connections** skip CRC (matching the C client behavior) +- **FPC UART connections** use CRC-16/A with byte-swapped wire format + +## Tests + +```bash +cd pm3py +python -m pytest tests/ -v +``` + +All tests use mock transports — no hardware required.