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
This commit is contained in:
drjones
2026-04-07 21:23:04 -07:00
parent ad814c6a12
commit 63db10d400
13 changed files with 1070 additions and 16 deletions

View File

@@ -13,7 +13,7 @@ ESP32-S3 + PN532 + a serious web UI — no desktop app, no dongle software, no m
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`**, password **`nfc-toolkit`** (WPA2). **mDNS** hostname **`pn532tool.local`** (HTTP port **80**). No cloud, no account, no telemetry.
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.
@@ -38,9 +38,12 @@ This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation
| **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`** returns **501** with a stub message — there is **no** in-field OTA in this repo yet; use **`idf.py flash`** (or the included scripts). |
| **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, session stats. |
| **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)
@@ -89,16 +92,24 @@ This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation
| 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`hex command bytes |
| `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` | **Not implemented** — returns **501** |
| `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`**.
@@ -112,7 +123,7 @@ The SPA keeps the **last 5000** WebSocket events in **sessionStorage**. **Dow
### Card emulation (PN532 target mode)
**Emulate** sends raw **`TgInitAsTarget` (0x8C)** payloads via **`POST /api/nfc/emulate-raw`**. You own the bytes (see NXP **UM0701**). This is **experimental**; a bad sequence can leave the front end wedged until **power cycle**.
**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.
---
@@ -145,6 +156,10 @@ idf.py menuconfig # PN532 Host: SPI / I2C / UART + pins; Board indicators (RGB
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).

View File

@@ -3,7 +3,7 @@
The PN532 is a **hosted NFC controller**, not a low-level RF lab instrument. This project exposes PN532 features honestly:
- **ISO14443-B**: reader support exists with **chip-level caveats** (anticollision / stack-dependent behavior). No promises of full mobile/PICC coverage.
- **Card emulation / TG modes**: PN532 firmware supports target commands; real-world mimicry depends on timing, UID size, and reader expectations — expose experimentally, not as “propable MIFARE magic.”
- **Card emulation / TG modes**: PN532 firmware supports target commands and this repo now exposes a structured target-mode API, but real-world mimicry still depends on timing, UID size, ATS/general bytes, and reader expectations. Treat it as a controllable subsystem, not magic full-card impersonation.
- **Key recovery**: on-device “brute force” at Proxmark scale is **infeasible**. The UI provides **dictionary / manual** key workflows.
- **Signal / RF metrics**: diagnostics use **PN532 status / timings / retries**, not calibrated dBm.
- **OTA via UI**: `POST /api/ota` is a **stub** (`501`) — ship OTA with `esp_https_ota` + signed images when you need production updates.
- **OTA via UI**: `POST /api/ota` now performs real HTTPS OTA with the ESP-IDF certificate bundle. For production fleets, add your own release signing, manifest control, and hardware validation gates before broad rollout.

View File

@@ -20,3 +20,13 @@ The firmware Kconfig ships **example** GPIOs:
## Transport selection
Start with the bus your breakout is jumpered for — **I2C is often simplest** on ESP32 for bring-up; SPI may require **lower clock** initially (e.g. **100 kHz**).
## Common red module switch matrix
For the common Elechouse-style red PN532 board, the onboard two-position switch matrix is typically:
- **HSU**: `OFF/OFF`
- **I2C**: `ON/OFF`
- **SPI**: `OFF/ON`
Many of these boards boot in **HSU** by default, and the **I2C** and **HSU** labels often refer to the same physical header pins from opposite sides of the PCB. If the bus looks dead, check the switches before changing firmware.

View File

@@ -2,5 +2,5 @@ idf_component_register(
SRCS "app_net.c"
INCLUDE_DIRS "include"
REQUIRES esp_http_server http_parser esp_wifi esp_netif nvs_flash mdns esp_timer
json spiffs vfs freertos nfc_engine pn532_host
json spiffs vfs freertos nfc_engine pn532_host esp_https_ota app_update mbedtls
)

View File

@@ -1,6 +1,8 @@
#include "net_service/app_net.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_crt_bundle.h"
#include "esp_https_ota.h"
#include "esp_system.h"
#include "esp_http_server.h"
#include "esp_netif.h"
@@ -36,6 +38,7 @@ static httpd_handle_t s_server;
static SemaphoreHandle_t s_nfc_op_mu;
static volatile bool s_scan = true;
static TaskHandle_t s_scan_task;
static volatile bool s_target_active;
static void nfc_access_lock(void)
{
@@ -105,6 +108,48 @@ static bool copy_json_string(const cJSON *item, char *dst, size_t cap)
return true;
}
static void bin_to_hex(const uint8_t *src, size_t len, char *dst, size_t dst_cap)
{
if (!dst || dst_cap == 0) {
return;
}
if (!src || dst_cap < (len * 2 + 1)) {
dst[0] = '\0';
return;
}
for (size_t i = 0; i < len; i++) {
snprintf(dst + i * 2, 3, "%02X", src[i]);
}
dst[len * 2] = '\0';
}
static bool add_hex_to_object(cJSON *obj, const char *name, const uint8_t *src, size_t len)
{
if (!obj || !name || !src) {
return false;
}
char *hex = calloc(1, len * 2 + 1);
if (!hex) {
return false;
}
bin_to_hex(src, len, hex, len * 2 + 1);
cJSON_AddStringToObject(obj, name, hex);
free(hex);
return true;
}
static int json_get_int_default(const cJSON *obj, const char *name, int fallback)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive((cJSON *)obj, name);
return cJSON_IsNumber(item) ? (int)cJSON_GetNumberValue(item) : fallback;
}
static bool json_get_bool_default(const cJSON *obj, const char *name, bool fallback)
{
cJSON *item = cJSON_GetObjectItemCaseSensitive((cJSON *)obj, name);
return cJSON_IsBool(item) ? cJSON_IsTrue(item) : fallback;
}
/** Read full POST body (httpd may return partial data in multiple recv calls). */
static int recv_body_capped(httpd_req_t *req, char *buf, size_t cap)
{
@@ -177,7 +222,7 @@ esp_err_t app_net_broadcast_json(const char *channel, const char *json_text)
httpd_ws_frame_t pkt = {.type = HTTPD_WS_TYPE_TEXT, .payload = (uint8_t *)line, .len = (size_t)n};
for (size_t i = 0; i < fdcount; i++) {
if (httpd_ws_get_fd_info(s_server, fds[i]) == HTTPD_WS_CLIENT_WEBSOCKET) {
(void)httpd_ws_send_frame_async(s_server, fds[i], &pkt);
(void)httpd_ws_send_data(s_server, fds[i], &pkt);
}
}
return ESP_OK;
@@ -250,6 +295,359 @@ static esp_err_t ensure_mifare_classic_tag_locked(nfc_tag_info_t *tag)
return nfc_tag_is_mifare_classic(tag) ? ESP_OK : ESP_ERR_NOT_SUPPORTED;
}
static esp_err_t reject_if_target_active(httpd_req_t *req)
{
if (!s_target_active) {
return ESP_OK;
}
return send_json(req, cJSON_CreateString("target mode active; stop target mode first"), 409);
}
static cJSON *build_probe_locked(const nfc_tag_info_t *tag)
{
if (!tag) {
return NULL;
}
cJSON *root = cJSON_CreateObject();
cJSON *tag_json = nfc_tag_to_json(tag);
cJSON *caps = cJSON_CreateObject();
if (!root || !tag_json || !caps) {
cJSON_Delete(root);
cJSON_Delete(tag_json);
cJSON_Delete(caps);
return NULL;
}
cJSON_AddItemToObject(root, "tag", tag_json);
if (nfc_tag_is_mifare_classic(tag)) {
cJSON_AddStringToObject(caps, "family", nfc_tag_is_mifare_classic_4k(tag) ? "mifareClassic4k" : "mifareClassic");
cJSON_AddNumberToObject(caps, "sectorCount", nfc_mifare_sector_count(tag));
cJSON_AddBoolToObject(caps, "canRead", true);
cJSON_AddBoolToObject(caps, "canWrite", true);
cJSON_AddBoolToObject(caps, "canDictionaryAttack", true);
cJSON_AddBoolToObject(caps, "structuredCloneCapture", true);
cJSON_AddBoolToObject(caps, "structuredCloneProgram", true);
cJSON_AddBoolToObject(caps, "manufacturerBlockWritable", false);
cJSON_AddBoolToObject(caps, "targetModeRelevant", false);
cJSON_AddStringToObject(caps, "note",
"Classic clone/program flow can write user blocks and optional trailers, but not manufacturer block 0 on normal cards.");
} else if (nfc_tag_is_type2(tag)) {
cJSON_AddStringToObject(caps, "family", "type2");
cJSON_AddBoolToObject(caps, "canRead", true);
cJSON_AddBoolToObject(caps, "canWrite", true);
cJSON_AddBoolToObject(caps, "structuredCloneCapture", true);
cJSON_AddBoolToObject(caps, "structuredCloneProgram", true);
cJSON_AddNumberToObject(caps, "safeUserPageStart", 4);
uint8_t version[8];
if (nfc_type2_get_version(version) == ESP_OK) {
(void)add_hex_to_object(caps, "getVersionHex", version, sizeof(version));
}
cJSON_AddStringToObject(caps, "note",
"Type 2 clone/program flow writes user pages by default and leaves manufacturer/lock pages untouched unless forced.");
} else {
cJSON_AddStringToObject(caps, "family", "unknown");
cJSON_AddBoolToObject(caps, "canRead", false);
cJSON_AddBoolToObject(caps, "canWrite", false);
cJSON_AddBoolToObject(caps, "structuredCloneCapture", false);
cJSON_AddBoolToObject(caps, "structuredCloneProgram", false);
cJSON_AddStringToObject(caps, "note",
"Use raw commands and manual analysis; this tag does not match the current structured Classic/Type 2 workflows.");
}
cJSON_AddItemToObject(root, "capabilities", caps);
return root;
}
static cJSON *find_sector_hit(const cJSON *attack, int sector)
{
if (!attack) {
return NULL;
}
cJSON *hits = cJSON_GetObjectItemCaseSensitive((cJSON *)attack, "sectorHits");
if (!cJSON_IsArray(hits)) {
return NULL;
}
int n = cJSON_GetArraySize(hits);
for (int i = 0; i < n; i++) {
cJSON *it = cJSON_GetArrayItem(hits, i);
cJSON *s = cJSON_GetObjectItemCaseSensitive(it, "sector");
if (cJSON_IsNumber(s) && (int)cJSON_GetNumberValue(s) == sector) {
return it;
}
}
return NULL;
}
static bool parse_sector_key_hit(const cJSON *hit, nfc_mifare_key_t *key, char *key_hex_out, size_t key_hex_cap)
{
if (!hit || !key) {
return false;
}
cJSON *kh = cJSON_GetObjectItemCaseSensitive((cJSON *)hit, "keyHex");
cJSON *kt = cJSON_GetObjectItemCaseSensitive((cJSON *)hit, "keyType");
if (!cJSON_IsString(kh) || !kh->valuestring || !cJSON_IsString(kt) || !kt->valuestring) {
return false;
}
if (!hex_to_bin(kh->valuestring, key->key, 6)) {
return false;
}
key->key_b = (strcmp(kt->valuestring, "B") == 0);
if (key_hex_out && key_hex_cap > 0) {
size_t len = strlen(kh->valuestring);
if (len >= key_hex_cap) {
return false;
}
memcpy(key_hex_out, kh->valuestring, len + 1);
}
return true;
}
static cJSON *build_type2_capture_locked(const nfc_tag_info_t *tag, int max_pages)
{
if (!tag) {
return NULL;
}
if (max_pages < 4) {
max_pages = 4;
}
if (max_pages > 240) {
max_pages = 240;
}
cJSON *root = cJSON_CreateObject();
cJSON *tag_json = nfc_tag_to_json(tag);
cJSON *pages = cJSON_CreateArray();
if (!root || !tag_json || !pages) {
cJSON_Delete(root);
cJSON_Delete(tag_json);
cJSON_Delete(pages);
return NULL;
}
cJSON_AddStringToObject(root, "format", "type2");
cJSON_AddItemToObject(root, "tag", tag_json);
uint8_t version[8];
if (nfc_type2_get_version(version) == ESP_OK) {
(void)add_hex_to_object(root, "getVersionHex", version, sizeof(version));
}
for (int page = 0; page < max_pages; page++) {
uint8_t data[4];
if (nfc_ultralight_read_page((uint8_t)page, data) != ESP_OK) {
break;
}
char hx[9];
bin_to_hex(data, sizeof(data), hx, sizeof(hx));
cJSON_AddItemToArray(pages, cJSON_CreateString(hx));
}
cJSON_AddNumberToObject(root, "pageCount", cJSON_GetArraySize(pages));
cJSON_AddItemToObject(root, "pagesHex", pages);
cJSON_AddStringToObject(root, "programNote",
"Programming skips pages 0-3 by default because they contain manufacturer and lock/config data.");
return root;
}
static cJSON *build_classic_capture_locked(const nfc_tag_info_t *tag, const uint8_t *extra_keys, size_t extra_n,
bool variations)
{
if (!tag) {
return NULL;
}
int sectors = nfc_mifare_sector_count(tag);
if (sectors <= 0) {
return NULL;
}
cJSON *attack = nfc_mifare_dictionary_attack((nfc_tag_info_t *)tag, 0, (uint8_t)(sectors - 1),
extra_keys, extra_n, variations, NULL);
cJSON *root = cJSON_CreateObject();
cJSON *tag_json = nfc_tag_to_json(tag);
cJSON *arr = cJSON_CreateArray();
if (!attack || !root || !tag_json || !arr) {
cJSON_Delete(attack);
cJSON_Delete(root);
cJSON_Delete(tag_json);
cJSON_Delete(arr);
return NULL;
}
cJSON_AddStringToObject(root, "format", nfc_tag_is_mifare_classic_4k(tag) ? "mifareClassic4k" : "mifareClassic");
cJSON_AddItemToObject(root, "tag", tag_json);
cJSON_AddNumberToObject(root, "sectorCount", sectors);
cJSON_AddItemToObject(root, "keySearch", attack);
for (int sector = 0; sector < sectors; sector++) {
int first_block = 0;
int block_count = 0;
uint8_t trailer_block = 0;
cJSON *sec = cJSON_CreateObject();
if (!sec) {
cJSON_Delete(root);
cJSON_Delete(arr);
return NULL;
}
cJSON_AddNumberToObject(sec, "sector", sector);
if (!nfc_mifare_sector_layout(tag, sector, &first_block, &block_count, &trailer_block)) {
cJSON_AddBoolToObject(sec, "layoutError", true);
cJSON_AddItemToArray(arr, sec);
continue;
}
cJSON_AddNumberToObject(sec, "firstBlock", first_block);
cJSON_AddNumberToObject(sec, "blockCount", block_count);
cJSON_AddNumberToObject(sec, "trailerBlock", trailer_block);
cJSON *hit = find_sector_hit(attack, sector);
nfc_mifare_key_t key;
char key_hex[13];
if (!parse_sector_key_hit(hit, &key, key_hex, sizeof(key_hex))) {
cJSON_AddBoolToObject(sec, "authFailed", true);
cJSON_AddItemToArray(arr, sec);
continue;
}
cJSON_AddStringToObject(sec, "keyHex", key_hex);
cJSON_AddStringToObject(sec, "keyType", key.key_b ? "B" : "A");
if (nfc_mifare_authenticate_block(tag, trailer_block, &key) != ESP_OK) {
cJSON_AddBoolToObject(sec, "authFailed", true);
cJSON_AddItemToArray(arr, sec);
continue;
}
cJSON *blocks = cJSON_CreateArray();
if (!blocks) {
cJSON_Delete(sec);
cJSON_Delete(root);
cJSON_Delete(arr);
return NULL;
}
for (int i = 0; i < block_count; i++) {
uint8_t block_data[NFC_BLOCK_LEN];
if (nfc_mifare_read_block((uint8_t)(first_block + i), block_data) == ESP_OK) {
char hx[NFC_BLOCK_LEN * 2 + 1];
bin_to_hex(block_data, sizeof(block_data), hx, sizeof(hx));
cJSON_AddItemToArray(blocks, cJSON_CreateString(hx));
} else {
cJSON_AddItemToArray(blocks, cJSON_CreateNull());
}
}
cJSON_AddItemToObject(sec, "blocksHex", blocks);
cJSON_AddItemToArray(arr, sec);
}
cJSON_AddItemToObject(root, "sectors", arr);
cJSON_AddStringToObject(root, "programNote",
"Programming writes data blocks and optionally trailers; block 0 is always skipped on normal Classic cards.");
return root;
}
static cJSON *program_type2_snapshot_locked(const cJSON *snapshot, bool include_lock_pages)
{
cJSON *pages = cJSON_GetObjectItemCaseSensitive((cJSON *)snapshot, "pagesHex");
if (!cJSON_IsArray(pages)) {
return NULL;
}
int count = cJSON_GetArraySize(pages);
int first_page = include_lock_pages ? 0 : 4;
cJSON *out = cJSON_CreateObject();
if (!out) {
return NULL;
}
int written = 0;
int skipped = 0;
int failed = 0;
for (int page = 0; page < count; page++) {
if (page < first_page) {
skipped++;
continue;
}
cJSON *it = cJSON_GetArrayItem(pages, page);
if (!cJSON_IsString(it) || !it->valuestring) {
skipped++;
continue;
}
uint8_t data[4];
if (!hex_to_bin(it->valuestring, data, sizeof(data))) {
failed++;
continue;
}
if (nfc_ultralight_write_page((uint8_t)page, data) == ESP_OK) {
written++;
} else {
failed++;
}
}
cJSON_AddStringToObject(out, "format", "type2");
cJSON_AddNumberToObject(out, "writtenPages", written);
cJSON_AddNumberToObject(out, "skippedPages", skipped);
cJSON_AddNumberToObject(out, "failedPages", failed);
cJSON_AddBoolToObject(out, "includeLockPages", include_lock_pages);
return out;
}
static cJSON *program_classic_snapshot_locked(const nfc_tag_info_t *tag, const cJSON *snapshot, bool include_trailers)
{
if (!tag) {
return NULL;
}
cJSON *sectors = cJSON_GetObjectItemCaseSensitive((cJSON *)snapshot, "sectors");
if (!cJSON_IsArray(sectors)) {
return NULL;
}
int written = 0;
int skipped = 0;
int failed = 0;
int sectors_n = cJSON_GetArraySize(sectors);
for (int si = 0; si < sectors_n; si++) {
cJSON *sec = cJSON_GetArrayItem(sectors, si);
cJSON *blocks = cJSON_GetObjectItemCaseSensitive(sec, "blocksHex");
cJSON *first = cJSON_GetObjectItemCaseSensitive(sec, "firstBlock");
cJSON *count = cJSON_GetObjectItemCaseSensitive(sec, "blockCount");
cJSON *trailer = cJSON_GetObjectItemCaseSensitive(sec, "trailerBlock");
if (!cJSON_IsArray(blocks) || !cJSON_IsNumber(first) || !cJSON_IsNumber(count) || !cJSON_IsNumber(trailer)) {
skipped++;
continue;
}
nfc_mifare_key_t key;
if (!parse_sector_key_hit(sec, &key, NULL, 0)) {
failed++;
continue;
}
uint8_t trailer_block = (uint8_t)cJSON_GetNumberValue(trailer);
if (nfc_mifare_authenticate_block(tag, trailer_block, &key) != ESP_OK) {
failed++;
continue;
}
int first_block = (int)cJSON_GetNumberValue(first);
int block_count = (int)cJSON_GetNumberValue(count);
int blocks_n = cJSON_GetArraySize(blocks);
int limit = block_count < blocks_n ? block_count : blocks_n;
for (int bi = 0; bi < limit; bi++) {
int block_no = first_block + bi;
if (block_no == 0) {
skipped++;
continue;
}
if (block_no == trailer_block && !include_trailers) {
skipped++;
continue;
}
cJSON *it = cJSON_GetArrayItem(blocks, bi);
if (!cJSON_IsString(it) || !it->valuestring) {
skipped++;
continue;
}
uint8_t data[NFC_BLOCK_LEN];
if (!hex_to_bin(it->valuestring, data, sizeof(data))) {
failed++;
continue;
}
if (nfc_mifare_write_block((uint8_t)block_no, data) == ESP_OK) {
written++;
} else {
failed++;
}
}
}
cJSON *out = cJSON_CreateObject();
if (!out) {
return NULL;
}
cJSON_AddStringToObject(out, "format", "mifareClassic");
cJSON_AddNumberToObject(out, "writtenBlocks", written);
cJSON_AddNumberToObject(out, "skippedBlocks", skipped);
cJSON_AddNumberToObject(out, "failedBlocks", failed);
cJSON_AddBoolToObject(out, "includeTrailers", include_trailers);
return out;
}
static esp_err_t api_status(httpd_req_t *req)
{
cJSON *o = cJSON_CreateObject();
@@ -274,6 +672,7 @@ static esp_err_t api_status(httpd_req_t *req)
}
nfc_access_unlock();
cJSON_AddBoolToObject(o, "scanning", s_scan);
cJSON_AddBoolToObject(o, "targetActive", s_target_active);
size_t cap_u = 0;
uint32_t cap_l = 0;
bool cap_f = false;
@@ -365,6 +764,10 @@ static esp_err_t api_session_deep(httpd_req_t *req)
static esp_err_t api_nfc_poll(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char drain[128];
(void)recv_body_capped(req, drain, sizeof(drain));
@@ -411,6 +814,10 @@ static esp_err_t api_scan(httpd_req_t *req)
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *en = cJSON_GetObjectItem(j, "enable");
if (cJSON_IsBool(en) && cJSON_IsTrue(en) && s_target_active) {
cJSON_Delete(j);
return send_json(req, cJSON_CreateString("target mode active; stop target mode first"), 409);
}
if (cJSON_IsBool(en)) {
app_net_set_continuous_scan(cJSON_IsTrue(en));
}
@@ -422,6 +829,10 @@ static esp_err_t api_scan(httpd_req_t *req)
static esp_err_t api_general_status(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
uint8_t gs[32];
size_t gl = 0;
nfc_access_lock();
@@ -445,8 +856,154 @@ static esp_err_t api_general_status(httpd_req_t *req)
return send_json(req, o, 200);
}
static esp_err_t api_nfc_probe(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char drain[64];
(void)recv_body_capped(req, drain, sizeof(drain));
nfc_tag_info_t tag;
nfc_access_lock();
esp_err_t err = nfc_poll_passive_target(&tag);
if (err != ESP_OK) {
nfc_access_unlock();
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", err == ESP_ERR_NOT_FOUND ? "no tag" : esp_err_to_name(err));
return send_json(req, o, err == ESP_ERR_NOT_FOUND ? 400 : 500);
}
cJSON *o = build_probe_locked(&tag);
nfc_access_unlock();
if (!o) {
return send_json(req, cJSON_CreateString("probe failed"), 500);
}
return send_json(req, o, 200);
}
static esp_err_t api_clone_capture(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char *body = NULL;
cJSON *j = NULL;
if (req->content_len > 0) {
body = recv_body_alloc(req, 8192, NULL);
if (!body) {
return send_json(req, cJSON_CreateString("body too large"), 400);
}
j = cJSON_Parse(body);
free(body);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
} else {
j = cJSON_CreateObject();
}
char mode[16] = "auto";
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "mode"), mode, sizeof(mode));
int max_pages = json_get_int_default(j, "maxPages", 64);
bool variations = json_get_bool_default(j, "variations", false);
uint8_t extra[96 * 6];
size_t extra_n = 0;
cJSON *keys = cJSON_GetObjectItemCaseSensitive(j, "keysHex");
if (cJSON_IsArray(keys)) {
int n = cJSON_GetArraySize(keys);
for (int i = 0; i < n && extra_n < 96; i++) {
cJSON *it = cJSON_GetArrayItem(keys, i);
if (cJSON_IsString(it) && it->valuestring && hex_to_bin(it->valuestring, extra + extra_n * 6, 6)) {
extra_n++;
}
}
}
cJSON_Delete(j);
nfc_tag_info_t tag;
nfc_access_lock();
esp_err_t err = nfc_poll_passive_target(&tag);
if (err != ESP_OK) {
nfc_access_unlock();
return send_json(req, cJSON_CreateString("no tag present"), 400);
}
cJSON *out = NULL;
if (strcmp(mode, "classic") == 0) {
if (!nfc_tag_is_mifare_classic(&tag)) {
nfc_access_unlock();
return send_json(req, cJSON_CreateString("tag is not MIFARE Classic"), 400);
}
out = build_classic_capture_locked(&tag, extra_n ? extra : NULL, extra_n, variations);
} else if (strcmp(mode, "type2") == 0) {
if (!nfc_tag_is_type2(&tag)) {
nfc_access_unlock();
return send_json(req, cJSON_CreateString("tag is not Type 2 / Ultralight"), 400);
}
out = build_type2_capture_locked(&tag, max_pages);
} else if (nfc_tag_is_mifare_classic(&tag)) {
out = build_classic_capture_locked(&tag, extra_n ? extra : NULL, extra_n, variations);
} else if (nfc_tag_is_type2(&tag)) {
out = build_type2_capture_locked(&tag, max_pages);
}
nfc_access_unlock();
if (!out) {
return send_json(req, cJSON_CreateString("capture unsupported or failed"), 400);
}
return send_json(req, out, 200);
}
static esp_err_t api_clone_program(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char *body = recv_body_alloc(req, 65536, NULL);
if (!body) {
return send_json(req, cJSON_CreateString("snapshot body too large"), 400);
}
cJSON *j = cJSON_Parse(body);
free(body);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
cJSON *snapshot = cJSON_GetObjectItemCaseSensitive(j, "snapshot");
cJSON *format = snapshot ? cJSON_GetObjectItemCaseSensitive(snapshot, "format") : NULL;
bool include_trailers = json_get_bool_default(j, "includeTrailers", false);
bool include_lock_pages = json_get_bool_default(j, "includeLockPages", false);
if (!cJSON_IsObject(snapshot) || !cJSON_IsString(format) || !format->valuestring) {
cJSON_Delete(j);
return send_json(req, cJSON_CreateString("snapshot.format required"), 400);
}
nfc_tag_info_t tag;
cJSON *out = NULL;
nfc_access_lock();
if (strcmp(format->valuestring, "type2") == 0) {
esp_err_t err = ensure_ultralight_tag_locked(&tag);
if (err == ESP_OK) {
out = program_type2_snapshot_locked(snapshot, include_lock_pages);
}
} else if (strncmp(format->valuestring, "mifareClassic", 13) == 0) {
esp_err_t err = ensure_mifare_classic_tag_locked(&tag);
if (err == ESP_OK) {
out = program_classic_snapshot_locked(&tag, snapshot, include_trailers);
}
}
nfc_access_unlock();
cJSON_Delete(j);
if (!out) {
return send_json(req, cJSON_CreateString("program failed or unsupported target"), 400);
}
return send_json(req, out, 200);
}
static esp_err_t api_mifare_read(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char buf[512];
char key_hex[13];
int r = recv_body_capped(req, buf, sizeof(buf));
@@ -506,6 +1063,10 @@ static esp_err_t api_mifare_read(httpd_req_t *req)
static esp_err_t api_mifare_write(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char buf[512];
char key_hex[13];
char data_hex[33];
@@ -560,6 +1121,10 @@ static esp_err_t api_mifare_write(httpd_req_t *req)
static esp_err_t api_ul_read(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char buf[128];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
@@ -602,6 +1167,10 @@ static esp_err_t api_ul_read(httpd_req_t *req)
static esp_err_t api_ul_write(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char buf[128];
char data_hex[9];
int r = recv_body_capped(req, buf, sizeof(buf));
@@ -646,14 +1215,255 @@ static esp_err_t api_ul_write(httpd_req_t *req)
return send_json(req, o, 200);
}
static esp_err_t api_ota_stub(httpd_req_t *req)
static esp_err_t api_target_status(httpd_req_t *req)
{
cJSON *o = cJSON_CreateString("Use idf.py app-flash or extend with esp_https_ota + bundle URL");
return send_json(req, o, 501);
char drain[64];
(void)recv_body_capped(req, drain, sizeof(drain));
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "active", s_target_active);
return send_json(req, o, 200);
}
static esp_err_t api_target_start(httpd_req_t *req)
{
char *body = req->content_len ? recv_body_alloc(req, 4096, NULL) : NULL;
cJSON *j = body ? cJSON_Parse(body) : cJSON_CreateObject();
free(body);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
char raw_params_hex[600] = {0};
char sens_res_hex[5] = "0400";
char nfcid1t_hex[7] = "112233";
char sel_res_hex[3] = "20";
char felica_hex[37] = "01FEA2A3A4A5A6A7C0C1C2C3C4C5C6C7FFFF";
char nfcid3t_hex[21] = "F0140102030405060708";
char gt_hex[256] = {0};
char tk_hex[256] = {0};
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "rawParamsHex"), raw_params_hex, sizeof(raw_params_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "sensResHex"), sens_res_hex, sizeof(sens_res_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "nfcid1tHex"), nfcid1t_hex, sizeof(nfcid1t_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "selResHex"), sel_res_hex, sizeof(sel_res_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "felicaParamsHex"), felica_hex, sizeof(felica_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "nfcid3tHex"), nfcid3t_hex, sizeof(nfcid3t_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "generalTargetHex"), gt_hex, sizeof(gt_hex));
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "historicalTargetHex"), tk_hex, sizeof(tk_hex));
int mode_byte = json_get_int_default(j, "modeByte", 0x04);
int timeout_ms = json_get_int_default(j, "timeoutMs", 1000);
cJSON_Delete(j);
uint8_t params[256];
size_t params_len = 0;
if (raw_params_hex[0]) {
if (!hex_decode_flex(raw_params_hex, params, sizeof(params), &params_len)) {
return send_json(req, cJSON_CreateString("rawParamsHex invalid"), 400);
}
} else {
uint8_t sens_res[2], nfcid1t[3], sel_res[1], felica[18], nfcid3t[10], gt[127], tk[127];
size_t gt_len = 0, tk_len = 0;
if (!hex_to_bin(sens_res_hex, sens_res, sizeof(sens_res)) ||
!hex_to_bin(nfcid1t_hex, nfcid1t, sizeof(nfcid1t)) ||
!hex_to_bin(sel_res_hex, sel_res, sizeof(sel_res)) ||
!hex_to_bin(felica_hex, felica, sizeof(felica)) ||
!hex_to_bin(nfcid3t_hex, nfcid3t, sizeof(nfcid3t)) ||
(gt_hex[0] && !hex_decode_flex(gt_hex, gt, sizeof(gt), &gt_len)) ||
(tk_hex[0] && !hex_decode_flex(tk_hex, tk, sizeof(tk), &tk_len))) {
return send_json(req, cJSON_CreateString("target parameter hex invalid"), 400);
}
params[params_len++] = (uint8_t)mode_byte;
memcpy(params + params_len, sens_res, sizeof(sens_res));
params_len += sizeof(sens_res);
memcpy(params + params_len, nfcid1t, sizeof(nfcid1t));
params_len += sizeof(nfcid1t);
memcpy(params + params_len, sel_res, sizeof(sel_res));
params_len += sizeof(sel_res);
memcpy(params + params_len, felica, sizeof(felica));
params_len += sizeof(felica);
memcpy(params + params_len, nfcid3t, sizeof(nfcid3t));
params_len += sizeof(nfcid3t);
params[params_len++] = (uint8_t)gt_len;
if (gt_len) {
memcpy(params + params_len, gt, gt_len);
params_len += gt_len;
}
params[params_len++] = (uint8_t)tk_len;
if (tk_len) {
memcpy(params + params_len, tk, tk_len);
params_len += tk_len;
}
}
uint8_t resp[128];
size_t rlen = 0;
bool prev_scan = s_scan;
s_scan = false;
nfc_access_lock();
esp_err_t err = pn532_tg_init_as_target(params, params_len, resp, sizeof(resp), &rlen, timeout_ms);
nfc_access_unlock();
if (err != ESP_OK) {
s_scan = prev_scan;
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
return send_json(req, o, 400);
}
s_target_active = true;
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "active", true);
cJSON_AddNumberToObject(o, "modeByte", mode_byte);
(void)add_hex_to_object(o, "responseHex", resp, rlen);
return send_json(req, o, 200);
}
static esp_err_t api_target_recv(httpd_req_t *req)
{
char buf[128];
int timeout_ms = 15000;
int r = recv_body_capped(req, buf, sizeof(buf));
if (r > 0) {
cJSON *j = cJSON_Parse(buf);
if (j) {
timeout_ms = json_get_int_default(j, "timeoutMs", timeout_ms);
cJSON_Delete(j);
}
}
if (!s_target_active) {
return send_json(req, cJSON_CreateString("target mode not active"), 409);
}
uint8_t resp[300];
size_t rlen = 0;
nfc_access_lock();
esp_err_t err = pn532_tg_get_data(resp, sizeof(resp), &rlen, timeout_ms);
nfc_access_unlock();
if (err != ESP_OK) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
return send_json(req, o, 400);
}
cJSON *o = cJSON_CreateObject();
uint8_t status = rlen ? resp[0] : 0xFF;
cJSON_AddBoolToObject(o, "ok", status == 0x00);
cJSON_AddNumberToObject(o, "status", status);
if (rlen > 1) {
(void)add_hex_to_object(o, "dataHex", resp + 1, rlen - 1);
} else {
cJSON_AddStringToObject(o, "dataHex", "");
}
return send_json(req, o, 200);
}
static esp_err_t api_target_send(httpd_req_t *req)
{
char buf[1024];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("body required"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
char data_hex[521] = {0};
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "dataHex"), data_hex, sizeof(data_hex));
int timeout_ms = json_get_int_default(j, "timeoutMs", 1000);
cJSON_Delete(j);
if (!s_target_active) {
return send_json(req, cJSON_CreateString("target mode not active"), 409);
}
uint8_t data[260];
size_t data_len = 0;
if (!data_hex[0] || !hex_decode_flex(data_hex, data, sizeof(data), &data_len)) {
return send_json(req, cJSON_CreateString("dataHex required"), 400);
}
uint8_t resp[64];
size_t rlen = 0;
nfc_access_lock();
esp_err_t err = pn532_tg_set_data(data, data_len, resp, sizeof(resp), &rlen, timeout_ms);
nfc_access_unlock();
if (err != ESP_OK) {
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
return send_json(req, o, 400);
}
cJSON *o = cJSON_CreateObject();
uint8_t status = rlen ? resp[0] : 0xFF;
cJSON_AddBoolToObject(o, "ok", status == 0x00);
cJSON_AddNumberToObject(o, "status", status);
(void)add_hex_to_object(o, "responseHex", resp, rlen);
return send_json(req, o, 200);
}
static esp_err_t api_target_stop(httpd_req_t *req)
{
char drain[64];
(void)recv_body_capped(req, drain, sizeof(drain));
nfc_access_lock();
esp_err_t err = pn532_sam_config_normal();
nfc_access_unlock();
s_target_active = false;
s_scan = true;
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "active", false);
if (err != ESP_OK) {
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
return send_json(req, o, 400);
}
return send_json(req, o, 200);
}
static esp_err_t api_ota_real(httpd_req_t *req)
{
char buf[1024];
int r = recv_body_capped(req, buf, sizeof(buf));
if (r < 0) {
return send_json(req, cJSON_CreateString("url required"), 400);
}
cJSON *j = cJSON_Parse(buf);
if (!j) {
return send_json(req, cJSON_CreateString("bad json"), 400);
}
char url[512] = {0};
(void)copy_json_string(cJSON_GetObjectItemCaseSensitive(j, "url"), url, sizeof(url));
bool reboot = json_get_bool_default(j, "reboot", true);
cJSON_Delete(j);
if (!url[0] || strncmp(url, "https://", 8) != 0) {
return send_json(req, cJSON_CreateString("https url required"), 400);
}
bool prev_scan = s_scan;
s_scan = false;
esp_http_client_config_t http_cfg = {
.url = url,
.crt_bundle_attach = esp_crt_bundle_attach,
.keep_alive_enable = true,
.timeout_ms = 120000,
};
esp_https_ota_config_t ota_cfg = {
.http_config = &http_cfg,
};
esp_err_t err = esp_https_ota(&ota_cfg);
if (err != ESP_OK) {
s_scan = prev_scan;
cJSON *o = cJSON_CreateObject();
cJSON_AddStringToObject(o, "error", esp_err_to_name(err));
return send_json(req, o, 500);
}
cJSON *o = cJSON_CreateObject();
cJSON_AddBoolToObject(o, "ok", true);
cJSON_AddBoolToObject(o, "rebooting", reboot);
esp_err_t send_err = send_json(req, o, 200);
if (send_err == ESP_OK && reboot) {
vTaskDelay(pdMS_TO_TICKS(500));
esp_restart();
}
return send_err;
}
static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char *body = recv_body_alloc(req, 8192, NULL);
if (!body) {
return send_json(req, cJSON_CreateString("no body or too large"), 400);
@@ -711,6 +1521,10 @@ static esp_err_t api_mifare_dictionary_attack(httpd_req_t *req)
static esp_err_t api_nfc_emulate_raw(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char buf[1024];
char hex[521];
int r = recv_body_capped(req, buf, sizeof(buf));
@@ -754,6 +1568,10 @@ static esp_err_t api_nfc_emulate_raw(httpd_req_t *req)
static esp_err_t api_raw_pn532(httpd_req_t *req)
{
esp_err_t gate = reject_if_target_active(req);
if (gate != ESP_OK) {
return gate;
}
char buf[1024];
char hex[521];
int r = recv_body_capped(req, buf, sizeof(buf));
@@ -1026,11 +1844,41 @@ static httpd_handle_t start_server(void)
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/probe", .method = HTTP_POST, .handler = api_nfc_probe};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/scan", .method = HTTP_POST, .handler = api_scan};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/target/status", .method = HTTP_POST, .handler = api_target_status};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/target/start", .method = HTTP_POST, .handler = api_target_start};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/target/recv", .method = HTTP_POST, .handler = api_target_recv};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/target/send", .method = HTTP_POST, .handler = api_target_send};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/nfc/target/stop", .method = HTTP_POST, .handler = api_target_stop};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/pn532/general-status", .method = HTTP_GET, .handler = api_general_status};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
@@ -1061,7 +1909,17 @@ static httpd_handle_t start_server(void)
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_stub};
u = (httpd_uri_t){.uri = "/api/clone/capture", .method = HTTP_POST, .handler = api_clone_capture};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/clone/program", .method = HTTP_POST, .handler = api_clone_program};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
}
u = (httpd_uri_t){.uri = "/api/ota", .method = HTTP_POST, .handler = api_ota_real};
if (!register_uri_checked(s, &u)) {
httpd_stop(s);
return NULL;
@@ -1187,6 +2045,8 @@ esp_err_t app_net_init(void)
}
if (xTaskCreate(scan_loop_task, "nfc_scan", 20480, NULL, 5, &s_scan_task) != pdPASS) {
httpd_stop(s_server);
s_server = NULL;
return ESP_ERR_NO_MEM;
}
return ESP_OK;

View File

@@ -32,6 +32,9 @@ esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out);
bool nfc_tag_is_mifare_classic(const nfc_tag_info_t *tag);
bool nfc_tag_is_mifare_classic_4k(const nfc_tag_info_t *tag);
bool nfc_tag_is_type2(const nfc_tag_info_t *tag);
int nfc_mifare_sector_count(const nfc_tag_info_t *tag);
bool nfc_mifare_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
uint8_t *trailer_block);
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
const nfc_mifare_key_t *key);
@@ -42,6 +45,7 @@ esp_err_t nfc_ultralight_read_page(uint8_t page, uint8_t data[4]);
esp_err_t nfc_ultralight_write_page(uint8_t page, const uint8_t data[4]);
esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, size_t *got);
esp_err_t nfc_type2_get_version(uint8_t version[8]);
/** Build JSON snapshot of last seen tag + optional blocks (caller frees cJSON). */
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);

View File

@@ -170,6 +170,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
attempts++;
if (try_key_on_trailer(tag, trailer, trial, false)) {
if (!add_sector_hit(hits, sec, trial, "A")) {
cJSON_Delete(hits);
cJSON_Delete(root);
return NULL;
}
@@ -178,6 +179,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
}
if (try_key_on_trailer(tag, trailer, trial, true)) {
if (!add_sector_hit(hits, sec, trial, "B")) {
cJSON_Delete(hits);
cJSON_Delete(root);
return NULL;
}
@@ -202,6 +204,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
attempts++;
if (try_key_on_trailer(tag, trailer, trial, false)) {
if (!add_sector_hit(hits, sec, trial, "A")) {
cJSON_Delete(hits);
cJSON_Delete(root);
return NULL;
}
@@ -210,6 +213,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
}
if (try_key_on_trailer(tag, trailer, trial, true)) {
if (!add_sector_hit(hits, sec, trial, "B")) {
cJSON_Delete(hits);
cJSON_Delete(root);
return NULL;
}
@@ -226,6 +230,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
if (!got) {
cJSON *h = cJSON_CreateObject();
if (!h) {
cJSON_Delete(hits);
cJSON_Delete(root);
return NULL;
}

View File

@@ -18,7 +18,12 @@ static void hint_type(nfc_tag_info_t *t)
t->type_hint = 1;
break;
case 0x00:
t->type_hint = 2;
/* Typical Type 2 inventory tuple is ATQA 0x0044 and 7-byte UID. */
if ((t->atqa == 0x4400 || t->atqa == 0x0044) && t->uid_len == 7) {
t->type_hint = 2;
} else {
t->type_hint = 0;
}
break;
default:
t->type_hint = 0;
@@ -41,6 +46,45 @@ bool nfc_tag_is_type2(const nfc_tag_info_t *tag)
return tag && tag->type_hint == 2;
}
int nfc_mifare_sector_count(const nfc_tag_info_t *tag)
{
if (!nfc_tag_is_mifare_classic(tag)) {
return 0;
}
return nfc_tag_is_mifare_classic_4k(tag) ? 40 : 16;
}
bool nfc_mifare_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
uint8_t *trailer_block)
{
if (!nfc_tag_is_mifare_classic(tag) || !first_block || !num_blocks || !trailer_block) {
return false;
}
if (!nfc_tag_is_mifare_classic_4k(tag)) {
if (sector < 0 || sector > 15) {
return false;
}
*first_block = sector * 4;
*num_blocks = 4;
*trailer_block = (uint8_t)(sector * 4 + 3);
return true;
}
if (sector < 0 || sector > 39) {
return false;
}
if (sector <= 31) {
*first_block = sector * 4;
*num_blocks = 4;
*trailer_block = (uint8_t)(sector * 4 + 3);
} else {
int r = sector - 32;
*first_block = 128 + r * 16;
*num_blocks = 16;
*trailer_block = (uint8_t)(128 + r * 16 + 15);
}
return true;
}
esp_err_t nfc_engine_init(void)
{
esp_err_t e = pn532_core_init();
@@ -248,6 +292,25 @@ esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, siz
return ESP_OK;
}
esp_err_t nfc_type2_get_version(uint8_t version[8])
{
if (!version) {
return ESP_ERR_INVALID_ARG;
}
uint8_t d[] = {0x60};
uint8_t resp[32];
size_t rlen = 0;
esp_err_t e = in_data_tg(d, sizeof(d), resp, sizeof(resp), &rlen);
if (e != ESP_OK) {
return e;
}
if (rlen < 2 + 8 || resp[0] != (uint8_t)(PN532_CMD_INDATAEXCHANGE + 1) || resp[1] != 0x00) {
return ESP_ERR_INVALID_RESPONSE;
}
memcpy(version, resp + 2, 8);
return ESP_OK;
}
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
{
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {

View File

@@ -57,6 +57,20 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len,
uint8_t *response, size_t response_max,
size_t *response_len);
/** Enter PN532 target mode using TgInitAsTarget parameters (mode + target descriptors + optional GT). */
esp_err_t pn532_tg_init_as_target(const uint8_t *params, size_t params_len,
uint8_t *response, size_t response_max,
size_t *response_len, int timeout_ms);
/** Receive bytes from the initiator while PN532 is in target mode. */
esp_err_t pn532_tg_get_data(uint8_t *response, size_t response_max,
size_t *response_len, int timeout_ms);
/** Send bytes back to the initiator while PN532 is in target mode. */
esp_err_t pn532_tg_set_data(const uint8_t *data, size_t data_len,
uint8_t *response, size_t response_max,
size_t *response_len, int timeout_ms);
/** RF field on/off via RFConfiguration (0x32) item 0x01, RF field */
esp_err_t pn532_rf_field(bool on);

View File

@@ -190,3 +190,81 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len, uint8_
*response_len = payload_len;
return ESP_OK;
}
esp_err_t pn532_tg_init_as_target(const uint8_t *params, size_t params_len, uint8_t *response,
size_t response_max, size_t *response_len, int timeout_ms)
{
if (!params || !response || !response_len || params_len == 0 || params_len > PN532_EEPROM_MAX_CMD_PAYLOAD - 1) {
return ESP_ERR_INVALID_ARG;
}
uint8_t buf[PN532_EEPROM_MAX_CMD_PAYLOAD];
buf[0] = PN532_CMD_TGINITASTARGET;
memcpy(buf + 1, params, params_len);
uint8_t raw[PN532_EEPROM_MAX_CMD_PAYLOAD];
size_t raw_len = 0;
esp_err_t e = pn532_send_cmd(buf, 1 + params_len, raw, sizeof(raw), &raw_len, timeout_ms);
if (e != ESP_OK) {
return e;
}
if (raw_len < 2 || raw[0] != (uint8_t)(PN532_CMD_TGINITASTARGET + 1)) {
return ESP_ERR_INVALID_RESPONSE;
}
size_t payload_len = raw_len - 1;
if (payload_len > response_max) {
return ESP_ERR_INVALID_SIZE;
}
memcpy(response, raw + 1, payload_len);
*response_len = payload_len;
return ESP_OK;
}
esp_err_t pn532_tg_get_data(uint8_t *response, size_t response_max, size_t *response_len, int timeout_ms)
{
if (!response || !response_len) {
return ESP_ERR_INVALID_ARG;
}
uint8_t cmd = PN532_CMD_TGGETDATA;
uint8_t raw[PN532_EEPROM_MAX_CMD_PAYLOAD];
size_t raw_len = 0;
esp_err_t e = pn532_send_cmd(&cmd, 1, raw, sizeof(raw), &raw_len, timeout_ms);
if (e != ESP_OK) {
return e;
}
if (raw_len < 2 || raw[0] != (uint8_t)(PN532_CMD_TGGETDATA + 1)) {
return ESP_ERR_INVALID_RESPONSE;
}
size_t payload_len = raw_len - 1;
if (payload_len > response_max) {
return ESP_ERR_INVALID_SIZE;
}
memcpy(response, raw + 1, payload_len);
*response_len = payload_len;
return ESP_OK;
}
esp_err_t pn532_tg_set_data(const uint8_t *data, size_t data_len, uint8_t *response, size_t response_max,
size_t *response_len, int timeout_ms)
{
if (!data || !response || !response_len || data_len == 0 || data_len > PN532_EEPROM_MAX_CMD_PAYLOAD - 1) {
return ESP_ERR_INVALID_ARG;
}
uint8_t buf[PN532_EEPROM_MAX_CMD_PAYLOAD];
buf[0] = PN532_CMD_TGSETDATA;
memcpy(buf + 1, data, data_len);
uint8_t raw[PN532_EEPROM_MAX_CMD_PAYLOAD];
size_t raw_len = 0;
esp_err_t e = pn532_send_cmd(buf, 1 + data_len, raw, sizeof(raw), &raw_len, timeout_ms);
if (e != ESP_OK) {
return e;
}
if (raw_len < 2 || raw[0] != (uint8_t)(PN532_CMD_TGSETDATA + 1)) {
return ESP_ERR_INVALID_RESPONSE;
}
size_t payload_len = raw_len - 1;
if (payload_len > response_max) {
return ESP_ERR_INVALID_SIZE;
}
memcpy(response, raw + 1, payload_len);
*response_len = payload_len;
return ESP_OK;
}

View File

@@ -352,7 +352,7 @@ esp_err_t pn532_transport_exchange(const uint8_t *tx_body, size_t tx_body_len, u
#if defined(CONFIG_PN532_TRANSPORT_SPI)
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
i2c_wakeup();
ESP_RETURN_ON_ERROR(i2c_wakeup(), TAG, "i2c wake");
ESP_RETURN_ON_ERROR(i2c_write_raw(frame, pos), TAG, "i2c wr");
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");

View File

@@ -1,4 +1,5 @@
#include "esp_log.h"
#include "esp_ota_ops.h"
#include "board_rgb_off.h"
#include "nfc_engine/nfc_engine.h"
#include "nfc_engine/session_capture.h"
@@ -8,6 +9,7 @@ static const char *TAG = "main";
void app_main(void)
{
(void)esp_ota_mark_app_valid_cancel_rollback();
board_rgb_led_quiet();
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
ESP_ERROR_CHECK(nfc_engine_init());

View File

@@ -31,3 +31,6 @@ CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y
# mDNS
CONFIG_MDNS_MAX_SERVICES=10
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y