Phase 2 of package refactor. Extracts trace parsing, ISO 15693 decoders, NDEF annotation, and ANSI formatting from core/hf_iso15.py into dedicated sniff/ modules: - sniff/trace.py — parse_tracelog (protocol-agnostic) - sniff/ndef.py — NDEF TLV/record decode - sniff/decode_iso15.py — 15693 command/response decoders - sniff/format.py — ANSI color formatting - sniff/session.py — SniffSession with iso15() method core/hf_iso15.py re-exports sniff symbols for backward compat. Also fixes NTAG 5 placement (iso15693, not iso14443a4) in design doc. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
114 lines
5.6 KiB
Markdown
114 lines
5.6 KiB
Markdown
# 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 .
|
|
```
|
|
|
|
## Package structure
|
|
|
|
```
|
|
pm3py/
|
|
__init__.py # re-exports Proxmark3, PM3Error, PM3Response, Cmd, PM3Status
|
|
core/ # wire protocol and device commands
|
|
protocol.py # wire constants, CRC-16/A, Cmd enum, PM3Status
|
|
transport.py # frame encode/decode, PM3Transport async serial
|
|
client.py # Proxmark3 class, _SyncProxy, FirmwareInfo
|
|
hw.py # hardware commands, LED API (platform-aware)
|
|
hf.py # HF core — tune, search, sniff, dropfield
|
|
hf_iso14a.py # ISO 14443-A — scan, raw. Uses MIX frames
|
|
hf_iso15.py # ISO 15693 — scan, rdbl, wrbl, sniff + trace decoders
|
|
hf_mfc.py # MIFARE Classic — rdbl, wrbl, rdsc, chk, nested, cident
|
|
lf.py # LF commands + T55xxCommands + LFSearchResult
|
|
sniff/ # sniff sessions, trace parsing, protocol decoders (scaffold)
|
|
sim/ # card simulation sessions, table compiler, relay (scaffold)
|
|
reader/ # higher-level reader modes (scaffold)
|
|
transponders/ # tag/transponder models independent of hardware (scaffold)
|
|
```
|
|
|
|
## Client API
|
|
|
|
```python
|
|
pm3.hw.ping() # hardware
|
|
pm3.hf.iso14a.scan() # ISO 14443-A
|
|
pm3.hf.iso15.rdbl(4) # ISO 15693
|
|
pm3.hf.mfc.rdbl(0) # MIFARE Classic
|
|
pm3.lf.t55.readbl(0) # T55xx
|
|
pm3.hf.tune() # HF antenna tune
|
|
pm3.hf.dropfield() # drop field
|
|
```
|
|
|
|
## 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` (in `core/client.py`) 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.
|
|
|
|
## Sim framework (worktree `feature/sim-framework`)
|
|
|
|
Software-defined transponder/reader simulation framework — 750+ tests. Pure-Python models for ISO 14443-A, 15693, MIFARE Classic, DESFire, JCOP, LF (EM4100, HID, T5577), NDEF, NXP ICODE/SLIX2/DNA/NTAG5, access control (Wiegand, OSDP), implant profiles. See `.worktrees/sim-framework/docs/SIM_FRAMEWORK_STATUS.md` for full status.
|
|
|
|
## Table compiler: proprietary command match patterns
|
|
|
|
**Critical:** For NXP custom commands (0xA0+), table entry match patterns must **NOT** include the manufacturer code byte (0x04). The firmware's UID addressing logic consumes the mfg byte as part of UID parsing, so after normalization the mfg byte is absent from the command passed to table lookup.
|
|
|
|
Addressing flow for `22 AB 04 <uid_8_bytes>`:
|
|
1. Firmware sees `cmd[0] & ADDRESS` → addressed mode
|
|
2. `cmd[2]` (mfg code 0x04) doesn't match UID → tries `cmd[3:11]` → UID matches
|
|
3. `cmdCpt` advances past mfg + UID → `cmdCpt = 11`
|
|
4. Normalization: `norm = [flags & ~ADDRESS, cmd] + cmd[cmdCpt:]` → `02 AB` (no mfg code!)
|
|
5. Table lookup on `02 AB` → match pattern must be `[0x02, 0xAB]` (PREFIX), NOT `[0x02, 0xAB, 0x04]`
|
|
|
|
For unaddressed commands (`02 AB 04`), mfg code stays → `02 AB 04`. PREFIX match on `[0x02, 0xAB]` matches both forms.
|
|
|
|
**Rule:** All NXP custom command table entries use `match=bytes([flags, cmd_byte])` with `MATCH_PREFIX`. Never include `0x04` in the match.
|
|
|
|
## Python-driven card simulation (in progress)
|
|
|
|
Design doc: `docs/PYTHON_SIM_DESIGN.md`. Firmware patch (~320 lines of C) + Python extensions to enable Python-controlled card simulation on unmodified PM3 Easy/RDV4 hardware. Two mechanisms:
|
|
- **Response table** in BigBuf — pre-compiled by Python, served by firmware at wire speed (86µs FDT for 14443-A Layer 3)
|
|
- **WTX relay** — firmware sends S(WTX) on Layer 4 table miss, relays APDU to Python over USB for real-time crypto (DESFire, JCOP, EMV)
|
|
- **15693 retry relay** — reader retry-based relay for unknown commands
|
|
|
|
Firmware maintenance: atomic single-file commits for easy rebase against upstream PM3. See design doc for CI workflow.
|
|
|
|
## Adding new commands
|
|
|
|
1. Find the `CMD_*` constant in `include/pm3_cmd.h` and add to `Cmd` enum in `core/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
|