Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2da1515f8d | ||
|
|
63db10d400 |
27
README.md
27
README.md
@@ -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.
|
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.
|
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. |
|
| **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). |
|
| **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. |
|
| **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 isn’t left on a random color — **not** on all boards; power LEDs are hardware. |
|
| **Onboard RGB (DevKit-style)** | Optional **boot-time** shutdown of a **WS2812/SK6812** on **GPIO 48** (Kconfig) so the addressable LED isn’t left on a random color — **not** on all boards; power LEDs are hardware. |
|
||||||
| **Status** | **`/api/status`**: uptime, free heap, Wi‑Fi mode, **PN532 firmware version** (when reachable), scan flag, session stats. |
|
| **Status** | **`/api/status`**: uptime, free heap, Wi‑Fi 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)
|
### Web UI (each page)
|
||||||
|
|
||||||
@@ -89,16 +92,24 @@ This stack is **not** a Proxmark replacement (no LF, no raw carrier manipulation
|
|||||||
| Method | Path | Purpose |
|
| Method | Path | Purpose |
|
||||||
|--------|------|---------|
|
|--------|------|---------|
|
||||||
| `POST` | `/api/nfc/poll` | One-shot poll; body may be `{}` — returns `present`, optional `tag` |
|
| `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 |
|
| `POST` | `/api/nfc/scan` | Body `{"enable": true}` or `false` — continuous background scan |
|
||||||
| `GET` | `/api/pn532/general-status` | Raw PN532 **general status** byte array |
|
| `GET` | `/api/pn532/general-status` | Raw PN532 **general status** byte array |
|
||||||
| `POST` | `/api/mifare/read-block` | `block`, `key` (12 hex), `keyB` |
|
| `POST` | `/api/mifare/read-block` | `block`, `key` (12 hex), `keyB` |
|
||||||
| `POST` | `/api/mifare/write-block` | `block`, `key`, `keyB`, `data` (32 hex) |
|
| `POST` | `/api/mifare/write-block` | `block`, `key`, `keyB`, `data` (32 hex) |
|
||||||
| `POST` | `/api/ul/read-page` | `page` |
|
| `POST` | `/api/ul/read-page` | `page` |
|
||||||
| `POST` | `/api/ul/write-page` | `page`, `data` (8 hex) |
|
| `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/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/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`**.
|
**WebSocket:** `GET /ws` — JSON text frames as above; send **`ping`**, receive **`pong`**.
|
||||||
|
|
||||||
@@ -112,7 +123,7 @@ The SPA keeps the **last 5 000** WebSocket events in **sessionStorage**. **Dow
|
|||||||
|
|
||||||
### Card emulation (PN532 target mode)
|
### 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
|
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).
|
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).
|
See [docs/FLASHING.md](docs/FLASHING.md) and [docs/PINOUT.md](docs/PINOUT.md). Limitations and scope notes: [docs/LIMITATIONS.md](docs/LIMITATIONS.md).
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
The PN532 is a **hosted NFC controller**, not a low-level RF lab instrument. This project exposes PN532 features honestly:
|
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.
|
- **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.
|
- **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.
|
- **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.
|
||||||
|
|||||||
@@ -20,3 +20,13 @@ The firmware Kconfig ships **example** GPIOs:
|
|||||||
## Transport selection
|
## 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**).
|
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.
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ idf_component_register(
|
|||||||
SRCS "app_net.c"
|
SRCS "app_net.c"
|
||||||
INCLUDE_DIRS "include"
|
INCLUDE_DIRS "include"
|
||||||
REQUIRES esp_http_server http_parser esp_wifi esp_netif nvs_flash mdns esp_timer
|
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
|
||||||
)
|
)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -26,12 +26,26 @@ typedef struct {
|
|||||||
bool key_b;
|
bool key_b;
|
||||||
} nfc_mifare_key_t;
|
} nfc_mifare_key_t;
|
||||||
|
|
||||||
|
/** Legacy: full init including transport — aborts on failure via caller's ESP_ERROR_CHECK. */
|
||||||
esp_err_t nfc_engine_init(void);
|
esp_err_t nfc_engine_init(void);
|
||||||
|
|
||||||
|
/** Non-fatal first-boot init. Returns true if PN532 is present and configured. */
|
||||||
|
bool nfc_engine_try_init(void);
|
||||||
|
|
||||||
|
/** Soft re-attach after transport is already open — skips bus re-init, tries chip commands.
|
||||||
|
* Call from a background retry loop; always safe to call even if already ready. */
|
||||||
|
bool nfc_engine_try_reattach(void);
|
||||||
|
|
||||||
|
/** Returns true when the PN532 was successfully initialised (or re-attached). */
|
||||||
|
bool nfc_engine_is_ready(void);
|
||||||
|
|
||||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out);
|
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(const nfc_tag_info_t *tag);
|
||||||
bool nfc_tag_is_mifare_classic_4k(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);
|
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,
|
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||||
const nfc_mifare_key_t *key);
|
const nfc_mifare_key_t *key);
|
||||||
@@ -42,6 +56,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_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_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). */
|
/** Build JSON snapshot of last seen tag + optional blocks (caller frees cJSON). */
|
||||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
|||||||
attempts++;
|
attempts++;
|
||||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "A")) {
|
if (!add_sector_hit(hits, sec, trial, "A")) {
|
||||||
|
cJSON_Delete(hits);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
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 (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "B")) {
|
if (!add_sector_hit(hits, sec, trial, "B")) {
|
||||||
|
cJSON_Delete(hits);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -202,6 +204,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
|||||||
attempts++;
|
attempts++;
|
||||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "A")) {
|
if (!add_sector_hit(hits, sec, trial, "A")) {
|
||||||
|
cJSON_Delete(hits);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
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 (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||||
if (!add_sector_hit(hits, sec, trial, "B")) {
|
if (!add_sector_hit(hits, sec, trial, "B")) {
|
||||||
|
cJSON_Delete(hits);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@@ -226,6 +230,7 @@ cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, u
|
|||||||
if (!got) {
|
if (!got) {
|
||||||
cJSON *h = cJSON_CreateObject();
|
cJSON *h = cJSON_CreateObject();
|
||||||
if (!h) {
|
if (!h) {
|
||||||
|
cJSON_Delete(hits);
|
||||||
cJSON_Delete(root);
|
cJSON_Delete(root);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@
|
|||||||
static const char *TAG = "nfc_engine";
|
static const char *TAG = "nfc_engine";
|
||||||
|
|
||||||
static uint8_t s_tg = 1;
|
static uint8_t s_tg = 1;
|
||||||
|
static volatile bool s_pn532_ready = false;
|
||||||
|
|
||||||
|
bool nfc_engine_is_ready(void) { return s_pn532_ready; }
|
||||||
|
|
||||||
static void hint_type(nfc_tag_info_t *t)
|
static void hint_type(nfc_tag_info_t *t)
|
||||||
{
|
{
|
||||||
@@ -18,7 +21,12 @@ static void hint_type(nfc_tag_info_t *t)
|
|||||||
t->type_hint = 1;
|
t->type_hint = 1;
|
||||||
break;
|
break;
|
||||||
case 0x00:
|
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;
|
break;
|
||||||
default:
|
default:
|
||||||
t->type_hint = 0;
|
t->type_hint = 0;
|
||||||
@@ -41,6 +49,45 @@ bool nfc_tag_is_type2(const nfc_tag_info_t *tag)
|
|||||||
return tag && tag->type_hint == 2;
|
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 nfc_engine_init(void)
|
||||||
{
|
{
|
||||||
esp_err_t e = pn532_core_init();
|
esp_err_t e = pn532_core_init();
|
||||||
@@ -58,9 +105,51 @@ esp_err_t nfc_engine_init(void)
|
|||||||
if (pn532_rf_max_retries() != ESP_OK) {
|
if (pn532_rf_max_retries() != ESP_OK) {
|
||||||
ESP_LOGW(TAG, "RF max retries config failed");
|
ESP_LOGW(TAG, "RF max retries config failed");
|
||||||
}
|
}
|
||||||
|
s_pn532_ready = true;
|
||||||
return ESP_OK;
|
return ESP_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool nfc_engine_try_init(void)
|
||||||
|
{
|
||||||
|
esp_err_t e = pn532_core_init();
|
||||||
|
if (e != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "PN532 not found (%s) — AP running, will retry every 5 s", esp_err_to_name(e));
|
||||||
|
s_pn532_ready = false;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
e = pn532_sam_config_normal();
|
||||||
|
if (e != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "SAM config: %s", esp_err_to_name(e));
|
||||||
|
}
|
||||||
|
uint8_t ic = 0, hi = 0, lo = 0;
|
||||||
|
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "PN532 fw ic=0x%02x %u.%u", ic, hi, lo);
|
||||||
|
}
|
||||||
|
if (pn532_rf_max_retries() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "RF max retries config failed");
|
||||||
|
}
|
||||||
|
s_pn532_ready = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool nfc_engine_try_reattach(void)
|
||||||
|
{
|
||||||
|
/* Transport already open — just ping the chip and re-apply configuration. */
|
||||||
|
esp_err_t e = pn532_sam_config_normal();
|
||||||
|
if (e != ESP_OK) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
uint8_t ic = 0, hi = 0, lo = 0;
|
||||||
|
if (pn532_get_firmware_version(&ic, &hi, &lo) == ESP_OK) {
|
||||||
|
ESP_LOGI(TAG, "PN532 reattached ic=0x%02x %u.%u", ic, hi, lo);
|
||||||
|
}
|
||||||
|
if (pn532_rf_max_retries() != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "RF max retries config failed");
|
||||||
|
}
|
||||||
|
s_pn532_ready = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
||||||
{
|
{
|
||||||
if (!out) {
|
if (!out) {
|
||||||
@@ -248,6 +337,25 @@ esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, siz
|
|||||||
return ESP_OK;
|
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)
|
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
|
||||||
{
|
{
|
||||||
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {
|
if (!tag || tag->uid_len > NFC_MAX_UID_LEN) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "nfc_engine/session_capture.h"
|
#include "nfc_engine/session_capture.h"
|
||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/semphr.h"
|
#include "freertos/semphr.h"
|
||||||
|
#include "esp_log.h"
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
|
||||||
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
||||||
@@ -15,6 +16,10 @@ static SemaphoreHandle_t s_mu;
|
|||||||
void session_capture_init(void)
|
void session_capture_init(void)
|
||||||
{
|
{
|
||||||
s_mu = xSemaphoreCreateMutex();
|
s_mu = xSemaphoreCreateMutex();
|
||||||
|
if (!s_mu) {
|
||||||
|
ESP_LOGE("session_capture", "mutex create failed — aborting");
|
||||||
|
abort();
|
||||||
|
}
|
||||||
session_capture_clear();
|
session_capture_clear();
|
||||||
s_deep = false;
|
s_deep = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
uint8_t *response, size_t response_max,
|
||||||
size_t *response_len);
|
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 */
|
/** RF field on/off via RFConfiguration (0x32) item 0x01, RF field */
|
||||||
esp_err_t pn532_rf_field(bool on);
|
esp_err_t pn532_rf_field(bool on);
|
||||||
|
|
||||||
|
|||||||
@@ -190,3 +190,81 @@ esp_err_t pn532_in_communicate_thru(const uint8_t *data, size_t data_len, uint8_
|
|||||||
*response_len = payload_len;
|
*response_len = payload_len;
|
||||||
return ESP_OK;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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)
|
#if defined(CONFIG_PN532_TRANSPORT_SPI)
|
||||||
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
|
ESP_RETURN_ON_ERROR(spi_write_frame(frame, pos), TAG, "spi wr");
|
||||||
#elif defined(CONFIG_PN532_TRANSPORT_I2C)
|
#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");
|
ESP_RETURN_ON_ERROR(i2c_write_raw(frame, pos), TAG, "i2c wr");
|
||||||
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
#elif defined(CONFIG_PN532_TRANSPORT_HSU)
|
||||||
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");
|
ESP_RETURN_ON_ERROR(hsu_write_raw(frame, pos), TAG, "hsu wr");
|
||||||
|
|||||||
1
firmware/data/assets/index-Bb0xTRCn.css
Normal file
1
firmware/data/assets/index-Bb0xTRCn.css
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -11,8 +11,8 @@
|
|||||||
href="https://fonts.googleapis.com/css2?family=Audiowide&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=Orbitron:wght@500;600;700;800&display=swap"
|
href="https://fonts.googleapis.com/css2?family=Audiowide&family=JetBrains+Mono:ital,wght@0,400;0,600;0,700;1,400&family=Orbitron:wght@500;600;700;800&display=swap"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<script type="module" crossorigin src="/assets/index-9oJp152C.js"></script>
|
<script type="module" crossorigin src="/assets/index-BmIGATlK.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-hoMg1Qkq.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Bb0xTRCn.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
<body class="bg-bubble-950 text-slate-200 antialiased selection:bg-bubble-accent/40 selection:text-bubble-950">
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "esp_log.h"
|
#include "esp_log.h"
|
||||||
|
#include "esp_ota_ops.h"
|
||||||
#include "board_rgb_off.h"
|
#include "board_rgb_off.h"
|
||||||
#include "nfc_engine/nfc_engine.h"
|
#include "nfc_engine/nfc_engine.h"
|
||||||
#include "nfc_engine/session_capture.h"
|
#include "nfc_engine/session_capture.h"
|
||||||
@@ -8,10 +9,15 @@ static const char *TAG = "main";
|
|||||||
|
|
||||||
void app_main(void)
|
void app_main(void)
|
||||||
{
|
{
|
||||||
|
(void)esp_ota_mark_app_valid_cancel_rollback();
|
||||||
board_rgb_led_quiet();
|
board_rgb_led_quiet();
|
||||||
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
ESP_LOGI(TAG, "PN532 NFC Toolkit starting");
|
||||||
ESP_ERROR_CHECK(nfc_engine_init());
|
nfc_engine_try_init(); /* non-fatal: logs warning if PN532 absent, AP starts regardless */
|
||||||
session_capture_init();
|
session_capture_init();
|
||||||
ESP_ERROR_CHECK(app_net_init());
|
ESP_ERROR_CHECK(app_net_init());
|
||||||
ESP_LOGI(TAG, "Open AP SSID PN532-Toolkit — http://192.168.4.1");
|
if (nfc_engine_is_ready()) {
|
||||||
|
ESP_LOGI(TAG, "PN532 ready · AP SSID PN532-Toolkit → http://192.168.4.1");
|
||||||
|
} else {
|
||||||
|
ESP_LOGW(TAG, "PN532 not found at boot — AP running, retrying · http://192.168.4.1");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,3 +31,6 @@ CONFIG_LOG_MAXIMUM_LEVEL_VERBOSE=y
|
|||||||
|
|
||||||
# mDNS
|
# mDNS
|
||||||
CONFIG_MDNS_MAX_SERVICES=10
|
CONFIG_MDNS_MAX_SERVICES=10
|
||||||
|
|
||||||
|
CONFIG_MBEDTLS_CERTIFICATE_BUNDLE=y
|
||||||
|
CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE=y
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { NavLink, Route, Routes } from "react-router-dom";
|
|||||||
import BrowserLogBar from "./BrowserLogBar";
|
import BrowserLogBar from "./BrowserLogBar";
|
||||||
import FlashBackdrop from "./FlashBackdrop";
|
import FlashBackdrop from "./FlashBackdrop";
|
||||||
import ScanCashFlourish from "./ScanCashFlourish";
|
import ScanCashFlourish from "./ScanCashFlourish";
|
||||||
import { NfcWsProvider } from "./NfcWsContext";
|
import { NfcWsProvider, useNfcWs } from "./NfcWsContext";
|
||||||
import { ToastHost } from "./toast";
|
import { ToastHost } from "./toast";
|
||||||
import Dashboard from "./pages/Dashboard";
|
import Dashboard from "./pages/Dashboard";
|
||||||
import ReadAnalyze from "./pages/ReadAnalyze";
|
import ReadAnalyze from "./pages/ReadAnalyze";
|
||||||
@@ -17,6 +17,29 @@ import Brute from "./pages/Brute";
|
|||||||
import Emulate from "./pages/Emulate";
|
import Emulate from "./pages/Emulate";
|
||||||
import KeyLab from "./pages/KeyLab";
|
import KeyLab from "./pages/KeyLab";
|
||||||
|
|
||||||
|
function HeaderBadge() {
|
||||||
|
const { wsOk, pn532Connected } = useNfcWs();
|
||||||
|
return (
|
||||||
|
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]">
|
||||||
|
<span className={wsOk ? "text-bubble-accent/90" : "animate-pulse text-bubble-rose/60"}>●</span>{" "}
|
||||||
|
RF_STACK{" "}
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
!wsOk
|
||||||
|
? "animate-pulse text-bubble-rose/80"
|
||||||
|
: pn532Connected
|
||||||
|
? "font-bold text-bubble-mint"
|
||||||
|
: "animate-pulse text-amber-400"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{!wsOk ? "WAIT" : pn532Connected ? "LIVE" : "NO RF"}
|
||||||
|
</span>
|
||||||
|
<span className="mx-1.5 text-bubble-mint/25">│</span>
|
||||||
|
<span className="text-bubble-mint/50">ws://stream</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const nav = [
|
const nav = [
|
||||||
["/", "Dash"],
|
["/", "Dash"],
|
||||||
["/capture", "Read-all"],
|
["/capture", "Read-all"],
|
||||||
@@ -70,12 +93,7 @@ export default function App() {
|
|||||||
MAXIMAL
|
MAXIMAL
|
||||||
</span>
|
</span>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<span className="hidden font-mono text-[9px] text-bubble-mint/40 md:inline lg:text-[10px]">
|
<HeaderBadge />
|
||||||
<span className="text-bubble-accent/90">●</span> RF_STACK{" "}
|
|
||||||
<span className="text-bubble-rose/80">LIVE</span>
|
|
||||||
<span className="mx-1.5 text-bubble-mint/25">│</span>
|
|
||||||
<span className="text-bubble-mint/50">ws://stream</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<nav className="flex max-w-full flex-wrap justify-end gap-1 text-[10px] font-mono sm:gap-1.5 sm:text-[11px]">
|
<nav className="flex max-w-full flex-wrap justify-end gap-1 text-[10px] font-mono sm:gap-1.5 sm:text-[11px]">
|
||||||
{nav.map(([to, label]) => (
|
{nav.map(([to, label]) => (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { createContext, useCallback, useContext, useMemo, useRef, useState } from "react";
|
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { Tag } from "./api";
|
import { apiGet, type Status, type Tag } from "./api";
|
||||||
import { useToolkitWs } from "./useWebSocket";
|
import { useToolkitWs } from "./useWebSocket";
|
||||||
|
|
||||||
export type BrowserLogEntry = {
|
export type BrowserLogEntry = {
|
||||||
@@ -12,6 +12,7 @@ export type CashVariant = "tag" | "vault";
|
|||||||
|
|
||||||
type NfcCtx = {
|
type NfcCtx = {
|
||||||
wsOk: boolean;
|
wsOk: boolean;
|
||||||
|
pn532Connected: boolean;
|
||||||
lastTag: Tag | null;
|
lastTag: Tag | null;
|
||||||
tagPresent: boolean;
|
tagPresent: boolean;
|
||||||
log: BrowserLogEntry[];
|
log: BrowserLogEntry[];
|
||||||
@@ -89,6 +90,7 @@ function isCaptureRecorded(payload: unknown): boolean {
|
|||||||
export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [lastTag, setLastTag] = useState<Tag | null>(null);
|
const [lastTag, setLastTag] = useState<Tag | null>(null);
|
||||||
const [tagPresent, setTagPresent] = useState(false);
|
const [tagPresent, setTagPresent] = useState(false);
|
||||||
|
const [pn532Connected, setPn532Connected] = useState(false);
|
||||||
const [log, setLog] = useState<BrowserLogEntry[]>(() => loadPersisted());
|
const [log, setLog] = useState<BrowserLogEntry[]>(() => loadPersisted());
|
||||||
const [cashWave, setCashWave] = useState(0);
|
const [cashWave, setCashWave] = useState(0);
|
||||||
const [cashVariant, setCashVariant] = useState<CashVariant>("tag");
|
const [cashVariant, setCashVariant] = useState<CashVariant>("tag");
|
||||||
@@ -121,6 +123,11 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
}
|
}
|
||||||
} else if (ch === "capture" && isCaptureRecorded(o.payload)) {
|
} else if (ch === "capture" && isCaptureRecorded(o.payload)) {
|
||||||
bump("vault");
|
bump("vault");
|
||||||
|
} else if (ch === "pn532") {
|
||||||
|
const p = o.payload as { connected?: boolean } | undefined;
|
||||||
|
if (typeof p?.connected === "boolean") {
|
||||||
|
setPn532Connected(p.connected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
@@ -131,6 +138,19 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
const wsOk = useToolkitWs(onMsg);
|
const wsOk = useToolkitWs(onMsg);
|
||||||
|
|
||||||
|
/* Fetch initial PN532 state as soon as WS connects (avoids waiting for first broadcast). */
|
||||||
|
useEffect(() => {
|
||||||
|
if (wsOk) {
|
||||||
|
apiGet<Status>("/api/status")
|
||||||
|
.then((s) => {
|
||||||
|
if (typeof s.pn532Connected === "boolean") {
|
||||||
|
setPn532Connected(s.pn532Connected);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(() => { /* silently ignore — badge will update on next WS event */ });
|
||||||
|
}
|
||||||
|
}, [wsOk]);
|
||||||
|
|
||||||
const applyScanPoll = useCallback(
|
const applyScanPoll = useCallback(
|
||||||
(present: boolean, tag?: Tag) => {
|
(present: boolean, tag?: Tag) => {
|
||||||
if (!present) {
|
if (!present) {
|
||||||
@@ -165,6 +185,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
const v = useMemo(
|
const v = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
wsOk,
|
wsOk,
|
||||||
|
pn532Connected,
|
||||||
lastTag,
|
lastTag,
|
||||||
tagPresent,
|
tagPresent,
|
||||||
log,
|
log,
|
||||||
@@ -174,7 +195,7 @@ export function NfcWsProvider({ children }: { children: React.ReactNode }) {
|
|||||||
exportBrowserLog,
|
exportBrowserLog,
|
||||||
applyScanPoll,
|
applyScanPoll,
|
||||||
}),
|
}),
|
||||||
[wsOk, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
[wsOk, pn532Connected, lastTag, tagPresent, log, cashWave, cashVariant, clearBrowserLog, exportBrowserLog, applyScanPoll],
|
||||||
);
|
);
|
||||||
|
|
||||||
return <Ctx.Provider value={v}>{children}</Ctx.Provider>;
|
return <Ctx.Provider value={v}>{children}</Ctx.Provider>;
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export type Status = {
|
|||||||
uptimeMs: number;
|
uptimeMs: number;
|
||||||
freeHeap: number;
|
freeHeap: number;
|
||||||
wifiMode: number;
|
wifiMode: number;
|
||||||
|
pn532Connected: boolean;
|
||||||
pn532?: { ic: number; fwHi: number; fwLo: number };
|
pn532?: { ic: number; fwHi: number; fwLo: number };
|
||||||
scanning: boolean;
|
scanning: boolean;
|
||||||
session?: SessionInfo;
|
session?: SessionInfo;
|
||||||
|
|||||||
@@ -19,7 +19,14 @@ export default function Brute() {
|
|||||||
const keysHex = extraKeys
|
const keysHex = extraKeys
|
||||||
.split(/\r?\n/)
|
.split(/\r?\n/)
|
||||||
.map((l) => l.replace(/\s/g, "").toUpperCase())
|
.map((l) => l.replace(/\s/g, "").toUpperCase())
|
||||||
.filter((l) => l.length === 12);
|
.filter((l) => l.length === 12 && /^[0-9A-F]+$/.test(l));
|
||||||
|
const invalidCount = extraKeys
|
||||||
|
.split(/\r?\n/)
|
||||||
|
.map((l) => l.replace(/\s/g, ""))
|
||||||
|
.filter((l) => l.length > 0 && (l.length !== 12 || !/^[0-9A-Fa-f]+$/.test(l))).length;
|
||||||
|
if (invalidCount > 0) {
|
||||||
|
toast(`${invalidCount} line(s) not valid 12-hex keys — skipped, firmware built-ins still run`);
|
||||||
|
}
|
||||||
const body = {
|
const body = {
|
||||||
readerType: reader,
|
readerType: reader,
|
||||||
variations,
|
variations,
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export default function Capture() {
|
|||||||
const refresh = useCallback(() => {
|
const refresh = useCallback(() => {
|
||||||
apiGet<Status>("/api/status")
|
apiGet<Status>("/api/status")
|
||||||
.then(setSt)
|
.then(setSt)
|
||||||
.catch(() => {});
|
.catch(() => toast("Status unreachable", "err"));
|
||||||
}, []);
|
}, [toast]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -152,9 +152,22 @@ export default function Dashboard() {
|
|||||||
</li>
|
</li>
|
||||||
<li className="flex justify-between">
|
<li className="flex justify-between">
|
||||||
<span className="text-slate-500">PN532</span>
|
<span className="text-slate-500">PN532</span>
|
||||||
<span className="text-bubble-accent">
|
{st.pn532Connected ? (
|
||||||
{st.pn532 ? `IC${st.pn532.ic} v${st.pn532.fwHi}.${st.pn532.fwLo}` : "n/a"}
|
<span className="flex items-center gap-2">
|
||||||
</span>
|
{st.pn532 && (
|
||||||
|
<span className="text-[10px] text-slate-500">
|
||||||
|
IC{st.pn532.ic} v{st.pn532.fwHi}.{st.pn532.fwLo}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className="rounded px-1.5 py-0.5 bg-bubble-mint/20 text-bubble-mint font-bold text-[10px] tracking-widest">
|
||||||
|
READY
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="animate-pulse rounded px-1.5 py-0.5 bg-amber-500/20 text-amber-400 font-bold text-[10px] tracking-widest">
|
||||||
|
SEARCHING
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -11,13 +11,26 @@ export default function Emulate() {
|
|||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const send = async () => {
|
const send = async () => {
|
||||||
|
const cleanHex = hex.replace(/\s/g, "");
|
||||||
|
if (!cleanHex) {
|
||||||
|
toast("Hex payload is empty", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cleanHex.length % 2 !== 0) {
|
||||||
|
toast("Hex must have an even number of characters", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!/^[0-9A-Fa-f]+$/.test(cleanHex)) {
|
||||||
|
toast("Hex must contain only 0-9 A-F characters", "err");
|
||||||
|
return;
|
||||||
|
}
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
setOut("");
|
setOut("");
|
||||||
try {
|
try {
|
||||||
const r = await fetch(apiUrl("/api/nfc/emulate-raw"), {
|
const r = await fetch(apiUrl("/api/nfc/emulate-raw"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ hex: hex.replace(/\s/g, "") }),
|
body: JSON.stringify({ hex: cleanHex }),
|
||||||
});
|
});
|
||||||
const t = await r.text();
|
const t = await r.text();
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ export default function RawConsole() {
|
|||||||
push(`RX ${j.response || j.error || "?"}`);
|
push(`RX ${j.response || j.error || "?"}`);
|
||||||
if (j.response) {
|
if (j.response) {
|
||||||
toast("Frame OK");
|
toast("Frame OK");
|
||||||
|
} else if (j.error) {
|
||||||
|
toast(j.error, "err");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
push(`ERR ${String(e)}`);
|
push(`ERR ${String(e)}`);
|
||||||
|
|||||||
@@ -31,10 +31,12 @@ export default function ReadAnalyze() {
|
|||||||
|
|
||||||
const readUl = async () => {
|
const readUl = async () => {
|
||||||
try {
|
try {
|
||||||
const j = await apiPost<{ data?: string }>("/api/ul/read-page", { page: block });
|
const j = await apiPost<{ data?: string; error?: string }>("/api/ul/read-page", { page: block });
|
||||||
if (j.data) {
|
if (j.data) {
|
||||||
setHex(j.data + " (UL page)");
|
setHex(j.data + " (UL page)");
|
||||||
toast("UL read OK");
|
toast("UL read OK");
|
||||||
|
} else {
|
||||||
|
toast(j.error ?? "no data returned", "err");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e), "err");
|
toast(String(e), "err");
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export default function WriteClone() {
|
|||||||
const [labBlob, setLabBlob] = useState<BinaryCardFixture | null>(null);
|
const [labBlob, setLabBlob] = useState<BinaryCardFixture | null>(null);
|
||||||
const [ulPage, setUlPage] = useState(4);
|
const [ulPage, setUlPage] = useState(4);
|
||||||
const [ulData, setUlData] = useState("00000000");
|
const [ulData, setUlData] = useState("00000000");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
const write = async () => {
|
const write = async () => {
|
||||||
const cleanKey = key.replace(/\s/g, "");
|
const cleanKey = key.replace(/\s/g, "");
|
||||||
@@ -36,11 +37,14 @@ export default function WriteClone() {
|
|||||||
if (!confirm("Write will modify tag memory. Continue?")) {
|
if (!confirm("Write will modify tag memory. Continue?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await apiPost("/api/mifare/write-block", { block, key: cleanKey, keyB, data: cleanData });
|
await apiPost("/api/mifare/write-block", { block, key: cleanKey, keyB, data: cleanData });
|
||||||
toast("Write OK");
|
toast("Write OK");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e), "err");
|
toast(String(e), "err");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,11 +57,14 @@ export default function WriteClone() {
|
|||||||
if (!confirm("Ultralight page write — can brick OTP/lock bytes if misused. Continue?")) {
|
if (!confirm("Ultralight page write — can brick OTP/lock bytes if misused. Continue?")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
setBusy(true);
|
||||||
try {
|
try {
|
||||||
await apiPost("/api/ul/write-page", { page: ulPage, data: h });
|
await apiPost("/api/ul/write-page", { page: ulPage, data: h });
|
||||||
toast("UL page write OK");
|
toast("UL page write OK");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast(String(e), "err");
|
toast(String(e), "err");
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,9 +137,10 @@ export default function WriteClone() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={write}
|
onClick={write}
|
||||||
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-400 px-6 py-3 font-bold text-white shadow-glow"
|
disabled={busy}
|
||||||
|
className="rounded-2xl bg-gradient-to-r from-bubble-rose to-orange-400 px-6 py-3 font-bold text-white shadow-glow disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Write block
|
{busy ? "Writing…" : "Write block"}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div className="border-t border-white/10 pt-8">
|
<div className="border-t border-white/10 pt-8">
|
||||||
@@ -162,9 +170,10 @@ export default function WriteClone() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={writeUl}
|
onClick={writeUl}
|
||||||
className="mt-4 rounded-2xl border border-bubble-accent/50 bg-bubble-accent/20 px-6 py-3 font-bold text-bubble-accent"
|
disabled={busy}
|
||||||
|
className="mt-4 rounded-2xl border border-bubble-accent/50 bg-bubble-accent/20 px-6 py-3 font-bold text-bubble-accent disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Write UL page
|
{busy ? "Writing…" : "Write UL page"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const Ctx = createContext<(msg: string, kind?: Toast["kind"]) => void>(() => {})
|
|||||||
export function ToastHost({ children }: { children: React.ReactNode }) {
|
export function ToastHost({ children }: { children: React.ReactNode }) {
|
||||||
const [list, setList] = useState<Toast[]>([]);
|
const [list, setList] = useState<Toast[]>([]);
|
||||||
const push = useCallback((msg: string, kind: Toast["kind"] = "info") => {
|
const push = useCallback((msg: string, kind: Toast["kind"] = "info") => {
|
||||||
const id = Date.now();
|
const id = Date.now() + Math.random();
|
||||||
setList((x) => [...x, { id, msg, kind }]);
|
setList((x) => [...x, { id, msg, kind }]);
|
||||||
setTimeout(() => setList((x) => x.filter((t) => t.id !== id)), 4200);
|
setTimeout(() => setList((x) => x.filter((t) => t.id !== id)), 4200);
|
||||||
}, []);
|
}, []);
|
||||||
|
|||||||
Reference in New Issue
Block a user