Files
nfc-pn532-warlord/README.md
drjones 63db10d400 fw: robust HTTP body reads, CORS preflight, LED off at boot; update docs
- app_net.c: replace bare httpd_req_recv with recv_body_capped/alloc
  helpers (TCP-safe, full-body reads); add OPTIONS/* CORS preflight
  handler; bump WS broadcast buffer to 2048; add CORS Allow-Methods
- CMakeLists (net_service): add http_parser dep for HTTP_OPTIONS
- nfc_engine/pn532_core: add nfc_access_lock/unlock mutex, board-RGB
  quiet helper, UL type detection, general-status improvements
- pn532_transport: minor cleanup
- main.c: call board_rgb_led_quiet() at boot to kill onboard LED
- sdkconfig.defaults: add board RGB Kconfig defaults
- README, docs/LIMITATIONS, docs/PINOUT: expand and correct

Made-with: Cursor
2026-04-07 21:23:04 -07:00

177 lines
14 KiB
Markdown
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# PN532 NFC Toolkit
**The browser-native NFC lab that fits in your pocket.**
ESP32-S3 + PN532 + a serious web UI — no desktop app, no dongle software, no mystery binaries. You join WiFi, open a URL, and youre operating the reader from **any phone or laptop**.
---
## Why this is the best tool for PN532 workflows
1. **Full remote control in the browser** — Dashboard, live scan, deep capture, read/write helpers, dictionary attack, raw PN532 commands, and live status — all over **HTTP + WebSocket**. You can stand across the room with your phone while the hardware sits on the bench.
2. **“Deep capture” is actually deep (for a PN532)** — On each **new UID** the firmware doesnt stop at inventory: it builds a **JSON profile** with **MIFARE Classic** sector sweeps (default keys, Key **A** then **B** per sector trailer, **1K and 4K layouts**) with **every readable block as hex**, or an **Ultralight/NTAG-style page sweep** (up to **240 pages** in firmware). Unknown or non-Classic paths still get solid **ATQA/SAK/UID** plus whatever the stack can return — nothing is silently dropped.
3. **RAM session buffer with a hard stop + phone download** — Captures live in **on-chip RAM** (**48 KB** buffer, NDJSON lines). When the buffer is full, **RF polling pauses** so you never lose data to silent overflow. You tap **Download NDJSON**, get one file with every profile, then **Clear** to resume. That workflow is built for field audits, bench sessions, and anything where “I need the dump on my phone, now” matters.
4. **Self-hosted on the device** — Default **SoftAP**: SSID **`PN532-Toolkit`**, **open network** (no password, lab default). **mDNS** hostname **`pn532tool.local`** (HTTP port **80**). No cloud, no account, no telemetry.
5. **Honest architecture****ESP-IDF**, explicit components (`pn532_host`, `nfc_engine`, `net_service`), **Vite/React** UI embedded in **SPIFFS**. You extend it like real firmware, not a black-box sketch. PN532 talks **SPI, I2C, or UART (HSU)** — pins and bus are **menuconfig**, not guesses.
6. **Raw PN532 path** — When the high-level UI isnt enough, **Raw** sends **command bytes**; the stack handles transport framing. **`GET /api/pn532/general-status`** exposes the controllers **general status bytes** for debugging. Thats how you stay aligned with the real chip, not a toy abstraction.
7. **UI that matches the ambition** — Live **WebSocket** stream (scan + capture), **browser-side log** (thousands of events, exportable JSON), optional **`apiBase`** for talking to the box from another host, **HashRouter** so refreshes dont fight the server, and **lab fixtures** (synthetic card blobs, keys, SHA checks) so you can validate the UI without burning tags.
This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation, no FPGA tricks). For **hosted NFC with a PN532**, its the **full pocket operator**: remote UI, deep reads, session export, Classic dictionary work, Ultralight read/write, card emulation hooks, and a straight line to the command set when you need it.
---
## Every feature, in one pass
### Firmware (what the chip actually does)
| Capability | What you get |
|------------|----------------|
| **Transport** | PN532 over **SPI** (default pins in Kconfig), **I2C**, or **UART (HSU)** — bit rate and GPIOs set in `menuconfig`. |
| **Continuous scan** | Background task polls passive targets; **~65 ms** when not in deep mode, **~220 ms** when deep capture is on (more time per tag for heavy work). Toggle via **`POST /api/nfc/scan`** or the Dashboard. |
| **RF sensitivity** | On init, **high passive-activation retries** (`RFConfiguration` **0x05**) so marginal tags get more chances to answer. |
| **Deep profile** | Per new UID: Classic sector layout detection (1K / 4K), per-sector auth with built-in default keys, block hex arrays; Ultralight/NTAG page reads up to firmware limit; structured **JSON** for export. |
| **Session RAM** | **~48 KB** NDJSON capture buffer; mutex-protected; **full** flag stops polling until you clear. |
| **HTTP API** | JSON in/out on documented routes; **CORS** headers on API responses; **`OPTIONS /*`** preflight for cross-origin clients. POST bodies are read **completely** (chunked TCP-safe). |
| **WebSocket** | **`/ws`**: JSON envelopes `{"channel":"…","payload":…}`**`scan`** (inventory / `present:false` when tag leaves), **`capture`** (`recorded`, `bufferFull`, etc.), **`pong`** for keepalive. |
| **OTA endpoint** | **`POST /api/ota`** now performs a real **HTTPS OTA** with the ESP-IDF certificate bundle; successful updates reboot into the new slot and rollback support is enabled. |
| **Onboard RGB (DevKit-style)** | Optional **boot-time** shutdown of a **WS2812/SK6812** on **GPIO 48** (Kconfig) so the addressable LED isnt left on a random color — **not** on all boards; power LEDs are hardware. |
| **Status** | **`/api/status`**: uptime, free heap, WiFi mode, **PN532 firmware version** (when reachable), scan flag, `targetActive`, and session stats. |
| **Probe API** | **`POST /api/nfc/probe`** builds a richer capability profile for the present tag, including structured-clone suitability and Type 2 `GET_VERSION` when supported. |
| **Structured clone flow** | **`POST /api/clone/capture`** normalizes MIFARE Classic and Type 2 tags into JSON snapshots; **`POST /api/clone/program`** writes supported snapshots back to a destination tag with safe defaults. |
| **Target mode** | **`POST /api/nfc/target/*`** provides `status`, `start`, `recv`, `send`, and `stop` around **`TgInitAsTarget` / `TgGetData` / `TgSetData`**, with a raw-parameter escape hatch when you need exact bytes. |
### Web UI (each page)
| Page | Purpose |
|------|---------|
| **Dash** | Live **status** poll, **continuous scan** on/off, **manual poll** (uses live tag from WebSocket when present), hero readout. |
| **Read-all (Capture)** | Toggle **deep capture**, **export** NDJSON from device RAM, **clear** buffer, progress vs **48 KB** budget. |
| **Read** | **MIFARE** block read with key + Key A/B; **Ultralight** page read; paste a **sector trailer** to decode **access bits** in the browser. |
| **Write** | **MIFARE** block write (16-byte hex, confirm dialog); **Ul** page write; **lab fixture** loader for offline testing flows; ties into **live UID** from WebSocket when available. |
| **Brute** | **Classic 1K / 4K** dictionary attack: firmwares built-in key set + your lines, optional **bounded variations**; shows JSON result. |
| **Emu** | **`TgInitAsTarget`** and friends via **`POST /api/nfc/emulate-raw`** — you supply hex; **experimental** (bad sequences can require power cycle). |
| **KeyLab** | **XOR span** across up to three 6-byte base keys (all combinations → candidates for Brute/Keys); **Shannon entropy** on arbitrary hex blobs. **Stage for Keys** copies candidates into **sessionStorage** for the Keys page. |
| **Lib** | **Saved hex snippets** in localStorage; **seed** from built-in **lab catalog** samples; fixture loader for repeatable demos. |
| **Keys** | **Browser-local** key dictionary (defaults + your lines); **merge lab keys**; **import Key Lab stash** from staged session. Used when you copy keys into Brute manually — **not** auto-uploaded to flash. |
| **Raw** | Hex **command** payload → **`/api/raw/pn532`**; **general status** dump from **`/api/pn532/general-status`**; local TX/RX log. |
| **Set** | **`apiBase`** (empty = same host), **theme** (light/dark), reminder of SoftAP + mDNS. |
### Browser extras (no extra server)
- **Top bar** — **Download log JSON**: last **5000** WebSocket messages from **sessionStorage** (scan + capture + anything else the firmware sends).
- **Scan feedback** — Visual/audio “hit” flourishes on new tag / capture events (debounced so it doesnt spam).
- **Validation fixtures** — Shared **lab** blobs and notes (`validationFixtures.ts`) for UI testing and teaching.
---
## Remote deep capture (how to use it)
1. **Power on** → firmware starts **continuous scan** by default (toggle on the Dashboard).
2. Open **Read-all (Capture)** → enable **passive read-all** (`POST /api/session/deep`).
3. Present tags; each **new UID** appends one **NDJSON** line: full deep profile JSON.
4. When the **progress bar hits the end**, scanning **pauses** automatically.
5. Tap **Download NDJSON** — browser saves `pn532-deep-capture-*.ndjson`.
6. Tap **Clear buffer** to free RAM and **resume**.
### Session API (automation)
| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/api/status` | App name, uptime, heap, WiFi mode, PN532 version, `scanning`, `session` (`usedBytes`, `maxBytes`, `lines`, `full`, `deepCapture`) |
| `POST` | `/api/session/deep` | Body `{"enable": true}` or `false` |
| `GET` | `/api/session/export` | `text/plain` / NDJSON attachment |
| `POST` | `/api/session/clear` | Clears RAM buffer |
### Live NFC API
| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/api/nfc/poll` | One-shot poll; body may be `{}` — returns `present`, optional `tag` |
| `POST` | `/api/nfc/probe` | Rich capability probe for the current tag; adds clone/program hints and Type 2 `GET_VERSION` when available |
| `POST` | `/api/nfc/scan` | Body `{"enable": true}` or `false` — continuous background scan |
| `GET` | `/api/pn532/general-status` | Raw PN532 **general status** byte array |
| `POST` | `/api/mifare/read-block` | `block`, `key` (12 hex), `keyB` |
| `POST` | `/api/mifare/write-block` | `block`, `key`, `keyB`, `data` (32 hex) |
| `POST` | `/api/ul/read-page` | `page` |
| `POST` | `/api/ul/write-page` | `page`, `data` (8 hex) |
| `POST` | `/api/raw/pn532` | `frame` — PN532 command bytes (not a full transport frame) |
| `POST` | `/api/mifare/dictionary-attack` | `readerType` (`classic1k` / `classic4k`), `variations`, optional `keysHex[]`, optional `sectorFirst` / `sectorLast` |
| `POST` | `/api/clone/capture` | Optional `mode` (`auto` / `classic` / `type2`), optional `keysHex[]`, `variations`, `maxPages` — returns a structured snapshot for supported tags |
| `POST` | `/api/clone/program` | `snapshot` plus optional `includeTrailers` / `includeLockPages` — programs a supported destination tag with safe skips by default |
| `POST` | `/api/nfc/target/status` | Returns whether PN532 target mode is active |
| `POST` | `/api/nfc/target/start` | Start target mode from semantic JSON fields or `rawParamsHex` |
| `POST` | `/api/nfc/target/recv` | Wait for initiator bytes and return status + payload hex |
| `POST` | `/api/nfc/target/send` | Send target response bytes back to the initiator |
| `POST` | `/api/nfc/target/stop` | Return PN532 to normal SAM mode |
| `POST` | `/api/nfc/emulate-raw` | `hex` — raw command bytes (e.g. **0x8C** sequences) |
| `POST` | `/api/ota` | `url` (HTTPS), optional `reboot` — performs a real OTA update via `esp_https_ota` |
**WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**.
### Browser memory log
The SPA keeps the **last 5000** WebSocket events in **sessionStorage**. **Download log JSON** saves a pretty file on your phone or PC **without** using device RAM. Clear that log separately from the device capture buffer.
### Dictionary “brute” (Classic)
**Brute** page → **Classic 1K / 4K**, optional **variations**, paste extra keys. Firmware runs **`POST /api/mifare/dictionary-attack`**: built-in **public default keys** (same family as community lists like [Proxmark3 `mfc_default_keys.dic`](https://github.com/RfidResearchGroup/proxmark3/blob/master/client/dictionaries/mfc_default_keys.dic)) plus your lines, Key **A** then **B** per sector trailer. **Variations** add bounded XOR / nibble-style tweaks — **not** a full 2⁴⁸ search. You wont magically open every sector; you **systematically** try keys people actually leak in the wild.
### Card emulation (PN532 target mode)
**Emulate** now has two layers: a higher-level **`/api/nfc/target/*`** subsystem for `TgInitAsTarget` / `TgGetData` / `TgSetData`, plus **`POST /api/nfc/emulate-raw`** when you want to drive the exact byte stream yourself. The structured path is safer; raw mode is still experimental and bad sequences can wedge the front end until re-init or power cycle.
---
## Build
### One-command local run
```bash
./start-firmware.sh
```
What it does (see script for flags): bootstraps ESP-IDF under a **local prefix** if needed, builds the web UI into **`firmware/data`**, targets **esp32s3**, auto-detects serial when possible, **builds and flashes**.
Options include **`--build-only`**, **`--monitor`**, **`--port`**, etc.
### Web UI → SPIFFS image
```bash
cd web
npm install
npm run build:fw
```
### Firmware (ESP-IDF 5.x)
```bash
cd firmware
idf.py set-target esp32s3
idf.py menuconfig # PN532 Host: SPI / I2C / UART + pins; Board indicators (RGB) if you use DevKitC-1
idf.py build flash monitor
```
### Common red PN532 module notes
The common red Elechouse-style PN532 board usually selects its bus with two onboard switches: **HSU = OFF/OFF**, **I2C = ON/OFF**, **SPI = OFF/ON**. HSU and I2C usually share header pins, and many boards ship in **HSU** by default, so a “dead” SPI/I2C setup is often just a switch mismatch rather than bad firmware.
From `firmware/`, with IDF already in your environment, you can also **`./flash.sh`** (uses **`ESPPORT`** or the first argument as the serial device).
See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md). Limitations and scope notes: [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
---
## Repo layout
| Path | Role |
|------|------|
| `firmware/` | ESP-IDF: PN532 transport, NFC engine, **session buffer**, **deep profile**, HTTP/WebSocket, SPIFFS |
| `web/` | React UI: Dashboard, Capture, Read, Write, Brute, Emulate, KeyLab, Library, Keys, Raw, Settings |
| `docs/` | Flashing, pinout, workflows, limitations |
| `start-firmware.sh` | One-shot IDF bootstrap + build + flash |