docs: add README.md and CLAUDE.md
This commit is contained in:
64
CLAUDE.md
Normal file
64
CLAUDE.md
Normal file
@@ -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
|
||||
185
README.md
Normal file
185
README.md
Normal file
@@ -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.
|
||||
Reference in New Issue
Block a user