MCU & FPGA geral Uso de NFC de Smartphones para Identificação Segura com ESP32 e PN532

Uso de NFC de Smartphones para Identificação Segura com ESP32 e PN532

NFC (Near Field Communication) é um conjunto de padrões que permite comunicação sem fio de curtíssimo alcance (tipicamente até ~4 cm) entre um iniciador e um alvo. Em sistemas de identificação, isso se traduz em um “leitor” (ex.: ESP32 + PN532) que energiza e interroga um “alvo” (ex.: um cartão MIFARE, um crachá, ou um smartphone operando em modo de emulação de cartão).

Benefícios para identificação:

  • Experiência de uso: aproxima e pronto; não exige pareamento.
  • Baixo consumo e tempo de transação curto.
  • Compatível com ecossistema existente: cartões ISO/IEC 14443, NDEF, etc.

Modos de operação relevantes

No contexto de identificação, três modos interessam:

  1. Reader/Writer (leitor/escritor)
    O leitor (PN532) lê/escreve dados de uma tag (cartão físico). Padrões comuns: ISO/IEC 14443 A/B (ex.: MIFARE Classic/Ultralight/DesFire), e dados em formato NDEF.
  2. Card Emulation (emulação de cartão)
    O smartphone se comporta como uma tag.
  • Android: permite HCE (Host Card Emulation), em que um app responde a comandos APDU do leitor; é o caminho para entregar dados consistentes (um ID estável, criptografado, etc.).
  • iOS: a emulação “aberta” é restrita. iPhones em geral não expõem HCE genérico para apps de terceiros; o acesso é limitado a casos de uso do ecossistema Apple (ex.: Apple Pay, cartões de transporte/estudante em “Express Mode”), o que não serve para enviar um ID arbitrário ao seu PN532.
  1. Peer‑to‑Peer (NFCIP-1/ECMA‑340/ISO/IEC 18092)
    Permite troca ativa entre dois dispositivos. Hoje é raro para identificação porque o suporte nas plataformas móveis é limitado e a UX é menos previsível.

💡 Resumo prático: para identificar um telefone com um mesmo dado toda vez, no Android você usa HCE e define o protocolo (APDUs) do lado do leitor (PN532) e do app. No iOS, por restrições da plataforma, não há um caminho equivalente para um app de terceiro emular um cartão com ID estável para leitores genéricos.

Sobre UIDs “aleatórios” e por que o seu leitor vê IDs diferentes

  • As tags clássicas (cartões) possuem um UID (número de série do chip) que o leitor enxerga logo no anticollision/seleção ISO14443A.
  • Smartphones em modo HCE não expõem um UID fixo de tag; ao invés disso, a pilha do sistema pode anunciar identificadores temporários/aleatórios durante o anti‑colisão (ou nem aparecer como tag se não houver um serviço HCE correspondente). Isso é intencional, por privacidade e segurança: o UID nunca foi projetado para ser um identificador pessoal confiável; ele é só um detalhe de baixo nível para a logística do enlace.
  • Resultado: se você tentar “identificar o telefone pelo UID”, com Android vai ver valores variáveis e com iPhone normalmente nada.

Como identificar com segurança (conceitos que guiaremos no artigo)

Basear‑se num UID estável não é seguro. Em vez disso, use dados de aplicação e protocolos com autenticação:

  • App Android (HCE) entrega um identificador lógico do usuário/dispositivo dentro de um frame APDU/NDEF, preferencialmente com assinatura (chave privada no app/secure element) ou MAC (chave simétrica provisionada).
  • O leitor (ESP32+PN532) valida a autenticidade do payload (ex.: verifica assinatura ECDSA/Ed25519 ou HMAC-SHA256) e associa esse ID a uma conta/permiso no seu backend.
  • Opcional: desafio–resposta (o leitor manda um nonce, o app responde assinando/MACando). Isso mitiga replay e relay simples.
  • iOS: como não há HCE aberto, alternativas práticas são:
    • Inverter papéis: o ESP32/PN532 emula uma tag (quando suportado) e o app iOS escreve (ou lê) um registro NDEF com o ID (com autenticação). Exige o app aberto e interação do usuário.
    • Out-of-band: BLE + NFC só para “toque para abrir o app”, e a autenticação forte ocorre via BLE/HTTP com o servidor.

Padrões e camadas (para situar o que virá no tutorial)

Ao longo do tutorial, vamos citar (em alto nível):

  • ISO/IEC 14443‑A/B: anti‑colisão, seleção, ATR, onde o PN532 opera como leitor.
  • ISO/IEC 7816‑4 (APDU): comandos/respostas usados pelo HCE no Android.
  • NDEF: contêiner de dados NFC (útil, mas para identificação recomendamos APDU + assinatura).
  • ESP‑IDF + PN532 (I²C/SPI/UART): driver, inicialização, polling e troca de frames, tudo sem Arduino.

O que você vai construir neste artigo

  1. Entender os requisitos de segurança (por que UID não serve; como evitar clonagem/replay/relay).
  2. Subir um projeto ESP‑IDF que fala com o PN532 e faz polling ISO14443A.
  3. Processar APDUs provenientes de um app Android HCE que envia um ID assinado (constante).
  4. Verificar a assinatura no ESP32 (ex.: Ed25519) e tomar uma ação (abrir porta, registrar presença, etc.).
  5. Explorar alternativas para iOS e “inversão de papéis” (PN532 como tag e app como escritor/leitor NDEF), com limitações e UX esperada.

Requisitos de Segurança no Uso de NFC para Autenticação

Por que segurança é crítica em NFC de identificação

O NFC, por operar em distâncias muito curtas, transmite a impressão de ser “seguro por proximidade”. No entanto, ataques como relay attack (prolongamento do alcance via retransmissão), replay attack (reenvio de dados previamente capturados) e clonagem de tags são viáveis se o protocolo de aplicação não adotar medidas de proteção. Em sistemas de autenticação, a segurança deve ser tratada como prioridade, já que a violação de identidade pode levar a acesso indevido, fraude ou sabotagem.

A premissa fundamental: o UID lido pelo PN532 não é suficiente para autenticar. Esse identificador pode ser forjado ou variar (como no caso dos smartphones em HCE ou iOS restrito), não servindo como prova confiável de identidade.

Ameaças comuns a sistemas NFC

  • Clonagem de UID: tags com UID fixo podem ser copiadas para cartões programáveis.
  • Relay Attack: atacante retransmite comunicação NFC em tempo real para outro local.
  • Replay Attack: captura de uma transação válida para reutilização posterior.
  • Eavesdropping: interceptação do tráfego NFC (apesar de difícil, é possível com antenas especializadas).
  • App malicioso em HCE: no Android, um aplicativo comprometido pode responder como se fosse o legítimo.

Boas práticas para autenticação segura

  1. Identificador lógico assinado
    • O app envia um payload contendo:
      • ID lógico do dispositivo/usuário.
      • Timestamp ou nonce (número aleatório único por transação).
      • Assinatura digital ou HMAC sobre esses dados.
    • No leitor, o ESP32 valida a assinatura usando chave pública pré-configurada (assinatura assimétrica) ou chave compartilhada (HMAC).
  2. Desafio–resposta
    • O leitor inicia o processo enviando um nonce para o app.
    • O app assina ou MACa o nonce + seu ID e retorna.
    • Previne replay, pois cada execução exige nova prova criptográfica.
  3. Criptografia de dados de aplicação
    • Além da assinatura, os dados podem ser criptografados (ex.: AES-GCM) para proteger a privacidade.
  4. Controle de sessão e tempo
    • Estabelecer janelas curtas de validade para respostas.
    • Rejeitar respostas fora do intervalo de tempo ou reutilizadas.

Diferenças práticas entre Android e iOS

  • Android:
    • HCE permite responder a APDUs definidos por você, enviando dados autenticados.
    • Pode rodar em background ou foreground dependendo do tipo de serviço NFC e permissões.
  • iOS:
    • Sem HCE aberto; NFC está limitado a leitura/gravação de NDEF e integração com Apple Wallet/Pay.
    • Alternativa prática: inversão de papéis — PN532 emula tag, app iOS escreve dados autenticados quando tocado.

Escolha de algoritmos e chaves

  • Assinatura assimétrica (ECDSA, Ed25519):
    • Vantagem: o leitor só precisa da chave pública, não há risco de vazamento de chave privada.
    • Boa para autenticação offline.
  • Assinatura simétrica (HMAC-SHA256):
    • Mais simples e rápida, mas exige proteger a chave no app (difícil no Android, quase impossível no iOS sem Secure Enclave).
  • Recomendação:
    • Use Ed25519 para alta segurança, rapidez e facilidade de implementação no ESP32 com bibliotecas otimizadas.

Tutorial prático: Leitura NFC com ESP32 e PN532 no ESP-IDF

Agora que entendemos o que é NFC e os requisitos de segurança, vamos colocar em prática a comunicação entre o ESP32 e o módulo PN532, usando o ESP-IDF (não Arduino).
O objetivo inicial será detectar e ler tags NFC, mas já com a estrutura preparada para processar dados vindos de um app Android em modo HCE (Host Card Emulation).


Conexão física (I²C)

O PN532 suporta comunicação via I²C, SPI ou UART. Aqui usaremos I²C pela simplicidade de conexão e disponibilidade de GPIOs no ESP32.

Exemplo de ligação (PN532 I²C → ESP32):

PN532ESP32
SDAGPIO21
SCLGPIO22
VCC3V3
GNDGND

💡 Nota: o PN532 opera a 3,3 V, então não é necessário conversor de nível para uso com o ESP32.


Estrutura do projeto no ESP-IDF

A estrutura mínima será:

/main
 ├── main.c
 ├── pn532.h
 ├── pn532.c
CMakeLists.txt

Teremos um driver básico para o PN532 (pn532.c/pn532.h) e o arquivo main.c para inicialização e loop principal.


3.3 Inicialização do I²C no ESP-IDF

#include "driver/i2c.h"
#include "esp_log.h"

#define I2C_MASTER_SCL_IO           22
#define I2C_MASTER_SDA_IO           21
#define I2C_MASTER_NUM              I2C_NUM_0
#define I2C_MASTER_FREQ_HZ          100000
#define I2C_MASTER_TX_BUF_DISABLE   0
#define I2C_MASTER_RX_BUF_DISABLE   0
#define PN532_I2C_ADDRESS           0x24 >> 1 // Endereço I²C do PN532

static const char *TAG = "PN532_I2C";

esp_err_t i2c_master_init(void) {
    i2c_config_t conf = {
        .mode = I2C_MODE_MASTER,
        .sda_io_num = I2C_MASTER_SDA_IO,
        .sda_pullup_en = GPIO_PULLUP_ENABLE,
        .scl_io_num = I2C_MASTER_SCL_IO,
        .scl_pullup_en = GPIO_PULLUP_ENABLE,
        .master.clk_speed = I2C_MASTER_FREQ_HZ,
    };
    esp_err_t err = i2c_param_config(I2C_MASTER_NUM, &conf);
    if (err != ESP_OK) return err;
    return i2c_driver_install(I2C_MASTER_NUM, conf.mode,
                               I2C_MASTER_RX_BUF_DISABLE,
                               I2C_MASTER_TX_BUF_DISABLE, 0);
}

Explicação:

  • Configuramos o I²C como mestre.
  • Usamos o endereço do PN532 ajustado (shift para 7 bits).
  • Ativamos pull-ups internos, mas é fortemente recomendado usar resistores de 4,7 kΩ externos para SDA e SCL.

Inicialização do PN532

#include "pn532.h"

void app_main(void) {
    ESP_ERROR_CHECK(i2c_master_init());
    ESP_LOGI(TAG, "I2C inicializado");

    pn532_init();
    pn532_sam_config();

    while (1) {
        uint8_t uid[7];
        uint8_t uid_len;
        if (pn532_read_passive_target(uid, &uid_len)) {
            ESP_LOGI(TAG, "Tag detectada - UID:");
            for (int i = 0; i < uid_len; i++) {
                printf("%02X", uid[i]);
            }
            printf("\n");
        }
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

Implementação básica do driver PN532

O driver conterá funções para:

  • pn532_init() — Inicializar o chip.
  • pn532_sam_config() — Configurar o modo de operação.
  • pn532_read_passive_target() — Ler UID de uma tag ou resposta de um app HCE.

Exemplo simplificado de pn532_sam_config():

void pn532_sam_config(void) {
    uint8_t command[] = { 0x14, 0x01, 0x14, 0x01 };
    pn532_write_command(command, sizeof(command));
    pn532_read_response(NULL, 0);
}

Próximos passos

Com essa base, já conseguimos:

  • Detectar tags NFC e ler seus UIDs.
  • Capturar o momento em que um app Android em HCE se apresenta como tag.

Na próxima seção, vamos:

  1. Implementar troca de APDUs com o app Android HCE.
  2. Fazer o processamento seguro de um ID assinado no ESP32.
  3. Integrar a validação de assinatura Ed25519.

Comunicação APDU com Android HCE (ID estável + assinatura)

Nesta seção vamos resolver o problema do “ID aleatório” usando HCE (Host Card Emulation) no Android: o celular se comporta como um “cartão” e responde a APDUs definidos por você. O leitor (ESP32+PN532) envia um SELECT AID; o app Android responde com um payload estável (ID lógico) + prova criptográfica (assinatura). O ESP32 valida a resposta e então toma a ação (abrir/registrar/etc.).

Linha do tempo do fluxo:

  1. PN532 detecta alvo ISO14443‑A (InListPassiveTarget).
  2. ESP32 envia APDU SELECT AID via InDataExchange.
  3. App Android (HCE) recebe no processCommandApdu() e responde com ID || TIMESTAMP || NONCE || ASSINATURA || 0x9000.
  4. ESP32 verifica assinatura → se válida, identifica o aparelho.

Conceitos rápidos (APDU + HCE)

  • APDU (ISO/IEC 7816‑4): pacotes do tipo comando/resposta.
    • Exemplo SELECT AID: 00 A4 04 00 Lc AID 00
      • 00 CLA, A4 INS (SELECT), 04 00 P1P2 (seletor por AID), Lc (tamanho do AID), AID (bytes), 00 Le (máximo).
  • AID (Application Identifier): identificador do seu “applet emulado”. Você define um AID único do seu sistema (ex.: F0 12 34 56 78 90).

No Android, você implementa um HostApduService que “ouve” SELECT para o(s) AID(s) declarados no Manifest e responde APDUs. No iOS isso não é exposto a apps comuns (veremos alternativas mais adiante, na seção dedicada ao iOS).


Comandos PN532 usados

No modo leitor ISO14443‑A:

  • InListPassiveTarget (0x4A): faz polling e seleciona o alvo (smartphone/tag).
  • InDataExchange (0x40): envia dados (APDU) ao alvo já selecionado e recebe a resposta.

Observação: abstrairemos a moldura PN532 (preamble, checksums) em funções utilitárias pn532_write_command()/pn532_read_response() criadas na seção anterior.


Estrutura do APDU no seu protocolo

Definiremos:

  • AID do serviço HCE: F0 12 34 56 78 90 (exemplo — troque pelo seu).
  • Resposta do app (campo de dados APDU): [ID_LEN (1B)] [ID (ID_LEN B)] [UNIX_TS (4B)] [NONCE (16B)] [SIG (64B Ed25519)] // 64 bytes Sufixo padrão de status word: 0x90 0x00 (sucesso).
  • Mensagem a ser assinada pelo app: M = "AID" || ID || UNIX_TS || NONCE (Você pode incluir versionamento e/ou domínio do sistema para evitar confusão entre apps.)

O ESP32 mantém a chave pública Ed25519Ed25519 para validar a assinatura do app. Assim nenhum segredo fica no leitor.


Código ESP‑IDF (PN532: SELECT AID e troca de APDU)

Abaixo estão trechos completos e comentados focados no fluxo APDU. Eles assumem que você já possui pn532_write_command(), pn532_read_response() e utilitários de I²C prontos (Seção 3). Mostramos também funções novas: pn532_inlistpassivetarget(), pn532_indataexchange() e a verificação Ed25519 (via biblioteca).

pn532.h

// pn532.h
#pragma once
#include <stdint.h>
#include <stdbool.h>
#include "esp_err.h"

#ifdef __cplusplus
extern "C" {
#endif

#define PN532_CMD_INLISTPASSIVETARGET  0x4A
#define PN532_CMD_INDATAEXCHANGE       0x40

// AID do serviço HCE (exemplo)
static const uint8_t HCE_AID[] = { 0xF0, 0x12, 0x34, 0x56, 0x78, 0x90 };

esp_err_t pn532_init(void);
esp_err_t pn532_sam_config(void);

// Poll ISO14443-A, retorna número de alvos (0 ou 1 normalmente)
int pn532_inlistpassivetarget(uint8_t max_targets, uint8_t br_ty, uint8_t *out_buf, size_t *out_len);

// Envia APDU ao target ativo (tg = 1 normalmente) e recebe resposta
esp_err_t pn532_indataexchange(uint8_t tg, const uint8_t *tx, size_t tx_len, uint8_t *rx, size_t *rx_len);

// Helpers de alto nível para SELECT AID e leitura da resposta
esp_err_t nfc_send_select_aid_and_recv(const uint8_t *aid, uint8_t aid_len,
                                       uint8_t *rx, size_t *rx_len);

// Verificação Ed25519 (interface)
bool nfc_verify_ed25519(const uint8_t *msg, size_t msg_len,
                        const uint8_t *sig64, const uint8_t *pubkey32);

#ifdef __cplusplus
}
#endif

pn532.c (núcleo da troca com HCE)

// pn532.c
#include "pn532.h"
#include "esp_log.h"
#include <string.h>

// Assume que você já implementou isto na seção anterior:
extern esp_err_t pn532_write_command(const uint8_t *cmd, size_t len);
extern esp_err_t pn532_read_response(uint8_t *buf, size_t *len, uint32_t timeout_ms);

static const char *TAG = "PN532_APDU";

// 4.4.2.1 — InListPassiveTarget (ISO14443A)
int pn532_inlistpassivetarget(uint8_t max_targets, uint8_t br_ty, uint8_t *out_buf, size_t *out_len)
{
    // Comando: 0x4A, max_targets, br_ty (=0x00 para 106 kbps type A), opcional: UID len + UID (para inlist específica)
    uint8_t cmd[] = { PN532_CMD_INLISTPASSIVETARGET, max_targets, br_ty };
    ESP_ERROR_CHECK(pn532_write_command(cmd, sizeof(cmd)));

    uint8_t resp[64];
    size_t  resp_len = sizeof(resp);
    esp_err_t err = pn532_read_response(resp, &resp_len, 1000);
    if (err != ESP_OK) return -1;

    // Resposta típica: TFI + 0x4B + NbTg + [Tg, ...parametros alvo...]
    if (resp_len < 3 || resp[0] != 0xD5 || resp[1] != 0x4B) {
        ESP_LOGW(TAG, "Resposta inesperada InListPassiveTarget");
        return -1;
    }
    int nb_tg = resp[2];
    if (out_buf && out_len) {
        size_t copy = (resp_len - 3 < *out_len) ? (resp_len - 3) : *out_len;
        memcpy(out_buf, &resp[3], copy);
        *out_len = copy;
    }
    return nb_tg;
}

// 4.4.2.2 — InDataExchange (enviar APDU ao target 'tg')
esp_err_t pn532_indataexchange(uint8_t tg, const uint8_t *tx, size_t tx_len, uint8_t *rx, size_t *rx_len)
{
    // Comando: 0x40, tg, data...
    uint8_t hdr[2] = { PN532_CMD_INDATAEXCHANGE, tg };
    uint8_t tmp[2 + 260]; // APDUs costumam < 255B
    memcpy(tmp, hdr, 2);
    memcpy(tmp + 2, tx, tx_len);

    ESP_ERROR_CHECK(pn532_write_command(tmp, 2 + tx_len));

    uint8_t resp[300];
    size_t  resp_len = sizeof(resp);
    esp_err_t err = pn532_read_response(resp, &resp_len, 1000);
    if (err != ESP_OK) return err;

    // Resposta: TFI + 0x41 + Status + Data...
    if (resp_len < 3 || resp[0] != 0xD5 || resp[1] != 0x41 || resp[2] != 0x00) {
        ESP_LOGW(TAG, "InDataExchange status != 0, len=%u", (unsigned)resp_len);
        return ESP_FAIL;
    }
    size_t payload = resp_len - 3;
    if (rx && rx_len) {
        size_t copy = (payload < *rx_len) ? payload : *rx_len;
        memcpy(rx, &resp[3], copy);
        *rx_len = copy;
    }
    return ESP_OK;
}

// 4.4.2.3 — Helper: monta SELECT AID e recebe resposta
esp_err_t nfc_send_select_aid_and_recv(const uint8_t *aid, uint8_t aid_len,
                                       uint8_t *rx, size_t *rx_len)
{
    // APDU SELECT AID: 00 A4 04 00 Lc AID 00
    uint8_t apdu[5 + 16 + 1]; // até 16B de AID, ajuste se necessário
    size_t  idx = 0;
    apdu[idx++] = 0x00; // CLA
    apdu[idx++] = 0xA4; // INS SELECT
    apdu[idx++] = 0x04; // P1: seletor por AID
    apdu[idx++] = 0x00; // P2
    apdu[idx++] = aid_len;
    memcpy(&apdu[idx], aid, aid_len); idx += aid_len;
    apdu[idx++] = 0x00; // Le (máximo)

    // No PN532, o target normalmente é 1 (primeiro da lista)
    return pn532_indataexchange(0x01, apdu, idx, rx, rx_len);
}

Exemplo de uso no app_main.c

// main.c (trecho principal do loop)
#include "pn532.h"
#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"

static const char *TAG = "APP_MAIN";

// Chave pública Ed25519 do seu app (32 bytes) — exemplo fictício
static const uint8_t PUBKEY_ED25519[32] = {
    0x11,0x22,0x33,0x44, /* ... preencha com sua chave real ... */ 0xAA
};

void app_main(void)
{
    // init I2C + PN532 (da Seção 3)
    // pn532_init();
    // pn532_sam_config();

    while (1) {
        // 1) Poll por alvo: 1 target, 106 kbps type A
        uint8_t buf[64]; size_t blen = sizeof(buf);
        int nb = pn532_inlistpassivetarget(1, 0x00, buf, &blen);
        if (nb <= 0) {
            vTaskDelay(pdMS_TO_TICKS(200));
            continue;
        }
        ESP_LOGI(TAG, "Alvo detectado (%d). Tentando SELECT AID...", nb);

        // 2) Envia SELECT AID e recebe resposta APDU
        uint8_t rx[256]; size_t rx_len = sizeof(rx);
        if (nfc_send_select_aid_and_recv(HCE_AID, sizeof(HCE_AID), rx, &rx_len) != ESP_OK) {
            ESP_LOGW(TAG, "SELECT AID falhou");
            continue;
        }

        // 3) Analisa a resposta: ... dados ... 0x90 0x00
        if (rx_len < 2 || rx[rx_len-2] != 0x90 || rx[rx_len-1] != 0x00) {
            ESP_LOGW(TAG, "Status word != 0x9000");
            continue;
        }
        size_t data_len = rx_len - 2;

        // Layout: ID_LEN(1) | ID | TS(4B) | NONCE(16B) | SIG(64B)
        if (data_len < 1 + 4 + 16 + 64) {
            ESP_LOGW(TAG, "Payload curto (%u)", (unsigned)data_len);
            continue;
        }
        uint8_t id_len = rx[0];
        if (data_len < 1 + id_len + 4 + 16 + 64) {
            ESP_LOGW(TAG, "ID_LEN inconsistente");
            continue;
        }

        const uint8_t *id     = &rx[1];
        const uint8_t *ts     = &rx[1 + id_len];         // 4 bytes (UNIX epoch)
        const uint8_t *nonce  = &rx[1 + id_len + 4];     // 16 bytes
        const uint8_t *sig    = &rx[1 + id_len + 4 + 16];// 64 bytes

        // Mensagem M = "AID" || ID || TS || NONCE
        uint8_t M[3 + sizeof(HCE_AID) + 64]; // tam suficiente
        size_t  mlen = 0;
        // "AID" tag para contexto (opcional)
        M[mlen++] = 'A'; M[mlen++] = 'I'; M[mlen++] = 'D';
        memcpy(&M[mlen], HCE_AID, sizeof(HCE_AID)); mlen += sizeof(HCE_AID);
        memcpy(&M[mlen], id, id_len); mlen += id_len;
        memcpy(&M[mlen], ts, 4); mlen += 4;
        memcpy(&M[mlen], nonce, 16); mlen += 16;

        bool ok = nfc_verify_ed25519(M, mlen, sig, PUBKEY_ED25519);
        if (!ok) {
            ESP_LOGW(TAG, "Assinatura inválida — rejeitado");
            continue;
        }

        // TODO (opcional): validar janela de tempo (TS) e nonce (anti-replay).
        ESP_LOGI(TAG, "ID autenticado: %.*s", id_len, (const char*)id);
        // → Execute a ação: abrir porta, logar presença, etc.
    }
}

Verificação Ed25519 no ESP32

Você pode usar uma implementação portátil (ex.: TweetNaCl, ed25519-donna) como componente do ESP‑IDF. A interface usada acima é:

bool nfc_verify_ed25519(const uint8_t *msg, size_t msg_len,
                        const uint8_t *sig64, const uint8_t *pubkey32)
{
    // Chame a função da sua lib Ed25519:
    // return ed25519_verify(sig64, msg, msg_len, pubkey32) == 1;
    // Abaixo deixamos como stub:
    extern int ed25519_verify(const unsigned char *signature,
                              const unsigned char *message, size_t message_len,
                              const unsigned char *public_key);
    int r = ed25519_verify(sig64, msg, msg_len, pubkey32);
    return (r == 1);
}

Boas práticas

  • Armazene a chave pública no firmware (ou obtenha de backend com TLS, se online).
  • Registre ts/nonce no lado do leitor para bloquear replays por um intervalo (cache LRU simples).
  • Considere usar Ed25519 por ser rápido e leve no ESP32.

App Android (HCE) — visão de alto nível

A seguir, um esqueleto didático (Kotlin) para o lado Android. Ele não compila “como‑está” sem seu Gradle/Manifest, mas mostra onde você coloca o AID e como monta a resposta.

Manifest + XML de AID

AndroidManifest.xml:

<service
    android:name=".MyHceService"
    android:exported="true"
    android:permission="android.permission.BIND_NFC_SERVICE">
    <intent-filter>
        <action android:name="android.nfc.cardemulation.action.HOST_APDU_SERVICE" />
    </intent-filter>
    <meta-data
        android:name="android.nfc.cardemulation.host_apdu_service"
        android:resource="@xml/apduservice" />
</service>

res/xml/apduservice.xml:

<host-apdu-service xmlns:android="http://schemas.android.com/apk/res/android"
    android:description="@string/app_name"
    android:requireDeviceUnlock="false">
    <aid-group android:description="@string/app_name" android:category="other">
        <aid-filter android:name="F01234567890" />
    </aid-group>
</host-apdu-service>

Observação: o AID aqui (F01234567890) deve casar com o usado no SELECT AID do ESP32.

Implementação do HostApduService

class MyHceService : HostApduService() {

    // ID lógico estável (pode ser UUID fixo, user handle, etc.)
    private val stableId = "user:alice@empresa.com".toByteArray(Charsets.UTF_8)

    // Chaves Ed25519 gerenciadas pelo app (ideal: armazenar em Keystore + StrongBox quando disponível)
    private val privKey: ByteArray = /* carregar do Keystore/secure storage */
    private val pubKey: ByteArray  = /* mesma par do leitor */

    override fun processCommandApdu(commandApdu: ByteArray, extras: Bundle?): ByteArray {
        // Parse do SELECT AID: 00 A4 04 00 Lc AID 00
        if (isSelectForOurAid(commandApdu)) {
            val ts = (System.currentTimeMillis() / 1000L).toInt()
            val nonce = SecureRandom().generateSeed(16)

            // Mensagem M = "AID" || AID || ID || TS || NONCE
            val aid = hexToBytes("F01234567890")
            val M = ByteArrayOutputStream().apply {
                write(byteArrayOf('A'.code.toByte(),'I'.code.toByte(),'D'.code.toByte()))
                write(aid)
                write(stableId)
                write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(ts).array())
                write(nonce)
            }.toByteArray()

            val sig = ed25519Sign(privKey, M) // 64 bytes

            val payload = ByteArrayOutputStream().apply {
                write(stableId.size)
                write(stableId)
                write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(ts).array())
                write(nonce)
                write(sig)
            }.toByteArray()

            return appendSw9000(payload)
        }

        // Caso deseje implementar outros INS (ex.: desafio–resposta separado)
        return sw6A82() // File not found / AID errado
    }

    override fun onDeactivated(reason: Int) { /* opcional */ }

    private fun isSelectForOurAid(apdu: ByteArray): Boolean {
        if (apdu.size < 6) return false
        val isSelect = apdu[0] == 0x00.toByte() && apdu[1] == 0xA4.toByte()
                && apdu[2] == 0x04.toByte()
        if (!isSelect) return false
        val lc = apdu[4].toInt() and 0xFF
        if (apdu.size < 5 + lc) return false
        val aid = apdu.copyOfRange(5, 5 + lc)
        return aid.contentEquals(hexToBytes("F01234567890"))
    }

    private fun appendSw9000(data: ByteArray): ByteArray =
        data + byteArrayOf(0x90.toByte(), 0x00.toByte())

    private fun sw6A82(): ByteArray = byteArrayOf(0x6A.toByte(), 0x82.toByte())

    private fun hexToBytes(s: String): ByteArray =
        s.chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}

Dica de segurança no Android: guarde a chave privada no Android Keystore com StrongBox quando disponível, e nunca serialize a private key em texto claro. Como o leitor só precisa da chave pública, um vazamento do lado do leitor não compromete a identidade.


Testes e diagnóstico

  • Se o SELECT AID retorna 6A 82, o AID não casou ou o serviço não está ativo.
  • Se retorna 90 00 sem payload, confira a montagem do ByteArrayOutputStream no app.
  • Use logs no ESP32 para imprimir id, ts e hexdump do nonce antes de verificar a assinatura.
  • Se a verificação falhar, valide mesma mensagem M nos dois lados e a endianness do timestamp.

Requisitos de segurança para usar NFC em autenticação

Nesta seção vamos “assentar o terreno” de segurança antes de escrever uma única linha de código. A ideia é: se o protocolo estiver certo, o hardware (ESP32 + PN532) e o app só precisam cumprir o combinado.

A matriz de ameaças (o que precisamos evitar)

  • Clonagem/replay: alguém captura um frame NFC e o reenvia depois para se passar pelo seu telefone.
  • IDs aleatórios/instáveis: telefones modernos não fornecem um identificador fixo de NFC. UIDs de ISO14443A podem ser randomizados (privacidade).
  • Ataques de relay: um atacante “estica” a comunicação (telefone longe do leitor) e encaminha APDUs.
  • Falsificação de app: um app malicioso tenta responder como se fosse o seu app.
  • Exfiltração de chaves: segredos no app/servidor vazam e comprometem todo o sistema.

O que não funciona (e por quê)

  • Usar UID da tag (Anti-collision ID): não é confiável com smartphones; Android pode randomizar e iOS normalmente nem apresenta UID em modo que você controle.
  • Gravar um NDEF fixo com a “minha matrícula/ID”: trivial de copiar/replay. Serve para casos de identificação não segura (ex.: abrir um link), não para autenticação.

O que funciona: desafios e provas criptográficas

O desenho robusto é um protocolo de desafio–resposta sobre ISO-DEP (ISO/IEC 14443-4) usando APDUs, com chaves do lado do telefone e verificação no leitor/servidor.

Pilares

  1. Identidade = chave, não = ID estático
    Cada telefone tem um par de chaves (ex.: Ed25519/ECDSA) gerado e fixado no dispositivo (Android Keystore/StrongBox). O “ID” do usuário vira a chave pública registrada no seu backend.
  2. Autenticação por desafio–resposta
    • O leitor (PN532) gera um nonce aleatório (ex.: 16–32 bytes) e manda via APDU.
    • O app assina esse nonce (e metadados) com a chave privada e devolve a assinatura.
    • O leitor/servidor valida com a chave pública daquela credencial.
      Resultado: sem replay (nonce único) e sem clonagem (precisa da chave privada).
  3. Vincular a sessão ao leitor
    Inclua no que é assinado: nonce || reader_id || timestamp. Assim, mesmo que alguém capture a resposta, ela só vale para aquele leitor e por poucos segundos.
  4. Tempo curto de validade
    Rejeite respostas com timestamp fora de janela (ex.: ±5 s). Dificulta relay.
  5. Rotação/recuperação
    Tenha mecanismo de reemitir credencial (novo par de chaves) em caso de perda/troca do aparelho.

Formato sugerido de payload (assinado)

struct AuthPayload {
  uint8_t  version;            // ex.: 1
  uint8_t  reader_id[6];       // ID do leitor
  uint8_t  nonce[16];          // gerado pelo leitor
  uint64_t unix_ms;            // relógio do telefone
}

Resposta do telefone:

SIGNATURE = Sign( AuthPayload || AID || APP_VERSION )
  • Algoritmo: Ed25519 (rápido e pequeno) ou ECDSA-P256.
  • Transporte: APDUs sobre ISO-DEP (PN532 dá suporte).

Observação: Você também pode transportar um JWT curto (JWS compact) assinado pelo app, contendo o nonce como claim, se preferir algo “HTTP-like”. Em NFC não há TLS; a prova de posse é a assinatura.

Android vs iOS — o que é possível

  • Android (HCE — Host Card Emulation)
    • Permite emular um cartão e responder a APDUs para um AID específico do seu app.
    • O app recebe SELECT AID, depois seus GET CHALLENGE, AUTH etc.
    • A chave privada fica no Android Keystore (idealmente StrongBox).
    • Você consegue entregar dados consistentes (a assinatura muda, a chave pública é a identidade).
  • iOS (Core NFC)
    • iOS não permite HCE genérico para apps de terceiros.
    • O iPhone não “se anuncia” como tag customizável pelo seu app.
    • Caminhos típicos no iOS:
      • Escrita em tag (o inverso de papéis; ver §2.7): o telefone escreve NDEF em uma tag (real ou emulada pelo PN532); útil para provisioning, não para prova forte de identidade.
      • Wallet/Passes/Keys (parcerias/entitlements específicos da Apple) — fora do alcance de apps comuns.

Conclusão prática: Autenticação ativa por NFC com telefone como “cartão” é Android via HCE. Para iOS, use fluxos alternativos (BLE + NFC de bootstrapping, QR + NFC, ou o inverso de papéis).

Esboço do protocolo (alto nível)

  1. Leitor → Telefone: SELECT AID do seu app (APDU).
  2. Leitor → Telefone: GET_CHALLENGE (envia nonce, reader_id).
  3. Telefone: monta AuthPayload, assina com chave privada (Keystore).
  4. Telefone → Leitor: AUTH_RESPONSE contendo AuthPayload + signature + pubkey_id (ou handle).
  5. Leitor: valida localmente (se tiver cache da pubkey) ou envia ao servidor para verificação/decisão.
  6. (Opcional): estabelecer canal de sessão curta (ex.: chaves efêmeras para trocar dados adicionais por mais 1–2 APDUs).

2.6. Boas práticas de implementação

  • Geração de nonce: use TRNG do ESP32 (esp_random() com entropy pool) e evite nonces previsíveis.
  • Janela de tempo: rejeite unix_ms fora de ±5 s; considere drifts.
  • Lista de revogação: mantenha IDs de chaves revogadas no leitor (cache) e/ou verifique online.
  • Fail-closed: qualquer erro de verificação ⇒ negação.
  • Proteção de chave no Android: KeyGenParameterSpec exigindo User Authentication opcional (biometria) para assinar — se a UX permitir.
  • Logs auditáveis: no leitor e no servidor (hash dos eventos).

Alternativa útil: inversão de papéis (PN532 emulando tag)

Quando você precisa que iOS também participe, considere inverter:

  • O PN532 emula uma tag (ex.: Type 4/ISO-DEP ou NDEF simples).
  • O telefone lê/escreve: o seu app pode escrever um token assinado na tag emulada (curto prazo).
  • O ESP32 valida a assinatura do token do app (sem HCE no iOS).
  • Limitações: requer o app aberto/ativo; latência de interação do usuário; não é tão à prova de replay sem nonce do leitor (dá para mitigar pedindo que o app busque o nonce online e grave o token com validade de segundos).

Limitações e soluções para o problema de IDs aleatórios em smartphones

O comportamento de enviar IDs NFC aleatórios em smartphones modernos é uma decisão de privacidade tomada pelos fabricantes e especificada nas normas mais recentes do NFC Forum. Isso afeta diretamente sistemas que dependiam de UIDs fixos para identificar dispositivos.


Por que os UIDs são aleatórios

  • Privacidade do usuário:
    Sem randomização, qualquer leitor poderia rastrear um aparelho no mundo físico sem consentimento, usando apenas o UID da interface NFC.
  • Especificações técnicas:
    Em modo ISO14443A (MIFARE, DESFire), o UID é negociado durante o anti-collision. O Android, desde a API 21, e fabricantes de NFC chips adotaram UIDs temporários para conexões P2P/reader.
  • Apple (iOS):
    A restrição é ainda maior: iOS normalmente não expõe UID algum para aplicativos, mesmo quando lendo tags.

Impacto para autenticação

  • Perda de identificador persistente:
    Você não consegue diferenciar, de forma confiável, o mesmo telefone em leituras distintas apenas pelo UID.
  • Impossibilidade de “whitelist” por UID:
    Não adianta gravar o UID detectado hoje para usar amanhã: ele mudará.

Soluções no Android

Host Card Emulation (HCE)

  • O telefone emula um cartão ISO-DEP com AID definido pelo app.
  • O app responde APDUs com um payload estável (ID lógico) + prova criptográfica.
  • O ID lógico é totalmente controlado pelo desenvolvedor, garantindo consistência.
  • Exemplo: NFCAndroid HCE Sample.

Aplicativo dedicado em modo leitura/gravação de tag

  • Em vez de emular, o telefone grava um valor fixo (ou token assinado) em uma tag física ou emulada pelo PN532.
  • Útil para integração cruzada com iOS.

Situação no iOS

  • Sem HCE para desenvolvedores comuns.
  • O iOS não se anuncia como tag NFC personalizada.
  • Soluções viáveis:
    1. Emulação de tag pelo PN532 e leitura/escrita via app iOS (Core NFC).
    2. BLE como canal principal com NFC apenas para pairing.
    3. Uso de Apple Wallet passes (limitado a casos com autorização da Apple).

Estratégias para um sistema híbrido Android/iOS

Para ter um sistema que funcione nos dois ecossistemas:

  1. Android:
    • HCE + assinatura digital
    • UID controlado pelo app
  2. iOS:
    • App lê tag emulada pelo PN532
    • Tag fornece nonce
    • App assina e envia a assinatura para o leitor (por BLE/Wi-Fi)

Referências úteis


Conclusão

O uso de NFC em sistemas de identificação baseados em ESP32 + PN532 abre possibilidades interessantes para controle de acesso, autenticação e automação, mas exige atenção especial à segurança e às limitações das plataformas móveis.
A dependência de UIDs fixos já não é uma prática segura nem confiável, especialmente porque:

  • No Android, o UID de tag NFC é frequentemente randomizado.
  • No iOS, o UID normalmente nem é exposto, e não há HCE aberto para desenvolvedores comuns.

A solução para Android é clara: implementar Host Card Emulation para enviar um ID lógico consistente, autenticado com assinatura digital. Já para iOS, é preciso recorrer a estratégias alternativas como a inversão de papéis (leitor atuando como tag), uso de BLE/Wi-Fi como canal principal, ou NFC como inicializador de sessão.

Além disso, o uso de desafio–resposta com nonces aleatórios, timestamps curtos e identificação de leitor é fundamental para evitar ataques de replay e clonagem.

Com as práticas descritas neste artigo, é possível construir um sistema híbrido, seguro e funcional, cobrindo tanto o ecossistema Android quanto iOS, cada um com a abordagem mais adequada às suas restrições.

0 0 votos
Classificação do artigo
Inscrever-se
Notificar de
guest
0 Comentários
mais antigos
mais recentes Mais votado
Feedbacks embutidos
Ver todos os comentários

Related Post

0
Adoraria saber sua opinião, comente.x