Add PN532 toolkit firmware, web UI, and embedded SPIFFS assets
Includes ESP-IDF NFC stack (deep capture, 4K Classic geometry, UL write API, open SoftAP), React dashboard with live tag diagnostics, and docs. README updated for APIs and lab Wi-Fi defaults. Made-with: Cursor
This commit is contained in:
5
firmware/components/nfc_engine/CMakeLists.txt
Normal file
5
firmware/components/nfc_engine/CMakeLists.txt
Normal file
@@ -0,0 +1,5 @@
|
||||
idf_component_register(
|
||||
SRCS "nfc_engine.c" "jobs.c" "session_capture.c" "nfc_deep.c" "nfc_brute.c"
|
||||
INCLUDE_DIRS "include"
|
||||
REQUIRES pn532_host esp_common freertos json esp_timer esp_system
|
||||
)
|
||||
26
firmware/components/nfc_engine/include/nfc_engine/jobs.h
Normal file
26
firmware/components/nfc_engine/include/nfc_engine/jobs.h
Normal file
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
typedef void (*job_progress_cb_t)(int pct, const char *msg, void *ctx);
|
||||
|
||||
typedef struct {
|
||||
uint32_t id;
|
||||
char name[32];
|
||||
volatile int pct;
|
||||
volatile int done;
|
||||
} nfc_job_t;
|
||||
|
||||
uint32_t jobs_create(const char *name);
|
||||
void jobs_set_progress(uint32_t id, int pct, const char *msg);
|
||||
void jobs_finish(uint32_t id);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Dictionary attack on MIFARE Classic sector trailer (Key A then Key B per candidate).
|
||||
* `extra` = packed 6-byte keys, `extra_n` = number of keys.
|
||||
* If `variations`, expands each base key with bounded bit/nibble tweaks (not full 2^48 space).
|
||||
* Returns JSON object: { "attempts": N, "sectorHits": [ {sector, keyHex, keyType}, ... ] }
|
||||
* Caller cJSON_Delete().
|
||||
*/
|
||||
cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, uint8_t sector_last,
|
||||
const uint8_t *extra, size_t extra_n, bool variations,
|
||||
int *attempts_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
18
firmware/components/nfc_engine/include/nfc_engine/nfc_deep.h
Normal file
18
firmware/components/nfc_engine/include/nfc_engine/nfc_deep.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "cJSON.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/**
|
||||
* Best-effort deep read for PN532 path: inventory + general status + MIFARE sector sweep
|
||||
* (common keys) and/or Ultralight/NTAG page sweep. Caller must cJSON_Delete() result.
|
||||
*/
|
||||
cJSON *nfc_tag_deep_profile(nfc_tag_info_t *tag);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "cJSON.h"
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#define NFC_MAX_UID_LEN 10
|
||||
#define NFC_BLOCK_LEN 16
|
||||
|
||||
typedef struct {
|
||||
uint8_t uid_len;
|
||||
uint8_t uid[NFC_MAX_UID_LEN];
|
||||
uint16_t atqa;
|
||||
uint8_t sak;
|
||||
uint8_t type_hint; /* 0 unknown, 1 classic, 2 ultralight/ntag */
|
||||
} nfc_tag_info_t;
|
||||
|
||||
typedef struct {
|
||||
uint8_t key[6];
|
||||
bool key_b;
|
||||
} nfc_mifare_key_t;
|
||||
|
||||
esp_err_t nfc_engine_init(void);
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out);
|
||||
|
||||
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||
const nfc_mifare_key_t *key);
|
||||
esp_err_t nfc_mifare_read_block(uint8_t block_no, uint8_t block[NFC_BLOCK_LEN]);
|
||||
esp_err_t nfc_mifare_write_block(uint8_t block_no, const uint8_t block[NFC_BLOCK_LEN]);
|
||||
|
||||
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);
|
||||
|
||||
/** Build JSON snapshot of last seen tag + optional blocks (caller frees cJSON). */
|
||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "esp_err.h"
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/** RAM-only capture buffer (NDJSON lines). When full, NFC deep capture pauses until clear. */
|
||||
void session_capture_init(void);
|
||||
void session_capture_clear(void);
|
||||
|
||||
bool session_capture_is_full(void);
|
||||
bool session_capture_deep_enabled(void);
|
||||
void session_capture_set_deep(bool on);
|
||||
|
||||
size_t session_capture_max(void);
|
||||
void session_capture_get_status(size_t *used_bytes, uint32_t *line_count, bool *full);
|
||||
|
||||
/** Append one NDJSON line (no trailing newline in `line`). Returns false if buffer full. */
|
||||
bool session_capture_append_line(const char *line);
|
||||
|
||||
/** Export raw bytes (entire buffer) for HTTP download. */
|
||||
size_t session_capture_export_size(void);
|
||||
const char *session_capture_export_ptr(void);
|
||||
|
||||
/** Copy up to `cap` bytes (typically cap >= session_capture_export_size()). */
|
||||
void session_capture_copy_to(char *dst, size_t cap, size_t *out_len);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
23
firmware/components/nfc_engine/jobs.c
Normal file
23
firmware/components/nfc_engine/jobs.c
Normal file
@@ -0,0 +1,23 @@
|
||||
#include "nfc_engine/jobs.h"
|
||||
#include "esp_log.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_jobs";
|
||||
|
||||
uint32_t jobs_create(const char *name)
|
||||
{
|
||||
static uint32_t s_next = 1;
|
||||
(void)name;
|
||||
ESP_LOGI(TAG, "job %s id=%lu", name ? name : "?", (unsigned long)s_next);
|
||||
return s_next++;
|
||||
}
|
||||
|
||||
void jobs_set_progress(uint32_t id, int pct, const char *msg)
|
||||
{
|
||||
ESP_LOGD(TAG, "job %lu %d%% %s", (unsigned long)id, pct, msg ? msg : "");
|
||||
}
|
||||
|
||||
void jobs_finish(uint32_t id)
|
||||
{
|
||||
ESP_LOGI(TAG, "job %lu done", (unsigned long)id);
|
||||
}
|
||||
232
firmware/components/nfc_engine/nfc_brute.c
Normal file
232
firmware/components/nfc_engine/nfc_brute.c
Normal file
@@ -0,0 +1,232 @@
|
||||
#include "nfc_engine/nfc_brute.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_task_wdt.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_brute";
|
||||
|
||||
/* Community default keys (subset from public Proxmark3 / MCT-style lists). */
|
||||
static const uint8_t k_builtin[][6] = {
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5}, {0xA5, 0xA4, 0xA3, 0xA2, 0xA1, 0xA0},
|
||||
{0x89, 0xEC, 0xA9, 0x7F, 0x8C, 0x2A}, {0x5C, 0x8F, 0xF9, 0x99, 0x0D, 0xA2},
|
||||
{0x75, 0xCC, 0xB5, 0x9C, 0x9B, 0xED}, {0xD0, 0x1A, 0xFE, 0xEB, 0x89, 0x0A},
|
||||
{0x4B, 0x79, 0x1B, 0xEA, 0x7B, 0xCC}, {0x26, 0x12, 0xC6, 0xDE, 0x84, 0xCA},
|
||||
{0x70, 0x7B, 0x11, 0xFC, 0x14, 0x81}, {0x03, 0xF9, 0x06, 0x76, 0x46, 0xAE},
|
||||
{0x23, 0x52, 0xC5, 0xB5, 0x6D, 0x85}, {0xB0, 0xB1, 0xB2, 0xB3, 0xB4, 0xB5},
|
||||
{0xC0, 0xC1, 0xC2, 0xC3, 0xC4, 0xC5}, {0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5},
|
||||
{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}, {0x4D, 0x3A, 0x99, 0xC3, 0x51, 0xDD},
|
||||
{0x1A, 0x98, 0x2C, 0x7E, 0x45, 0x9A}, {0xFA, 0xFA, 0xFA, 0xFA, 0xFA, 0xFA},
|
||||
{0xFB, 0xFB, 0xFB, 0xFB, 0xFB, 0xFB}, {0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7},
|
||||
{0x5A, 0x1B, 0x85, 0xFC, 0xE2, 0x0A}, {0x71, 0x4C, 0x5C, 0x88, 0x6E, 0x97},
|
||||
{0x58, 0x7E, 0xE5, 0xF9, 0x35, 0x0F}, {0xA0, 0x47, 0x8C, 0xC3, 0x90, 0x91},
|
||||
{0x53, 0x3C, 0xB6, 0xC7, 0x23, 0xF6}, {0x8F, 0xD0, 0xA4, 0xF2, 0x56, 0xE9},
|
||||
{0xE0, 0x00, 0x00, 0x00, 0x00, 0x00}, {0xE7, 0xD6, 0x06, 0x4C, 0x58, 0x60},
|
||||
{0xB2, 0x7C, 0xCA, 0xB3, 0x0D, 0xBD}, {0xD2, 0xEC, 0xE8, 0xB9, 0x39, 0x5E},
|
||||
{0x14, 0x94, 0xE8, 0x16, 0x63, 0xD7}, {0x7C, 0x9F, 0xB8, 0x47, 0x42, 0x42},
|
||||
{0x56, 0x93, 0x69, 0xC5, 0xA0, 0xE5}, {0x63, 0x21, 0x93, 0xBE, 0x1C, 0x3C},
|
||||
{0x8E, 0x26, 0x5B, 0xE2, 0x45, 0xBF}, {0xF4, 0x6B, 0x6D, 0xC0, 0xD6, 0xC4},
|
||||
{0x2A, 0xA0, 0x5E, 0xD1, 0x85, 0x6F}, {0xAE, 0x3F, 0xF4, 0xEE, 0xA0, 0xDB},
|
||||
};
|
||||
|
||||
#define NBUILTIN (sizeof(k_builtin) / sizeof(k_builtin[0]))
|
||||
#define MAX_VARIANTS_PER_KEY 14
|
||||
#define MAX_TRIES_BEFORE_WDT 48
|
||||
|
||||
static int push_variant(const uint8_t base[6], int idx, uint8_t out[6])
|
||||
{
|
||||
memcpy(out, base, 6);
|
||||
switch (idx) {
|
||||
case 0:
|
||||
return 0;
|
||||
case 1:
|
||||
out[5] ^= 0xFF;
|
||||
return 0;
|
||||
case 2:
|
||||
out[0] ^= 0xFF;
|
||||
return 0;
|
||||
case 3:
|
||||
out[5] ^= 0xAA;
|
||||
return 0;
|
||||
case 4:
|
||||
out[5] ^= 0x55;
|
||||
return 0;
|
||||
default: {
|
||||
int n = idx - 5;
|
||||
if (n >= 0 && n < 10) {
|
||||
out[5] = (uint8_t)((out[5] & 0xF0) | (uint8_t)n);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
static bool try_key_on_trailer(nfc_tag_info_t *tag, uint8_t trailer, const uint8_t key[6], bool key_b)
|
||||
{
|
||||
nfc_mifare_key_t k;
|
||||
memcpy(k.key, key, 6);
|
||||
k.key_b = key_b;
|
||||
return nfc_mifare_authenticate_block(tag, trailer, &k) == ESP_OK;
|
||||
}
|
||||
|
||||
/** Trailer block for MIFARE Classic sector index (0..15 for 1K, 0..39 for 4K). */
|
||||
static uint8_t classic_trailer_for_sector(const nfc_tag_info_t *tag, uint8_t sec)
|
||||
{
|
||||
if (tag->sak == 0x19) {
|
||||
if (sec <= 31) {
|
||||
return (uint8_t)(sec * 4 + 3);
|
||||
}
|
||||
if (sec <= 39) {
|
||||
return (uint8_t)(128 + (sec - 32) * 16 + 15);
|
||||
}
|
||||
return 0xFF;
|
||||
}
|
||||
return (uint8_t)(sec * 4 + 3);
|
||||
}
|
||||
|
||||
cJSON *nfc_mifare_dictionary_attack(nfc_tag_info_t *tag, uint8_t sector_first, uint8_t sector_last,
|
||||
const uint8_t *extra, size_t extra_n, bool variations,
|
||||
int *attempts_out)
|
||||
{
|
||||
int64_t t0_us = esp_timer_get_time();
|
||||
int attempts = 0;
|
||||
cJSON *root = cJSON_CreateObject();
|
||||
cJSON *hits = cJSON_CreateArray();
|
||||
if (!root || !hits) {
|
||||
cJSON_Delete(root);
|
||||
cJSON_Delete(hits);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (tag->type_hint != 1) {
|
||||
cJSON_AddStringToObject(root, "error", "not_classic_sak_hint");
|
||||
cJSON_AddItemToObject(root, "sectorHits", hits);
|
||||
if (attempts_out) {
|
||||
*attempts_out = 0;
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
if (sector_first > sector_last) {
|
||||
uint8_t t = sector_first;
|
||||
sector_first = sector_last;
|
||||
sector_last = t;
|
||||
}
|
||||
|
||||
for (uint8_t sec = sector_first; sec <= sector_last; sec++) {
|
||||
uint8_t trailer = classic_trailer_for_sector(tag, sec);
|
||||
if (trailer == 0xFF) {
|
||||
continue;
|
||||
}
|
||||
bool got = false;
|
||||
|
||||
for (size_t bi = 0; bi < NBUILTIN && !got; bi++) {
|
||||
uint8_t trial[6];
|
||||
int maxv = variations ? MAX_VARIANTS_PER_KEY : 1;
|
||||
for (int vi = 0; vi < maxv; vi++) {
|
||||
if (push_variant(k_builtin[bi], vi, trial) != 0) {
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "A");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "B");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if ((attempts % MAX_TRIES_BEFORE_WDT) == 0) {
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t ei = 0; ei < extra_n && !got; ei++) {
|
||||
const uint8_t *ek = extra + ei * 6;
|
||||
uint8_t trial[6];
|
||||
int maxv = variations ? MAX_VARIANTS_PER_KEY : 1;
|
||||
for (int vi = 0; vi < maxv; vi++) {
|
||||
if (push_variant(ek, vi, trial) != 0) {
|
||||
break;
|
||||
}
|
||||
attempts++;
|
||||
if (try_key_on_trailer(tag, trailer, trial, false)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "A");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if (try_key_on_trailer(tag, trailer, trial, true)) {
|
||||
char hx[16];
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(hx + i * 2, 3, "%02X", trial[i]);
|
||||
}
|
||||
hx[12] = 0;
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddStringToObject(h, "keyHex", hx);
|
||||
cJSON_AddStringToObject(h, "keyType", "B");
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
got = true;
|
||||
break;
|
||||
}
|
||||
if ((attempts % MAX_TRIES_BEFORE_WDT) == 0) {
|
||||
esp_task_wdt_reset();
|
||||
vTaskDelay(pdMS_TO_TICKS(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!got) {
|
||||
cJSON *h = cJSON_CreateObject();
|
||||
cJSON_AddNumberToObject(h, "sector", sec);
|
||||
cJSON_AddBoolToObject(h, "miss", true);
|
||||
cJSON_AddItemToArray(hits, h);
|
||||
}
|
||||
}
|
||||
|
||||
cJSON_AddItemToObject(root, "sectorHits", hits);
|
||||
cJSON_AddNumberToObject(root, "attempts", attempts);
|
||||
cJSON_AddNumberToObject(root, "durationMs", (double)((esp_timer_get_time() - t0_us) / 1000));
|
||||
cJSON_AddStringToObject(root, "note",
|
||||
"Dictionary + bounded variants only — not exhaustive 48-bit keyspace.");
|
||||
if (attempts_out) {
|
||||
*attempts_out = attempts;
|
||||
}
|
||||
ESP_LOGI(TAG, "dictionary attack attempts=%d", attempts);
|
||||
return root;
|
||||
}
|
||||
188
firmware/components/nfc_engine/nfc_deep.c
Normal file
188
firmware/components/nfc_engine/nfc_deep.c
Normal file
@@ -0,0 +1,188 @@
|
||||
#include "nfc_engine/nfc_deep.h"
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "pn532_host/pn532_core.h"
|
||||
#include "esp_log.h"
|
||||
#include "esp_timer.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_deep";
|
||||
|
||||
#define MAX_UL_PAGES 240
|
||||
|
||||
static const uint8_t k_default_keys[][6] = {
|
||||
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF},
|
||||
{0xA0, 0xA1, 0xA2, 0xA3, 0xA4, 0xA5},
|
||||
{0xD3, 0xF7, 0xD3, 0xF7, 0xD3, 0xF7},
|
||||
{0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
|
||||
};
|
||||
|
||||
static void block_to_hex(const uint8_t blk[NFC_BLOCK_LEN], char *out33)
|
||||
{
|
||||
for (int i = 0; i < NFC_BLOCK_LEN; i++) {
|
||||
snprintf(out33 + i * 2, 3, "%02X", blk[i]);
|
||||
}
|
||||
out33[32] = 0;
|
||||
}
|
||||
|
||||
static void key_to_hex(const uint8_t k[6], char *out13)
|
||||
{
|
||||
for (int i = 0; i < 6; i++) {
|
||||
snprintf(out13 + i * 2, 3, "%02X", k[i]);
|
||||
}
|
||||
out13[12] = 0;
|
||||
}
|
||||
|
||||
/** Classic 1K: 16 sectors x 4 blocks. Classic 4K: sectors 0–31 x 4 blocks, sectors 32–39 x 16 blocks. */
|
||||
static bool mfc_sector_layout(const nfc_tag_info_t *tag, int sector, int *first_block, int *num_blocks,
|
||||
uint8_t *trailer_block)
|
||||
{
|
||||
bool is4k = (tag->sak == 0x19);
|
||||
if (!is4k) {
|
||||
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;
|
||||
}
|
||||
|
||||
static bool try_sector(nfc_tag_info_t *tag, int sector, cJSON *sec_out)
|
||||
{
|
||||
int fb = 0;
|
||||
int nb = 0;
|
||||
uint8_t trailer = 0;
|
||||
if (!mfc_sector_layout(tag, sector, &fb, &nb, &trailer)) {
|
||||
return false;
|
||||
}
|
||||
cJSON_AddNumberToObject(sec_out, "sector", sector);
|
||||
cJSON_AddNumberToObject(sec_out, "firstBlock", fb);
|
||||
cJSON_AddNumberToObject(sec_out, "trailerBlock", trailer);
|
||||
cJSON_AddNumberToObject(sec_out, "blockCount", nb);
|
||||
|
||||
for (size_t ki = 0; ki < sizeof(k_default_keys) / 6; ki++) {
|
||||
nfc_mifare_key_t key;
|
||||
memcpy(key.key, k_default_keys[ki], 6);
|
||||
key.key_b = false;
|
||||
if (nfc_mifare_authenticate_block(tag, trailer, &key) == ESP_OK) {
|
||||
cJSON_AddStringToObject(sec_out, "keyType", "A");
|
||||
char kh[16];
|
||||
key_to_hex(key.key, kh);
|
||||
cJSON_AddStringToObject(sec_out, "keyHex", kh);
|
||||
cJSON *blocks = cJSON_CreateArray();
|
||||
for (int b = 0; b < nb; b++) {
|
||||
uint8_t bn = (uint8_t)(fb + b);
|
||||
uint8_t blk[NFC_BLOCK_LEN];
|
||||
if (nfc_mifare_read_block(bn, blk) == ESP_OK) {
|
||||
char hx[36];
|
||||
block_to_hex(blk, hx);
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateString(hx));
|
||||
} else {
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateNull());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(sec_out, "blocksHex", blocks);
|
||||
return true;
|
||||
}
|
||||
key.key_b = true;
|
||||
if (nfc_mifare_authenticate_block(tag, trailer, &key) == ESP_OK) {
|
||||
cJSON_AddStringToObject(sec_out, "keyType", "B");
|
||||
char kh[16];
|
||||
key_to_hex(key.key, kh);
|
||||
cJSON_AddStringToObject(sec_out, "keyHex", kh);
|
||||
cJSON *blocks = cJSON_CreateArray();
|
||||
for (int b = 0; b < nb; b++) {
|
||||
uint8_t bn = (uint8_t)(fb + b);
|
||||
uint8_t blk[NFC_BLOCK_LEN];
|
||||
if (nfc_mifare_read_block(bn, blk) == ESP_OK) {
|
||||
char hx[36];
|
||||
block_to_hex(blk, hx);
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateString(hx));
|
||||
} else {
|
||||
cJSON_AddItemToArray(blocks, cJSON_CreateNull());
|
||||
}
|
||||
}
|
||||
cJSON_AddItemToObject(sec_out, "blocksHex", blocks);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
cJSON_AddBoolToObject(sec_out, "authFailed", true);
|
||||
return false;
|
||||
}
|
||||
|
||||
static void add_mifare_classic(nfc_tag_info_t *tag, cJSON *root)
|
||||
{
|
||||
int sectors = (tag->sak == 0x19) ? 40 : 16;
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
for (int s = 0; s < sectors; s++) {
|
||||
cJSON *sec = cJSON_CreateObject();
|
||||
(void)try_sector(tag, s, sec);
|
||||
cJSON_AddItemToArray(arr, sec);
|
||||
}
|
||||
cJSON_AddItemToObject(root, "mifareClassic", arr);
|
||||
}
|
||||
|
||||
static void add_ultralight(nfc_tag_info_t *tag, cJSON *root)
|
||||
{
|
||||
(void)tag;
|
||||
cJSON *pages = cJSON_CreateArray();
|
||||
uint8_t buf[4];
|
||||
for (int p = 0; p < MAX_UL_PAGES; p++) {
|
||||
if (nfc_ultralight_read_page((uint8_t)p, buf) != ESP_OK) {
|
||||
break;
|
||||
}
|
||||
char line[12];
|
||||
snprintf(line, sizeof line, "%02X%02X%02X%02X", buf[0], buf[1], buf[2], buf[3]);
|
||||
cJSON_AddItemToArray(pages, cJSON_CreateString(line));
|
||||
}
|
||||
cJSON_AddItemToObject(root, "ultralightPagesHex", pages);
|
||||
}
|
||||
|
||||
cJSON *nfc_tag_deep_profile(nfc_tag_info_t *tag)
|
||||
{
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
return NULL;
|
||||
}
|
||||
cJSON_AddNumberToObject(o, "capturedMs", (double)(esp_timer_get_time() / 1000));
|
||||
cJSON_AddItemToObject(o, "tag", nfc_tag_to_json(tag));
|
||||
|
||||
uint8_t gs[32];
|
||||
size_t gl = 0;
|
||||
if (pn532_get_general_status(gs, sizeof(gs), &gl) == ESP_OK && gl > 0) {
|
||||
cJSON *g = cJSON_CreateArray();
|
||||
for (size_t i = 0; i < gl; i++) {
|
||||
cJSON_AddItemToArray(g, cJSON_CreateNumber(gs[i]));
|
||||
}
|
||||
cJSON_AddItemToObject(o, "pn532GeneralStatus", g);
|
||||
}
|
||||
|
||||
if (tag->type_hint == 1) {
|
||||
add_mifare_classic(tag, o);
|
||||
} else if (tag->type_hint == 2) {
|
||||
add_ultralight(tag, o);
|
||||
} else {
|
||||
cJSON_AddStringToObject(
|
||||
o, "note",
|
||||
"Unknown type from SAK/ATQA — stored inventory + PN532 status only. Use Raw console for ISO14443-4 / other stacks.");
|
||||
}
|
||||
|
||||
ESP_LOGI(TAG, "deep profile done type_hint=%u", tag->type_hint);
|
||||
return o;
|
||||
}
|
||||
255
firmware/components/nfc_engine/nfc_engine.c
Normal file
255
firmware/components/nfc_engine/nfc_engine.c
Normal file
@@ -0,0 +1,255 @@
|
||||
#include "nfc_engine/nfc_engine.h"
|
||||
#include "pn532_host/pn532_core.h"
|
||||
#include "esp_log.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
static const char *TAG = "nfc_engine";
|
||||
|
||||
static uint8_t s_tg = 1;
|
||||
|
||||
static void hint_type(nfc_tag_info_t *t)
|
||||
{
|
||||
switch (t->sak) {
|
||||
case 0x08:
|
||||
case 0x00:
|
||||
t->type_hint = 2;
|
||||
break;
|
||||
case 0x09:
|
||||
case 0x18:
|
||||
case 0x19:
|
||||
t->type_hint = 1;
|
||||
break;
|
||||
default:
|
||||
t->type_hint = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
esp_err_t nfc_engine_init(void)
|
||||
{
|
||||
esp_err_t e = pn532_core_init();
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
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");
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_poll_passive_target(nfc_tag_info_t *out)
|
||||
{
|
||||
memset(out, 0, sizeof(*out));
|
||||
uint8_t resp[64];
|
||||
size_t rlen = 0;
|
||||
esp_err_t e = pn532_in_list_passive_target(1, 0x00, resp, sizeof(resp), &rlen);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (rlen < 2 || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
if (resp[1] < 1) {
|
||||
return ESP_ERR_NOT_FOUND;
|
||||
}
|
||||
if (rlen < 8) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
s_tg = resp[2];
|
||||
out->atqa = (uint16_t)(((uint16_t)resp[3] << 8) | resp[4]);
|
||||
out->sak = resp[5];
|
||||
out->uid_len = resp[6];
|
||||
if (out->uid_len > NFC_MAX_UID_LEN || (size_t)(7 + out->uid_len) > rlen) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(out->uid, resp + 7, out->uid_len);
|
||||
hint_type(out);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
static esp_err_t in_data_tg(const uint8_t *data, size_t data_len, uint8_t *response, size_t response_max,
|
||||
size_t *response_len)
|
||||
{
|
||||
uint8_t buf[64];
|
||||
if (data_len > sizeof(buf) - 3) {
|
||||
return ESP_ERR_INVALID_SIZE;
|
||||
}
|
||||
buf[0] = PN532_CMD_INDATAEXCHANGE;
|
||||
buf[1] = s_tg;
|
||||
memcpy(buf + 2, data, data_len);
|
||||
return pn532_send_cmd(buf, 2 + data_len, response, response_max, response_len, 500);
|
||||
}
|
||||
|
||||
esp_err_t nfc_mifare_authenticate_block(const nfc_tag_info_t *tag, uint8_t block_no,
|
||||
const nfc_mifare_key_t *key)
|
||||
{
|
||||
uint8_t data[12];
|
||||
data[0] = key->key_b ? PN532_MIFARE_CMD_AUTH_B : PN532_MIFARE_CMD_AUTH_A;
|
||||
data[1] = block_no;
|
||||
memcpy(data + 2, key->key, 6);
|
||||
uint8_t uid_use[4];
|
||||
if (tag->uid_len == 4) {
|
||||
memcpy(uid_use, tag->uid, 4);
|
||||
} else if (tag->uid_len >= 7) {
|
||||
memcpy(uid_use, tag->uid + tag->uid_len - 4, 4);
|
||||
} else if (tag->uid_len > 4) {
|
||||
memcpy(uid_use, tag->uid + tag->uid_len - 4, 4);
|
||||
} else {
|
||||
return ESP_ERR_INVALID_ARG;
|
||||
}
|
||||
memcpy(data + 8, uid_use, 4);
|
||||
uint8_t resp[32];
|
||||
size_t rlen = 0;
|
||||
esp_err_t e = in_data_tg(data, sizeof(data), resp, sizeof(resp), &rlen);
|
||||
if (e != ESP_OK) {
|
||||
return e;
|
||||
}
|
||||
if (rlen < 1 || resp[0] != 0x00) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_mifare_read_block(uint8_t block_no, uint8_t block[NFC_BLOCK_LEN])
|
||||
{
|
||||
uint8_t d[] = {PN532_MIFARE_CMD_READ, block_no};
|
||||
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 < 1 + NFC_BLOCK_LEN || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(block, resp + 1, NFC_BLOCK_LEN);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_mifare_write_block(uint8_t block_no, const uint8_t block[NFC_BLOCK_LEN])
|
||||
{
|
||||
uint8_t d[2 + NFC_BLOCK_LEN];
|
||||
d[0] = PN532_MIFARE_CMD_WRITE;
|
||||
d[1] = block_no;
|
||||
memcpy(d + 2, block, NFC_BLOCK_LEN);
|
||||
uint8_t resp[16];
|
||||
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 < 1 || resp[0] != 0x00) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_ultralight_read_page(uint8_t page, uint8_t data[4])
|
||||
{
|
||||
uint8_t d[] = {PN532_MIFARE_CMD_READ, page};
|
||||
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 < 1 + 16 || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
memcpy(data, resp + 1, 4);
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_ultralight_write_page(uint8_t page, const uint8_t data[4])
|
||||
{
|
||||
uint8_t d[6] = {0xA2, page, data[0], data[1], data[2], data[3]};
|
||||
uint8_t resp[16];
|
||||
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 < 1 || resp[0] != 0x00) {
|
||||
return ESP_FAIL;
|
||||
}
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
esp_err_t nfc_ul_fast_read(uint8_t start_page, uint8_t *out, size_t out_max, size_t *got)
|
||||
{
|
||||
*got = 0;
|
||||
size_t off = 0;
|
||||
uint8_t d[2] = {0x3A, start_page};
|
||||
uint8_t resp[256];
|
||||
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 || resp[0] != 0x00) {
|
||||
return ESP_ERR_INVALID_RESPONSE;
|
||||
}
|
||||
size_t payload = rlen - 1;
|
||||
if (payload > out_max) {
|
||||
payload = out_max;
|
||||
}
|
||||
memcpy(out, resp + 1, payload);
|
||||
off = payload;
|
||||
*got = off;
|
||||
return ESP_OK;
|
||||
}
|
||||
|
||||
cJSON *nfc_tag_to_json(const nfc_tag_info_t *tag)
|
||||
{
|
||||
cJSON *o = cJSON_CreateObject();
|
||||
if (!o) {
|
||||
return NULL;
|
||||
}
|
||||
char uidhex[NFC_MAX_UID_LEN * 2 + 4];
|
||||
for (int i = 0; i < tag->uid_len; i++) {
|
||||
snprintf(uidhex + i * 2, 3, "%02X", tag->uid[i]);
|
||||
}
|
||||
uidhex[tag->uid_len * 2] = 0;
|
||||
cJSON_AddStringToObject(o, "uid", uidhex);
|
||||
cJSON_AddNumberToObject(o, "uidLen", tag->uid_len);
|
||||
cJSON_AddNumberToObject(o, "atqa", tag->atqa);
|
||||
cJSON_AddNumberToObject(o, "sak", tag->sak);
|
||||
cJSON_AddNumberToObject(o, "typeHint", tag->type_hint);
|
||||
|
||||
char aq[8];
|
||||
snprintf(aq, sizeof aq, "%04X", (unsigned)tag->atqa);
|
||||
cJSON_AddStringToObject(o, "atqaHex", aq);
|
||||
char sk[8];
|
||||
snprintf(sk, sizeof sk, "%02X", tag->sak);
|
||||
cJSON_AddStringToObject(o, "sakHex", sk);
|
||||
|
||||
const char *guess = "Unknown / use Raw or RATS";
|
||||
if (tag->type_hint == 1) {
|
||||
guess = (tag->sak == 0x19) ? "MIFARE Classic 4K" : "MIFARE Classic 1K or compatible";
|
||||
} else if (tag->type_hint == 2) {
|
||||
guess = "Ultralight / NTAG / Type 2 family";
|
||||
}
|
||||
cJSON_AddStringToObject(o, "typeGuess", guess);
|
||||
|
||||
uint8_t gs[32];
|
||||
size_t gl = 0;
|
||||
if (pn532_get_general_status(gs, sizeof(gs), &gl) == ESP_OK && gl > 0) {
|
||||
cJSON *arr = cJSON_CreateArray();
|
||||
if (arr) {
|
||||
for (size_t i = 0; i < gl; i++) {
|
||||
cJSON_AddItemToArray(arr, cJSON_CreateNumber(gs[i]));
|
||||
}
|
||||
cJSON_AddItemToObject(o, "pn532GeneralStatus", arr);
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
129
firmware/components/nfc_engine/session_capture.c
Normal file
129
firmware/components/nfc_engine/session_capture.c
Normal file
@@ -0,0 +1,129 @@
|
||||
#include "nfc_engine/session_capture.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/semphr.h"
|
||||
#include <string.h>
|
||||
|
||||
#define SESSION_CAPTURE_BYTES (48 * 1024)
|
||||
|
||||
static char s_buf[SESSION_CAPTURE_BYTES];
|
||||
static size_t s_len;
|
||||
static uint32_t s_lines;
|
||||
static bool s_full;
|
||||
static bool s_deep;
|
||||
static SemaphoreHandle_t s_mu;
|
||||
|
||||
void session_capture_init(void)
|
||||
{
|
||||
s_mu = xSemaphoreCreateMutex();
|
||||
session_capture_clear();
|
||||
s_deep = false;
|
||||
}
|
||||
|
||||
void session_capture_clear(void)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
s_len = 0;
|
||||
s_lines = 0;
|
||||
s_full = false;
|
||||
s_buf[0] = 0;
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
}
|
||||
|
||||
bool session_capture_is_full(void)
|
||||
{
|
||||
return s_full;
|
||||
}
|
||||
|
||||
bool session_capture_deep_enabled(void) { return s_deep; }
|
||||
|
||||
void session_capture_set_deep(bool on) { s_deep = on; }
|
||||
|
||||
size_t session_capture_max(void) { return sizeof(s_buf) - 2; }
|
||||
|
||||
void session_capture_get_status(size_t *used_bytes, uint32_t *line_count, bool *full)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
if (used_bytes) {
|
||||
*used_bytes = s_len;
|
||||
}
|
||||
if (line_count) {
|
||||
*line_count = s_lines;
|
||||
}
|
||||
if (full) {
|
||||
*full = s_full;
|
||||
}
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
}
|
||||
|
||||
bool session_capture_append_line(const char *line)
|
||||
{
|
||||
if (!line || s_full) {
|
||||
return false;
|
||||
}
|
||||
size_t l = strlen(line);
|
||||
size_t need = l + 1; /* newline */
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
if (s_full || s_len + need >= sizeof(s_buf)) {
|
||||
s_full = true;
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
memcpy(s_buf + s_len, line, l);
|
||||
s_len += l;
|
||||
s_buf[s_len++] = '\n';
|
||||
s_buf[s_len] = 0;
|
||||
s_lines++;
|
||||
if (s_len + 2 >= sizeof(s_buf)) {
|
||||
s_full = true;
|
||||
}
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
size_t session_capture_export_size(void)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
size_t n = s_len;
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
const char *session_capture_export_ptr(void) { return s_buf; }
|
||||
|
||||
void session_capture_copy_to(char *dst, size_t cap, size_t *out_len)
|
||||
{
|
||||
if (s_mu) {
|
||||
xSemaphoreTake(s_mu, portMAX_DELAY);
|
||||
}
|
||||
size_t n = s_len;
|
||||
if (n > cap) {
|
||||
n = cap;
|
||||
}
|
||||
if (dst && n) {
|
||||
memcpy(dst, s_buf, n);
|
||||
}
|
||||
if (out_len) {
|
||||
*out_len = n;
|
||||
}
|
||||
if (s_mu) {
|
||||
xSemaphoreGive(s_mu);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user