Lettore RFID per accessi con ESP32-C6 e RC522 (MFRC522)
Un lettore di chiavette RFID è il classico progetto “porta di casa”: avvicini il portachiavi, il relè scatta e la serratura si apre. In questo tutorial lo costruisci con l’ESP32-C6 e il diffusissimo modulo RC522 (chip NXP MFRC522, 13,56 MHz), scrivendo da zero un piccolo driver SPI che esegue la sequenza ISO/IEC 14443-A fino a leggere l’UID della carta. Le chiavette autorizzate sono salvate in NVS, e un relè, due LED e un buzzer danno il riscontro.
Alla fine c’è una sezione che non devi saltare: leggere il solo UID è comodo per imparare, ma non è sicuro per un controllo accessi vero. Vedremo perché e cosa usare al suo posto.
Cosa serve
| Componente | Note |
|---|---|
| ESP32-C6-DevKitC-1 | Qualsiasi scheda ESP32-C6, adatta i pin |
| Modulo RC522 (MFRC522) | Alimentazione 3,3 V; di solito arriva con una carta e un portachiavi MIFARE Classic 1K |
| Modulo relè con transistor e diodo di ricircolo | Mai pilotare la bobina direttamente dal GPIO |
| Buzzer attivo + transistor NPN | Il buzzer attivo suona con un semplice livello alto |
| LED verde e rosso con resistori da 330 Ω | Accesso consentito / negato |
| Pulsante normalmente aperto | Tenuto premuto durante la lettura, registra la chiavetta |
| Chiavette MIFARE (o DESFire, vedi sicurezza) | 13,56 MHz, ISO/IEC 14443-A |
Come funziona
RFID a 13,56 MHz e ISO/IEC 14443-A
Il lettore (in gergo PCD) genera un campo magnetico a 13,56 MHz con la sua antenna a spira. La carta (PICC) non ha batteria: si alimenta dal campo e risponde modulandone il carico. Il dialogo segue lo standard ISO/IEC 14443-3 tipo A:
- REQA (0x26, un frame corto da soli 7 bit): “c’è qualcuno?”. Ogni carta in stato di riposo risponde con due byte, l’ATQA.
- ANTICOLLISION (0x93 0x20): la carta invia 4 byte di UID più un byte di controllo BCC, lo XOR dei quattro.
- SELECT (0x93 0x70 + UID + BCC + CRC_A): la carta selezionata risponde con il SAK, che dice se l’UID è completo e che tipo di carta è.
- Se il primo byte ricevuto è il cascade tag 0x88, l’UID è più lungo di 4 byte: si ripete anticollisione e selezione al livello 2 (0x95) ed eventualmente al livello 3 (0x97). Gli UID possono essere di 4, 7 o 10 byte.
- HLTA (0x50 0x00 + CRC): mette la carta in stato HALT. Finché resta nel campo non risponde più a REQA, e così non la rileggi dieci volte al secondo.
RC522 o PN532?
| RC522 (MFRC522) | PN532 | |
|---|---|---|
| Interfacce | SPI (sui moduli comuni), I2C e UART previsti dal chip | I2C, SPI, HSU (UART) |
| Standard | ISO/IEC 14443-A, MIFARE | ISO/IEC 14443-A/B, FeliCa, NFC peer-to-peer, emulazione di carta (MIFARE Classic, FeliCa) |
| Livello di astrazione | Registri e FIFO: il protocollo lo scrivi tu | Microcontrollore interno con comandi di alto livello (per esempio InListPassiveTarget) |
| ISO 14443-4 (necessario per DESFire) | Da implementare nel firmware | Il datasheet non lo cita: dichiara il framing completo ISO/IEC 14443A (parità e CRC) e, con il firmware adatto, l’intero protocollo 14443A/MIFARE |
| Costo e diffusione | Bassissimo, ovunque | Un po’ più alto |
Per imparare il protocollo l’RC522 è perfetto, perché ti obbliga a vedere ogni passaggio. Per un prodotto che deve parlare con carte DESFire il PN532 (o un lettore dedicato) ti risparmia molto lavoro grazie ai comandi di alto livello; come gestisca il livello ISO 14443-4, però, va verificato nella documentazione NXP del firmware, perché il datasheet non lo descrive.
Collegamenti
| Modulo | Pin | ESP32-C6 |
|---|---|---|
| RC522 | SDA (è il chip select SPI) | GPIO10 |
| RC522 | SCK | GPIO6 |
| RC522 | MOSI | GPIO7 |
| RC522 | MISO | GPIO2 |
| RC522 | RST | GPIO11 |
| RC522 | IRQ | non collegato (usiamo il polling) |
| RC522 | 3.3V / GND | 3V3 / GND (non 5 V) |
| Relè | IN | GPIO18 |
| LED verde / rosso | anodo (via 330 Ω) | GPIO19 / GPIO20 |
| Buzzer | base del transistor (via 1 kΩ) | GPIO21 |
| Pulsante registrazione | verso GND | GPIO23 (pull-up interno) |
I pin evitano i GPIO di strapping (8, 9, 15), l’USB (12, 13) e la UART della console (16, 17). GPIO2, 6 e 7 sono i pin IO_MUX dello SPI2 sul C6.
La periferica dell’ESP32
L’MFRC522 si comanda leggendo e scrivendo registri a 8 bit. Via SPI ogni accesso è una transazione di due byte: il primo contiene l’indirizzo del registro spostato di un bit a sinistra, con il bit 7 a 1 per la lettura e a 0 per la scrittura; il secondo è il dato (in lettura, il byte che torna indietro). Il driver SPI master di ESP-IDF (driver/spi_master.h) gestisce il chip select e il clock: con SPI_TRANS_USE_TXDATA e SPI_TRANS_USE_RXDATA i due byte stanno direttamente nella struttura della transazione, senza buffer esterni, e spi_device_polling_transmit() è la scelta più veloce per transazioni così brevi. Il bus gira a 4 MHz, sotto il limite di 10 Mbit/s del datasheet e con margine per cavi volanti.
Per i comandi che richiedono il CRC (SELECT e HLTA) calcoliamo il CRC_A in software: è un CRC a 16 bit con valore iniziale 0x6363 definito nella ISO/IEC 14443-3. Un controllo facile: il CRC di 50 00 deve dare 57 CD, i due byte finali della frase HLTA.
Il codice
Il progetto usa solo componenti di ESP-IDF 5.4: basta questo main.c, target esp32c6.
/*
* Lettore chiavette RFID per accessi - ESP32-C6 + MFRC522 (RC522), ESP-IDF 5.4
* SPI2: SCK=GPIO6, MOSI=GPIO7, MISO=GPIO2, CS(SDA)=GPIO10, RST=GPIO11
* Relè=GPIO18, LED verde=GPIO19, LED rosso=GPIO20, buzzer=GPIO21, tasto registra=GPIO23
* www.webappportal.com
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/spi_master.h"
#include "driver/gpio.h"
#include "nvs_flash.h"
#include "nvs.h"
#include "esp_timer.h"
#include "esp_log.h"
static const char *TAG = "rfid";
/* ---------- Pin ---------- */
#define PIN_SCK GPIO_NUM_6
#define PIN_MOSI GPIO_NUM_7
#define PIN_MISO GPIO_NUM_2
#define PIN_CS GPIO_NUM_10
#define PIN_RST GPIO_NUM_11
#define PIN_RELE GPIO_NUM_18
#define PIN_LED_OK GPIO_NUM_19
#define PIN_LED_KO GPIO_NUM_20
#define PIN_BUZZER GPIO_NUM_21
#define PIN_REGISTRA GPIO_NUM_23 /* tenuto premuto = registra la chiavetta */
/* ---------- Registri MFRC522 (datasheet NXP, cap. 9) ---------- */
#define REG_COMMAND 0x01
#define REG_COM_IRQ 0x04
#define REG_ERROR 0x06
#define REG_FIFO_DATA 0x09
#define REG_FIFO_LEVEL 0x0A
#define REG_CONTROL 0x0C
#define REG_BIT_FRAMING 0x0D
#define REG_COLL 0x0E
#define REG_MODE 0x11
#define REG_TX_MODE 0x12
#define REG_RX_MODE 0x13
#define REG_TX_CONTROL 0x14
#define REG_TX_ASK 0x15
#define REG_MOD_WIDTH 0x24
#define REG_T_MODE 0x2A
#define REG_T_PRESCALER 0x2B
#define REG_T_RELOAD_H 0x2C
#define REG_T_RELOAD_L 0x2D
#define REG_VERSION 0x37
#define CMD_IDLE 0x00
#define CMD_TRANSCEIVE 0x0C
#define CMD_SOFT_RESET 0x0F
/* ---------- Comandi ISO/IEC 14443-3 tipo A ---------- */
#define PICC_REQA 0x26
#define PICC_HLTA 0x50
#define PICC_CASCADE 0x88
#define MAX_UID 10
#define MAX_AUTORIZZATI 32
typedef struct {
uint8_t len;
uint8_t uid[MAX_UID];
} chiave_t;
static spi_device_handle_t s_spi;
static chiave_t s_lista[MAX_AUTORIZZATI];
static size_t s_num_autorizzati;
/* ================= Accesso ai registri via SPI ================= */
static void rc522_scrivi(uint8_t reg, uint8_t val)
{
spi_transaction_t t = {
.flags = SPI_TRANS_USE_TXDATA,
.length = 16,
.tx_data = { (uint8_t)((reg << 1) & 0x7E), val },
};
ESP_ERROR_CHECK(spi_device_polling_transmit(s_spi, &t));
}
static uint8_t rc522_leggi(uint8_t reg)
{
spi_transaction_t t = {
.flags = SPI_TRANS_USE_TXDATA | SPI_TRANS_USE_RXDATA,
.length = 16,
.tx_data = { (uint8_t)(0x80 | ((reg << 1) & 0x7E)), 0x00 },
};
ESP_ERROR_CHECK(spi_device_polling_transmit(s_spi, &t));
return t.rx_data[1];
}
static void rc522_set_bit(uint8_t reg, uint8_t mask)
{
rc522_scrivi(reg, rc522_leggi(reg) | mask);
}
static void rc522_clr_bit(uint8_t reg, uint8_t mask)
{
rc522_scrivi(reg, rc522_leggi(reg) & ~mask);
}
/* CRC_A di ISO/IEC 14443-3 (polinomio x^16+x^12+x^5+1, valore iniziale 0x6363) */
static void crc_a(const uint8_t *dati, size_t len, uint8_t out[2])
{
uint16_t crc = 0x6363;
for (size_t i = 0; i < len; i++) {
uint8_t ch = dati[i] ^ (uint8_t)(crc & 0xFF);
ch ^= (uint8_t)(ch << 4);
crc = (crc >> 8) ^ ((uint16_t)ch << 8) ^ ((uint16_t)ch << 3) ^ (ch >> 4);
}
out[0] = crc & 0xFF;
out[1] = crc >> 8;
}
static void rc522_init(void)
{
gpio_set_level(PIN_RST, 0); /* reset hardware */
vTaskDelay(pdMS_TO_TICKS(2));
gpio_set_level(PIN_RST, 1);
vTaskDelay(pdMS_TO_TICKS(50));
rc522_scrivi(REG_COMMAND, CMD_SOFT_RESET);
vTaskDelay(pdMS_TO_TICKS(50));
rc522_scrivi(REG_TX_MODE, 0x00); /* 106 kbit/s, CRC gestito da noi */
rc522_scrivi(REG_RX_MODE, 0x00);
rc522_scrivi(REG_MOD_WIDTH, 0x26);
/* timer interno: 13,56 MHz / (2*169+1) = 40 kHz, ricarica 1000 -> 25 ms */
rc522_scrivi(REG_T_MODE, 0x80); /* TAuto: parte a fine trasmissione */
rc522_scrivi(REG_T_PRESCALER, 0xA9);
rc522_scrivi(REG_T_RELOAD_H, 0x03);
rc522_scrivi(REG_T_RELOAD_L, 0xE8);
rc522_scrivi(REG_TX_ASK, 0x40); /* modulazione ASK 100% */
rc522_scrivi(REG_MODE, 0x3D); /* preset CRC 0x6363 */
rc522_set_bit(REG_TX_CONTROL, 0x03); /* accende l'antenna (TX1, TX2) */
}
/* Invia 'tx' alla carta e riceve la risposta in 'rx' */
static esp_err_t rc522_transceive(const uint8_t *tx, uint8_t tx_len, uint8_t bit_framing,
uint8_t *rx, uint8_t *rx_len)
{
rc522_scrivi(REG_COMMAND, CMD_IDLE);
rc522_scrivi(REG_COM_IRQ, 0x7F); /* azzera i flag di interrupt */
rc522_scrivi(REG_FIFO_LEVEL, 0x80); /* svuota la FIFO */
for (uint8_t i = 0; i < tx_len; i++) {
rc522_scrivi(REG_FIFO_DATA, tx[i]);
}
rc522_scrivi(REG_BIT_FRAMING, bit_framing);
rc522_scrivi(REG_COMMAND, CMD_TRANSCEIVE);
rc522_set_bit(REG_BIT_FRAMING, 0x80); /* StartSend */
int64_t scadenza = esp_timer_get_time() + 40000;
while (1) {
uint8_t irq = rc522_leggi(REG_COM_IRQ);
if (irq & 0x30) { /* RxIRq o IdleIRq: finito */
break;
}
if ((irq & 0x01) || esp_timer_get_time() > scadenza) {
return ESP_ERR_TIMEOUT; /* nessuna carta ha risposto */
}
}
uint8_t err = rc522_leggi(REG_ERROR);
if (err & 0x13) { /* BufferOvfl, ParityErr, ProtocolErr */
return ESP_FAIL;
}
if (err & 0x08) { /* CollErr: più carte nel campo */
return ESP_ERR_INVALID_STATE;
}
uint8_t n = rc522_leggi(REG_FIFO_LEVEL);
if (n > *rx_len) {
return ESP_ERR_NO_MEM;
}
for (uint8_t i = 0; i < n; i++) {
rx[i] = rc522_leggi(REG_FIFO_DATA);
}
*rx_len = n;
return ESP_OK;
}
/* REQA -> anticollisione -> SELECT (fino a 3 livelli) -> UID completo */
static esp_err_t rc522_leggi_uid(chiave_t *uid)
{
uint8_t buf[10];
uint8_t len = sizeof(buf);
uint8_t reqa = PICC_REQA;
rc522_clr_bit(REG_COLL, 0x80);
/* REQA è un frame corto da 7 bit: BitFramingReg = 0x07 */
esp_err_t ret = rc522_transceive(&reqa, 1, 0x07, buf, &len);
if (ret != ESP_OK || len != 2) {
return ESP_ERR_NOT_FOUND; /* ATQA assente: nessuna carta */
}
uid->len = 0;
const uint8_t sel[3] = { 0x93, 0x95, 0x97 };
for (int livello = 0; livello < 3; livello++) {
uint8_t ac[2] = { sel[livello], 0x20 }; /* ANTICOLLISION */
uint8_t risp[5];
len = sizeof(risp);
ret = rc522_transceive(ac, 2, 0x00, risp, &len);
if (ret != ESP_OK || len != 5) {
return ESP_FAIL;
}
if ((risp[0] ^ risp[1] ^ risp[2] ^ risp[3]) != risp[4]) {
return ESP_ERR_INVALID_CRC; /* BCC errato */
}
uint8_t s[9] = { sel[livello], 0x70, risp[0], risp[1], risp[2], risp[3], risp[4] };
crc_a(s, 7, &s[7]); /* SELECT con CRC_A */
uint8_t sak[3];
len = sizeof(sak);
ret = rc522_transceive(s, 9, 0x00, sak, &len);
if (ret != ESP_OK || len != 3) {
return ESP_FAIL;
}
if (risp[0] == PICC_CASCADE) { /* UID lungo: continua */
memcpy(&uid->uid[uid->len], &risp[1], 3);
uid->len += 3;
} else {
memcpy(&uid->uid[uid->len], risp, 4);
uid->len += 4;
ESP_LOGD(TAG, "SAK 0x%02X", sak[0]);
break;
}
}
/* HLTA: la carta non risponderà più a REQA finché resta nel campo */
uint8_t hlta[4] = { PICC_HLTA, 0x00 };
crc_a(hlta, 2, &hlta[2]);
len = sizeof(buf);
rc522_transceive(hlta, 4, 0x00, buf, &len); /* timeout atteso */
return ESP_OK;
}
/* ================= Lista autorizzati in NVS ================= */
static void lista_carica(void)
{
nvs_handle_t h;
size_t dim = sizeof(s_lista);
s_num_autorizzati = 0;
if (nvs_open("accessi", NVS_READONLY, &h) != ESP_OK) {
return; /* primo avvio: lista vuota */
}
if (nvs_get_blob(h, "uid", s_lista, &dim) == ESP_OK) {
s_num_autorizzati = dim / sizeof(chiave_t);
}
nvs_close(h);
}
static esp_err_t lista_salva(void)
{
nvs_handle_t h;
esp_err_t err = nvs_open("accessi", NVS_READWRITE, &h);
if (err != ESP_OK) {
return err;
}
err = nvs_set_blob(h, "uid", s_lista, s_num_autorizzati * sizeof(chiave_t));
if (err == ESP_OK) {
err = nvs_commit(h);
}
nvs_close(h);
return err;
}
static bool lista_contiene(const chiave_t *u)
{
for (size_t i = 0; i < s_num_autorizzati; i++) {
if (s_lista[i].len == u->len && memcmp(s_lista[i].uid, u->uid, u->len) == 0) {
return true;
}
}
return false;
}
/* ================= Feedback ================= */
static void segnala(bool ok)
{
gpio_num_t led = ok ? PIN_LED_OK : PIN_LED_KO;
gpio_set_level(led, 1);
gpio_set_level(PIN_BUZZER, 1);
vTaskDelay(pdMS_TO_TICKS(ok ? 100 : 600)); /* bip corto = OK, lungo = negato */
gpio_set_level(PIN_BUZZER, 0);
if (ok) {
gpio_set_level(PIN_RELE, 1); /* apre la serratura per 3 s */
vTaskDelay(pdMS_TO_TICKS(3000));
gpio_set_level(PIN_RELE, 0);
} else {
vTaskDelay(pdMS_TO_TICKS(1000));
}
gpio_set_level(led, 0);
}
static void gpio_init(void)
{
const gpio_config_t out = {
.pin_bit_mask = (1ULL << PIN_RST) | (1ULL << PIN_RELE) | (1ULL << PIN_LED_OK) |
(1ULL << PIN_LED_KO) | (1ULL << PIN_BUZZER),
.mode = GPIO_MODE_OUTPUT,
};
ESP_ERROR_CHECK(gpio_config(&out));
const gpio_config_t in = {
.pin_bit_mask = 1ULL << PIN_REGISTRA,
.mode = GPIO_MODE_INPUT,
.pull_up_en = GPIO_PULLUP_ENABLE,
};
ESP_ERROR_CHECK(gpio_config(&in));
}
static void spi_init(void)
{
const spi_bus_config_t bus = {
.sclk_io_num = PIN_SCK,
.mosi_io_num = PIN_MOSI,
.miso_io_num = PIN_MISO,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
};
ESP_ERROR_CHECK(spi_bus_initialize(SPI2_HOST, &bus, SPI_DMA_DISABLED));
const spi_device_interface_config_t dev = {
.clock_speed_hz = 4 * 1000 * 1000, /* MFRC522: max 10 Mbit/s */
.mode = 0,
.spics_io_num = PIN_CS,
.queue_size = 1,
};
ESP_ERROR_CHECK(spi_bus_add_device(SPI2_HOST, &dev, &s_spi));
}
void app_main(void)
{
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
gpio_init();
spi_init();
rc522_init();
uint8_t ver = rc522_leggi(REG_VERSION);
ESP_LOGI(TAG, "MFRC522 VersionReg = 0x%02X (0x91 = v1.0, 0x92 = v2.0)", ver);
if (ver == 0x00 || ver == 0xFF) {
ESP_LOGE(TAG, "Nessuna risposta: controlla cablaggio SPI e alimentazione 3,3 V");
}
lista_carica();
ESP_LOGI(TAG, "Chiavette autorizzate in NVS: %u", (unsigned)s_num_autorizzati);
while (1) {
chiave_t uid;
if (rc522_leggi_uid(&uid) == ESP_OK) {
char hex[3 * MAX_UID + 1] = { 0 };
for (int i = 0; i < uid.len; i++) {
sprintf(&hex[i * 3], "%02X ", uid.uid[i]);
}
bool registra = (gpio_get_level(PIN_REGISTRA) == 0);
if (registra && !lista_contiene(&uid) && s_num_autorizzati < MAX_AUTORIZZATI) {
s_lista[s_num_autorizzati++] = uid;
ESP_LOGW(TAG, "REGISTRATA UID %s(%d byte) -> %s", hex, uid.len,
esp_err_to_name(lista_salva()));
segnala(true);
} else if (lista_contiene(&uid)) {
ESP_LOGI(TAG, "ACCESSO CONSENTITO UID %s", hex);
segnala(true);
} else {
ESP_LOGW(TAG, "ACCESSO NEGATO UID %s", hex);
segnala(false);
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
Come funziona il codice
rc522_init()fa un reset hardware e software, poi imposta il timer interno (prescaler 0xA9 e ricarica 1000: circa 25 ms di timeout per ogni risposta), la modulazione ASK al 100% richiesta dal tipo A, il preset del CRC e accende i due driver d’antenna. Sono i valori usati dalle librerie MFRC522 più diffuse; il significato di ogni bit è nel datasheet NXP.rc522_transceive()è il cuore: svuota la FIFO, ci carica i byte da trasmettere, avvia il comando Transceive e aspetta i flag RxIRq o IdleIRq inComIrqReg. Se scatta il timer (nessuna carta) tornaESP_ERR_TIMEOUT; poi controllaErrorRegper errori di protocollo, parità e collisione, e legge la risposta dalla FIFO.rc522_leggi_uid()esegue REQA (conBitFramingReg = 0x07per trasmettere solo 7 bit), anticollisione e SELECT per ogni livello di cascata, verifica il BCC e chiude con HLTA. Il driver è volutamente minimo: con due carte nel campo segnala collisione invece di risolverla bit per bit.- Lista autorizzati: un array di strutture {lunghezza, UID} salvato come blob nel namespace NVS
accessi. Sopravvive ai riavvii e agli aggiornamenti firmware. - Registrazione: se tieni premuto il pulsante su GPIO23 mentre avvicini una chiavetta nuova, questa viene aggiunta alla lista. In un impianto vero proteggeresti questa funzione (pulsante interno alla scatola, carta master).
segnala(): bip corto, LED verde e relè attivo per 3 secondi se l’accesso è consentito; bip lungo e LED rosso se è negato.
Prova e risultati attesi
Al primo avvio la lista è vuota: tieni premuto il pulsante e avvicina la carta per registrarla, poi rilascia e riprova. Il log sarà simile a questo (gli UID sono di esempio):
I (298) rfid: MFRC522 VersionReg = 0x92 (0x91 = v1.0, 0x92 = v2.0)
I (298) rfid: Chiavette autorizzate in NVS: 0
W (5120) rfid: REGISTRATA UID 3A 7F 21 C5 (4 byte) -> ESP_OK
I (9870) rfid: ACCESSO CONSENTITO UID 3A 7F 21 C5
W (14210) rfid: ACCESSO NEGATO UID 04 5B 92 1A 6C 11 90
L’ultima riga mostra un UID da 7 byte, tipico delle carte NTAG e DESFire: la cascata funziona.
Problemi comuni
- VersionReg vale 0x00 o 0xFF: il chip non risponde. Controlla MISO/MOSI (spesso invertiti), il chip select su SDA e che RST sia alto. Alcuni cloni del chip restituiscono valori diversi da 0x91/0x92 ma funzionano lo stesso.
- Nessuna carta rilevata: la distanza utile dei moduli economici è di pochi centimetri. Metalli vicini all’antenna (viti, scatole di alluminio) la riducono ancora.
- Letture intermittenti: alimentazione debole o cavi lunghi. Aggiungi un condensatore da 10 µF vicino al modulo e accorcia i fili SPI.
- Il relè resetta l’ESP32: disturbi sulla bobina. Usa un modulo con diodo di ricircolo e, meglio ancora, un’alimentazione separata per il relè.
Sicurezza: perché l’UID non basta
Questo è il punto più importante dell’articolo. L’UID viene trasmesso in chiaro, senza alcuna autenticazione, e chiunque lo può leggere con uno smartphone NFC. Esistono poi le cosiddette magic card, carte compatibili MIFARE Classic in cui il blocco 0 (quello che contiene l’UID) è riscrivibile: con pochi euro si ottiene un clone perfetto della tua chiavetta, e gli strumenti di analisi RFID possono anche emulare un UID arbitrario. Anche la crittografia Crypto1 delle MIFARE Classic è violata da anni: proteggere i settori con le chiavi A/B non basta a renderla sicura.
Per un controllo accessi reale usa carte MIFARE DESFire EV2 o EV3:
- autenticazione reciproca AES-128 tra lettore e carta: la chiave non viaggia mai sul canale;
- applicazioni e file separati sulla stessa carta, ognuno con le sue chiavi;
- opzione di UID casuale, che rende inutile tracciare o copiare l’UID;
- la decisione di aprire si basa sull’autenticazione riuscita con una chiave diversificata per ogni carta, non sul numero di serie.
È la scelta fatta nel progetto Antifurto del sito, che usa carte DESFire EV3. Lato ESP32 ricordati anche di proteggere le chiavi: attiva flash encryption e secure boot, altrimenti chi ha accesso fisico alla scheda può leggerle dalla flash.
Va oltre
- Passa a DESFire con un PN532, sfruttando i suoi comandi di alto livello per lo scambio dati con la carta (verifica nella documentazione NXP come tratta il livello ISO 14443-4), e implementa l’autenticazione AES seguendo la documentazione NXP.
- Aggiungi il registro accessi: salva data, ora (via SNTP) e UID di ogni evento e inviali via Wi-Fi o MQTT.
- Usa l’interrupt: collega il pin IRQ e configura
ComIEnRegper non fare polling continuo e risparmiare energia. - Altri progetti con ESP32 e domotica: sezione ESP32 e sezione domotica.
