Files
pm3py/CLAUDE.md
michael 615d4a6115 docs: roadmap index + custom-14a-command plans + backfill plan docs
- Add docs/plans/README.md as the roadmap index cataloguing all design/plan docs.
- Add the custom ISO14443-A command handling design + plan (L3 NTAG I2C
  SECTOR_SELECT / cross-sector reads + native auth; L4 static APDUs + WTX relay),
  produced from a multi-agent design workflow.
- Backfill previously-uncommitted plan docs (trace-formatter, ndef-trace-decode,
  live-sniff, firmware-upstream-rebase, 14a-live-trace) and NTAG5_SECURITY.md.
- Sync CLAUDE.md package structure with the committed transponder models.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 12:58:48 -07:00

148 lines
7.8 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, thin sniff cmd
hf_mfc.py # MIFARE Classic — rdbl, wrbl, rdsc, chk, nested, cident
lf.py # LF commands + T55xxCommands + LFSearchResult
trace/ # firmware trace infrastructure (shared by sniff, sim, reader)
trace.py # parse_tracelog, TRACELOG_HDR_SIZE
decode_iso15.py # ISO 15693 command/response decoders
ndef.py # NDEF TLV/record decode for trace annotation
format.py # ANSI color formatting, format_sniff_line
sniff/ # sniff orchestration — protocol detection, session lifecycle
session.py # SniffSession — start/download/decode per protocol
sim/ # card simulation infrastructure (19 files)
transponder.py # Transponder ABC, MemoryRegion
medium.py # Medium, SoftwareMedium (RF simulation)
reader.py # Reader ABC, ScriptedReader, InteractiveReader
frame.py # RFFrame (bit/byte level)
sim_session.py # SimSession (table compile + WTX relay)
dual_session.py # DualInterfaceSession (PM3 RF + MCU I2C)
table_compiler.py # ResponseTable, TableCompiler
trace_fmt.py # TraceFormatter (14443-A + 15693 decoders)
pm3medium.py # PM3-backed Medium for real hardware
mcu_bridge.py # COBS serial bridge to MCU
mcu_protocol.py # MCU message types and protocol
fuzzer.py # Transponder fuzzer
relay.py # Card relay
replay.py # Trace replay
access_control/ # Wiegand, OSDP
transponders/ # tag/transponder models (extracted from sim/)
hf/iso14443a/ # Tag14443A_3/4, MifareClassic, DESFire, NfcType2/4
# nxp/type2,ntag21x,ultralight,ntag_i2c
# (NTAG210-216, Ultralight/C/EV1, NTAG I2C plus)
# st/st25tn (Type 2), st/st25ta (Type 4)
# infineon/optiga_nbt (OPTIGA Authenticate NBT, Type 4)
hf/iso14443b/ # base.py: Tag14443B (SRIX slot-marker) +
# Tag14443B_4 (standard REQB/ATTRIB + ISO-DEP).
# Vendor models (organised by mfg, like the rest):
# st/st25tb, ti/rf430cl330h (NFC Type 4B)
hf/iso15693/ # Tag15693, NfcType5, NXP ICODE/SLIX2/DNA/NTAG5
# st/st25tv, st/st25dv, infineon/myd_vicinity
# ti/tagit (Tag-it HF-I), ti/rf430frl (sensor)
lf/ # EM4100, HID, T5577
reader/ # higher-level reader modes by protocol/vendor (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
Software-defined transponder/reader simulation framework — 750+ tests, merged to master. 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. Transponder models in `transponders/`, sim infrastructure in `sim/`.
## 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
Design doc: `docs/PYTHON_SIM_DESIGN.md`. Firmware patch in `firmware/` submodule (proxmark3-pm3py) + Python sim framework. 15693 sim fully working — phone reads all blocks, NDEF, NXP custom commands. 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